mirror of
https://github.com/pure-admin/pure-admin-thin.git
synced 2025-04-24 23:47:17 +08:00
update
This commit is contained in:
parent
5f6e80bbe8
commit
f0c4f1f39e
4
.env
4
.env
@ -1,5 +1,5 @@
|
|||||||
# 平台本地运行端口号
|
# Cổng chạy cục bộ nền tảng
|
||||||
VITE_PORT = 8848
|
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
|
VITE_HIDE_HOME = false
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# 平台本地运行端口号
|
# Cổng chạy cục bộ nền tảng
|
||||||
VITE_PORT = 8848
|
VITE_PORT = 8848
|
||||||
|
|
||||||
# 开发环境读取配置文件路径
|
# Đường dẫn đọc tệp cấu hình môi trường phát triển
|
||||||
VITE_PUBLIC_PATH = /
|
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"
|
VITE_ROUTER_HISTORY = "hash"
|
||||||
|
@ -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 = /
|
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"
|
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
|
VITE_CDN = false
|
||||||
|
|
||||||
# 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件)
|
# 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)
|
||||||
# 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
# 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)
|
||||||
# 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
# 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"
|
VITE_COMPRESSION = "none"
|
||||||
|
14
.env.staging
14
.env.staging
@ -1,16 +1,16 @@
|
|||||||
# 预发布也需要生产环境的行为
|
# Pre-release cũng cần hành vi của môi trường sản xuất
|
||||||
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
|
# https://vitejs.dev/guide/env-and-mode.html#modes
|
||||||
# NODE_ENV = development
|
# NODE_ENV = development
|
||||||
|
|
||||||
VITE_PUBLIC_PATH = /
|
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"
|
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
|
VITE_CDN = true
|
||||||
|
|
||||||
# 是否启用gzip压缩或brotli压缩(分两种情况,删除原始文件和不删除原始文件)
|
# 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)
|
||||||
# 压缩时不删除原始文件的配置:gzip、brotli、both(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
# 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)
|
||||||
# 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认)
|
# 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"
|
VITE_COMPRESSION = "none"
|
||||||
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -36,7 +36,7 @@
|
|||||||
"js"
|
"js"
|
||||||
],
|
],
|
||||||
"i18n-ally.sourceLanguage": "en",
|
"i18n-ally.sourceLanguage": "en",
|
||||||
"i18n-ally.displayLanguage": "zh-CN",
|
"i18n-ally.displayLanguage": "vi",
|
||||||
"i18n-ally.enabledFrameworks": [
|
"i18n-ally.enabledFrameworks": [
|
||||||
"vue"
|
"vue"
|
||||||
],
|
],
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
**English** | [中文](./README.md)
|
**English** | [TIẾNG VIỆT](./README.md)
|
||||||
|
|
||||||
## Introduce
|
## Introduce
|
||||||
|
|
||||||
|
46
README.md
46
README.md
@ -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)
|
[](LICENSE)
|
||||||
|
|
||||||
**中文** | [English](./README.en-US.md)
|
**中文** | [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)
|
[Nhấp vào đây để xem thiết kế UI](https://www.bilibili.com/video/BV17g411T7rq)
|
||||||
[点我查看快速开发教程](https://www.bilibili.com/video/BV1kg411v7QT)
|
[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)
|
[Nhấp vào đây để xem tài liệu 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 @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)
|
[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` và `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)
|
[MIT © 2020-present, pure-admin](./LICENSE)
|
||||||
|
147
locales/en.yaml
147
locales/en.yaml
@ -1,4 +1,5 @@
|
|||||||
buttons:
|
buttons:
|
||||||
|
pureAccountSettings: Account
|
||||||
pureLoginOut: LoginOut
|
pureLoginOut: LoginOut
|
||||||
pureLogin: Login
|
pureLogin: Login
|
||||||
pureOpenSystemSet: Open System Configs
|
pureOpenSystemSet: Open System Configs
|
||||||
@ -63,13 +64,124 @@ panel:
|
|||||||
menus:
|
menus:
|
||||||
pureHome: Home
|
pureHome: Home
|
||||||
pureLogin: Login
|
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
|
pureAbnormal: Abnormal Page
|
||||||
pureFourZeroFour: "404"
|
pureFourZeroFour: "404"
|
||||||
pureFourZeroOne: "403"
|
pureFourZeroOne: "403"
|
||||||
pureFive: "500"
|
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
|
purePermission: Permission Manage
|
||||||
purePermissionPage: Page Permission
|
purePermissionPage: Page Permission
|
||||||
purePermissionButton: Button 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:
|
status:
|
||||||
pureLoad: Loading...
|
pureLoad: Loading...
|
||||||
pureMessage: Message
|
pureMessage: Message
|
||||||
@ -81,9 +193,42 @@ status:
|
|||||||
login:
|
login:
|
||||||
pureUsername: Username
|
pureUsername: Username
|
||||||
purePassword: Password
|
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
|
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
|
pureLoginSuccess: Login Success
|
||||||
pureLoginFail: Login Fail
|
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
|
pureUsernameReg: Please enter username
|
||||||
purePassWordReg: Please enter password
|
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
238
locales/vi.yaml
Normal 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
|
@ -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位数字、字母、符号的任意两种组合
|
|
@ -45,6 +45,8 @@ export default defineFakeRoute([
|
|||||||
response: () => {
|
response: () => {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
msg: "success",
|
||||||
|
msgDev: null,
|
||||||
data: [permissionRouter]
|
data: [permissionRouter]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
14
package.json
14
package.json
@ -50,8 +50,13 @@
|
|||||||
"@pureadmin/descriptions": "^1.2.1",
|
"@pureadmin/descriptions": "^1.2.1",
|
||||||
"@pureadmin/table": "^3.1.2",
|
"@pureadmin/table": "^3.1.2",
|
||||||
"@pureadmin/utils": "^2.4.7",
|
"@pureadmin/utils": "^2.4.7",
|
||||||
|
"@vue-flow/background": "^1.3.0",
|
||||||
|
"@vue-flow/core": "^1.38.2",
|
||||||
"@vueuse/core": "^10.11.0",
|
"@vueuse/core": "^10.11.0",
|
||||||
"@vueuse/motion": "^2.2.3",
|
"@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",
|
"animate.css": "^4.1.1",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
@ -70,8 +75,15 @@
|
|||||||
"vue": "^3.4.31",
|
"vue": "^3.4.31",
|
||||||
"vue-i18n": "^9.13.1",
|
"vue-i18n": "^9.13.1",
|
||||||
"vue-router": "^4.4.0",
|
"vue-router": "^4.4.0",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"vue-tippy": "^6.4.4",
|
"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": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^19.3.0",
|
"@commitlint/cli": "^19.3.0",
|
||||||
|
875
pnpm-lock.yaml
generated
875
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@
|
|||||||
"HiddenSideBar": false,
|
"HiddenSideBar": false,
|
||||||
"MultiTagsCache": false,
|
"MultiTagsCache": false,
|
||||||
"KeepAlive": true,
|
"KeepAlive": true,
|
||||||
"Locale": "zh",
|
"Locale": "vi",
|
||||||
"Layout": "vertical",
|
"Layout": "vertical",
|
||||||
"Theme": "light",
|
"Theme": "light",
|
||||||
"DarkMode": false,
|
"DarkMode": false,
|
||||||
|
@ -10,7 +10,7 @@ import { defineComponent } from "vue";
|
|||||||
import { ElConfigProvider } from "element-plus";
|
import { ElConfigProvider } from "element-plus";
|
||||||
import { ReDialog } from "@/components/ReDialog";
|
import { ReDialog } from "@/components/ReDialog";
|
||||||
import en from "element-plus/es/locale/lang/en";
|
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({
|
export default defineComponent({
|
||||||
name: "app",
|
name: "app",
|
||||||
@ -20,7 +20,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentLocale() {
|
currentLocale() {
|
||||||
return this.$storage.locale?.locale === "zh" ? zhCn : en;
|
return this.$storage.locale?.locale === "vi" ? viVN : en;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,9 @@ import { http } from "@/utils/http";
|
|||||||
|
|
||||||
type Result = {
|
type Result = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data: Array<any>;
|
msg: string;
|
||||||
|
msgDev: string;
|
||||||
|
data: object;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAsyncRoutes = () => {
|
export const getAsyncRoutes = () => {
|
||||||
|
@ -3,19 +3,19 @@ import { http } from "@/utils/http";
|
|||||||
export type UserResult = {
|
export type UserResult = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data: {
|
data: {
|
||||||
/** 头像 */
|
/** Đường dẫn ảnh đại diện */
|
||||||
avatar: string;
|
avatar: string;
|
||||||
/** 用户名 */
|
/** Tên người dùng */
|
||||||
username: string;
|
username: string;
|
||||||
/** 昵称 */
|
/** Bút danh */
|
||||||
nickname: string;
|
nickname: string;
|
||||||
/** 当前登录用户的角色 */
|
/** Các vai trò của người dùng hiện tại */
|
||||||
roles: Array<string>;
|
roles: Array<string>;
|
||||||
/** `token` */
|
/** Mã thông báo truy cập */
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
/** 用于调用刷新`accessToken`的接口时所需的`token` */
|
/** Mã thông báo cần thiết để gọi API làm mới `accessToken` */
|
||||||
refreshToken: string;
|
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;
|
expires: Date;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -23,21 +23,21 @@ export type UserResult = {
|
|||||||
export type RefreshTokenResult = {
|
export type RefreshTokenResult = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data: {
|
data: {
|
||||||
/** `token` */
|
/** Mã thông báo truy cập */
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
/** 用于调用刷新`accessToken`的接口时所需的`token` */
|
/** Mã thông báo cần thiết để gọi API làm mới `accessToken` */
|
||||||
refreshToken: string;
|
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;
|
expires: Date;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 登录 */
|
/** Đăng nhập */
|
||||||
export const getLogin = (data?: object) => {
|
export const getLogin = (data?: object) => {
|
||||||
return http.request<UserResult>("post", "/login", { data });
|
return http.request<UserResult>("post", "/login", { data });
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 刷新`token` */
|
/** Làm mới `token` */
|
||||||
export const refreshTokenApi = (data?: object) => {
|
export const refreshTokenApi = (data?: object) => {
|
||||||
return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
|
return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ElCol } from "element-plus";
|
import { ElCol } from "element-plus";
|
||||||
import { h, defineComponent } from "vue";
|
import { h, defineComponent } from "vue";
|
||||||
|
|
||||||
// 封装element-plus的el-col组件
|
// Bao bọc thành phần ElCol của element-plus
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "ReCol",
|
name: "ReCol",
|
||||||
props: {
|
props: {
|
||||||
|
2
src/components/ReCountTo/README.md
Normal file
2
src/components/ReCountTo/README.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
normal 普通数字动画组件
|
||||||
|
rebound 回弹式数字动画组件
|
11
src/components/ReCountTo/index.ts
Normal file
11
src/components/ReCountTo/index.ts
Normal 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 };
|
179
src/components/ReCountTo/src/normal/index.tsx
Normal file
179
src/components/ReCountTo/src/normal/index.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
32
src/components/ReCountTo/src/normal/props.ts
Normal file
32
src/components/ReCountTo/src/normal/props.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
72
src/components/ReCountTo/src/rebound/index.tsx
Normal file
72
src/components/ReCountTo/src/rebound/index.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
15
src/components/ReCountTo/src/rebound/props.ts
Normal file
15
src/components/ReCountTo/src/rebound/props.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
77
src/components/ReCountTo/src/rebound/rebound.css
Normal file
77
src/components/ReCountTo/src/rebound/rebound.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,7 @@ import type {
|
|||||||
|
|
||||||
const dialogStore = ref<Array<DialogOptions>>([]);
|
const dialogStore = ref<Array<DialogOptions>>([]);
|
||||||
|
|
||||||
/** 打开弹框 */
|
/** Mở hộp thoại */
|
||||||
const addDialog = (options: DialogOptions) => {
|
const addDialog = (options: DialogOptions) => {
|
||||||
const open = () =>
|
const open = () =>
|
||||||
dialogStore.value.push(Object.assign(options, { visible: true }));
|
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) => {
|
const closeDialog = (options: DialogOptions, index: number, args?: any) => {
|
||||||
dialogStore.value[index].visible = false;
|
dialogStore.value[index].visible = false;
|
||||||
options.closeCallBack && options.closeCallBack({ options, index, args });
|
options.closeCallBack && options.closeCallBack({ options, index, args });
|
||||||
@ -37,21 +37,21 @@ const closeDialog = (options: DialogOptions, index: number, args?: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 更改弹框自身属性值
|
* @description Thay đổi giá trị thuộc tính của hộp thoại
|
||||||
* @param value 属性值
|
* @param value Giá trị thuộc tính
|
||||||
* @param key 属性,默认`title`
|
* @param key Thuộc tính (mặc định là `title`)
|
||||||
* @param index 弹框索引(默认`0`,代表只有一个弹框,对于嵌套弹框要改哪个弹框的属性值就把该弹框索引赋给`index`)
|
* @param index Chỉ số của hộp thoại (mặc định là `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) => {
|
const updateDialog = (value: any, key = "title", index = 0) => {
|
||||||
dialogStore.value[index][key] = value;
|
dialogStore.value[index][key] = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 关闭所有弹框 */
|
/** Đóng tất cả hộp thoại */
|
||||||
const closeAllDialog = () => {
|
const closeAllDialog = () => {
|
||||||
dialogStore.value = [];
|
dialogStore.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 千万别忘了在下面这三处引入并注册下,放心注册,不使用`addDialog`调用就不会被挂载
|
/** Chắc chắn phải nhập và đăng ký ở ba dưới đây, yên tâm đăng ký, không sử dụng `addDialog` thì sẽ không bị gắn 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#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#L12
|
||||||
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L22
|
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L22
|
||||||
|
@ -23,7 +23,7 @@ const footerButtons = computed(() => {
|
|||||||
? options.footerButtons
|
? options.footerButtons
|
||||||
: ([
|
: ([
|
||||||
{
|
{
|
||||||
label: "取消",
|
label: "Hủy",
|
||||||
text: true,
|
text: true,
|
||||||
bg: true,
|
bg: true,
|
||||||
btnClick: ({ dialog: { options, index } }) => {
|
btnClick: ({ dialog: { options, index } }) => {
|
||||||
@ -37,7 +37,7 @@ const footerButtons = computed(() => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "确定",
|
label: "OK",
|
||||||
type: "primary",
|
type: "primary",
|
||||||
text: true,
|
text: true,
|
||||||
bg: true,
|
bg: true,
|
||||||
|
39
src/components/ReFlicker/index.css
Normal file
39
src/components/ReFlicker/index.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
44
src/components/ReFlicker/index.ts
Normal file
44
src/components/ReFlicker/index.ts
Normal 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 传0为方形、传50%或者不传为圆形
|
||||||
|
* @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: () => []
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -2,11 +2,11 @@ import iconifyIconOffline from "./src/iconifyIconOffline";
|
|||||||
import iconifyIconOnline from "./src/iconifyIconOnline";
|
import iconifyIconOnline from "./src/iconifyIconOnline";
|
||||||
import fontIcon from "./src/iconfont";
|
import fontIcon from "./src/iconfont";
|
||||||
|
|
||||||
/** 本地图标组件 */
|
/** icon offline*/
|
||||||
const IconifyIconOffline = iconifyIconOffline;
|
const IconifyIconOffline = iconifyIconOffline;
|
||||||
/** 在线图标组件 */
|
/** icon online */
|
||||||
const IconifyIconOnline = iconifyIconOnline;
|
const IconifyIconOnline = iconifyIconOnline;
|
||||||
/** `iconfont`组件 */
|
/** `iconfont` */
|
||||||
const FontIcon = fontIcon;
|
const FontIcon = fontIcon;
|
||||||
|
|
||||||
export { IconifyIconOffline, IconifyIconOnline, FontIcon };
|
export { IconifyIconOffline, IconifyIconOnline, FontIcon };
|
||||||
|
@ -3,16 +3,16 @@ import { h, defineComponent, type Component } from "vue";
|
|||||||
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
|
import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支持 `iconfont`、自定义 `svg` 以及 `iconify` 中所有的图标
|
* Hỗ trợ `iconfont`, `svg` tùy chỉnh và tất cả các biểu tượng trong `iconify`
|
||||||
* @see 点击查看文档图标篇 {@link https://pure-admin.github.io/pure-admin-doc/pages/icon/}
|
* @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 必传 图标
|
* @param icon Bắt buộc Biểu tượng
|
||||||
* @param attrs 可选 iconType 属性
|
* @param attrs Tùy chọn Các thuộc tính loại iconType
|
||||||
* @returns Component
|
* @returns Component
|
||||||
*/
|
*/
|
||||||
export function useRenderIcon(icon: any, attrs?: iconType): Component {
|
export function useRenderIcon(icon: any, attrs?: iconType): Component {
|
||||||
// iconfont
|
// iconfont
|
||||||
const ifReg = /^IF-/;
|
const ifReg = /^IF-/;
|
||||||
// typeof icon === "function" 属于SVG
|
// typeof icon === "function" belongs to SVG
|
||||||
if (ifReg.test(icon)) {
|
if (ifReg.test(icon)) {
|
||||||
// iconfont
|
// iconfont
|
||||||
const name = icon.split(ifReg)[1];
|
const name = icon.split(ifReg)[1];
|
||||||
@ -45,7 +45,7 @@ export function useRenderIcon(icon: any, attrs?: iconType): Component {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} 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({
|
return defineComponent({
|
||||||
name: "Icon",
|
name: "Icon",
|
||||||
render() {
|
render() {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { h, defineComponent } from "vue";
|
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({
|
export default defineComponent({
|
||||||
name: "FontIcon",
|
name: "FontIcon",
|
||||||
props: {
|
props: {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { h, defineComponent } from "vue";
|
import { h, defineComponent } from "vue";
|
||||||
import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline";
|
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({
|
export default defineComponent({
|
||||||
name: "IconifyIconOffline",
|
name: "IconifyIconOffline",
|
||||||
components: { IconifyIcon },
|
components: { IconifyIcon },
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { h, defineComponent } from "vue";
|
import { h, defineComponent } from "vue";
|
||||||
import { Icon as IconifyIcon } from "@iconify/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({
|
export default defineComponent({
|
||||||
name: "IconifyIconOnline",
|
name: "IconifyIconOnline",
|
||||||
components: { IconifyIcon },
|
components: { IconifyIcon },
|
||||||
|
@ -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";
|
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
|
// @iconify-icons/ep
|
||||||
import Lollipop from "@iconify-icons/ep/lollipop";
|
import Lollipop from "@iconify-icons/ep/lollipop";
|
||||||
import HomeFilled from "@iconify-icons/ep/home-filled";
|
import HomeFilled from "@iconify-icons/ep/home-filled";
|
||||||
addIcon("ep:lollipop", Lollipop);
|
addIcon("ep:lollipop", Lollipop);
|
||||||
addIcon("ep:home-filled", HomeFilled);
|
addIcon("ep:home-filled", HomeFilled);
|
||||||
|
|
||||||
// @iconify-icons/ri
|
// @iconify-icons/ri
|
||||||
import Search from "@iconify-icons/ri/search-line";
|
import Search from "@iconify-icons/ri/search-line";
|
||||||
import InformationLine from "@iconify-icons/ri/information-line";
|
import InformationLine from "@iconify-icons/ri/information-line";
|
||||||
|
@ -13,7 +13,7 @@ export interface iconType {
|
|||||||
align?: string;
|
align?: string;
|
||||||
onLoad?: Function;
|
onLoad?: Function;
|
||||||
includes?: 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;
|
fill?: string;
|
||||||
// all icon
|
// all icon
|
||||||
style?: object;
|
style?: object;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import pureTableBar from "./src/bar";
|
import pureTableBar from "./src/bar";
|
||||||
import { withInstall } from "@pureadmin/utils";
|
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);
|
export const PureTableBar = withInstall(pureTableBar);
|
||||||
|
@ -25,24 +25,26 @@ import SettingIcon from "@/assets/table-bar/settings.svg?component";
|
|||||||
import CollapseIcon from "@/assets/table-bar/collapse.svg?component";
|
import CollapseIcon from "@/assets/table-bar/collapse.svg?component";
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
/** 头部最左边的标题 */
|
/** Tiêu đề ở phía trên bên trái của bảng */
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
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: {
|
tableRef: {
|
||||||
type: Object as PropType<any>
|
type: Object as PropType<any>
|
||||||
},
|
},
|
||||||
/** 需要展示的列 */
|
/** Các cột cần hiển thị */
|
||||||
columns: {
|
columns: {
|
||||||
type: Array as PropType<TableColumnList>,
|
type: Array as PropType<TableColumnList>,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
/** Cho biết có mở rộng tất cả các hàng ban đầu hay không */
|
||||||
isExpandAll: {
|
isExpandAll: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
/** Khóa duy nhất cho bảng */
|
||||||
tableKey: {
|
tableKey: {
|
||||||
type: [String, Number] as PropType<string | number>,
|
type: [String, Number] as PropType<string | number>,
|
||||||
default: "0"
|
default: "0"
|
||||||
@ -161,25 +163,25 @@ export default defineComponent({
|
|||||||
style={getDropdownItemStyle.value("large")}
|
style={getDropdownItemStyle.value("large")}
|
||||||
onClick={() => (size.value = "large")}
|
onClick={() => (size.value = "large")}
|
||||||
>
|
>
|
||||||
宽松
|
Lỏng lẻo
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item
|
<el-dropdown-item
|
||||||
style={getDropdownItemStyle.value("default")}
|
style={getDropdownItemStyle.value("default")}
|
||||||
onClick={() => (size.value = "default")}
|
onClick={() => (size.value = "default")}
|
||||||
>
|
>
|
||||||
默认
|
Mặc định
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item
|
<el-dropdown-item
|
||||||
style={getDropdownItemStyle.value("small")}
|
style={getDropdownItemStyle.value("small")}
|
||||||
onClick={() => (size.value = "small")}
|
onClick={() => (size.value = "small")}
|
||||||
>
|
>
|
||||||
紧凑
|
Gọn nhẹ
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 列展示拖拽排序 */
|
/** Cột hiển thị kéo và thả sắp xếp */
|
||||||
const rowDrop = (event: { preventDefault: () => void }) => {
|
const rowDrop = (event: { preventDefault: () => void }) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
@ -195,7 +197,7 @@ export default defineComponent({
|
|||||||
const oldColumn = dynamicColumns.value[oldIndex];
|
const oldColumn = dynamicColumns.value[oldIndex];
|
||||||
const newColumn = dynamicColumns.value[newIndex];
|
const newColumn = dynamicColumns.value[newIndex];
|
||||||
if (oldColumn?.fixed || newColumn?.fixed) {
|
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;
|
const oldThElem = wrapperElem.children[oldIndex] as HTMLElement;
|
||||||
if (newIndex > oldIndex) {
|
if (newIndex > oldIndex) {
|
||||||
wrapperElem.insertBefore(targetThElem, oldThElem);
|
wrapperElem.insertBefore(targetThElem, oldThElem);
|
||||||
@ -237,7 +239,7 @@ export default defineComponent({
|
|||||||
reference: () => (
|
reference: () => (
|
||||||
<SettingIcon
|
<SettingIcon
|
||||||
class={["w-[16px]", iconClass.value]}
|
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)"
|
transform: isExpandAll.value ? "none" : "rotate(-90deg)"
|
||||||
}}
|
}}
|
||||||
v-tippy={rendTippyProps(
|
v-tippy={rendTippyProps(
|
||||||
isExpandAll.value ? "折叠" : "展开"
|
isExpandAll.value ? "Thu nhỏ" : "Mở rộng"
|
||||||
)}
|
)}
|
||||||
onClick={() => onExpand()}
|
onClick={() => onExpand()}
|
||||||
/>
|
/>
|
||||||
@ -276,14 +278,14 @@ export default defineComponent({
|
|||||||
iconClass.value,
|
iconClass.value,
|
||||||
loading.value ? "animate-spin" : ""
|
loading.value ? "animate-spin" : ""
|
||||||
]}
|
]}
|
||||||
v-tippy={rendTippyProps("刷新")}
|
v-tippy={rendTippyProps("Đặt lại")}
|
||||||
onClick={() => onReFresh()}
|
onClick={() => onReFresh()}
|
||||||
/>
|
/>
|
||||||
<el-divider direction="vertical" />
|
<el-divider direction="vertical" />
|
||||||
<el-dropdown
|
<el-dropdown
|
||||||
v-slots={dropdown}
|
v-slots={dropdown}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
v-tippy={rendTippyProps("密度")}
|
v-tippy={rendTippyProps("Tỉ trọng")}
|
||||||
>
|
>
|
||||||
<CollapseIcon class={["w-[16px]", iconClass.value]} />
|
<CollapseIcon class={["w-[16px]", iconClass.value]} />
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
@ -299,13 +301,13 @@ export default defineComponent({
|
|||||||
<div class={[topClass.value]}>
|
<div class={[topClass.value]}>
|
||||||
<el-checkbox
|
<el-checkbox
|
||||||
class="!-mr-1"
|
class="!-mr-1"
|
||||||
label="列展示"
|
label="Hiển thị cột"
|
||||||
v-model={checkAll.value}
|
v-model={checkAll.value}
|
||||||
indeterminate={isIndeterminate.value}
|
indeterminate={isIndeterminate.value}
|
||||||
onChange={value => handleCheckAllChange(value)}
|
onChange={value => handleCheckAllChange(value)}
|
||||||
/>
|
/>
|
||||||
<el-button type="primary" link onClick={() => onReset()}>
|
<el-button type="primary" link onClick={() => onReset()}>
|
||||||
重置
|
Đặt lại
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import reSegmented from "./src/index";
|
import reSegmented from "./src/index";
|
||||||
import { withInstall } from "@pureadmin/utils";
|
import { withInstall } from "@pureadmin/utils";
|
||||||
|
|
||||||
/** 分段控制器组件 */
|
/** Thành phần điều khiển phân đoạn */
|
||||||
export const ReSegmented = withInstall(reSegmented);
|
export const ReSegmented = withInstall(reSegmented);
|
||||||
|
|
||||||
export default ReSegmented;
|
export default ReSegmented;
|
||||||
|
@ -23,27 +23,27 @@ const props = {
|
|||||||
type: Array<OptionsType>,
|
type: Array<OptionsType>,
|
||||||
default: () => []
|
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: {
|
modelValue: {
|
||||||
type: undefined,
|
type: undefined,
|
||||||
require: false,
|
require: false,
|
||||||
default: "0"
|
default: "0"
|
||||||
},
|
},
|
||||||
/** 将宽度调整为父元素宽度 */
|
/** Thay đổi chiều rộng để phù hợp với chiều rộng của phần tử cha */
|
||||||
block: {
|
block: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
/** 控件尺寸 */
|
/** Kích thước của điều khiển */
|
||||||
size: {
|
size: {
|
||||||
type: String as PropType<"small" | "default" | "large">
|
type: String as PropType<"small" | "default" | "large">
|
||||||
},
|
},
|
||||||
/** 是否全局禁用,默认 `false` */
|
/** Vô hiệu hóa toàn cầu, mặc định `false` */
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
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: {
|
resize: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
@ -2,19 +2,19 @@ import type { VNode, Component } from "vue";
|
|||||||
import type { iconType } from "@/components/ReIcon/src/types.ts";
|
import type { iconType } from "@/components/ReIcon/src/types.ts";
|
||||||
|
|
||||||
export interface OptionsType {
|
export interface OptionsType {
|
||||||
/** 文字 */
|
/** Nhãn */
|
||||||
label?: string | (() => VNode | Component);
|
label?: string | (() => VNode | Component);
|
||||||
/**
|
/**
|
||||||
* @description 图标,采用平台内置的 `useRenderIcon` 函数渲染
|
* @description Biểu tượng, được render bằng hàm `useRenderIcon` được tích hợp trong nền tảng
|
||||||
* @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 }
|
* @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;
|
icon?: string | Component;
|
||||||
/** 图标属性、样式配置 */
|
/** Thuộc tính và style của biểu tượng */
|
||||||
iconAttrs?: iconType;
|
iconAttrs?: iconType;
|
||||||
/** 值 */
|
/** Giá trị */
|
||||||
value?: any;
|
value?: any;
|
||||||
/** 是否禁用 */
|
/** Đã bị vô hiệu hóa hay chưa */
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
/** `tooltip` 提示 */
|
/** Chú thích tooltip */
|
||||||
tip?: string;
|
tip?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import reText from "./src/index.vue";
|
import reText from "./src/index.vue";
|
||||||
import { withInstall } from "@pureadmin/utils";
|
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 const ReText = withInstall(reText);
|
||||||
|
|
||||||
export default ReText;
|
export default ReText;
|
||||||
|
@ -7,7 +7,7 @@ defineOptions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
// 行数
|
// Số dòng
|
||||||
lineClamp: {
|
lineClamp: {
|
||||||
type: [String, Number]
|
type: [String, Number]
|
||||||
},
|
},
|
||||||
@ -24,10 +24,10 @@ const tippyFunc = ref();
|
|||||||
|
|
||||||
const isTextEllipsis = (el: HTMLElement) => {
|
const isTextEllipsis = (el: HTMLElement) => {
|
||||||
if (!props.lineClamp) {
|
if (!props.lineClamp) {
|
||||||
// 单行省略判断
|
// Kiểm tra có dòng đơn bị cắt ngắn
|
||||||
return el.scrollWidth > el.clientWidth;
|
return el.scrollWidth > el.clientWidth;
|
||||||
} else {
|
} else {
|
||||||
// 多行省略判断
|
// Kiểm tra có dòng nhiều bị cắt ngắn
|
||||||
return el.scrollHeight > el.clientHeight;
|
return el.scrollHeight > el.clientHeight;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -26,7 +26,7 @@ const getConfig = (key?: string): PlatformConfigs => {
|
|||||||
return config;
|
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> => {
|
export const getPlatformConfig = async (app: App): Promise<undefined> => {
|
||||||
app.config.globalProperties.$config = getConfig();
|
app.config.globalProperties.$config = getConfig();
|
||||||
return axios({
|
return axios({
|
||||||
@ -35,21 +35,21 @@ export const getPlatformConfig = async (app: App): Promise<undefined> => {
|
|||||||
})
|
})
|
||||||
.then(({ data: config }) => {
|
.then(({ data: config }) => {
|
||||||
let $config = app.config.globalProperties.$config;
|
let $config = app.config.globalProperties.$config;
|
||||||
// 自动注入系统配置
|
// Tự động chèn cấu hình hệ thống
|
||||||
if (app && $config && typeof config === "object") {
|
if (app && $config && typeof config === "object") {
|
||||||
$config = Object.assign($config, config);
|
$config = Object.assign($config, config);
|
||||||
app.config.globalProperties.$config = $config;
|
app.config.globalProperties.$config = $config;
|
||||||
// 设置全局配置
|
// Đặt cấu hình toàn cầu
|
||||||
setConfig($config);
|
setConfig($config);
|
||||||
}
|
}
|
||||||
return $config;
|
return $config;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.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;
|
const responsiveStorageNameSpace = () => getConfig().ResponsiveStorageNameSpace;
|
||||||
|
|
||||||
export { getConfig, setConfig, responsiveStorageNameSpace };
|
export { getConfig, setConfig, responsiveStorageNameSpace };
|
||||||
|
@ -7,23 +7,23 @@ export interface CopyEl extends HTMLElement {
|
|||||||
copyValue: string;
|
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 = {
|
export const copy: Directive = {
|
||||||
mounted(el: CopyEl, binding: DirectiveBinding<string>) {
|
mounted(el: CopyEl, binding: DirectiveBinding<string>) {
|
||||||
const { value } = binding;
|
const { value } = binding;
|
||||||
if (value) {
|
if (value) {
|
||||||
el.copyValue = value;
|
el.copyValue = value;
|
||||||
const arg = binding.arg ?? "dblclick";
|
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, () => {
|
useEventListener(el, arg, () => {
|
||||||
const success = copyTextToClipboard(el.copyValue);
|
const success = copyTextToClipboard(el.copyValue);
|
||||||
success
|
success
|
||||||
? message("复制成功", { type: "success" })
|
? message("Sao chép thành công", { type: "success" })
|
||||||
: message("复制失败", { type: "error" });
|
: message("Sao chép thất bại", { type: "error" });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'[Directive: copy]: need value! Like v-copy="modelValue"'
|
'[Directive: copy]: cần giá trị! Ví dụ: v-copy="modelValue"'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -9,19 +9,19 @@ import { useEventListener } from "@vueuse/core";
|
|||||||
import type { Directive, DirectiveBinding } from "vue";
|
import type { Directive, DirectiveBinding } from "vue";
|
||||||
|
|
||||||
export interface OptimizeOptions {
|
export interface OptimizeOptions {
|
||||||
/** 事件名 */
|
/** Tên sự kiện */
|
||||||
event: string;
|
event: string;
|
||||||
/** 事件触发的方法 */
|
/** Phương thức gọi sự kiện */
|
||||||
fn: (...params: any) => any;
|
fn: (...params: any) => any;
|
||||||
/** 是否立即执行 */
|
/** Có thực thi ngay lập tức hay không */
|
||||||
immediate?: boolean;
|
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;
|
timeout?: number;
|
||||||
/** 传递的参数 */
|
/** Tham số truyền vào */
|
||||||
params?: any;
|
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 = {
|
export const optimize: Directive = {
|
||||||
mounted(el: HTMLElement, binding: DirectiveBinding<OptimizeOptions>) {
|
mounted(el: HTMLElement, binding: DirectiveBinding<OptimizeOptions>) {
|
||||||
const { value } = binding;
|
const { value } = binding;
|
||||||
@ -35,11 +35,11 @@ export const optimize: Directive = {
|
|||||||
params = isObject(params) ? Array.of(params) : params;
|
params = isObject(params) ? Array.of(params) : params;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
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(
|
useEventListener(
|
||||||
el,
|
el,
|
||||||
value.event,
|
value.event,
|
||||||
@ -56,12 +56,12 @@ export const optimize: Directive = {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
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 {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"[Directive: optimize]: only `debounce` and `throttle` are supported"
|
"[Chỉ thị: optimize]: chỉ hỗ trợ `debounce` và `throttle`"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,7 +189,7 @@ const transitionMain = defineComponent({
|
|||||||
</template>
|
</template>
|
||||||
</router-view>
|
</router-view>
|
||||||
|
|
||||||
<!-- 页脚 -->
|
<!-- Chân trang -->
|
||||||
<LayFooter v-if="!hideFooter && !fixedHeader" />
|
<LayFooter v-if="!hideFooter && !fixedHeader" />
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
@ -48,9 +48,9 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
|
|||||||
<LayNavMix v-if="layout === 'mix'" />
|
<LayNavMix v-if="layout === 'mix'" />
|
||||||
|
|
||||||
<div v-if="layout === 'vertical'" class="vertical-header-right">
|
<div v-if="layout === 'vertical'" class="vertical-header-right">
|
||||||
<!-- 菜单搜索 -->
|
<!-- Menu Search -->
|
||||||
<LaySearch id="header-search" />
|
<LaySearch id="header-search" />
|
||||||
<!-- 国际化 -->
|
<!-- Globalization -->
|
||||||
<el-dropdown id="header-translation" trigger="click">
|
<el-dropdown id="header-translation" trigger="click">
|
||||||
<GlobalizationIcon
|
<GlobalizationIcon
|
||||||
class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none"
|
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>
|
<template #dropdown>
|
||||||
<el-dropdown-menu class="translation">
|
<el-dropdown-menu class="translation">
|
||||||
<el-dropdown-item
|
<el-dropdown-item
|
||||||
:style="getDropdownItemStyle(locale, 'zh')"
|
:style="getDropdownItemStyle(locale, 'vi')"
|
||||||
:class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]"
|
:class="['dark:!text-white', getDropdownItemClass(locale, 'vi')]"
|
||||||
@click="translationCh"
|
@click="translationCh"
|
||||||
>
|
>
|
||||||
<IconifyIconOffline
|
<IconifyIconOffline
|
||||||
v-show="locale === 'zh'"
|
v-show="locale === 'vi'"
|
||||||
class="check-zh"
|
class="check-vi"
|
||||||
:icon="Check"
|
:icon="Check"
|
||||||
/>
|
/>
|
||||||
简体中文
|
Tiếng Việt
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item
|
<el-dropdown-item
|
||||||
:style="getDropdownItemStyle(locale, 'en')"
|
:style="getDropdownItemStyle(locale, 'en')"
|
||||||
@ -82,11 +82,11 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
|
|||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
<!-- 全屏 -->
|
<!-- Full Screen -->
|
||||||
<LaySidebarFullScreen id="full-screen" />
|
<LaySidebarFullScreen id="full-screen" />
|
||||||
<!-- 消息通知 -->
|
<!-- Notification -->
|
||||||
<LayNotice id="header-notice" />
|
<LayNotice id="header-notice" />
|
||||||
<!-- 退出登录 -->
|
<!-- Logout -->
|
||||||
<el-dropdown trigger="click">
|
<el-dropdown trigger="click">
|
||||||
<span class="el-dropdown-link navbar-bg-hover select-none">
|
<span class="el-dropdown-link navbar-bg-hover select-none">
|
||||||
<img :src="userAvatar" :style="avatarsStyle" />
|
<img :src="userAvatar" :style="avatarsStyle" />
|
||||||
@ -168,7 +168,7 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
|
|||||||
padding: 5px 40px;
|
padding: 5px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-zh {
|
.check-vi {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
}
|
}
|
||||||
|
@ -65,9 +65,9 @@ nextTick(() => {
|
|||||||
/>
|
/>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
<div class="horizontal-header-right">
|
<div class="horizontal-header-right">
|
||||||
<!-- 菜单搜索 -->
|
<!-- Menu Search -->
|
||||||
<LaySearch id="header-search" />
|
<LaySearch id="header-search" />
|
||||||
<!-- 国际化 -->
|
<!-- Globalization -->
|
||||||
<el-dropdown id="header-translation" trigger="click">
|
<el-dropdown id="header-translation" trigger="click">
|
||||||
<GlobalizationIcon
|
<GlobalizationIcon
|
||||||
class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none"
|
class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none"
|
||||||
@ -75,14 +75,14 @@ nextTick(() => {
|
|||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu class="translation">
|
<el-dropdown-menu class="translation">
|
||||||
<el-dropdown-item
|
<el-dropdown-item
|
||||||
:style="getDropdownItemStyle(locale, 'zh')"
|
:style="getDropdownItemStyle(locale, 'vi')"
|
||||||
:class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]"
|
:class="['dark:!text-white', getDropdownItemClass(locale, 'vi')]"
|
||||||
@click="translationCh"
|
@click="translationCh"
|
||||||
>
|
>
|
||||||
<span v-show="locale === 'zh'" class="check-zh">
|
<span v-show="locale === 'vi'" class="check-vi">
|
||||||
<IconifyIconOffline :icon="Check" />
|
<IconifyIconOffline :icon="Check" />
|
||||||
</span>
|
</span>
|
||||||
简体中文
|
Tiếng Việt
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item
|
<el-dropdown-item
|
||||||
:style="getDropdownItemStyle(locale, 'en')"
|
:style="getDropdownItemStyle(locale, 'en')"
|
||||||
@ -97,11 +97,11 @@ nextTick(() => {
|
|||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
<!-- 全屏 -->
|
<!-- Full Screen -->
|
||||||
<LaySidebarFullScreen id="full-screen" />
|
<LaySidebarFullScreen id="full-screen" />
|
||||||
<!-- 消息通知 -->
|
<!-- Notification -->
|
||||||
<LayNotice id="header-notice" />
|
<LayNotice id="header-notice" />
|
||||||
<!-- 退出登录 -->
|
<!-- Logout -->
|
||||||
<el-dropdown trigger="click">
|
<el-dropdown trigger="click">
|
||||||
<span class="el-dropdown-link navbar-bg-hover">
|
<span class="el-dropdown-link navbar-bg-hover">
|
||||||
<img :src="userAvatar" :style="avatarsStyle" />
|
<img :src="userAvatar" :style="avatarsStyle" />
|
||||||
@ -140,7 +140,7 @@ nextTick(() => {
|
|||||||
padding: 5px 40px;
|
padding: 5px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-zh {
|
.check-vi {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ const {
|
|||||||
|
|
||||||
function getDefaultActive(routePath) {
|
function getDefaultActive(routePath) {
|
||||||
const wholeMenus = usePermissionStoreHook().wholeMenus;
|
const wholeMenus = usePermissionStoreHook().wholeMenus;
|
||||||
/** 当前路由的父级路径 */
|
/** Current route's parent paths */
|
||||||
const parentRoutes = getParentPaths(routePath, wholeMenus)[0];
|
const parentRoutes = getParentPaths(routePath, wholeMenus)[0];
|
||||||
defaultActive.value = !isAllEmpty(route.meta?.activePath)
|
defaultActive.value = !isAllEmpty(route.meta?.activePath)
|
||||||
? route.meta.activePath
|
? route.meta.activePath
|
||||||
@ -99,9 +99,9 @@ watch(
|
|||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
<div class="horizontal-header-right">
|
<div class="horizontal-header-right">
|
||||||
<!-- 菜单搜索 -->
|
<!-- Menu Search -->
|
||||||
<LaySearch id="header-search" />
|
<LaySearch id="header-search" />
|
||||||
<!-- 国际化 -->
|
<!-- Globalization -->
|
||||||
<el-dropdown id="header-translation" trigger="click">
|
<el-dropdown id="header-translation" trigger="click">
|
||||||
<GlobalizationIcon
|
<GlobalizationIcon
|
||||||
class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none"
|
class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none"
|
||||||
@ -109,14 +109,14 @@ watch(
|
|||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu class="translation">
|
<el-dropdown-menu class="translation">
|
||||||
<el-dropdown-item
|
<el-dropdown-item
|
||||||
:style="getDropdownItemStyle(locale, 'zh')"
|
:style="getDropdownItemStyle(locale, 'vi')"
|
||||||
:class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]"
|
:class="['dark:!text-white', getDropdownItemClass(locale, 'vi')]"
|
||||||
@click="translationCh"
|
@click="translationCh"
|
||||||
>
|
>
|
||||||
<span v-show="locale === 'zh'" class="check-zh">
|
<span v-show="locale === 'vi'" class="check-vi">
|
||||||
<IconifyIconOffline :icon="Check" />
|
<IconifyIconOffline :icon="Check" />
|
||||||
</span>
|
</span>
|
||||||
简体中文
|
Tiếng Việt
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item
|
<el-dropdown-item
|
||||||
:style="getDropdownItemStyle(locale, 'en')"
|
:style="getDropdownItemStyle(locale, 'en')"
|
||||||
@ -131,11 +131,11 @@ watch(
|
|||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
<!-- 全屏 -->
|
<!-- Full Screen -->
|
||||||
<LaySidebarFullScreen id="full-screen" />
|
<LaySidebarFullScreen id="full-screen" />
|
||||||
<!-- 消息通知 -->
|
<!-- Notification -->
|
||||||
<LayNotice id="header-notice" />
|
<LayNotice id="header-notice" />
|
||||||
<!-- 退出登录 -->
|
<!-- Logout -->
|
||||||
<el-dropdown trigger="click">
|
<el-dropdown trigger="click">
|
||||||
<span class="el-dropdown-link navbar-bg-hover select-none">
|
<span class="el-dropdown-link navbar-bg-hover select-none">
|
||||||
<img :src="userAvatar" :style="avatarsStyle" />
|
<img :src="userAvatar" :style="avatarsStyle" />
|
||||||
@ -174,7 +174,7 @@ watch(
|
|||||||
padding: 5px 40px;
|
padding: 5px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-zh {
|
.check-vi {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
}
|
}
|
||||||
|
@ -18,21 +18,21 @@ import {
|
|||||||
export function useDataThemeChange() {
|
export function useDataThemeChange() {
|
||||||
const { layoutTheme, layout } = useLayout();
|
const { layoutTheme, layout } = useLayout();
|
||||||
const themeColors = ref<Array<themeColorsType>>([
|
const themeColors = ref<Array<themeColorsType>>([
|
||||||
/* 亮白色 */
|
/* Màu sáng trắng */
|
||||||
{ color: "#ffffff", themeColor: "light" },
|
{ color: "#ffffff", themeColor: "light" },
|
||||||
/* 道奇蓝 */
|
/* Xanh Đậm */
|
||||||
{ color: "#1b2a47", themeColor: "default" },
|
{ color: "#1b2a47", themeColor: "default" },
|
||||||
/* 深紫罗兰色 */
|
/* Tím Đậm */
|
||||||
{ color: "#722ed1", themeColor: "saucePurple" },
|
{ color: "#722ed1", themeColor: "saucePurple" },
|
||||||
/* 深粉色 */
|
/* Hồng Đậm */
|
||||||
{ color: "#eb2f96", themeColor: "pink" },
|
{ color: "#eb2f96", themeColor: "pink" },
|
||||||
/* 猩红色 */
|
/* Đỏ Lửa */
|
||||||
{ color: "#f5222d", themeColor: "dusk" },
|
{ color: "#f5222d", themeColor: "dusk" },
|
||||||
/* 橙红色 */
|
/* Cam Đỏ */
|
||||||
{ color: "#fa541c", themeColor: "volcano" },
|
{ color: "#fa541c", themeColor: "volcano" },
|
||||||
/* 绿宝石 */
|
/* Ngọc Lục Bảo */
|
||||||
{ color: "#13c2c2", themeColor: "mingQing" },
|
{ color: "#13c2c2", themeColor: "mingQing" },
|
||||||
/* 酸橙绿 */
|
/* Xanh Lục Giác */
|
||||||
{ color: "#52c41a", themeColor: "auroraGreen" }
|
{ color: "#52c41a", themeColor: "auroraGreen" }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ export function useDataThemeChange() {
|
|||||||
targetEl.className = flag ? `${className} ${clsName}` : className;
|
targetEl.className = flag ? `${className} ${clsName}` : className;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 设置导航主题色 */
|
/** Set màu chủ đề điều hướng */
|
||||||
function setLayoutThemeColor(
|
function setLayoutThemeColor(
|
||||||
theme = getConfig().Theme ?? "light",
|
theme = getConfig().Theme ?? "light",
|
||||||
isClick = true
|
isClick = true
|
||||||
@ -57,7 +57,7 @@ export function useDataThemeChange() {
|
|||||||
toggleTheme({
|
toggleTheme({
|
||||||
scopeName: `layout-theme-${theme}`
|
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;
|
const storageThemeColor = $storage.layout.themeColor;
|
||||||
$storage.layout = {
|
$storage.layout = {
|
||||||
layout: layout.value,
|
layout: layout.value,
|
||||||
@ -84,7 +84,7 @@ export function useDataThemeChange() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 设置 `element-plus` 主题色 */
|
/** Set màu chủ đề `element-plus` */
|
||||||
const setEpThemeColor = (color: string) => {
|
const setEpThemeColor = (color: string) => {
|
||||||
useEpThemeStoreHook().setEpThemeColor(color);
|
useEpThemeStoreHook().setEpThemeColor(color);
|
||||||
document.documentElement.style.setProperty("--el-color-primary", 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) {
|
function dataThemeChange(overall?: string) {
|
||||||
overallStyle.value = overall;
|
overallStyle.value = overall;
|
||||||
if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) {
|
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() {
|
function onReset() {
|
||||||
removeToken();
|
removeToken();
|
||||||
storageLocal().clear();
|
storageLocal().clear();
|
||||||
|
68
src/main.ts
68
src/main.ts
@ -1,37 +1,35 @@
|
|||||||
import App from "./App.vue";
|
import App from "./App.vue"; // Nhập component App.vue chính của ứng dụng
|
||||||
import router from "./router";
|
import router from "./router"; // Nhập định tuyến của Vue Router
|
||||||
import { setupStore } from "@/store";
|
import { setupStore } from "@/store"; // Sử dụng hàm setupStore từ module store
|
||||||
import { useI18n } from "@/plugins/i18n";
|
import { useI18n } from "@/plugins/i18n"; // Sử dụng plugin i18n cho quản lý ngôn ngữ
|
||||||
import { getPlatformConfig } from "./config";
|
import { getPlatformConfig } from "./config"; // Lấy cấu hình nền tảng từ module config
|
||||||
import { MotionPlugin } from "@vueuse/motion";
|
import { MotionPlugin } from "@vueuse/motion"; // Sử dụng plugin motion từ VueUse
|
||||||
// import { useEcharts } from "@/plugins/echarts";
|
import { useEcharts } from "@/plugins/echarts"; // Sử dụng plugin Echarts
|
||||||
import { createApp, type Directive } from "vue";
|
import { createApp, type Directive } from "vue"; // Tạo ứng dụng Vue mới
|
||||||
import { useElementPlus } from "@/plugins/elementPlus";
|
import { useVxeTable } from "@/plugins/vxeTable"; // Sử dụng plugin VxeTable
|
||||||
import { injectResponsiveStorage } from "@/utils/responsive";
|
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 Table from "@pureadmin/table"; // Nhập component Table từ thư viện @pureadmin/table
|
||||||
// import PureDescriptions from "@pureadmin/descriptions";
|
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/reset.scss";
|
||||||
// 导入公共样式
|
|
||||||
import "./style/index.scss";
|
import "./style/index.scss";
|
||||||
// 一定要在main.ts中导入tailwind.css,防止vite每次hmr都会请求src/style/index.scss整体css文件导致热更新慢的问题
|
|
||||||
import "./style/tailwind.css";
|
import "./style/tailwind.css";
|
||||||
import "element-plus/dist/index.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.js";
|
import "./assets/iconfont/iconfont.css"; // Import CSS của icon font
|
||||||
import "./assets/iconfont/iconfont.css";
|
|
||||||
|
|
||||||
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";
|
import * as directives from "@/directives";
|
||||||
Object.keys(directives).forEach(key => {
|
Object.keys(directives).forEach(key => {
|
||||||
app.directive(key, (directives as { [key: string]: Directive })[key]);
|
app.directive(key, (directives as { [key: string]: Directive })[key]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 全局注册@iconify/vue图标库
|
// Đăng ký toàn cục thư viện iconify/vue
|
||||||
import {
|
import {
|
||||||
IconifyIconOffline,
|
IconifyIconOffline,
|
||||||
IconifyIconOnline,
|
IconifyIconOnline,
|
||||||
@ -41,23 +39,29 @@ app.component("IconifyIconOffline", IconifyIconOffline);
|
|||||||
app.component("IconifyIconOnline", IconifyIconOnline);
|
app.component("IconifyIconOnline", IconifyIconOnline);
|
||||||
app.component("FontIcon", FontIcon);
|
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";
|
import { Auth } from "@/components/ReAuth";
|
||||||
app.component("Auth", Auth);
|
app.component("Auth", Auth);
|
||||||
|
|
||||||
// 全局注册vue-tippy
|
// Đăng ký toàn cục Vue Tippy
|
||||||
import "tippy.js/dist/tippy.css";
|
import "tippy.js/dist/tippy.css";
|
||||||
import "tippy.js/themes/light.css";
|
import "tippy.js/themes/light.css";
|
||||||
import VueTippy from "vue-tippy";
|
import VueTippy from "vue-tippy";
|
||||||
app.use(VueTippy);
|
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 => {
|
getPlatformConfig(app).then(async config => {
|
||||||
setupStore(app);
|
setupStore(app); // Thiết lập store Vuex
|
||||||
app.use(router);
|
app.use(router); // Sử dụng router Vue
|
||||||
await router.isReady();
|
await router.isReady(); // Chờ router sẵn sàng
|
||||||
injectResponsiveStorage(app, config);
|
injectResponsiveStorage(app, config); // Inject cấu hình đáp ứng vào ứng dụng
|
||||||
app.use(MotionPlugin).use(useI18n).use(useElementPlus).use(Table);
|
app
|
||||||
// .use(PureDescriptions)
|
.use(MotionPlugin) // Sử dụng MotionPlugin từ VueUse
|
||||||
// .use(useEcharts);
|
.use(useI18n) // Sử dụng plugin i18n
|
||||||
app.mount("#app");
|
.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"
|
||||||
});
|
});
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
// 多组件库的国际化和本地项目国际化兼容
|
// Internationalization compatibility for multiple component libraries and local projects
|
||||||
import { type I18n, createI18n } from "vue-i18n";
|
import { type I18n, createI18n } from "vue-i18n";
|
||||||
import type { App, WritableComputedRef } from "vue";
|
import type { App, WritableComputedRef } from "vue";
|
||||||
import { responsiveStorageNameSpace } from "@/config";
|
import { responsiveStorageNameSpace } from "@/config";
|
||||||
import { storageLocal, isObject } from "@pureadmin/utils";
|
import { storageLocal, isObject } from "@pureadmin/utils";
|
||||||
|
|
||||||
// element-plus国际化
|
// Element Plus internationalization
|
||||||
import enLocale from "element-plus/es/locale/lang/en";
|
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 () {
|
const siphonI18n = (function () {
|
||||||
// 仅初始化一次国际化配置
|
// Initialize internationalization configuration only once
|
||||||
let cache = Object.fromEntries(
|
let cache = Object.fromEntries(
|
||||||
Object.entries(
|
Object.entries(
|
||||||
import.meta.glob("../../locales/*.y(a)?ml", { eager: true })
|
import.meta.glob("../../locales/*.y(a)?ml", { eager: true })
|
||||||
@ -18,14 +18,14 @@ const siphonI18n = (function () {
|
|||||||
return [matched, value.default];
|
return [matched, value.default];
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return (prefix = "zh-CN") => {
|
return (prefix = "vi") => {
|
||||||
return cache[prefix];
|
return cache[prefix];
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
export const localesConfigs = {
|
export const localesConfigs = {
|
||||||
zh: {
|
vi: {
|
||||||
...siphonI18n("zh-CN"),
|
...siphonI18n("vi"),
|
||||||
...zhLocale
|
...zhLocale
|
||||||
},
|
},
|
||||||
en: {
|
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) {
|
function getObjectKeys(obj) {
|
||||||
const stack = [];
|
const stack = [];
|
||||||
const keys: Set<string> = new Set();
|
const keys: Set<string> = new Set();
|
||||||
@ -58,9 +58,9 @@ function getObjectKeys(obj) {
|
|||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将展开的key缓存 */
|
/** Cache expanded keys */
|
||||||
const keysCache: Map<string, Set<string>> = new Map();
|
const keysCache: Map<string, Set<string>> = new Map();
|
||||||
const flatI18n = (prefix = "zh-CN") => {
|
const flatI18n = (prefix = "vi") => {
|
||||||
let cache = keysCache.get(prefix);
|
let cache = keysCache.get(prefix);
|
||||||
if (!cache) {
|
if (!cache) {
|
||||||
cache = getObjectKeys(siphonI18n(prefix));
|
cache = getObjectKeys(siphonI18n(prefix));
|
||||||
@ -70,16 +70,16 @@ const flatI18n = (prefix = "zh-CN") => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 国际化转换工具函数(自动读取根目录locales文件夹下文件进行国际化匹配)
|
* Internationalization transformation utility function
|
||||||
* @param message message
|
* @param message message to transform
|
||||||
* @returns 转化后的message
|
* @returns transformed message
|
||||||
*/
|
*/
|
||||||
export function transformI18n(message: any = "") {
|
export function transformI18n(message: any = "") {
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理存储动态路由的title,格式 {zh:"",en:""}
|
// Handle dynamic route titles stored in format {zh:"",en:""}
|
||||||
if (typeof message === "object") {
|
if (typeof message === "object") {
|
||||||
const locale: string | WritableComputedRef<string> | any =
|
const locale: string | WritableComputedRef<string> | any =
|
||||||
i18n.global.locale;
|
i18n.global.locale;
|
||||||
@ -88,17 +88,17 @@ export function transformI18n(message: any = "") {
|
|||||||
|
|
||||||
const key = message.match(/(\S*)\./)?.input;
|
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);
|
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);
|
return i18n.global.t.call(i18n.global.locale, message);
|
||||||
} else {
|
} else {
|
||||||
return message;
|
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 $t = (key: string) => key;
|
||||||
|
|
||||||
export const i18n: I18n = createI18n({
|
export const i18n: I18n = createI18n({
|
||||||
@ -106,7 +106,7 @@ export const i18n: I18n = createI18n({
|
|||||||
locale:
|
locale:
|
||||||
storageLocal().getItem<StorageConfigs>(
|
storageLocal().getItem<StorageConfigs>(
|
||||||
`${responsiveStorageNameSpace()}locale`
|
`${responsiveStorageNameSpace()}locale`
|
||||||
)?.locale ?? "zh",
|
)?.locale ?? "vi",
|
||||||
fallbackLocale: "en",
|
fallbackLocale: "en",
|
||||||
messages: localesConfigs
|
messages: localesConfigs
|
||||||
});
|
});
|
||||||
|
103
src/plugins/vxeTable.ts
Normal file
103
src/plugins/vxeTable.ts
Normal 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);
|
||||||
|
}
|
@ -26,7 +26,9 @@ const IFrame = () => import("@/layout/frame.vue");
|
|||||||
// https://cn.vitejs.dev/guide/features.html#glob-import
|
// https://cn.vitejs.dev/guide/features.html#glob-import
|
||||||
const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");
|
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";
|
import { getAsyncRoutes } from "@/api/routes";
|
||||||
|
|
||||||
function handRank(routeInfo: any) {
|
function handRank(routeInfo: any) {
|
||||||
@ -39,10 +41,10 @@ function handRank(routeInfo: any) {
|
|||||||
: false;
|
: 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[]) {
|
function ascending(arr: any[]) {
|
||||||
arr.forEach((v, index) => {
|
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;
|
if (handRank(v)) v.meta.rank = index + 2;
|
||||||
});
|
});
|
||||||
return arr.sort(
|
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[]) {
|
function filterTree(data: RouteComponent[]) {
|
||||||
const newTree = cloneDeep(data).filter(
|
const newTree = cloneDeep(data).filter(
|
||||||
(v: { meta: { showLink: boolean } }) => v.meta?.showLink !== false
|
(v: { meta: { showLink: boolean } }) => v.meta?.showLink !== false
|
||||||
@ -63,7 +65,7 @@ function filterTree(data: RouteComponent[]) {
|
|||||||
return newTree;
|
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[]) {
|
function filterChildrenTree(data: RouteComponent[]) {
|
||||||
const newTree = cloneDeep(data).filter((v: any) => v?.children?.length !== 0);
|
const newTree = cloneDeep(data).filter((v: any) => v?.children?.length !== 0);
|
||||||
newTree.forEach(
|
newTree.forEach(
|
||||||
@ -72,7 +74,7 @@ function filterChildrenTree(data: RouteComponent[]) {
|
|||||||
return newTree;
|
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>) {
|
function isOneOfArray(a: Array<string>, b: Array<string>) {
|
||||||
return Array.isArray(a) && Array.isArray(b)
|
return Array.isArray(a) && Array.isArray(b)
|
||||||
? intersection(a, b).length > 0
|
? intersection(a, b).length > 0
|
||||||
@ -81,7 +83,7 @@ function isOneOfArray(a: Array<string>, b: Array<string>) {
|
|||||||
: true;
|
: 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[]) {
|
function filterNoPermissionTree(data: RouteComponent[]) {
|
||||||
const currentRoles =
|
const currentRoles =
|
||||||
storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
|
storageLocal().getItem<DataInfo<number>>(userKey)?.roles ?? [];
|
||||||
@ -94,31 +96,31 @@ function filterNoPermissionTree(data: RouteComponent[]) {
|
|||||||
return filterChildrenTree(newTree);
|
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") {
|
function getParentPaths(value: string, routes: RouteRecordRaw[], key = "path") {
|
||||||
// 深度遍历查找
|
// Duyệt sâu tìm kiếm
|
||||||
function dfs(routes: RouteRecordRaw[], value: string, parents: string[]) {
|
function dfs(routes: RouteRecordRaw[], value: string, parents: string[]) {
|
||||||
for (let i = 0; i < routes.length; i++) {
|
for (let i = 0; i < routes.length; i++) {
|
||||||
const item = routes[i];
|
const item = routes[i];
|
||||||
// 返回父级path
|
// Trả về danh sách path cha
|
||||||
if (item[key] === value) return parents;
|
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;
|
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);
|
parents.push(item.path);
|
||||||
|
|
||||||
if (dfs(item.children, value, parents).length) return parents;
|
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();
|
parents.pop();
|
||||||
}
|
}
|
||||||
// 未找到时返回空数组
|
// Khi không tìm thấy thì trả về mảng rỗng
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return dfs(routes, value, []);
|
return dfs(routes, value, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查找对应 `path` 的路由信息 */
|
/** Tìm thông tin định tuyến theo `path` đã chỉ định */
|
||||||
function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
|
function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
|
||||||
let res = routes.find((item: { path: string }) => item.path == path);
|
let res = routes.find((item: { path: string }) => item.path == path);
|
||||||
if (res) {
|
if (res) {
|
||||||
@ -149,14 +151,14 @@ function addPathMatch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理动态路由(后端返回的路由) */
|
/** Xử lý định tuyến động (các route từ backend) */
|
||||||
function handleAsyncRoutes(routeList) {
|
function handleAsyncRoutes(routeList) {
|
||||||
if (routeList.length === 0) {
|
if (routeList.length === 0) {
|
||||||
usePermissionStoreHook().handleWholeMenus(routeList);
|
usePermissionStoreHook().handleWholeMenus(routeList);
|
||||||
} else {
|
} else {
|
||||||
formatFlatteningRoutes(addAsyncRoutes(routeList)).map(
|
formatFlatteningRoutes(addAsyncRoutes(routeList)).map(
|
||||||
(v: RouteRecordRaw) => {
|
(v: RouteRecordRaw) => {
|
||||||
// 防止重复添加路由
|
// Tránh thêm định tuyến trùng lặp
|
||||||
if (
|
if (
|
||||||
router.options.routes[0].children.findIndex(
|
router.options.routes[0].children.findIndex(
|
||||||
value => value.path === v.path
|
value => value.path === v.path
|
||||||
@ -164,9 +166,9 @@ function handleAsyncRoutes(routeList) {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
} else {
|
} 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);
|
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);
|
ascending(router.options.routes[0].children);
|
||||||
if (!router.hasRoute(v?.name)) router.addRoute(v);
|
if (!router.hasRoute(v?.name)) router.addRoute(v);
|
||||||
const flattenRouters: any = router
|
const flattenRouters: any = router
|
||||||
@ -189,10 +191,10 @@ function handleAsyncRoutes(routeList) {
|
|||||||
addPathMatch();
|
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() {
|
function initRouter() {
|
||||||
if (getConfig()?.CachingAsyncRoutes) {
|
if (getConfig()?.CachingAsyncRoutes) {
|
||||||
// 开启动态路由缓存本地localStorage
|
// Bật cache định tuyến động vào localStorage
|
||||||
const key = "async-routes";
|
const key = "async-routes";
|
||||||
const asyncRouteList = storageLocal().getItem(key) as any;
|
const asyncRouteList = storageLocal().getItem(key) as any;
|
||||||
if (asyncRouteList && asyncRouteList?.length > 0) {
|
if (asyncRouteList && asyncRouteList?.length > 0) {
|
||||||
@ -220,9 +222,9 @@ function initRouter() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将多级嵌套路由处理成一维数组
|
* Chuyển đổi mảng định tuyến nhiều cấp thành mảng một cấp
|
||||||
* @param routesList 传入路由
|
* @param routesList Mảng định tuyến đầu vào
|
||||||
* @returns 返回处理后的一维路由
|
* @returns Trả về mảng định tuyến đã được xử lý
|
||||||
*/
|
*/
|
||||||
function formatFlatteningRoutes(routesList: RouteRecordRaw[]) {
|
function formatFlatteningRoutes(routesList: RouteRecordRaw[]) {
|
||||||
if (routesList.length === 0) return routesList;
|
if (routesList.length === 0) return routesList;
|
||||||
@ -238,10 +240,10 @@ function formatFlatteningRoutes(routesList: RouteRecordRaw[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一维数组处理成多级嵌套数组(三级及以上的路由全部拍成二级,keep-alive 只支持到二级缓存)
|
* Xử lý 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
|
* https://github.com/pure-admin/vue-pure-admin/issues/67
|
||||||
* @param routesList 处理后的一维路由菜单数组
|
* @param routesList Mảng định tuyến menu đã được xử lý
|
||||||
* @returns 返回将一维数组重新处理成规定路由的格式
|
* @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[]) {
|
function formatTwoStageRoutes(routesList: RouteRecordRaw[]) {
|
||||||
if (routesList.length === 0) return routesList;
|
if (routesList.length === 0) return routesList;
|
||||||
@ -263,7 +265,7 @@ function formatTwoStageRoutes(routesList: RouteRecordRaw[]) {
|
|||||||
return newRoutesList;
|
return newRoutesList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理缓存路由(添加、删除、刷新) */
|
/** Xử lý định tuyến cache (Thêm, xóa, làm mới) */
|
||||||
function handleAliveRoute({ name }: ToRouteType, mode?: string) {
|
function handleAliveRoute({ name }: ToRouteType, mode?: string) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "add":
|
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>) {
|
function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
|
||||||
if (!arrRoutes || !arrRoutes.length) return;
|
if (!arrRoutes || !arrRoutes.length) return;
|
||||||
const modulesRoutesKeys = Object.keys(modulesRoutes);
|
const modulesRoutesKeys = Object.keys(modulesRoutes);
|
||||||
arrRoutes.forEach((v: RouteRecordRaw) => {
|
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;
|
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)
|
if (v?.children && v.children.length && !v.redirect)
|
||||||
v.redirect = v.children[0].path;
|
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)
|
if (v?.children && v.children.length && !v.name)
|
||||||
v.name = (v.children[0].name as string) + "Parent";
|
v.name = (v.children[0].name as string) + "Parent";
|
||||||
if (v.meta?.frameSrc) {
|
if (v.meta?.frameSrc) {
|
||||||
v.component = IFrame;
|
v.component = IFrame;
|
||||||
} else {
|
} 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
|
const index = v?.component
|
||||||
? modulesRoutesKeys.findIndex(ev => ev.includes(v.component as any))
|
? modulesRoutesKeys.findIndex(ev => ev.includes(v.component as any))
|
||||||
: modulesRoutesKeys.findIndex(ev => ev.includes(v.path));
|
: modulesRoutesKeys.findIndex(ev => ev.includes(v.path));
|
||||||
@ -327,20 +329,20 @@ function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
|
|||||||
return arrRoutes;
|
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 {
|
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 historyMode = routerHistory.split(",");
|
||||||
const leftMode = historyMode[0];
|
const leftMode = historyMode[0];
|
||||||
const rightMode = historyMode[1];
|
const rightMode = historyMode[1];
|
||||||
// no param
|
// Không có tham số
|
||||||
if (historyMode.length === 1) {
|
if (historyMode.length === 1) {
|
||||||
if (leftMode === "hash") {
|
if (leftMode === "hash") {
|
||||||
return createWebHashHistory("");
|
return createWebHashHistory("");
|
||||||
} else if (leftMode === "h5") {
|
} else if (leftMode === "h5") {
|
||||||
return createWebHistory("");
|
return createWebHistory("");
|
||||||
}
|
}
|
||||||
} //has param
|
} // Có tham số
|
||||||
else if (historyMode.length === 2) {
|
else if (historyMode.length === 2) {
|
||||||
if (leftMode === "hash") {
|
if (leftMode === "hash") {
|
||||||
return createWebHashHistory(rightMode);
|
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> {
|
function getAuths(): Array<string> {
|
||||||
return router.currentRoute.value.meta.auths as 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 {
|
function hasAuth(value: string | Array<string>): boolean {
|
||||||
if (!value) return false;
|
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();
|
const metaAuths = getAuths();
|
||||||
if (!metaAuths) return false;
|
if (!metaAuths) return false;
|
||||||
const isAuths = isString(value)
|
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 {
|
function getTopMenu(tag = false): menuType {
|
||||||
const topMenu = handleTopMenu(
|
const topMenu = handleTopMenu(
|
||||||
usePermissionStoreHook().wholeMenus[0]?.children[0]
|
usePermissionStoreHook().wholeMenus[0]?.children[0]
|
||||||
|
@ -19,13 +19,13 @@ export const useAppStore = defineStore({
|
|||||||
withoutAnimation: false,
|
withoutAnimation: false,
|
||||||
isClickCollapse: 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:
|
layout:
|
||||||
storageLocal().getItem<StorageConfigs>(
|
storageLocal().getItem<StorageConfigs>(
|
||||||
`${responsiveStorageNameSpace()}layout`
|
`${responsiveStorageNameSpace()}layout`
|
||||||
)?.layout ?? getConfig().Layout,
|
)?.layout ?? getConfig().Layout,
|
||||||
device: deviceDetection() ? "mobile" : "desktop",
|
device: deviceDetection() ? "mobile" : "desktop",
|
||||||
// 浏览器窗口的可视区域大小
|
// Kích thước vùng hiển thị của cửa sổ trình duyệt
|
||||||
viewportSize: {
|
viewportSize: {
|
||||||
width: document.documentElement.clientWidth,
|
width: document.documentElement.clientWidth,
|
||||||
height: document.documentElement.clientHeight
|
height: document.documentElement.clientHeight
|
||||||
|
@ -23,14 +23,15 @@ html {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
height: 100%;
|
||||||
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
|
margin: 0;
|
||||||
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
font-family: "Segoe UI", Arial, sans-serif;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
text-rendering: optimizelegibility;
|
text-rendering: optimizelegibility;
|
||||||
|
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
|
@ -2,60 +2,62 @@ import Cookies from "js-cookie";
|
|||||||
import { storageLocal } from "@pureadmin/utils";
|
import { storageLocal } from "@pureadmin/utils";
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
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> {
|
export interface DataInfo<T> {
|
||||||
/** token */
|
/** Token truy cập */
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
/** `accessToken`的过期时间(时间戳) */
|
/** Thời gian hết hạn của accessToken (dưới dạng timestamp) */
|
||||||
expires: T;
|
expires: T;
|
||||||
/** 用于调用刷新accessToken的接口时所需的token */
|
/** Token dùng để làm mới accessToken */
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
/** 头像 */
|
/** Ảnh đại diện */
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
/** 用户名 */
|
/** Tên đăng nhập */
|
||||||
username?: string;
|
username?: string;
|
||||||
/** 昵称 */
|
/** Biệt danh */
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
/** 当前登录用户的角色 */
|
/** Các vai trò của người dùng hiện tại */
|
||||||
roles?: Array<string>;
|
roles?: Array<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userKey = "user-info";
|
// Khai báo các hằng số
|
||||||
export const TokenKey = "authorized-token";
|
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
|
||||||
* 通过`multiple-tabs`是否在`cookie`中,判断用户是否已经登录系统,
|
export const multipleTabsKey = "multiple-tabs"; // Key cho cookie kiểm tra nhiều tab mở
|
||||||
* 从而支持多标签页打开已经登录的系统后无需再登录。
|
|
||||||
* 浏览器完全关闭后`multiple-tabs`将自动从`cookie`中销毁,
|
|
||||||
* 再次打开浏览器需要重新登录系统
|
|
||||||
* */
|
|
||||||
export const multipleTabsKey = "multiple-tabs";
|
|
||||||
|
|
||||||
/** 获取`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> {
|
export function getToken(): DataInfo<number> {
|
||||||
// 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
|
|
||||||
return Cookies.get(TokenKey)
|
return Cookies.get(TokenKey)
|
||||||
? JSON.parse(Cookies.get(TokenKey))
|
? JSON.parse(Cookies.get(TokenKey))
|
||||||
: storageLocal().getItem(userKey);
|
: storageLocal().getItem(userKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 设置`token`以及一些必要信息并采用无感刷新`token`方案
|
* Hàm thiết lập thông tin token và thông tin người dùng
|
||||||
* 无感刷新:后端返回`accessToken`(访问接口使用的`token`)、`refreshToken`(用于调用刷新`accessToken`的接口时所需的`token`,`refreshToken`的过期时间(比如30天)应大于`accessToken`的过期时间(比如2小时))、`expires`(`accessToken`的过期时间)
|
* Sử dụng phương thức làm mới token mà không cần đăng nhập lại
|
||||||
* 将`accessToken`、`expires`、`refreshToken`这三条信息放在key值为authorized-token的cookie里(过期自动销毁)
|
* Lưu accessToken, expires và refreshToken vào cookie với key là TokenKey
|
||||||
* 将`avatar`、`username`、`nickname`、`roles`、`refreshToken`、`expires`这六条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁)
|
* Lưu avatar, username, nickname, roles, refreshToken và expires vào localStorage với key là userKey
|
||||||
*/
|
*/
|
||||||
export function setToken(data: DataInfo<Date>) {
|
export function setToken(data: DataInfo<Date>) {
|
||||||
let expires = 0;
|
let expires = 0;
|
||||||
const { accessToken, refreshToken } = data;
|
const { accessToken, refreshToken } = data;
|
||||||
const { isRemembered, loginDay } = useUserStoreHook();
|
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 });
|
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
|
expires > 0
|
||||||
? Cookies.set(TokenKey, cookieString, {
|
? Cookies.set(TokenKey, cookieString, {
|
||||||
expires: (expires - Date.now()) / 86400000
|
expires: (expires - Date.now()) / 86400000
|
||||||
})
|
})
|
||||||
: Cookies.set(TokenKey, cookieString);
|
: Cookies.set(TokenKey, cookieString);
|
||||||
|
|
||||||
|
// Thiết lập cookie multipleTabsKey để kiểm tra nhiều tab
|
||||||
Cookies.set(
|
Cookies.set(
|
||||||
multipleTabsKey,
|
multipleTabsKey,
|
||||||
"true",
|
"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 }) {
|
function setUserKey({ avatar, username, nickname, roles }) {
|
||||||
useUserStoreHook().SET_AVATAR(avatar);
|
useUserStoreHook().SET_AVATAR(avatar);
|
||||||
useUserStoreHook().SET_USERNAME(username);
|
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) {
|
if (data.username && data.roles) {
|
||||||
const { username, roles } = data;
|
const { username, roles } = data;
|
||||||
setUserKey({
|
setUserKey({
|
||||||
@ -107,14 +111,18 @@ export function setToken(data: DataInfo<Date>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除`token`以及key值为`user-info`的localStorage信息 */
|
/**
|
||||||
|
* Hàm xóa thông tin token và thông tin người dùng từ cookie và localStorage
|
||||||
|
*/
|
||||||
export function removeToken() {
|
export function removeToken() {
|
||||||
Cookies.remove(TokenKey);
|
Cookies.remove(TokenKey); // Xóa cookie TokenKey
|
||||||
Cookies.remove(multipleTabsKey);
|
Cookies.remove(multipleTabsKey); // Xóa cookie multipleTabsKey
|
||||||
storageLocal().removeItem(userKey);
|
storageLocal().removeItem(userKey); // Xóa localStorage userKey
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 格式化token(jwt格式) */
|
/**
|
||||||
|
* Hàm định dạng token JWT
|
||||||
|
*/
|
||||||
export const formatToken = (token: string): string => {
|
export const formatToken = (token: string): string => {
|
||||||
return "Bearer " + token;
|
return "Bearer " + token;
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
// 如果项目出现 `global is not defined` 报错,可能是您引入某个库的问题,比如 aws-sdk-js https://github.com/aws/aws-sdk-js
|
// Nếu biến `global` chưa được định nghĩa, gán `window` cho `global`
|
||||||
// 解决办法就是将该文件引入 src/main.ts 即可 import "@/utils/globalPolyfills";
|
|
||||||
if (typeof (window as any).global === "undefined") {
|
if (typeof (window as any).global === "undefined") {
|
||||||
(window as any).global = window;
|
(window as any).global = window;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// Axios instance and related configurations
|
||||||
import Axios, {
|
import Axios, {
|
||||||
type AxiosInstance,
|
type AxiosInstance,
|
||||||
type AxiosRequestConfig,
|
type AxiosRequestConfig,
|
||||||
@ -14,16 +15,16 @@ import NProgress from "../progress";
|
|||||||
import { getToken, formatToken } from "@/utils/auth";
|
import { getToken, formatToken } from "@/utils/auth";
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
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 = {
|
const defaultConfig: AxiosRequestConfig = {
|
||||||
// 请求超时时间
|
// Request timeout
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json, text/plain, */*",
|
Accept: "application/json, text/plain, */*",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Requested-With": "XMLHttpRequest"
|
"X-Requested-With": "XMLHttpRequest"
|
||||||
},
|
},
|
||||||
// 数组格式参数序列化(https://github.com/axios/axios/issues/5142)
|
// Array format parameter serialization (https://github.com/axios/axios/issues/5142)
|
||||||
paramsSerializer: {
|
paramsSerializer: {
|
||||||
serialize: stringify as unknown as CustomParamsSerializer
|
serialize: stringify as unknown as CustomParamsSerializer
|
||||||
}
|
}
|
||||||
@ -35,19 +36,19 @@ class PureHttp {
|
|||||||
this.httpInterceptorsResponse();
|
this.httpInterceptorsResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `token`过期后,暂存待执行的请求 */
|
/** Queue for storing requests while token is refreshing */
|
||||||
private static requests = [];
|
private static requests = [];
|
||||||
|
|
||||||
/** 防止重复刷新`token` */
|
/** Prevents duplicate token refresh */
|
||||||
private static isRefreshing = false;
|
private static isRefreshing = false;
|
||||||
|
|
||||||
/** 初始化配置对象 */
|
/** Initial configuration object */
|
||||||
private static initConfig: PureHttpRequestConfig = {};
|
private static initConfig: PureHttpRequestConfig = {};
|
||||||
|
|
||||||
/** 保存当前`Axios`实例对象 */
|
/** Stores the current Axios instance */
|
||||||
private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
|
private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
|
||||||
|
|
||||||
/** 重连原始请求 */
|
/** Retry the original request */
|
||||||
private static retryOriginalRequest(config: PureHttpRequestConfig) {
|
private static retryOriginalRequest(config: PureHttpRequestConfig) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
PureHttp.requests.push((token: string) => {
|
PureHttp.requests.push((token: string) => {
|
||||||
@ -57,13 +58,13 @@ class PureHttp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 请求拦截 */
|
/** Request interceptor */
|
||||||
private httpInterceptorsRequest(): void {
|
private httpInterceptorsRequest(): void {
|
||||||
PureHttp.axiosInstance.interceptors.request.use(
|
PureHttp.axiosInstance.interceptors.request.use(
|
||||||
async (config: PureHttpRequestConfig): Promise<any> => {
|
async (config: PureHttpRequestConfig): Promise<any> => {
|
||||||
// 开启进度条动画
|
// Start progress bar animation
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
// Prioritize checking if post/get methods have a callback, otherwise execute initialization settings callback, etc.
|
||||||
if (typeof config.beforeRequestCallback === "function") {
|
if (typeof config.beforeRequestCallback === "function") {
|
||||||
config.beforeRequestCallback(config);
|
config.beforeRequestCallback(config);
|
||||||
return config;
|
return config;
|
||||||
@ -72,7 +73,7 @@ class PureHttp {
|
|||||||
PureHttp.initConfig.beforeRequestCallback(config);
|
PureHttp.initConfig.beforeRequestCallback(config);
|
||||||
return 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"];
|
const whiteList = ["/refresh-token", "/login"];
|
||||||
return whiteList.some(url => config.url.endsWith(url))
|
return whiteList.some(url => config.url.endsWith(url))
|
||||||
? config
|
? config
|
||||||
@ -84,7 +85,7 @@ class PureHttp {
|
|||||||
if (expired) {
|
if (expired) {
|
||||||
if (!PureHttp.isRefreshing) {
|
if (!PureHttp.isRefreshing) {
|
||||||
PureHttp.isRefreshing = true;
|
PureHttp.isRefreshing = true;
|
||||||
// token过期刷新
|
// Refresh token when expired
|
||||||
useUserStoreHook()
|
useUserStoreHook()
|
||||||
.handRefreshToken({ refreshToken: data.refreshToken })
|
.handRefreshToken({ refreshToken: data.refreshToken })
|
||||||
.then(res => {
|
.then(res => {
|
||||||
@ -115,15 +116,15 @@ class PureHttp {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 响应拦截 */
|
/** Response interceptor */
|
||||||
private httpInterceptorsResponse(): void {
|
private httpInterceptorsResponse(): void {
|
||||||
const instance = PureHttp.axiosInstance;
|
const instance = PureHttp.axiosInstance;
|
||||||
instance.interceptors.response.use(
|
instance.interceptors.response.use(
|
||||||
(response: PureHttpResponse) => {
|
(response: PureHttpResponse) => {
|
||||||
const $config = response.config;
|
const $config = response.config;
|
||||||
// 关闭进度条动画
|
// End progress bar animation
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
// Prioritize checking if post/get methods have a callback, otherwise execute initialization settings callback, etc.
|
||||||
if (typeof $config.beforeResponseCallback === "function") {
|
if (typeof $config.beforeResponseCallback === "function") {
|
||||||
$config.beforeResponseCallback(response);
|
$config.beforeResponseCallback(response);
|
||||||
return response.data;
|
return response.data;
|
||||||
@ -137,15 +138,15 @@ class PureHttp {
|
|||||||
(error: PureHttpError) => {
|
(error: PureHttpError) => {
|
||||||
const $error = error;
|
const $error = error;
|
||||||
$error.isCancelRequest = Axios.isCancel($error);
|
$error.isCancelRequest = Axios.isCancel($error);
|
||||||
// 关闭进度条动画
|
// End progress bar animation
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
// 所有的响应异常 区分来源为取消请求/非取消请求
|
// All response errors distinguish between canceled requests and non-canceled requests
|
||||||
return Promise.reject($error);
|
return Promise.reject($error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通用请求工具函数 */
|
/** Common request utility function */
|
||||||
public request<T>(
|
public request<T>(
|
||||||
method: RequestMethods,
|
method: RequestMethods,
|
||||||
url: string,
|
url: string,
|
||||||
@ -159,7 +160,7 @@ class PureHttp {
|
|||||||
...axiosConfig
|
...axiosConfig
|
||||||
} as PureHttpRequestConfig;
|
} as PureHttpRequestConfig;
|
||||||
|
|
||||||
// 单独处理自定义请求/响应回调
|
// Handle custom request/response callbacks separately
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
PureHttp.axiosInstance
|
PureHttp.axiosInstance
|
||||||
.request(config)
|
.request(config)
|
||||||
@ -172,7 +173,7 @@ class PureHttp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单独抽离的`post`工具函数 */
|
/** Separated 'post' utility function */
|
||||||
public post<T, P>(
|
public post<T, P>(
|
||||||
url: string,
|
url: string,
|
||||||
params?: AxiosRequestConfig<P>,
|
params?: AxiosRequestConfig<P>,
|
||||||
@ -181,7 +182,7 @@ class PureHttp {
|
|||||||
return this.request<T>("post", url, params, config);
|
return this.request<T>("post", url, params, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单独抽离的`get`工具函数 */
|
/** Separated 'get' utility function */
|
||||||
public get<T, P>(
|
public get<T, P>(
|
||||||
url: string,
|
url: string,
|
||||||
params?: AxiosRequestConfig<P>,
|
params?: AxiosRequestConfig<P>,
|
||||||
|
@ -6,17 +6,17 @@ class StorageProxy implements ProxyStorage {
|
|||||||
constructor(storageModel) {
|
constructor(storageModel) {
|
||||||
this.storage = storageModel;
|
this.storage = storageModel;
|
||||||
this.storage.config({
|
this.storage.config({
|
||||||
// 首选IndexedDB作为第一驱动,不支持IndexedDB会自动降级到localStorage(WebSQL被弃用,详情看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],
|
driver: [this.storage.INDEXEDDB, this.storage.LOCALSTORAGE],
|
||||||
name: "pure-admin"
|
name: "pure-admin"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 将对应键名的数据保存到离线仓库
|
* @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 键名
|
* @param k Tên khóa
|
||||||
* @param v 键值
|
* @param v Giá trị
|
||||||
* @param m 缓存时间(单位`分`,默认`0`分钟,永久缓存)
|
* @param m Thời gian lưu trữ (đơn vị phút, mặc định là 0 phút, lưu trữ vĩnh viễn)
|
||||||
*/
|
*/
|
||||||
public async setItem<T>(k: string, v: T, m = 0): Promise<T> {
|
public async setItem<T>(k: string, v: T, m = 0): Promise<T> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -35,8 +35,8 @@ class StorageProxy implements ProxyStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 从离线仓库中获取对应键名的值
|
* @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 键名
|
* @param k Tên khóa
|
||||||
*/
|
*/
|
||||||
public async getItem<T>(k: string): Promise<T> {
|
public async getItem<T>(k: string): Promise<T> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -54,8 +54,8 @@ class StorageProxy implements ProxyStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 从离线仓库中删除对应键名的值
|
* @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 键名
|
* @param k Tên khóa
|
||||||
*/
|
*/
|
||||||
public async removeItem(k: string) {
|
public async removeItem(k: string) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
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 cơ sở dữ liệu
|
||||||
*/
|
*/
|
||||||
public async clear() {
|
public async clear() {
|
||||||
return new Promise<void>((resolve, reject) => {
|
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() {
|
public async keys() {
|
||||||
return new Promise<string[]>((resolve, reject) => {
|
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);
|
export const localForage = () => new StorageProxy(forage);
|
||||||
|
@ -6,34 +6,34 @@ type messageStyle = "el" | "antd";
|
|||||||
type messageTypes = "info" | "success" | "warning" | "error";
|
type messageTypes = "info" | "success" | "warning" | "error";
|
||||||
|
|
||||||
interface MessageParams {
|
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?: 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;
|
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;
|
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;
|
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;
|
duration?: number;
|
||||||
/** 是否显示关闭按钮,默认值 `false` */
|
/** Hiển thị nút đóng, mặc định là `false` */
|
||||||
showClose?: boolean;
|
showClose?: boolean;
|
||||||
/** 文字是否居中,默认值 `false` */
|
/** Văn bản có căn giữa hay không, mặc định là `false` */
|
||||||
center?: boolean;
|
center?: boolean;
|
||||||
/** `Message` 距离窗口顶部的偏移量,默认 `20` */
|
/** Độ lệch của `Message` so với đỉnh cửa sổ, mặc định là `20` */
|
||||||
offset?: number;
|
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;
|
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;
|
grouping?: boolean;
|
||||||
/** 关闭时的回调函数, 参数为被关闭的 `message` 实例 */
|
/** Callback khi đóng thông báo, tham số là instance `message` đã đóng */
|
||||||
onClose?: Function | null;
|
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 = (
|
const message = (
|
||||||
message: string | VNode | (() => VNode),
|
message: string | VNode | (() => VNode),
|
||||||
@ -70,7 +70,7 @@ const message = (
|
|||||||
offset,
|
offset,
|
||||||
appendTo,
|
appendTo,
|
||||||
grouping,
|
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" : "",
|
customClass: customClass === "antd" ? "pure-message" : "",
|
||||||
onClose: () => (isFunction(onClose) ? onClose() : null)
|
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();
|
const closeAllMessage = (): void => ElMessage.closeAll();
|
||||||
|
|
||||||
|
@ -1,13 +1,20 @@
|
|||||||
import type { Emitter } from "mitt";
|
import type { Emitter } from "mitt";
|
||||||
import mitt 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 = {
|
type Events = {
|
||||||
openPanel: string;
|
openPanel: string;
|
||||||
tagViewsChange: string;
|
tagViewsChange: string;
|
||||||
tagViewsShowModel: string;
|
tagViewsShowModel: string;
|
||||||
logoChange: boolean;
|
logoChange: boolean;
|
||||||
changLayoutRoute: string;
|
changLayoutRoute: string;
|
||||||
|
imageInfo: {
|
||||||
|
img: HTMLImageElement;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const emitter: Emitter<Events> = mitt<Events>();
|
export const emitter: Emitter<Events> = mitt<Events>();
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
import { useEventListener } from "@vueuse/core";
|
import { useEventListener } from "@vueuse/core";
|
||||||
|
|
||||||
/** 是否为`img`标签 */
|
/** Kiểm tra xem có phải là thẻ `img` */
|
||||||
function isImgElement(element) {
|
function isImgElement(element) {
|
||||||
return typeof HTMLImageElement !== "undefined"
|
return typeof HTMLImageElement !== "undefined"
|
||||||
? element instanceof HTMLImageElement
|
? element instanceof HTMLImageElement
|
||||||
: element.tagName.toLowerCase() === "img";
|
: 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 = () => {
|
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(
|
useEventListener(
|
||||||
window.document,
|
window.document,
|
||||||
"keydown",
|
"keydown",
|
||||||
ev => ev.key === "F12" && ev.preventDefault()
|
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());
|
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());
|
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(
|
useEventListener(
|
||||||
window.document,
|
window.document,
|
||||||
"dragstart",
|
"dragstart",
|
||||||
|
@ -2,15 +2,15 @@ import NProgress from "nprogress";
|
|||||||
import "nprogress/nprogress.css";
|
import "nprogress/nprogress.css";
|
||||||
|
|
||||||
NProgress.configure({
|
NProgress.configure({
|
||||||
// 动画方式
|
// Animation mode
|
||||||
easing: "ease",
|
easing: "ease",
|
||||||
// 递增进度条的速度
|
// Tăng tốc độ của thanh tiến trình
|
||||||
speed: 500,
|
speed: 500,
|
||||||
// 是否显示加载ico
|
// Có hiển thị ico đang tải hay không
|
||||||
showSpinner: false,
|
showSpinner: false,
|
||||||
// 自动递增间隔
|
// khoảng thời gian tự động tăng
|
||||||
trickleSpeed: 200,
|
trickleSpeed: 200,
|
||||||
// 初始化时的最小百分比
|
// Tỷ lệ phần trăm tối thiểu khi khởi tạo
|
||||||
minimum: 0.3
|
minimum: 0.3
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ const newPropTypes = createTypes({
|
|||||||
integer: undefined
|
integer: undefined
|
||||||
}) as PropTypes;
|
}) 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 {
|
export default class propTypes extends newPropTypes {
|
||||||
// a native-like validator that supports the `.validable` method
|
// a native-like validator that supports the `.validable` method
|
||||||
static get style() {
|
static get style() {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// 响应式storage
|
// Storage phản ứng
|
||||||
import type { App } from "vue";
|
import type { App } from "vue";
|
||||||
import Storage from "responsive-storage";
|
import Storage from "responsive-storage";
|
||||||
import { routerArrays } from "@/layout/types";
|
import { routerArrays } from "@/layout/types";
|
||||||
@ -8,35 +8,35 @@ export const injectResponsiveStorage = (app: App, config: PlatformConfigs) => {
|
|||||||
const nameSpace = responsiveStorageNameSpace();
|
const nameSpace = responsiveStorageNameSpace();
|
||||||
const configObj = Object.assign(
|
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: 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: Storage.getData("layout", nameSpace) ?? {
|
||||||
layout: config.Layout ?? "vertical",
|
layout: config.Layout ?? "vertical", // Bố cục mặc định là "vertical" nếu không có dữ liệu
|
||||||
theme: config.Theme ?? "light",
|
theme: config.Theme ?? "light", // Chủ đề mặc định là "light" nếu không có dữ liệu
|
||||||
darkMode: config.DarkMode ?? false,
|
darkMode: config.DarkMode ?? false, // Chế độ tối mặc định là false nếu không có dữ liệu
|
||||||
sidebarStatus: config.SidebarStatus ?? true,
|
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",
|
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", // 主题色(对应系统配置中的主题色,与theme不同的是它不会受到浅色、深色整体风格切换的影响,只会在手动点击主题色时改变)
|
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" // 整体风格(浅色:light、深色:dark、自动:system)
|
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) ?? {
|
configure: Storage.getData("configure", nameSpace) ?? {
|
||||||
grey: config.Grey ?? 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,
|
weak: config.Weak ?? false, // Yếu mặc định là false nếu không có dữ liệu
|
||||||
hideTabs: config.HideTabs ?? false,
|
hideTabs: config.HideTabs ?? false, // Ẩn tab mặc định là false nếu không có dữ liệu
|
||||||
hideFooter: config.HideFooter ?? true,
|
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,
|
showLogo: config.ShowLogo ?? true, // Hiển thị logo mặc định là true nếu không có dữ liệu
|
||||||
showModel: config.ShowModel ?? "smart",
|
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,
|
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
|
stretch: config.Stretch ?? false // Kéo dài mặc định là false nếu không có dữ liệu
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
config.MultiTagsCache
|
config.MultiTagsCache
|
||||||
? {
|
? {
|
||||||
// 默认显示顶级菜单tag
|
// Hiển thị thẻ menu cấp độ cao nhất mặc định
|
||||||
tags: Storage.getData("tags", nameSpace) ?? routerArrays
|
tags: Storage.getData("tags", nameSpace) ?? routerArrays
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
|
@ -2,22 +2,22 @@ import { removeToken, setToken, type DataInfo } from "./auth";
|
|||||||
import { subBefore, getQueryMap } from "@pureadmin/utils";
|
import { subBefore, getQueryMap } from "@pureadmin/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 简版前端单点登录,根据实际业务自行编写,平台启动后本地可以跳后面这个链接进行测试 http://localhost:8848/#/permission/page/index?username=sso&roles=admin&accessToken=eyJhbGciOiJIUzUxMiJ9.admin
|
* Đă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 có 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 có phải đăng nhập đơn giản không, nếu không phải thì trả về mà không thực hiện bất kỳ xử lý logic nào, dưới đây là xử lý logic sau khi đăng nhập đơn giản
|
||||||
* 1.清空本地旧信息;
|
* 1. Xóa thông tin cũ trên thiết bị địa phương;
|
||||||
* 2.获取url中的重要参数信息,然后通过 setToken 保存在本地;
|
* 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.删除不需要显示在 url 的参数
|
* 3. Xóa các tham số không cần thiết để hiển thị trong URL;
|
||||||
* 4.使用 window.location.replace 跳转正确页面
|
* 4. Sử dụng window.location.replace để chuyển hướng đến trang chính xác.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
// 获取 url 中的参数
|
// Lấy các tham số từ URL
|
||||||
const params = getQueryMap(location.href) as DataInfo<Date>;
|
const params = getQueryMap(location.href) as DataInfo<Date>;
|
||||||
const must = ["username", "roles", "accessToken"];
|
const must = ["username", "roles", "accessToken"];
|
||||||
const mustLength = must.length;
|
const mustLength = must.length;
|
||||||
if (Object.keys(params).length !== mustLength) return;
|
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 sso = [];
|
||||||
let start = 0;
|
let start = 0;
|
||||||
|
|
||||||
@ -31,15 +31,15 @@ import { subBefore, getQueryMap } from "@pureadmin/utils";
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sso.length === mustLength) {
|
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();
|
removeToken();
|
||||||
|
|
||||||
// 保存新信息到本地
|
// Lưu trữ thông tin mới vào thiết bị client
|
||||||
setToken(params);
|
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.roles;
|
||||||
delete params.accessToken;
|
delete params.accessToken;
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ import { subBefore, getQueryMap } from "@pureadmin/utils";
|
|||||||
.replace(/:/g, "=")
|
.replace(/:/g, "=")
|
||||||
.replace(/,/g, "&")}`;
|
.replace(/,/g, "&")}`;
|
||||||
|
|
||||||
// 替换历史记录项
|
// Thay thế mục lịch sử
|
||||||
window.location.replace(newUrl);
|
window.location.replace(newUrl);
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* @description 提取菜单树中的每一项uniqueId
|
* @description Trích xuất mỗi mục uniqueId từ cây menu
|
||||||
* @param tree 树
|
* @param tree Cây menu
|
||||||
* @returns 每一项uniqueId组成的数组
|
* @returns Mảng chứa mỗi mục uniqueId
|
||||||
*/
|
*/
|
||||||
export const extractPathList = (tree: any[]): any => {
|
export const extractPathList = (tree: any[]): any => {
|
||||||
if (!Array.isArray(tree)) {
|
if (!Array.isArray(tree)) {
|
||||||
@ -21,10 +21,10 @@ export const extractPathList = (tree: any[]): any => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 如果父级下children的length为1,删除children并自动组建唯一uniqueId
|
* @description Nếu số lượng children của parent là 1, xóa children và tự động xây dựng uniqueId duy nhất
|
||||||
* @param tree 树
|
* @param tree Cây menu
|
||||||
* @param pathList 每一项的id组成的数组
|
* @param pathList Mảng chứa mỗi mục id
|
||||||
* @returns 组件唯一uniqueId后的树
|
* @returns Cây với uniqueId duy nhất đã được xây dựng
|
||||||
*/
|
*/
|
||||||
export const deleteChildren = (tree: any[], pathList = []): any => {
|
export const deleteChildren = (tree: any[], pathList = []): any => {
|
||||||
if (!Array.isArray(tree)) {
|
if (!Array.isArray(tree)) {
|
||||||
@ -48,10 +48,10 @@ export const deleteChildren = (tree: any[], pathList = []): any => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 创建层级关系
|
* @description Xây dựng cấu trúc cây
|
||||||
* @param tree 树
|
* @param tree Cây menu
|
||||||
* @param pathList 每一项的id组成的数组
|
* @param pathList Mảng chứa mỗi mục id
|
||||||
* @returns 创建层级关系后的树
|
* @returns Cây sau khi đã xây dựng cấu trúc cây
|
||||||
*/
|
*/
|
||||||
export const buildHierarchyTree = (tree: any[], pathList = []): any => {
|
export const buildHierarchyTree = (tree: any[], pathList = []): any => {
|
||||||
if (!Array.isArray(tree)) {
|
if (!Array.isArray(tree)) {
|
||||||
@ -72,10 +72,10 @@ export const buildHierarchyTree = (tree: any[], pathList = []): any => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 广度优先遍历,根据唯一uniqueId找当前节点信息
|
* @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 树
|
* @param tree Cây menu
|
||||||
* @param uniqueId 唯一uniqueId
|
* @param uniqueId uniqueId
|
||||||
* @returns 当前节点信息
|
* @returns Thông tin của node hiện tại
|
||||||
*/
|
*/
|
||||||
export const getNodeByUniqueId = (
|
export const getNodeByUniqueId = (
|
||||||
tree: any[],
|
tree: any[],
|
||||||
@ -96,11 +96,11 @@ export const getNodeByUniqueId = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 向当前唯一uniqueId节点中追加字段
|
* @description Thêm trường vào node hiện tại dựa trên uniqueId
|
||||||
* @param tree 树
|
* @param tree Cây menu
|
||||||
* @param uniqueId 唯一uniqueId
|
* @param uniqueId uniqueId
|
||||||
* @param fields 需要追加的字段
|
* @param fields Các trường cần thêm
|
||||||
* @returns 追加字段后的树
|
* @returns Cây sau khi đã thêm trường
|
||||||
*/
|
*/
|
||||||
export const appendFieldByUniqueId = (
|
export const appendFieldByUniqueId = (
|
||||||
tree: any[],
|
tree: any[],
|
||||||
@ -127,12 +127,12 @@ export const appendFieldByUniqueId = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 构造树型结构数据
|
* @description Xây dựng cấu trúc dữ liệu cây
|
||||||
* @param data 数据源
|
* @param data Dữ liệu nguồn
|
||||||
* @param id id字段 默认id
|
* @param id Trường id, mặc định là "id"
|
||||||
* @param parentId 父节点字段,默认parentId
|
* @param parentId Trường parentId, mặc định là "parentId"
|
||||||
* @param children 子节点字段,默认children
|
* @param children Trường children, mặc định là "children"
|
||||||
* @returns 追加字段后的树
|
* @returns Cây sau khi đã xây dựng cấu trúc dữ liệu
|
||||||
*/
|
*/
|
||||||
export const handleTree = (
|
export const handleTree = (
|
||||||
data: any[],
|
data: any[],
|
||||||
|
@ -53,7 +53,7 @@ const onLogin = async (formEl: FormInstance | undefined) => {
|
|||||||
.loginByUsername({ username: ruleForm.username, password: "admin123" })
|
.loginByUsername({ username: ruleForm.username, password: "admin123" })
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
// 获取后端路由
|
// Fetch backend routes
|
||||||
return initRouter().then(() => {
|
return initRouter().then(() => {
|
||||||
router.push(getTopMenu(true).path).then(() => {
|
router.push(getTopMenu(true).path).then(() => {
|
||||||
message(t("login.pureLoginSuccess"), { type: "success" });
|
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) {
|
function onkeypress({ code }: KeyboardEvent) {
|
||||||
if (["Enter", "NumpadEnter"].includes(code)) {
|
if (["Enter", "NumpadEnter"].includes(code)) {
|
||||||
onLogin(ruleFormRef.value);
|
onLogin(ruleFormRef.value);
|
||||||
@ -88,7 +88,7 @@ onBeforeUnmount(() => {
|
|||||||
<div class="select-none">
|
<div class="select-none">
|
||||||
<img :src="bg" class="wave" />
|
<img :src="bg" class="wave" />
|
||||||
<div class="flex-c absolute right-5 top-3">
|
<div class="flex-c absolute right-5 top-3">
|
||||||
<!-- 主题 -->
|
<!-- Theme -->
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="dataTheme"
|
v-model="dataTheme"
|
||||||
inline-prompt
|
inline-prompt
|
||||||
@ -96,7 +96,7 @@ onBeforeUnmount(() => {
|
|||||||
:inactive-icon="darkIcon"
|
:inactive-icon="darkIcon"
|
||||||
@change="dataThemeChange"
|
@change="dataThemeChange"
|
||||||
/>
|
/>
|
||||||
<!-- 国际化 -->
|
<!-- Globalization -->
|
||||||
<el-dropdown trigger="click">
|
<el-dropdown trigger="click">
|
||||||
<globalization
|
<globalization
|
||||||
class="hover:text-primary hover:!bg-[transparent] w-[20px] h-[20px] ml-1.5 cursor-pointer outline-none duration-300"
|
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>
|
<template #dropdown>
|
||||||
<el-dropdown-menu class="translation">
|
<el-dropdown-menu class="translation">
|
||||||
<el-dropdown-item
|
<el-dropdown-item
|
||||||
:style="getDropdownItemStyle(locale, 'zh')"
|
:style="getDropdownItemStyle(locale, 'vi')"
|
||||||
:class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]"
|
:class="['dark:!text-white', getDropdownItemClass(locale, 'vi')]"
|
||||||
@click="translationCh"
|
@click="translationCh"
|
||||||
>
|
>
|
||||||
<IconifyIconOffline
|
<IconifyIconOffline
|
||||||
v-show="locale === 'zh'"
|
v-show="locale === 'vi'"
|
||||||
class="check-zh"
|
class="check-vi"
|
||||||
:icon="Check"
|
:icon="Check"
|
||||||
/>
|
/>
|
||||||
简体中文
|
Tiếng Việt
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item
|
<el-dropdown-item
|
||||||
:style="getDropdownItemStyle(locale, 'en')"
|
:style="getDropdownItemStyle(locale, 'en')"
|
||||||
@ -210,7 +210,7 @@ onBeforeUnmount(() => {
|
|||||||
padding: 5px 40px;
|
padding: 5px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-zh {
|
.check-vi {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
}
|
}
|
||||||
|
111
src/views/welcome/components/charts/ChartBar.vue
Normal file
111
src/views/welcome/components/charts/ChartBar.vue
Normal 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 bảo cập nhật DOM hoàn tất trước khi thực 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>
|
62
src/views/welcome/components/charts/ChartLine.vue
Normal file
62
src/views/welcome/components/charts/ChartLine.vue
Normal 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>
|
73
src/views/welcome/components/charts/ChartRound.vue
Normal file
73
src/views/welcome/components/charts/ChartRound.vue
Normal 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>
|
3
src/views/welcome/components/charts/index.ts
Normal file
3
src/views/welcome/components/charts/index.ts
Normal 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";
|
104
src/views/welcome/components/table/columns.tsx
Normal file
104
src/views/welcome/components/table/columns.tsx
Normal 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
|
||||||
|
};
|
||||||
|
}
|
1
src/views/welcome/components/table/empty.svg
Normal file
1
src/views/welcome/components/table/empty.svg
Normal 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 |
71
src/views/welcome/components/table/index.vue
Normal file
71
src/views/welcome/components/table/index.vue
Normal 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
142
src/views/welcome/data.ts
Normal 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 };
|
@ -1,9 +1,276 @@
|
|||||||
<script setup lang="ts">
|
<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({
|
defineOptions({
|
||||||
name: "Welcome"
|
name: "Welcome"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { isDark } = useDark();
|
||||||
|
|
||||||
|
let curWeek = ref(1); // 0 tuần trước, 1 tuần này
|
||||||
|
const optionsBasis: Array<OptionsType> = [
|
||||||
|
{
|
||||||
|
label: "Tuần trước"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Tuần này"
|
||||||
|
}
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 kê</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>
|
</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>
|
||||||
|
6
src/views/welcome/utils.ts
Normal file
6
src/views/welcome/utils.ts
Normal 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
18
types/directives.d.ts
vendored
@ -3,21 +3,21 @@ import type { CopyEl, OptimizeOptions, RippleOptions } from "@/directives";
|
|||||||
|
|
||||||
declare module "vue" {
|
declare module "vue" {
|
||||||
export interface ComponentCustomProperties {
|
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>;
|
vLoading: Directive<Element, boolean>;
|
||||||
/** 按钮权限指令 */
|
/** Chỉ thị quyền nút */
|
||||||
vAuth: Directive<HTMLElement, string | Array<string>>;
|
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>;
|
vCopy: Directive<CopyEl, string>;
|
||||||
/** 长按指令 */
|
/** Chỉ thị nhấn giữ */
|
||||||
vLongpress: Directive<HTMLElement, Function>;
|
vLongpress: Directive<HTMLElement, Function>;
|
||||||
/** 防抖、节流指令 */
|
/** Chỉ thị chống rung, tiết lưu */
|
||||||
vOptimize: Directive<HTMLElement, OptimizeOptions>;
|
vOptimize: Directive<HTMLElement, OptimizeOptions>;
|
||||||
/**
|
/**
|
||||||
* `v-ripple`指令,用法如下:
|
* Chỉ thị `v-ripple`, cách sử dụng như sau:
|
||||||
* 1. `v-ripple`代表启用基本的`ripple`功能
|
* 1. `v-ripple` để kích hoạt tính năng `ripple` cơ bản
|
||||||
* 2. `v-ripple="{ class: 'text-red' }"`代表自定义`ripple`颜色,支持`tailwindcss`,生效样式是`color`
|
* 2. `v-ripple="{ class: 'text-red' }"` để tùy chỉnh màu `ripple`, hỗ trợ `tailwindcss`, màu hiệu lực là `color`
|
||||||
* 3. `v-ripple.center`代表从中心扩散
|
* 3. `v-ripple.center` để lan tỏa từ trung tâm
|
||||||
*/
|
*/
|
||||||
vRipple: Directive<HTMLElement, RippleOptions>;
|
vRipple: Directive<HTMLElement, RippleOptions>;
|
||||||
}
|
}
|
||||||
|
4
types/global-components.d.ts
vendored
4
types/global-components.d.ts
vendored
@ -1,6 +1,6 @@
|
|||||||
declare module "vue" {
|
declare module "vue" {
|
||||||
/**
|
/**
|
||||||
* 自定义全局组件获得 Volar 提示(自定义的全局组件需要在这里声明下才能获得 Volar 类型提示哦)
|
* Đăng ký các thành phần toàn cầu tùy chỉnh để nhận được gợi ý từ Volar
|
||||||
*/
|
*/
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
IconifyIconOffline: (typeof import("../src/components/ReIcon"))["IconifyIconOffline"];
|
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
|
* 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" {
|
declare module "vue" {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
31
types/global.d.ts
vendored
31
types/global.d.ts
vendored
@ -2,11 +2,12 @@ import type { ECharts } from "echarts";
|
|||||||
import type { TableColumns } from "@pureadmin/table";
|
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 {
|
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` và `pnpm`, các phụ thuộc,
|
||||||
|
* các phụ thuộc phát triển và thời gian xây dựng cuối cùng của nó.
|
||||||
*/
|
*/
|
||||||
const __APP_INFO__: {
|
const __APP_INFO__: {
|
||||||
pkg: {
|
pkg: {
|
||||||
@ -23,10 +24,10 @@ declare global {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Window 的类型提示
|
* Kiểu gợi ý cho đối tượng Window
|
||||||
*/
|
*/
|
||||||
interface Window {
|
interface Window {
|
||||||
// Global vue app instance
|
// Thể hiện ứng dụng Vue toàn cục
|
||||||
__APP__: App<Element>;
|
__APP__: App<Element>;
|
||||||
webkitCancelAnimationFrame: (handle: number) => void;
|
webkitCancelAnimationFrame: (handle: number) => void;
|
||||||
mozCancelAnimationFrame: (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 {
|
interface Document {
|
||||||
webkitFullscreenElement?: Element;
|
webkitFullscreenElement?: Element;
|
||||||
@ -48,7 +49,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打包压缩格式的类型声明
|
* Kiểu gợi ý cho các lựa chọn nén và đóng gói
|
||||||
*/
|
*/
|
||||||
type ViteCompression =
|
type ViteCompression =
|
||||||
| "none"
|
| "none"
|
||||||
@ -60,8 +61,7 @@ declare global {
|
|||||||
| "both-clear";
|
| "both-clear";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局自定义环境变量的类型声明
|
* Kiểu gợi ý cho các biến môi trường tùy chỉnh toàn cầu
|
||||||
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/config/#%E5%85%B7%E4%BD%93%E9%85%8D%E7%BD%AE}
|
|
||||||
*/
|
*/
|
||||||
interface ViteEnv {
|
interface ViteEnv {
|
||||||
VITE_PORT: number;
|
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> {}
|
interface TableColumnList extends Array<TableColumns> {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对应 `public/platform-config.json` 文件的类型声明
|
* Kiểu dữ liệu tương ứng với tệp `public/platform-config.json`
|
||||||
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/config/#platform-config-json}
|
|
||||||
*/
|
*/
|
||||||
interface PlatformConfigs {
|
interface PlatformConfigs {
|
||||||
Version?: string;
|
Version?: string;
|
||||||
@ -111,8 +110,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 与 `PlatformConfigs` 类型不同,这里是缓存到浏览器本地存储的类型声明
|
* Kiểu dữ liệu lưu trữ trong bộ nhớ cục bộ, khác biệt so với `PlatformConfigs`
|
||||||
* @see {@link https://pure-admin.github.io/pure-admin-doc/pages/config/#platform-config-json}
|
|
||||||
*/
|
*/
|
||||||
interface StorageConfigs {
|
interface StorageConfigs {
|
||||||
version?: string;
|
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 {
|
interface ResponsiveStorage {
|
||||||
locale: {
|
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 {
|
interface GlobalPropertiesApi {
|
||||||
$echarts: ECharts;
|
$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 {
|
interface Element {
|
||||||
// v-ripple 作用于 src/directives/ripple/index.ts 文件
|
|
||||||
_ripple?: {
|
_ripple?: {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
centered?: boolean;
|
centered?: boolean;
|
||||||
|
2
types/index.d.ts
vendored
2
types/index.d.ts
vendored
@ -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;
|
type RefType<T> = T | null;
|
||||||
|
|
||||||
|
76
types/router.d.ts
vendored
76
types/router.d.ts
vendored
@ -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 { RouteComponent, RouteLocationNormalized } from "vue-router";
|
||||||
import type { FunctionalComponent } from "vue";
|
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 {
|
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;
|
title: string;
|
||||||
/** 菜单图标 `可选` */
|
/** Biểu tượng của menu `Tùy chọn` */
|
||||||
icon?: string | FunctionalComponent | IconifyIcon;
|
icon?: string | FunctionalComponent | IconifyIcon;
|
||||||
/** 菜单名称右侧的额外图标 */
|
/** Biểu tượng bổ sung bên phải của tên menu */
|
||||||
extraIcon?: string | FunctionalComponent | IconifyIcon;
|
extraIcon?: string | FunctionalComponent | IconifyIcon;
|
||||||
/** 是否在菜单中显示(默认`true`)`可选` */
|
/** Hiển thị trong menu hay không (mặc định là `true`) `Tùy chọn` */
|
||||||
showLink?: boolean;
|
showLink?: boolean;
|
||||||
/** 是否显示父级菜单 `可选` */
|
/** Hiển thị menu cha hay không `Tùy chọn` */
|
||||||
showParent?: boolean;
|
showParent?: boolean;
|
||||||
/** 页面级别权限设置 `可选` */
|
/** Cài đặt quyền trang `Tùy chọn` */
|
||||||
roles?: Array<string>;
|
roles?: Array<string>;
|
||||||
/** 按钮级别权限设置 `可选` */
|
/** Cài đặt quyền nút `Tùy chọn` */
|
||||||
auths?: Array<string>;
|
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;
|
keepAlive?: boolean;
|
||||||
/** 内嵌的`iframe`链接 `可选` */
|
/** Liên kết `iframe` nhúng `Tùy chọn` */
|
||||||
frameSrc?: string;
|
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;
|
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?: {
|
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 {@link https://next.router.vuejs.org/guide/advanced/transitions.html#transitions}
|
||||||
* @see animate.css {@link https://animate.style}
|
* @see animate.css {@link https://animate.style}
|
||||||
*/
|
*/
|
||||||
name?: string;
|
name?: string;
|
||||||
/** 进场动画 */
|
/** Hiệu ứng khi vào */
|
||||||
enterTransition?: string;
|
enterTransition?: string;
|
||||||
/** 离场动画 */
|
/** Hiệu ứng khi ra đi */
|
||||||
leaveTransition?: string;
|
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;
|
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;
|
fixedTag?: boolean;
|
||||||
/** 动态路由可打开的最大数量 `可选` */
|
/** Số lượng tối đa của độ sâu định tuyến có thể mở `Tùy chọn` */
|
||||||
dynamicLevel?: number;
|
dynamicLevel?: number;
|
||||||
/** 将某个菜单激活
|
/** Kích hoạt một menu cụ thể
|
||||||
* (主要用于通过`query`或`params`传参的路由,当它们通过配置`showLink: false`后不在菜单中显示,就不会有任何菜单高亮,
|
* (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 có bất kỳ menu nào được làm nổi bật,
|
||||||
* 而通过设置`activePath`指定激活菜单即可获得高亮,`activePath`为指定激活菜单的`path`)
|
* thay vào đó chỉ cần đặt `activePath` để chỉ định menu cần kích hoạt, `activePath` là `path` của menu cần kích hoạt)
|
||||||
*/
|
*/
|
||||||
activePath?: string;
|
activePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 完整子路由配置表
|
* @description Bảng cấu hình định tuyến con đầy đủ
|
||||||
*/
|
*/
|
||||||
interface RouteChildrenConfigsTable {
|
interface RouteChildrenConfigsTable {
|
||||||
/** 子路由地址 `必填` */
|
/** Địa chỉ định tuyến con `Bắt buộc` */
|
||||||
path: string;
|
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;
|
name?: string;
|
||||||
/** 路由重定向 `可选` */
|
/** Điều hướng lại định tuyến `Tùy chọn` */
|
||||||
redirect?: string;
|
redirect?: string;
|
||||||
/** 按需加载组件 `可选` */
|
/** Thành phần tải theo yêu cầu `Tùy chọn` */
|
||||||
component?: RouteComponent;
|
component?: RouteComponent;
|
||||||
meta?: CustomizeRouteMeta;
|
meta?: CustomizeRouteMeta;
|
||||||
/** 子路由配置项 */
|
/** Các mục cấu hình định tuyến con */
|
||||||
children?: Array<RouteChildrenConfigsTable>;
|
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 {
|
interface RouteConfigsTable {
|
||||||
/** 路由地址 `必填` */
|
/** Địa chỉ định tuyến `Bắt buộc` */
|
||||||
path: string;
|
path: string;
|
||||||
/** 路由名字(保持唯一)`可选` */
|
/** Tên định tuyến (duy trì duy nhất) `Tùy chọn` */
|
||||||
name?: string;
|
name?: string;
|
||||||
/** `Layout`组件 `可选` */
|
/** Thành phần `Layout` `Tùy chọn` */
|
||||||
component?: RouteComponent;
|
component?: RouteComponent;
|
||||||
/** 路由重定向 `可选` */
|
/** Điều hướng lại định tuyến `Tùy chọn` */
|
||||||
redirect?: string;
|
redirect?: string;
|
||||||
meta?: {
|
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;
|
title: string;
|
||||||
/** 菜单图标 `可选` */
|
/** Biểu tượng của menu `Tùy chọn` */
|
||||||
icon?: string | FunctionalComponent | IconifyIcon;
|
icon?: string | FunctionalComponent | IconifyIcon;
|
||||||
/** 是否在菜单中显示(默认`true`)`可选` */
|
/** Hiển thị trong menu hay không (mặc định là `true`) `Tùy chọn` */
|
||||||
showLink?: boolean;
|
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;
|
rank?: number;
|
||||||
};
|
};
|
||||||
/** 子路由配置项 */
|
/** Các mục cấu hình định tuyến con */
|
||||||
children?: Array<RouteChildrenConfigsTable>;
|
children?: Array<RouteChildrenConfigsTable>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,14 +18,14 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias
|
alias
|
||||||
},
|
},
|
||||||
// 服务端渲染
|
// Server-side rendering
|
||||||
server: {
|
server: {
|
||||||
// 端口号
|
// Port number
|
||||||
port: VITE_PORT,
|
port: VITE_PORT,
|
||||||
host: "0.0.0.0",
|
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: {},
|
proxy: {},
|
||||||
// 预热文件以提前转换和缓存结果,降低启动期间的初始页面加载时长并防止转换瀑布
|
// Preheat files to pre-transform and cache results, reducing initial page load time during startup and preventing transformation waterfall
|
||||||
warmup: {
|
warmup: {
|
||||||
clientFiles: ["./index.html", "./src/{views,components}/*"]
|
clientFiles: ["./index.html", "./src/{views,components}/*"]
|
||||||
}
|
}
|
||||||
@ -40,13 +40,13 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
|
|||||||
// https://cn.vitejs.dev/guide/build.html#browser-compatibility
|
// https://cn.vitejs.dev/guide/build.html#browser-compatibility
|
||||||
target: "es2015",
|
target: "es2015",
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
// 消除打包大小超过500kb警告
|
// Eliminate package size warning over 500kb
|
||||||
chunkSizeWarningLimit: 4000,
|
chunkSizeWarningLimit: 4000,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
index: pathResolve("./index.html", import.meta.url)
|
index: pathResolve("./index.html", import.meta.url)
|
||||||
},
|
},
|
||||||
// 静态资源分类打包
|
// Static resource classification packaging
|
||||||
output: {
|
output: {
|
||||||
chunkFileNames: "static/js/[name]-[hash].js",
|
chunkFileNames: "static/js/[name]-[hash].js",
|
||||||
entryFileNames: "static/js/[name]-[hash].js",
|
entryFileNames: "static/js/[name]-[hash].js",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user