!18 更新到master分支

Merge pull request !18 from nongyehong/dev
This commit is contained in:
nongyehong 2024-09-01 13:43:35 +00:00 committed by Gitee
commit c1ba5e00fb
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
115 changed files with 1930 additions and 493 deletions

View File

@ -1,3 +1,26 @@
# [2.0.0](https://github.com/nongyehong/HuLa-IM-Tauri/compare/v1.6.0...v2.0.0) (2024-08-15)
### Bug Fixes
* **system:** :bug: 修复mac端兼容问题 ([0daef59](https://github.com/nongyehong/HuLa-IM-Tauri/commit/0daef59a9f41326a8e82885c3b84857ec3761e92))
### Features
* **common:** :sparkles: 新增修改字体功能 ([6bd6f64](https://github.com/nongyehong/HuLa-IM-Tauri/commit/6bd6f641f1c012dd53bd7dcb5cf4a314bf7d527b))
* **component:** :sparkles: 新增是否启用界面阴影功能、收缩页面按钮功能 ([085a773](https://github.com/nongyehong/HuLa-IM-Tauri/commit/085a773967fd0a26525a2f87dc1d8fddb8d71f1a))
* **view:** :sparkles: 新增搜索页面功能 ([866ba89](https://github.com/nongyehong/HuLa-IM-Tauri/commit/866ba89b93d1a2587afb16fac745779093b9af19))
* **view:** :sparkles: 新增锁屏功能 ([1407343](https://github.com/nongyehong/HuLa-IM-Tauri/commit/14073438d5a9dc82117a84f97b5bd8f239fdfcd4))
### Performance Improvements
* :zap: 优化锁屏页面功能 ([85b6cad](https://github.com/nongyehong/HuLa-IM-Tauri/commit/85b6cad03fdcd538adbdae9fc2e63e0ef72b465a))
* **system:** :zap: 升级tauri-v2版本 ([57dcad1](https://github.com/nongyehong/HuLa-IM-Tauri/commit/57dcad1e9306421c161d555181a9deda48f5685e))
# [1.6.0](https://github.com/nongyehong/HuLa-IM-Tauri/compare/v1.5.0...v1.6.0) (2024-07-03)
@ -12,21 +35,37 @@
* :sparkles: 发布v1.6.0版本 ([71a1dd9](https://github.com/nongyehong/HuLa-IM-Tauri/commit/71a1dd93833d4c9534945f28fe636115ef59e862))
* **component:** :sparkles: 新增GPT欢迎页面完善设置页面 ([9b771e0](https://github.com/nongyehong/HuLa-IM-Tauri/commit/9b771e02ec31af1238f9662e839df6197f501376))
* **component:** :sparkles: 新增GPT页面设置功能 ([4c85b4a](https://github.com/nongyehong/HuLa-IM-Tauri/commit/4c85b4afccdafe83aa0fcbd53e94ef5fc63a7a70))
* **component:** :sparkles: 新增GPT组件 ([7260840](https://github.com/nongyehong/HuLa-IM-Tauri/commit/7260840f4b50bcbb4dad8645a84ade8280de4036))
* **components:** :sparkles: 实现群聊回复表情功能 ([1fb3530](https://github.com/nongyehong/HuLa-IM-Tauri/commit/1fb3530cbdceef702430b272b99d3e99277c52d0))
* **component:** :sparkles: 新增GPT页面设置功能 ([4c85b4a](https://github.com/nongyehong/HuLa-IM-Tauri/commit/4c85b4afccdafe83aa0fcbd53e94ef5fc63a7a70))
* **components:** :sparkles: 完善右键功能的显示资料 ([cf4820b](https://github.com/nongyehong/HuLa-IM-Tauri/commit/cf4820bffbdee50fc1e7b44c72b51cd2c4d80091))
* **components:** :sparkles: 实现群聊回复表情功能 ([1fb3530](https://github.com/nongyehong/HuLa-IM-Tauri/commit/1fb3530cbdceef702430b272b99d3e99277c52d0))
* **style:** :sparkles: 新增项目版本信息打印 ([e17cb7c](https://github.com/nongyehong/HuLa-IM-Tauri/commit/e17cb7c24a233417ab34a1de3b04cbdc32ebc2e0))
* **view:** :sparkles: 新增GPT首页推荐功能样式 ([e927a95](https://github.com/nongyehong/HuLa-IM-Tauri/commit/e927a95fa4f95da7299459941b00d2f633217bca))
### Performance Improvements
* **components:** :zap: 优化表情回应 ([94d2cb1](https://github.com/nongyehong/HuLa-IM-Tauri/commit/94d2cb1fec8db8901ffc85cdf8680919c58abf11))
* **components:** :zap: 优化群聊回复表情功能 ([0c4615d](https://github.com/nongyehong/HuLa-IM-Tauri/commit/0c4615d4135fb3f740cb88f8f38502c9fc90bc5d))
* **components:** :zap: 优化表情回应 ([94d2cb1](https://github.com/nongyehong/HuLa-IM-Tauri/commit/94d2cb1fec8db8901ffc85cdf8680919c58abf11))
* **services:** :zap: 优化请求接口以及消息提示 ([0355f97](https://github.com/nongyehong/HuLa-IM-Tauri/commit/0355f976b854d96e613160d2bf6cc7e5605ea0ac))
* **system:** :zap: 对接后端服务 ([ea4b82b](https://github.com/nongyehong/HuLa-IM-Tauri/commit/ea4b82be25a058a198716cebcf8becfcf252819c))
* **views:** :zap: 优化页面收缩功能 ([31f7e17](https://github.com/nongyehong/HuLa-IM-Tauri/commit/31f7e1732cbe571e3f53564c57a339812b2c1a5b))
# [1.5.0](https://github.com/nongyehong/HuLa-IM-Tauri/compare/v1.4.0...v1.5.0) (2024-04-19)
# [1.4.0](https://github.com/nongyehong/HuLa-IM-Tauri/compare/v1.3.0-beta...v1.4.0) (2024-04-01)
# [1.3.0-beta](https://github.com/nongyehong/HuLa-IM-Tauri/compare/v1.2.9-alpha...v1.3.0-beta) (2024-03-12)
## 1.2.9-alpha (2024-03-08)

201
LICENSE
View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -7,7 +7,7 @@
<title>HuLa</title>
<!--引入iconpark图标库-->
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_113.b759c39620e6fa93d145499865d57e99.js"></script>
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_118.ee039811e5b75b41f6c09e4bb8e9edcd.js"></script>
</head>
<body>

View File

@ -39,6 +39,8 @@
},
"dependencies": {
"@tauri-apps/api": "2.0.0-rc.0",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-rc.0",
"@tauri-apps/plugin-http": "2.0.0-rc.1",
"@tauri-apps/plugin-os": "2.0.0-rc.0",
"@tauri-apps/plugin-process": "2.0.0-rc.0",
"axios": "^1.7.3",

View File

@ -11,6 +11,12 @@ importers:
'@tauri-apps/api':
specifier: 2.0.0-rc.0
version: 2.0.0-rc.0
'@tauri-apps/plugin-clipboard-manager':
specifier: 2.0.0-rc.0
version: 2.0.0-rc.0
'@tauri-apps/plugin-http':
specifier: 2.0.0-rc.1
version: 2.0.0-rc.1
'@tauri-apps/plugin-os':
specifier: 2.0.0-rc.0
version: 2.0.0-rc.0
@ -933,6 +939,9 @@ packages:
resolution: {integrity: sha512-v454Qs3REHc3Za59U+/eSmBsdmF+3NE5+76+lFDaitVqN4ZglDHENDaMARYKGJVZuxiSkzyqG0SeG7lLQjVkPA==}
engines: {node: '>= 18.18', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
'@tauri-apps/api@2.0.0-rc.1':
resolution: {integrity: sha512-qubAWjM9sqofUh7fe+7UAbBY3wlkfCyxm+PNRYpq9mnNng7lvSQq3sYsFUEB12AYvgGARZSb54VMVUvRuVLi7w==}
'@tauri-apps/cli-darwin-arm64@2.0.0-rc.3':
resolution: {integrity: sha512-szYCSr/ChbCF+S6Wnr15TYpI2cZR07d+AQOiFGuScP0preM8Pbsk/sb0hfLwqzepjVFFNVWQba9sG7FEW2Y2XA==}
engines: {node: '>= 10'}
@ -1002,6 +1011,12 @@ packages:
engines: {node: '>= 10'}
hasBin: true
'@tauri-apps/plugin-clipboard-manager@2.0.0-rc.0':
resolution: {integrity: sha512-2fS3wbRQEtorkk3Np2msJUeKCXRqLQ9sSo2FzlFdUPYNzThsu43uWCF55McGLAfltNOvXQIcQLUBf05jbBL/5w==}
'@tauri-apps/plugin-http@2.0.0-rc.1':
resolution: {integrity: sha512-j4WdTEKx0CFa6u8ubke0mo75pCrnu6XtrFtvjsh+zjuNYgMG/l0+A1woWXHm73f2Levskhs+KbKcLQA/nr8k2w==}
'@tauri-apps/plugin-os@2.0.0-rc.0':
resolution: {integrity: sha512-OWAl8mooKnGykSD4iog8WRqcnOSx0gGmTJBlEExHdFeIuOHg0Ezvd+WiVLhT9LBg7go3ibNWRWpe/ZG7YEp4Vw==}
@ -4335,6 +4350,8 @@ snapshots:
'@tauri-apps/api@2.0.0-rc.0': {}
'@tauri-apps/api@2.0.0-rc.1': {}
'@tauri-apps/cli-darwin-arm64@2.0.0-rc.3':
optional: true
@ -4378,6 +4395,14 @@ snapshots:
'@tauri-apps/cli-win32-ia32-msvc': 2.0.0-rc.3
'@tauri-apps/cli-win32-x64-msvc': 2.0.0-rc.3
'@tauri-apps/plugin-clipboard-manager@2.0.0-rc.0':
dependencies:
'@tauri-apps/api': 2.0.0-rc.0
'@tauri-apps/plugin-http@2.0.0-rc.1':
dependencies:
'@tauri-apps/api': 2.0.0-rc.1
'@tauri-apps/plugin-os@2.0.0-rc.0':
dependencies:
'@tauri-apps/api': 2.0.0-rc.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -1,6 +0,0 @@
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -26,13 +26,42 @@
"core:window:allow-set-fullscreen",
"core:webview:allow-create-webview",
"core:webview:allow-create-webview-window",
"core:webview:allow-internal-toggle-devtools",
"core:event:default",
"core:event:allow-listen",
"os:default",
"os:allow-os-type",
"os:allow-arch",
"os:allow-version",
"os:allow-hostname",
"process:default",
"process:allow-exit"
"process:allow-exit",
"clipboard-manager:allow-clear",
"clipboard-manager:allow-write-image",
"http:allow-fetch",
"http:allow-fetch-cancel",
"http:allow-fetch-read-body",
"http:allow-fetch-send",
"core:tray:default",
"core:tray:allow-get-by-id",
"shell:default",
"shell:allow-open",
{
"identifier": "http:default",
"allow": [
{
"url": "http://**"
},
{
"url": "https://**"
},
{
"url": "http://*:*"
},
{
"url": "https://*:*"
}
]
}
]
}

View File

@ -1 +1 @@
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["*"],"permissions":["core:window:default","core:window:allow-create","core:window:allow-start-dragging","core:window:allow-close","core:window:allow-hide","core:window:allow-center","core:window:allow-show","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-destroy","core:window:allow-is-focused","core:window:allow-is-fullscreen","core:window:allow-set-focus","core:window:allow-set-position","core:window:allow-scale-factor","core:window:allow-unminimize","core:window:allow-set-always-on-top","core:window:allow-set-size","core:window:allow-unmaximize","core:window:allow-set-fullscreen","core:webview:allow-create-webview","core:webview:allow-create-webview-window","core:event:default","core:event:allow-listen","os:default","os:allow-os-type","os:allow-arch","os:allow-version","process:default","process:allow-exit"]}}
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["*"],"permissions":["core:window:default","core:window:allow-create","core:window:allow-start-dragging","core:window:allow-close","core:window:allow-hide","core:window:allow-center","core:window:allow-show","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-destroy","core:window:allow-is-focused","core:window:allow-is-fullscreen","core:window:allow-set-focus","core:window:allow-set-position","core:window:allow-scale-factor","core:window:allow-unminimize","core:window:allow-set-always-on-top","core:window:allow-set-size","core:window:allow-unmaximize","core:window:allow-set-fullscreen","core:webview:allow-create-webview","core:webview:allow-create-webview-window","core:webview:allow-internal-toggle-devtools","core:event:default","core:event:allow-listen","os:default","os:allow-os-type","os:allow-arch","os:allow-version","os:allow-hostname","process:default","process:allow-exit","clipboard-manager:allow-clear","clipboard-manager:allow-write-image","http:allow-fetch","http:allow-fetch-cancel","http:allow-fetch-read-body","http:allow-fetch-send","core:tray:default","core:tray:allow-get-by-id","shell:default","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**"},{"url":"https://**"},{"url":"http://*:*"},{"url":"https://*:*"}]}]}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -2,11 +2,13 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod tray;
mod user_cmd;
use user_cmd::{get_user_info, save_user_info,default_window_icon,screenshot,audio};
use tauri::{LogicalSize, Manager};
use user_cmd::{get_user_info, save_user_info, default_window_icon, screenshot, audio};
use tauri_plugin_autostart::MacosLauncher;
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_websocket::init())
@ -19,9 +21,22 @@ fn main() {
.setup(move |app| {
app.handle().plugin(tauri_plugin_global_shortcut::Builder::new().build())?;
tray::create_tray(app.handle())?;
let os = std::env::consts::OS;
match os {
"windows" => {
// 如果是 Windows找到 label 为 "login" 的窗口并设置 decorations 为 false
let window = app.get_webview_window("login").expect("Failed to get window");
window.set_decorations(false).expect("Failed to set decorations");
let new_size = LogicalSize { width: 320.0, height: 448.0 };
window.set_size(new_size).expect("Failed to set size");
}
_ => {}
}
Ok(())
})
.invoke_handler(tauri::generate_handler![get_user_info, save_user_info,default_window_icon,screenshot,audio])
.invoke_handler(tauri::generate_handler![get_user_info, save_user_info, default_window_icon, screenshot, audio])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -4,7 +4,7 @@ use tauri::{
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
let _ = TrayIconBuilder::with_id("tray")
.tooltip("linyu")
.tooltip("HuLa")
.icon(app.default_window_icon().unwrap().clone())
.on_tray_icon_event(|tray, event| match event {
TrayIconEvent::Click {

View File

@ -9,10 +9,10 @@
"devUrl": "http://127.0.0.1:6130"
},
"bundle": {
"active": true,
"resources": [
"tray"
],
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
@ -31,7 +31,6 @@
"withGlobalTauri": true,
"windows": [
{
"title": "登录",
"label": "login",
"url": "/login",
"fullscreen": false,
@ -40,8 +39,9 @@
"width": 320,
"height": 448,
"skipTaskbar": false,
"decorations": false,
"transparent": true
"transparent": false,
"titleBarStyle": "Overlay",
"hiddenTitle": true
},
{
"label": "tray",
@ -54,6 +54,16 @@
"alwaysOnTop": true,
"skipTaskbar": true,
"decorations": false
},
{
"label": "capture",
"url": "/capture",
"fullscreen": false,
"transparent": true,
"resizable": false,
"skipTaskbar": false,
"decorations": false,
"visible": false
}
],
"security": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" width="647.63626" height="632.17383" viewBox="0 0 647.63626 632.17383" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M687.3279,276.08691H512.81813a15.01828,15.01828,0,0,0-15,15v387.85l-2,.61005-42.81006,13.11a8.00676,8.00676,0,0,1-9.98974-5.31L315.678,271.39691a8.00313,8.00313,0,0,1,5.31006-9.99l65.97022-20.2,191.25-58.54,65.96972-20.2a7.98927,7.98927,0,0,1,9.99024,5.3l32.5498,106.32Z" transform="translate(-276.18187 -133.91309)" fill="#f2f2f2"/><path d="M725.408,274.08691l-39.23-128.14a16.99368,16.99368,0,0,0-21.23-11.28l-92.75,28.39L380.95827,221.60693l-92.75,28.4a17.0152,17.0152,0,0,0-11.28028,21.23l134.08008,437.93a17.02661,17.02661,0,0,0,16.26026,12.03,16.78926,16.78926,0,0,0,4.96972-.75l63.58008-19.46,2-.62v-2.09l-2,.61-64.16992,19.65a15.01489,15.01489,0,0,1-18.73-9.95l-134.06983-437.94a14.97935,14.97935,0,0,1,9.94971-18.73l92.75-28.4,191.24024-58.54,92.75-28.4a15.15551,15.15551,0,0,1,4.40966-.66,15.01461,15.01461,0,0,1,14.32032,10.61l39.0498,127.56.62012,2h2.08008Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M398.86279,261.73389a9.0157,9.0157,0,0,1-8.61133-6.3667l-12.88037-42.07178a8.99884,8.99884,0,0,1,5.9712-11.24023l175.939-53.86377a9.00867,9.00867,0,0,1,11.24072,5.9707l12.88037,42.07227a9.01029,9.01029,0,0,1-5.9707,11.24072L401.49219,261.33887A8.976,8.976,0,0,1,398.86279,261.73389Z" transform="translate(-276.18187 -133.91309)" fill="#13987f"/><circle cx="190.15351" cy="24.95465" r="20" fill="#13987f"/><circle cx="190.15351" cy="24.95465" r="12.66462" fill="#fff"/><path d="M878.81836,716.08691h-338a8.50981,8.50981,0,0,1-8.5-8.5v-405a8.50951,8.50951,0,0,1,8.5-8.5h338a8.50982,8.50982,0,0,1,8.5,8.5v405A8.51013,8.51013,0,0,1,878.81836,716.08691Z" transform="translate(-276.18187 -133.91309)" fill="#e6e6e6"/><path d="M723.31813,274.08691h-210.5a17.02411,17.02411,0,0,0-17,17v407.8l2-.61v-407.19a15.01828,15.01828,0,0,1,15-15H723.93825Zm183.5,0h-394a17.02411,17.02411,0,0,0-17,17v458a17.0241,17.0241,0,0,0,17,17h394a17.0241,17.0241,0,0,0,17-17v-458A17.02411,17.02411,0,0,0,906.81813,274.08691Zm15,475a15.01828,15.01828,0,0,1-15,15h-394a15.01828,15.01828,0,0,1-15-15v-458a15.01828,15.01828,0,0,1,15-15h394a15.01828,15.01828,0,0,1,15,15Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M801.81836,318.08691h-184a9.01015,9.01015,0,0,1-9-9v-44a9.01016,9.01016,0,0,1,9-9h184a9.01016,9.01016,0,0,1,9,9v44A9.01015,9.01015,0,0,1,801.81836,318.08691Z" transform="translate(-276.18187 -133.91309)" fill="#13987f"/><circle cx="433.63626" cy="105.17383" r="20" fill="#13987f"/><circle cx="433.63626" cy="105.17383" r="12.18187" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -174,6 +174,7 @@ const handleAfterEnter = (el: any) => {
}
}
.menu-list {
-webkit-backdrop-filter: blur(10px);
padding: 5px;
display: flex;
flex-direction: column;

View File

@ -0,0 +1,486 @@
<template>
<div ref="canvasbox" class="canvasbox">
<canvas ref="drawCanvas" class="draw-canvas"></canvas>
<canvas ref="maskCanvas" class="mask-canvas"></canvas>
<canvas ref="imgCanvas" class="img-canvas"></canvas>
<div ref="magnifier" class="magnifier">
<canvas ref="magnifierCanvas"></canvas>
</div>
<div ref="buttonGroup" class="button-group" v-show="showButtonGroup" :style="buttonGroupStyle">
<button @click="drawImgCanvas('rect')">矩形</button>
<button @click="drawImgCanvas('circle')">圆形</button>
<button @click="drawImgCanvas('arrow')">箭头</button>
<button @click="drawImgCanvas('mosaic')">马赛克</button>
<button @click="drawImgCanvas('redo')">重做</button>
<button @click="drawImgCanvas('undo')">撤销</button>
<button @click="confirmSelection">确定</button>
<button @click="cancelSelection">取消</button>
</div>
</div>
</template>
<script setup>
import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event'
import { writeImage } from '@tauri-apps/plugin-clipboard-manager'
import { useCanvasTool } from '@/hooks/useCanvasTool'
const canvasbox = ref(null)
//
const imgCanvas = ref(null)
const imgCtx = ref(null)
//
const maskCanvas = ref(null)
const maskCtx = ref(null)
//
const drawCanvas = ref(null)
const drawCtx = ref(null)
let drawTools
//
const magnifier = ref(null)
const magnifierCanvas = ref(null)
const magnifierCtx = ref(null)
const magnifierSize = 150 //
const zoomFactor = 3 //
//
const buttonGroup = ref(null)
const showButtonGroup = ref(false) //
const buttonGroupStyle = ref({
width: 300,
height: 40
})
//
const screenConfig = ref({
startX: 0,
startY: 0,
endX: 0,
endY: 0,
scaleX: 0,
scaleY: 0,
isDrawing: false,
width: 0,
height: 0
})
//
let screenshotImage
onMounted(async () => {
await listen('capture', () => {
initCanvas()
initMagnifier()
})
})
/**
* 绘制图形
* @param {string} type - 图形类型
*/
function drawImgCanvas(type) {
if (!drawTools) return
//isDrawGraphics.value = true;
const drawableTypes = ['rect', 'circle', 'arrow', 'mosaic']
//
if (type === 'mosaic') {
drawTools.drawMosaicBrushSize(20) //
}
if (drawableTypes.includes(type)) {
drawTools.draw(type) //
} else if (type === 'redo') {
drawTools.redo() //
} else if (type === 'undo') {
drawTools.undo() //
}
}
/**
* 初始化canvas
*/
async function initCanvas() {
const canvasWidth = screen.width * window.devicePixelRatio
const canvasHeight = screen.height * window.devicePixelRatio
const config = {
x: '0',
y: '0',
width: `${canvasWidth}`,
height: `${canvasHeight}`
}
const screenshotData = await invoke('screenshot', config)
if (imgCanvas.value && maskCanvas.value) {
imgCanvas.value.width = canvasWidth
imgCanvas.value.height = canvasHeight
maskCanvas.value.width = canvasWidth
maskCanvas.value.height = canvasHeight
drawCanvas.value.width = canvasWidth
drawCanvas.value.height = canvasHeight
imgCtx.value = imgCanvas.value.getContext('2d')
maskCtx.value = maskCanvas.value.getContext('2d')
drawCtx.value = drawCanvas.value.getContext('2d', { willReadFrequently: true })
//
const { clientWidth: containerWidth, clientHeight: containerHeight } = imgCanvas.value
screenConfig.value.scaleX = canvasWidth / containerWidth
screenConfig.value.scaleY = canvasHeight / containerHeight
screenshotImage = new Image()
screenshotImage.src = `data:image/png;base64,${screenshotData}`
screenshotImage.onload = () => {
if (imgCtx.value) {
imgCtx.value.drawImage(screenshotImage, 0, 0, canvasWidth, canvasHeight)
// 绿
drawRectangle(maskCtx.value, screenConfig.value.startX, screenConfig.value.startY, canvasWidth, canvasHeight, 4)
drawTools = useCanvasTool(drawCanvas, drawCtx, imgCtx, screenConfig)
}
}
}
//
maskCanvas.value.addEventListener('mousedown', handleMaskMouseDown)
maskCanvas.value.addEventListener('mousemove', handleMaskMouseMove)
maskCanvas.value.addEventListener('mouseup', handleMaskMouseUp)
}
function handleMaskMouseDown(event) {
//
if (showButtonGroup.value) return
screenConfig.value.startX = event.offsetX * screenConfig.value.scaleX
screenConfig.value.startY = event.offsetY * screenConfig.value.scaleY
screenConfig.value.isDrawing = true
if (!screenConfig.value.isDrawing) {
drawMask()
} //
}
function handleMaskMouseMove(event) {
handleMagnifierMouseMove(event)
if (!screenConfig.value.isDrawing || !maskCtx.value) return
const mouseX = event.offsetX * screenConfig.value.scaleX
const mouseY = event.offsetY * screenConfig.value.scaleY
const width = mouseX - screenConfig.value.startX
const height = mouseY - screenConfig.value.startY
//
maskCtx.value.clearRect(0, 0, maskCanvas.value.width, maskCanvas.value.height)
//
drawMask()
//
maskCtx.value.clearRect(screenConfig.value.startX, screenConfig.value.startY, width, height)
//
drawRectangle(maskCtx.value, screenConfig.value.startX, screenConfig.value.startY, width, height)
}
function handleMaskMouseUp(event) {
if (!screenConfig.value.isDrawing) return
screenConfig.value.isDrawing = false
//
screenConfig.value.endX = event.offsetX * screenConfig.value.scaleX
screenConfig.value.endY = event.offsetY * screenConfig.value.scaleY
//
screenConfig.value.width = Math.abs(screenConfig.value.endX - screenConfig.value.startX)
screenConfig.value.height = Math.abs(screenConfig.value.endY - screenConfig.value.startY)
//
if (screenConfig.value.width > 5 && screenConfig.value.height > 5) {
//
updateButtonGroupPosition()
showButtonGroup.value = true //
}
}
//
function updateButtonGroupPosition() {
const { scaleX, scaleY, startX, startY, endX, endY } = screenConfig.value
//
const minX = Math.min(startX, endX) / scaleX
const minY = Math.min(startY, endY) / scaleY
const maxX = Math.max(startX, endX) / scaleX
const maxY = Math.max(startY, endY) / scaleY
//
const buttonGroupHeight = buttonGroupStyle.value.height
const buttonGroupWidth = buttonGroupStyle.value.width
//
const availableHeight = screen.availHeight
const availableWidth = screen.availWidth
//
let topPosition = maxY + 10 + buttonGroupHeight > availableHeight ? minY - 10 - buttonGroupHeight : maxY + 10
//
let leftPosition = maxX + buttonGroupWidth > availableWidth ? maxX - buttonGroupWidth : minX
//
if (Math.abs(maxY - minY) + buttonGroupHeight + 10 > screen.height) {
topPosition = screen.height - buttonGroupHeight - 10
}
buttonGroup.value.style.top = `${topPosition}px`
buttonGroup.value.style.left = `${leftPosition}px`
}
/**
* 绘制矩形
*/
function drawRectangle(context, x, y, width, height, lineWidth = 2) {
context.strokeStyle = 'green'
context.lineWidth = lineWidth
context.strokeRect(x, y, width, height)
drawSizeText(context, x, y, width, height)
}
/**
* 绘制矩形尺寸文本
*/
function drawSizeText(context, x, y, width, height) {
if (context) {
//
const roundedWidth = Math.round(Math.abs(width))
const roundedHeight = Math.round(Math.abs(height))
const sizeText = `${roundedWidth} x ${roundedHeight}`
//
const textX = width >= 0 ? x : x + width
const textY = height >= 0 ? y : y + height
//
context.font = '14px Arial'
context.fillStyle = 'white'
//
context.imageSmoothingEnabled = true
context.imageSmoothingQuality = 'high'
context.fillText(sizeText, textX + 5, textY - 10) //
}
}
/**
* 绘制蒙版
*/
function drawMask() {
if (maskCtx.value) {
maskCtx.value.fillStyle = 'rgba(0, 0, 0, 0.4)'
maskCtx.value.fillRect(0, 0, maskCanvas.value.width, maskCanvas.value.height)
}
}
/**
* 初始化放大镜
*/
function initMagnifier() {
if (magnifierCanvas.value) {
magnifierCanvas.value.width = magnifierSize
magnifierCanvas.value.height = magnifierSize
magnifierCtx.value = magnifierCanvas.value.getContext('2d', { willReadFrequently: true })
}
}
/**
* 放大镜事件
*/
function handleMagnifierMouseMove(event) {
const { offsetX, offsetY } = event
//
const winHeight = window.innerHeight
const winWidth = window.innerWidth
// 使
let magnifierLeft = offsetX + 20
let magnifierTop = offsetY + 20
//
if (magnifierLeft + magnifierSize > winWidth) {
magnifierLeft = winWidth - magnifierSize
}
if (magnifierTop + magnifierSize > winHeight) {
magnifierTop = winHeight - magnifierSize
}
magnifier.value.style.left = `${magnifierLeft}px`
magnifier.value.style.top = `${magnifierTop}px`
magnifier.value.style.display = 'block'
//
drawMagnifiedContent(offsetX, offsetY)
}
/**
* 绘制放大镜内容
*/
function drawMagnifiedContent(mouseX, mouseY) {
const canvasWidth = imgCanvas.value.width
const canvasHeight = imgCanvas.value.height
//
const magnifierX = Math.max(0, mouseX * window.devicePixelRatio - magnifierSize / (2 * zoomFactor))
const magnifierY = Math.max(0, mouseY * window.devicePixelRatio - magnifierSize / (2 * zoomFactor))
//
const adjustedX = Math.min(magnifierX, canvasWidth - magnifierSize / zoomFactor)
const adjustedY = Math.min(magnifierY, canvasHeight - magnifierSize / zoomFactor)
magnifierCtx.value.clearRect(0, 0, magnifierSize, magnifierSize)
//
magnifierCtx.value.drawImage(
imgCanvas.value,
adjustedX,
adjustedY,
magnifierSize / zoomFactor,
magnifierSize / zoomFactor,
0,
0,
magnifierSize,
magnifierSize
)
}
function confirmSelection() {
const { startX, startY, endX, endY } = screenConfig.value
//
const width = Math.abs(endX - startX)
const height = Math.abs(endY - startY)
//
const rectX = Math.min(startX, endX)
const rectY = Math.min(startY, endY)
const devicePixelRatio = window.devicePixelRatio || 1
imgCtx.value.scale(devicePixelRatio, devicePixelRatio)
// drawCanvas imgCanvas
imgCtx.value.drawImage(drawCanvas.value, 0, 0)
// canvas
const offscreenCanvas = document.createElement('canvas')
const offscreenCtx = offscreenCanvas.getContext('2d')
// canvas
offscreenCanvas.width = width
offscreenCanvas.height = height
if (offscreenCtx) {
// imgCanvas canvas
offscreenCtx.drawImage(
imgCanvas.value,
rectX,
rectY,
width,
height, //
0,
0,
width,
height // canvas
)
// Blob
offscreenCanvas.toBlob(async (blob) => {
if (blob) {
const arrayBuffer = await blob.arrayBuffer()
const buffer = new Uint8Array(arrayBuffer)
writeImage(buffer).then(() => {
cancelSelection()
})
}
}, 'image/png')
}
showButtonGroup.value = false //
}
function cancelSelection() {
console.log('取消选区')
//
showButtonGroup.value = false //
}
</script>
<style lang="scss" scoped>
.canvasbox {
width: 100vw;
height: 100vh;
position: relative;
background-color: transparent;
}
canvas {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.magnifier {
position: absolute;
pointer-events: none;
width: 150px;
height: 150px;
border: 2px solid #ccc;
border-radius: 50%;
overflow: hidden;
display: none;
}
.img-canvas {
z-index: 0;
}
.mask-canvas {
z-index: 1;
}
.draw-canvas {
z-index: 1;
pointer-events: none;
/* 确保事件穿透到下面的 canvas */
}
.magnifier canvas {
display: block;
z-index: 2;
}
.button-group {
position: absolute;
display: flex;
gap: 10px;
//transform: translate(-50%, 0);
background: white;
border: 1px solid #ccc;
border-radius: 5px;
padding: 5px;
z-index: 3;
button {
padding: 5px 10px;
cursor: pointer;
}
}
</style>

View File

@ -10,7 +10,14 @@
@paste="handlePaste($event, messageInputDom)"
@input="handleInput"
@keydown.exact.enter="inputKeyDown"
@keydown.exact.meta.enter="inputKeyDown"
@keydown.exact.ctrl.enter="inputKeyDown"></div>
<span
v-if="isEntering"
@click.stop="messageInputDom.focus()"
class="absolute select-none top-8px left-6px w-fit text-(12px #777)">
聊点什么吧...
</span>
</n-scrollbar>
</ContextMenu>
@ -49,38 +56,51 @@
</div>
<!-- 发送按钮 -->
<n-config-provider :theme="lightTheme">
<n-button-group size="small" class="pr-20px">
<n-button
color="#13987f"
:disabled="msgInput.length === 0 || msgInput.trim() === ''"
class="w-65px"
@click="send">
发送
</n-button>
<n-button color="#13987f" class="p-[0_6px]">
<template #icon>
<n-config-provider :theme="themes.content === ThemeEnum.DARK ? darkTheme : lightTheme">
<n-popselect
v-model:show="arrow"
v-model:value="chatKey"
:options="sendOptions"
trigger="click"
placement="top-end">
<svg @click="arrow = true" v-if="!arrow" class="w-22px h-22px mt-2px outline-none">
<use href="#down"></use>
</svg>
<svg @click="arrow = false" v-else class="w-22px h-22px mt-2px outline-none"><use href="#up"></use></svg>
</n-popselect>
</n-config-provider>
</template>
</n-button>
</n-button-group>
</n-config-provider>
<n-flex align="center" justify="space-between" :size="12">
<n-flex align="center" :size="4" class="text-(12px #777) tracking-1">
<svg class="size-12px"><use href="#Enter"></use></svg>
发送/
<n-flex align="center" :size="0">
{{ type() === 'macos' ? MacOsKeyEnum['⌘'] : WinKeyEnum.ctrl }}
<svg class="size-12px"><use href="#Enter"></use></svg>
</n-flex>
换行
</n-flex>
<n-config-provider :theme="lightTheme">
<n-button-group size="small" class="pr-20px">
<n-button
color="#13987f"
:disabled="msgInput.length === 0 || msgInput.trim() === ''"
class="w-65px"
@click="send">
发送
</n-button>
<n-button color="#13987f" class="p-[0_6px]">
<template #icon>
<n-config-provider :theme="themes.content === ThemeEnum.DARK ? darkTheme : lightTheme">
<n-popselect
v-model:show="arrow"
v-model:value="chatKey"
:options="sendOptions"
trigger="click"
placement="top-end">
<svg @click="arrow = true" v-if="!arrow" class="w-22px h-22px mt-2px outline-none">
<use href="#down"></use>
</svg>
<svg @click="arrow = false" v-else class="w-22px h-22px mt-2px outline-none">
<use href="#up"></use>
</svg>
</n-popselect>
</n-config-provider>
</template>
</n-button>
</n-button-group>
</n-config-provider>
</n-flex>
</template>
<script setup lang="ts">
import { lightTheme, darkTheme, VirtualListInst } from 'naive-ui'
import { MittEnum, RoomTypeEnum, ThemeEnum } from '@/enums'
import { MacOsKeyEnum, MittEnum, RoomTypeEnum, ThemeEnum, WinKeyEnum } from '@/enums'
import Mitt from '@/utils/Bus.ts'
import { CacheUserItem, MockItem } from '@/services/types.ts'
import { emit, listen } from '@tauri-apps/api/event'
@ -90,6 +110,7 @@ import { sendOptions } from '@/views/homeWindow/more/settings/config.ts'
import { useMsgInput } from '@/hooks/useMsgInput.ts'
import { useCommon } from '@/hooks/useCommon.ts'
import { onKeyStroke } from '@vueuse/core'
import { type } from '@tauri-apps/plugin-os'
const settingStore = setting()
const { themes } = storeToRefs(settingStore)
@ -100,6 +121,10 @@ const messageInputDom = ref()
const activeItem = ref(inject('activeItem') as MockItem)
/** 虚拟列表 */
const virtualListInst = ref<VirtualListInst>()
/** 是否处于输入状态 */
const isEntering = computed(() => {
return msgInput.value === ''
})
const { handlePaste } = useCommon()
/** 引入useMsgInput的相关方法 */
const { inputKeyDown, handleAit, handleInput, send, personList, ait, msgInput, chatKey, menuList, selectedAitKey } =

View File

@ -16,6 +16,7 @@
padding: 0;
background: var(--bg-emoji);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 2px 2px 12px 2px var(--box-shadow-color);
border: 1px solid var(--box-shadow-color);
">
@ -33,7 +34,7 @@
<n-popover trigger="hover" :show-arrow="false" placement="bottom">
<template #trigger>
<div class="flex-center gap-2px mr-12px">
<svg><use href="#screenshot"></use></svg>
<svg @click="handleCap()"><use href="#screenshot"></use></svg>
<svg style="width: 14px; height: 14px"><use href="#down"></use></svg>
</div>
</template>
@ -93,6 +94,8 @@
import { useFileDialog } from '@vueuse/core'
import { LimitEnum, MsgEnum } from '@/enums'
import { useCommon } from '@/hooks/useCommon.ts'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { emit } from '@tauri-apps/api/event'
const { open, onChange } = useFileDialog()
const MsgInputRef = ref()
@ -114,6 +117,11 @@ const emojiHandle = (item: string) => {
triggerInputEvent(msgInputDom.value)
}
const handleCap = async () => {
WebviewWindow.getByLabel('capture')?.show()
await emit('capture', true)
}
onChange((files) => {
if (!files) return
if (files.length > LimitEnum.COM_COUNT) {

View File

@ -99,7 +99,16 @@
<!-- 弹出框 -->
<n-modal v-model:show="modalShow" class="w-350px rounded-8px">
<div class="bg-[--bg-popover] w-360px h-full p-6px box-border flex flex-col">
<svg @click="modalShow = false" class="size-12px ml-a cursor-pointer select-none">
<div
v-if="type() === 'macos'"
@click="modalShow = false"
class="mac-close z-999 size-13px shadow-inner bg-#ed6a5eff rounded-50% select-none absolute left-6px">
<svg class="hidden size-7px color-#000 font-bold select-none absolute top-3px left-3px">
<use href="#close"></use>
</svg>
</div>
<svg v-if="type() === 'windows'" @click="modalShow = false" class="size-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
</svg>
<div class="flex flex-col gap-30px p-[22px_10px_10px_22px] select-none">
@ -123,6 +132,7 @@ import { IsAllUserEnum, SessionItem } from '@/services/types.ts'
import { useDisplayMedia } from '@vueuse/core'
import { EventEnum } from '@/enums'
import { emit, listen } from '@tauri-apps/api/event'
import { type } from '@tauri-apps/plugin-os'
// 使useDisplayMedia
const { stream, start, stop } = useDisplayMedia()

View File

@ -284,7 +284,16 @@
<!-- 弹出框 -->
<n-modal v-model:show="modalShow" class="w-350px border-rd-8px">
<div class="bg-[--bg-popover] w-360px h-full p-6px box-border flex flex-col">
<svg @click="modalShow = false" class="w-12px h-12px ml-a cursor-pointer select-none">
<div
v-if="type() === 'macos'"
@click="modalShow = false"
class="mac-close z-999 size-13px shadow-inner bg-#ed6a5eff rounded-50% select-none absolute left-6px">
<svg class="hidden size-7px color-#000 font-bold select-none absolute top-3px left-3px">
<use href="#close"></use>
</svg>
</div>
<svg v-if="type() === 'windows'" @click="modalShow = false" class="w-12px h-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
</svg>
<div class="flex flex-col gap-30px p-[22px_10px_10px_22px] select-none">
@ -344,6 +353,7 @@ import { storeToRefs } from 'pinia'
import { formatTimestamp } from '@/utils/ComputedTime.ts'
import { useUserInfo, useBadgeInfo } from '@/hooks/useCached.ts'
import { useChatStore } from '@/stores/chat.ts'
import { type } from '@tauri-apps/plugin-os'
const { activeItem } = defineProps<{
activeItem: SessionItem

View File

@ -1,47 +1,50 @@
<template>
<!-- user-select: none让元素不可以选中 -->
<div data-tauri-drag-region class="flex justify-end select-none">
<!-- 固定在最顶层 -->
<div v-if="topWinLabel !== void 0" @click="handleAlwaysOnTop" class="hover-box">
<n-popover trigger="hover">
<template #trigger>
<svg v-if="alwaysOnTopStatus" class="size-14px color-[--action-bar-icon-color] outline-none cursor-pointer">
<use href="#onTop"></use>
</svg>
<svg v-else class="size-16px color-[--action-bar-icon-color] outline-none cursor-pointer">
<use href="#notOnTop"></use>
</svg>
</template>
<span v-if="alwaysOnTopStatus">取消置顶</span>
<span v-else>置顶</span>
</n-popover>
</div>
<!-- 收缩页面 -->
<div v-if="shrink" @click="shrinkWindow" class="hover-box">
<svg class="size-16px color-[--action-bar-icon-color] cursor-pointer"><use href="#left-bar"></use></svg>
</div>
<!-- 最小化 -->
<div v-if="minW" @click="appWindow.minimize()" class="hover-box">
<svg class="size-24px color-[--action-bar-icon-color] opacity-66 cursor-pointer">
<use href="#maximize"></use>
</svg>
</div>
<!-- 最大化 -->
<div v-if="maxW" @click="restoreWindow" class="hover-box">
<svg v-show="!windowMaximized" class="size-18px color-[--action-bar-icon-color] cursor-pointer">
<use href="#rectangle-small"></use>
</svg>
<svg v-show="windowMaximized" class="size-16px color-[--action-bar-icon-color] cursor-pointer">
<use href="#internal-reduction"></use>
</svg>
</div>
<!-- 关闭窗口 -->
<div v-if="closeW" @click="handleCloseWin" class="action-close rounded-rt-8px">
<svg class="size-14px color-[--action-bar-icon-color] cursor-pointer">
<use href="#close"></use>
</svg>
</div>
<!-- user-select: none让元素不可以选中-->
<div
data-tauri-drag-region
:class="osType === 'windows' ? 'flex justify-end select-none' : 'h-24px select-none w-full'">
<template v-if="osType === 'windows'">
<!-- 固定在最顶层 -->
<div v-if="topWinLabel !== void 0" @click="handleAlwaysOnTop" class="hover-box">
<n-popover trigger="hover">
<template #trigger>
<svg v-if="alwaysOnTopStatus" class="size-14px color-[--action-bar-icon-color] outline-none cursor-pointer">
<use href="#onTop"></use>
</svg>
<svg v-else class="size-16px color-[--action-bar-icon-color] outline-none cursor-pointer">
<use href="#notOnTop"></use>
</svg>
</template>
<span v-if="alwaysOnTopStatus">取消置顶</span>
<span v-else>置顶</span>
</n-popover>
</div>
<!-- 收缩页面 -->
<div v-if="shrink" @click="shrinkWindow" class="hover-box">
<svg class="size-16px color-[--action-bar-icon-color] cursor-pointer"><use href="#left-bar"></use></svg>
</div>
<!-- 最小化 -->
<div v-if="minW" @click="appWindow.minimize()" class="hover-box">
<svg class="size-24px color-[--action-bar-icon-color] opacity-66 cursor-pointer">
<use href="#maximize"></use>
</svg>
</div>
<!-- 最大化 -->
<div v-if="maxW" @click="restoreWindow" class="hover-box">
<svg v-show="!windowMaximized" class="size-18px color-[--action-bar-icon-color] cursor-pointer">
<use href="#rectangle-small"></use>
</svg>
<svg v-show="windowMaximized" class="size-16px color-[--action-bar-icon-color] cursor-pointer">
<use href="#internal-reduction"></use>
</svg>
</div>
<!-- 关闭窗口 -->
<div v-if="closeW" @click="handleCloseWin" class="action-close rounded-rt-8px">
<svg class="size-14px color-[--action-bar-icon-color] cursor-pointer">
<use href="#close"></use>
</svg>
</div>
</template>
<!-- 是否退到托盘提示框 -->
<n-modal v-if="!tips.notTips" v-model:show="tipsRef.show" class="rounded-8px">
<div class="bg-[--bg-popover] w-290px h-full p-6px box-border flex flex-col">
@ -84,6 +87,7 @@ import { CloseBxEnum, EventEnum, MittEnum } from '@/enums'
import { storeToRefs } from 'pinia'
import { PersistedStateOptions } from 'pinia-plugin-persistedstate'
import { exit } from '@tauri-apps/plugin-process'
import { type } from '@tauri-apps/plugin-os'
const appWindow = WebviewWindow.getCurrent()
/**
@ -234,7 +238,7 @@ const handleCloseWin = async () => {
// resize
onMounted(() => {
window.addEventListener('resize', handleResize)
osType.value = 'macos'
osType.value = type()
})
onUnmounted(() => {

View File

@ -240,3 +240,31 @@ export enum ModalEnum {
/** 检查更新弹窗 */
CHECK_UPDATE
}
/** MacOS键盘映射 */
export enum MacOsKeyEnum {
'⌘' = '⌘',
'⌥' = '⌥',
'⇧' = '⇧'
}
/** Windows键盘映射 */
export enum WinKeyEnum {
ctrl = 'Ctrl',
win = 'Win',
alt = 'Alt'
}
/** 插件状态 */
export enum PluginEnum {
/** 已安装 */
INSTALLED,
/** 下载中 */
DOWNLOADING,
/** 未安装 */
NOT_INSTALLED,
/** 卸载中 */
UNINSTALLING,
/** 可更新 */
CAN_UPDATE
}

277
src/hooks/useCanvasTool.ts Normal file
View File

@ -0,0 +1,277 @@
import { ref, onBeforeUnmount } from 'vue'
export function useCanvasTool(drawCanvas: any, drawCtx: any, imgCtx: any, screenConfig: any) {
const drawConfig = ref({
startX: 0,
startY: 0,
endX: 0,
endY: 0,
scaleX: 1,
scaleY: 1,
lineWidth: 2, // 线宽
color: 'red', // 颜色
isDrawing: false, // 正在绘制
brushSize: 10, // 马赛克大小
actions: [], // 存储绘制动作
undoStack: []
})
const currentTool = ref('')
const draw = (type: string) => {
const { clientWidth: containerWidth, clientHeight: containerHeight } = drawCanvas.value
drawConfig.value.scaleX = (screen.width * window.devicePixelRatio) / containerWidth
drawConfig.value.scaleY = (screen.height * window.devicePixelRatio) / containerHeight
currentTool.value = type
startListen()
}
onBeforeUnmount(() => {
closeListen()
})
const handleMouseDown = (event: MouseEvent) => {
const { offsetX, offsetY } = event
drawConfig.value.isDrawing = true
// 限制起点坐标在框选矩形区域内
drawConfig.value.startX = Math.min(
Math.max(offsetX * drawConfig.value.scaleX, screenConfig.value.startX),
screenConfig.value.endX
)
drawConfig.value.startY = Math.min(
Math.max(offsetY * drawConfig.value.scaleY, screenConfig.value.startY),
screenConfig.value.endY
)
}
const handleMouseMove = (event: MouseEvent) => {
const { offsetX, offsetY } = event
if (!drawConfig.value.isDrawing || !drawCtx.value) return
// 限制绘制区域在框选矩形区域内
const limitedX = Math.min(
Math.max(offsetX * drawConfig.value.scaleX, screenConfig.value.startX),
screenConfig.value.endX
)
const limitedY = Math.min(
Math.max(offsetY * drawConfig.value.scaleY, screenConfig.value.startY),
screenConfig.value.endY
)
drawConfig.value.endX = limitedX
drawConfig.value.endY = limitedY
// 清除非马赛克的情况下重新绘制
if (currentTool.value !== 'mosaic') {
drawCtx.value.clearRect(0, 0, drawCanvas.value.width, drawCanvas.value.height)
drawConfig.value.actions.forEach((action) => {
drawCtx.value.putImageData(action, 0, 0)
})
}
const x = Math.min(drawConfig.value.startX, drawConfig.value.endX)
const y = Math.min(drawConfig.value.startY, drawConfig.value.endY)
const width = Math.abs(drawConfig.value.startX - drawConfig.value.endX)
const height = Math.abs(drawConfig.value.startY - drawConfig.value.endY)
switch (currentTool.value) {
case 'rect':
drawRectangle(drawCtx.value, x, y, width, height)
break
case 'circle':
drawCircle(
drawCtx.value,
drawConfig.value.startX,
drawConfig.value.startY,
drawConfig.value.endX,
drawConfig.value.endY
)
break
case 'arrow':
drawArrow(
drawCtx.value,
drawConfig.value.startX,
drawConfig.value.startY,
drawConfig.value.endX,
drawConfig.value.endY
)
break
case 'mosaic':
drawMosaic(drawCtx.value, limitedX, limitedY, drawConfig.value.brushSize)
break
default:
break
}
}
const handleMouseUp = () => {
// const { offsetX, offsetY } = event;
drawConfig.value.isDrawing = false
drawCtx.value.drawImage(drawCanvas.value!, 0, 0, drawCanvas.value.width, drawCanvas.value.height)
saveAction()
}
const drawRectangle = (context: any, x: any, y: any, width: any, height: any) => {
context.strokeStyle = drawConfig.value.color
context.lineWidth = drawConfig.value.lineWidth
context.strokeRect(x, y, width, height)
}
const drawCircle = (context: any, startX: any, startY: any, endX: any, endY: any) => {
// 限制圆形的绘制范围在框选矩形区域内
const limitedEndX = Math.min(Math.max(endX, screenConfig.value.startX), screenConfig.value.endX)
const limitedEndY = Math.min(Math.max(endY, screenConfig.value.startY), screenConfig.value.endY)
// 计算半径,保证半径不会超过限定矩形的边界
const deltaX = limitedEndX - startX
const deltaY = limitedEndY - startY
// 检查圆形是否会超出矩形区域的边界
const maxRadiusX = Math.min(startX - screenConfig.value.startX, screenConfig.value.endX - startX)
const maxRadiusY = Math.min(startY - screenConfig.value.startY, screenConfig.value.endY - startY)
const maxRadius = Math.min(maxRadiusX, maxRadiusY)
// 使用 min 函数确保半径不会超过限定范围
const radius = Math.min(Math.sqrt(deltaX * deltaX + deltaY * deltaY), maxRadius)
// 绘制圆形
context.strokeStyle = drawConfig.value.color
context.lineWidth = drawConfig.value.lineWidth
context.beginPath()
context.arc(startX, startY, radius, 0, Math.PI * 2)
context.stroke()
}
const drawArrow = (context: any, fromX: any, fromY: any, toX: any, toY: any) => {
const headLength = 15 // 箭头的长度
const angle = Math.atan2(toY - fromY, toX - fromX) // 算出箭头的角度
context.strokeStyle = drawConfig.value.color
context.lineWidth = drawConfig.value.lineWidth
// 绘制箭头的主线
context.beginPath()
context.moveTo(fromX, fromY)
context.lineTo(toX, toY)
context.stroke()
// 计算箭头三角形的三个角
context.beginPath()
context.moveTo(toX, toY)
context.lineTo(toX - headLength * Math.cos(angle - Math.PI / 6), toY - headLength * Math.sin(angle - Math.PI / 6))
context.lineTo(toX - headLength * Math.cos(angle + Math.PI / 6), toY - headLength * Math.sin(angle + Math.PI / 6))
context.closePath()
// 填充箭头三角形
context.fillStyle = drawConfig.value.color
context.fill()
}
// 设置马赛克画笔大小
const drawMosaicBrushSize = (size: any) => {
drawConfig.value.brushSize = size
}
// 实时马赛克涂抹
const drawMosaic = (context: any, x: any, y: any, size: any) => {
const imageData = imgCtx.value.getImageData(x - size, y - size, size, size)
const blurredData = blurImageData(imageData, size)
context.putImageData(blurredData, x - size, y - size)
}
const blurImageData = (imageData: ImageData, size: any): ImageData => {
const data = imageData.data
const width = imageData.width
const height = imageData.height
const radius = size / 2 // 模糊半径
const tempData = new Uint8ClampedArray(data) // 用于保存原始图像数据
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = (y * width + x) * 4
let r = 0,
g = 0,
b = 0,
a = 0
let count = 0
for (let ky = -radius; ky <= radius; ky++) {
for (let kx = -radius; kx <= radius; kx++) {
const newX = x + kx
const newY = y + ky
if (newX >= 0 && newX < width && newY >= 0 && newY < height) {
const newIndex = (newY * width + newX) * 4
r += tempData[newIndex]
g += tempData[newIndex + 1]
b += tempData[newIndex + 2]
a += tempData[newIndex + 3]
count++
}
}
}
data[index] = r / count
data[index + 1] = g / count
data[index + 2] = b / count
data[index + 3] = a / count
}
}
return imageData
}
const saveAction = () => {
const imageData = drawCtx.value.getImageData(0, 0, drawCanvas.value.width, drawCanvas.value.height)
drawConfig.value.actions.push(imageData as never)
drawConfig.value.undoStack = [] // 清空撤销堆栈
}
const undo = () => {
closeListen()
if (drawConfig.value.actions.length > 0) {
drawConfig.value.undoStack.push(drawConfig.value.actions.pop() as never)
drawCtx.value.clearRect(0, 0, drawCanvas.value.width, drawCanvas.value.height)
if (drawConfig.value.actions.length > 0) {
drawCtx.value.putImageData(drawConfig.value.actions[drawConfig.value.actions.length - 1], 0, 0)
}
}
}
const redo = () => {
closeListen()
if (drawConfig.value.undoStack.length > 0) {
const imageData = drawConfig.value.undoStack.pop()
drawConfig.value.actions.push(imageData as never)
drawCtx.value.putImageData(imageData, 0, 0)
}
}
const startListen = () => {
document.addEventListener('mousedown', handleMouseDown)
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
const closeListen = () => {
document.removeEventListener('mousedown', handleMouseDown)
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
return {
draw,
drawMosaicBrushSize,
drawRectangle,
drawCircle,
drawArrow,
undo,
redo
}
}

View File

@ -12,6 +12,7 @@ import { useGlobalStore } from '@/stores/global.ts'
import { useChatStore } from '@/stores/chat.ts'
import { useUserInfo } from '@/hooks/useCached.ts'
import { useCachedStore } from '@/stores/cached.ts'
import { type } from '@tauri-apps/plugin-os'
export const useMsgInput = (messageInputDom: Ref) => {
const chatStore = useChatStore()
@ -258,14 +259,30 @@ export const useMsgInput = (messageInputDom: Ref) => {
/** input的keydown事件 */
const inputKeyDown = (e: KeyboardEvent) => {
console.log(chat.value.sendKey)
const isWindows = type() === 'windows'
const isEnterKey = e.key === 'Enter'
const isCtrlOrMetaKey = isWindows ? e.ctrlKey : e.metaKey
const sendKeyIsEnter = chat.value.sendKey === 'Enter'
const sendKeyIsCtrlEnter = chat.value.sendKey === `${isWindows ? 'Ctrl' : '⌘'}+Enter`
// 如果当前的系统是mac我需要判断当前的chat.value.sendKey是否是Enter再判断当前是否是按下⌘+Enter
if (!isWindows && chat.value.sendKey === 'Enter' && e.metaKey && e.key === 'Enter') {
// 就进行换行操作
e.preventDefault()
insertNode(MsgEnum.TEXT, '\n')
triggerInputEvent(messageInputDom.value)
}
if (msgInput.value === '' || msgInput.value.trim() === '' || ait.value) {
e?.preventDefault()
return
}
if (
(chat.value.sendKey === 'Enter' && e.key === 'Enter' && !e.ctrlKey) ||
(chat.value.sendKey === 'Ctrl+Enter' && e.ctrlKey && e.key === 'Enter')
) {
if (!isWindows && e.ctrlKey && isEnterKey && sendKeyIsEnter) {
e?.preventDefault()
return
}
if ((sendKeyIsEnter && isEnterKey && !isCtrlOrMetaKey) || (sendKeyIsCtrlEnter && isCtrlOrMetaKey && isEnterKey)) {
e?.preventDefault()
send()
}

View File

@ -1,6 +1,7 @@
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { EventEnum } from '@/enums'
import { LogicalSize } from '@tauri-apps/api/dpi'
import { type } from '@tauri-apps/plugin-os'
export const useWindow = () => {
/**
@ -43,8 +44,10 @@ export const useWindow = () => {
minHeight: minH,
minWidth: minW,
skipTaskbar: false,
decorations: false,
transparent: true
decorations: type() !== 'windows',
transparent: false,
titleBarStyle: 'overlay', // mac覆盖标签栏
hiddenTitle: true // mac隐藏标题栏
})
await webview.once('tauri://created', async () => {

View File

@ -15,8 +15,8 @@ export const useWindowState = (label: string) => {
await emit(EventEnum.WIN_CLOSE, e)
})
// 监听窗口关闭事件,当窗口是非正常关闭的时候触发
win?.onCloseRequested(async (e) => {
await emit(EventEnum.WIN_CLOSE, e)
win?.onCloseRequested(async () => {
await emit(EventEnum.WIN_CLOSE, WebviewWindow.getCurrent().label)
})
})

View File

@ -1,5 +1,6 @@
<template>
<main
data-tauri-drag-region
id="center"
:class="{ 'rounded-r-8px': shrinkStatus }"
class="resizable select-none flex flex-col border-r-(1px solid [--line-color])"
@ -23,45 +24,49 @@
:current-label="appWindow.label" />
<!-- 顶部搜索栏 -->
<header
style="box-shadow: var(--shadow-enabled) 4px 4px var(--box-shadow-color)"
class="mt-30px w-full h-40px flex flex-col items-center border-b-(1px solid [--line-color])">
<header class="mt-30px w-full h-40px flex flex-col items-center border-b-(1px solid [--line-color])">
<div class="flex-center gap-5px w-full pr-16px pl-16px box-border">
<n-input
id="search"
@focus="() => router.push('/searchDetails')"
@focus="() => handleSearchFocus()"
@blur="() => (searchText = '搜索')"
class="rounded-6px w-full relative text-12px"
style="background: var(--search-bg-color)"
:maxlength="20"
clearable
size="small"
placeholder="搜索">
:placeholder="searchText">
<template #prefix>
<svg class="w-12px h-12px"><use href="#search"></use></svg>
</template>
</n-input>
<n-button @click="addPanels.show = !addPanels.show" size="small" secondary style="padding: 0 5px">
<template #icon>
<svg class="w-24px h-24px"><use href="#plus"></use></svg>
</template>
</n-button>
<!-- 添加面板 -->
<div v-if="addPanels.show" class="add-item">
<div class="menu-list">
<div v-for="(item, index) in addPanels.list" :key="index">
<div class="menu-item" @click="() => item.click()">
<svg><use :href="`#${item.icon}`"></use></svg>
{{ item.label }}
<n-popover v-model:show="addPanels.show" style="padding: 0" :show-arrow="false" trigger="click">
<template #trigger>
<n-button size="small" secondary style="padding: 0 5px">
<template #icon>
<svg class="w-24px h-24px"><use href="#plus"></use></svg>
</template>
</n-button>
</template>
<div @click.stop="addPanels.show = false" class="add-item">
<div class="menu-list">
<div v-for="(item, index) in addPanels.list" :key="index">
<div class="menu-item" @click="() => item.click()">
<svg><use :href="`#${item.icon}`"></use></svg>
{{ item.label }}
</div>
</div>
</div>
</div>
</div>
</n-popover>
</div>
</header>
<!-- 列表 -->
<div id="centerList" class="h-full">
<div id="centerList" class="h-full" :class="{ 'shadow-inner': page.shadow }">
<router-view />
</div>
</main>
@ -73,7 +78,11 @@ import router from '@/router'
import { MittEnum } from '@/enums'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { useWindowSize } from '@vueuse/core'
import { setting } from '@/stores/setting.ts'
import { storeToRefs } from 'pinia'
const settingStore = setting()
const { page } = storeToRefs(settingStore)
const appWindow = WebviewWindow.getCurrent()
/** 设置最小宽度 */
const minWidth = 160
@ -87,6 +96,8 @@ const { width } = useWindowSize()
const isDrag = ref(true)
/** 当前消息 */
const currentMsg = ref()
/** 搜索框文字 */
const searchText = ref('搜索')
/** 添加面板是否显示 */
const addPanels = ref({
show: false,
@ -131,6 +142,11 @@ watchEffect(() => {
}
})
const handleSearchFocus = () => {
router.push('/searchDetails')
searchText.value = ''
}
const closeMenu = (event: Event) => {
const e = event.target as HTMLInputElement
const route = router.currentRoute.value.path
@ -138,9 +154,6 @@ const closeMenu = (event: Event) => {
if (!e.matches('#search, #search *, #centerList *, #centerList') && route === '/searchDetails') {
router.go(-1)
}
if (!e.matches('.add-item')) {
addPanels.value.show = false
}
}
/** 定义一个函数,在鼠标拖动时调用 */

View File

@ -23,7 +23,7 @@
.add-item {
@include menu-item-style(absolute);
top: 60px;
right: 30px;
top: 36px;
right: 10px;
@include menu-list();
}

View File

@ -45,6 +45,26 @@
</svg>
</n-badge>
</div>
<!-- (独立)菜单选项 -->
<n-popover style="padding: 8px; margin-left: 4px" :show-arrow="false" trigger="hover" placement="right">
<template #trigger>
<svg class="size-22px top-action">
<use href="#menu"></use>
</svg>
</template>
<n-flex
@click="menuShow = true"
class="p-[6px_10px] rounded-4px cursor-pointer hover:bg-[--setting-item-line]"
align="center"
justify="space-between"
:size="10">
<svg class="size-16px">
<use href="#settings"></use>
</svg>
<p class="select-none">插件管理</p>
</n-flex>
</n-popover>
</header>
<!-- 下部分操作栏 -->
@ -90,31 +110,38 @@
</n-badge>
</div>
<svg
:class="{ 'color-#13987f': settingShow }"
class="more size-22px relative"
@click="settingShow = !settingShow">
<use :href="settingShow ? '#hamburger-button-action' : '#hamburger-button'"></use>
</svg>
<!-- 更多选项面板 -->
<div v-if="settingShow" class="setting-item">
<div class="menu-list">
<div v-for="(item, index) in moreList" :key="index">
<div class="menu-item" @click="() => item.click()">
<svg><use :href="`#${item.icon}`"></use></svg>
{{ item.label }}
<n-popover v-model:show="settingShow" style="padding: 0" :show-arrow="false" trigger="click">
<template #trigger>
<svg
:class="{ 'color-#13987f': settingShow }"
class="more size-22px relative"
@click="settingShow = !settingShow">
<use :href="settingShow ? '#hamburger-button-action' : '#hamburger-button'"></use>
</svg>
</template>
<div class="setting-item">
<div class="menu-list">
<div v-for="(item, index) in moreList" :key="index">
<div class="menu-item" @click="() => item.click()">
<svg><use :href="`#${item.icon}`"></use></svg>
{{ item.label }}
</div>
</div>
</div>
</div>
</div>
</n-popover>
</footer>
</div>
<DefinePlugins :show="menuShow" @close="(e) => (menuShow = e)" />
</template>
<script setup lang="ts">
import { itemsBottom, itemsTop, moreList } from '../config.tsx'
import { leftHook } from '../hook.ts'
import DefinePlugins from './DefinePlugins.vue'
const menuShow = ref(false)
const dotShow = ref(false)
const { activeUrl, openWindowsList, settingShow, tipShow, pageJumps } = leftHook()
@ -135,4 +162,8 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
@import '../style';
.setting-item {
left: 24px;
bottom: -40px;
}
</style>

View File

@ -0,0 +1,283 @@
<template>
<!-- 弹出框 -->
<n-modal v-model:show="isShow" :mask-closable="false" class="w-450px border-rd-8px">
<div class="bg-[--bg-popover] h-full box-border flex flex-col">
<!-- 顶部图片加上操作栏 -->
<div class="h-140px relative w-full p-6px box-border">
<img
class="absolute blur-6px rounded-t-6px z-1 top-0 left-0 w-full h-140px object-cover"
src="@/assets/img/dispersion-bg.png"
alt="" />
<img
class="absolute rounded-t-6px z-2 top-0 left-0 w-full h-140px object-cover"
src="@/assets/img/dispersion-bg.png"
alt="" />
<div
v-if="type() === 'macos'"
@click="handleClose"
class="mac-close z-10 relative size-13px shadow-inner bg-#ed6a5eff rounded-50% select-none">
<svg class="hidden size-7px color-#000 font-bold select-none absolute top-3px left-3px">
<use href="#close"></use>
</svg>
</div>
<svg
v-if="type() === 'windows'"
@click="handleClose"
class="z-10 w-12px h-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
</svg>
</div>
<n-flex :size="4" align="center" class="p-18px">
<p class="text-(16px [--text-color])">插件管理</p>
<div class="ml-6px p-[4px_8px] size-fit bg-[--bate-bg] rounded-8px text-(12px [--bate-color] center)">Beta</div>
</n-flex>
<n-scrollbar style="max-height: 320px">
<n-flex :size="26" class="z-10 p-[4px_18px] bg-#cc w-full h-280px">
<template v-for="(plugin, index) in plugins" :key="index">
<n-flex :size="12">
<Transition name="fade" mode="out-in">
<!-- 未安装和下载中状态 -->
<n-flex
v-if="plugin.state === PluginEnum.NOT_INSTALLED || plugin.state === PluginEnum.DOWNLOADING"
vertical
justify="center"
align="center"
:size="8"
class="box bg-#f1f1f1">
<svg class="size-38px color-#555"><use :href="`#${plugin.icon}`"></use></svg>
<p class="text-(12px #666)">{{ plugin.title }}</p>
<n-flex
@click="handleState(plugin)"
class="relative rounded-22px border-(1px solid #4C77BD)"
:class="[
plugin.state === PluginEnum.DOWNLOADING ? 'downloading' : 'bg-#e0e9fc size-fit p-[4px_8px]'
]">
<div
:style="{
width: plugin.state === PluginEnum.DOWNLOADING ? `${plugin.progress * 0.8}px` : 'auto'
}"
:class="[
plugin.progress < 100 ? 'rounded-l-24px rounded-r-0' : 'rounded-24px',
plugin.progress > 0 ? 'h-18px border-(1px solid transparent)' : 'h-20px'
]"
v-if="plugin.state === PluginEnum.DOWNLOADING"
class="bg-#8CA9F4">
<p class="absolute-center text-(12px #4C77BD)">{{ plugin.progress }}%</p>
</div>
<p v-else class="text-(12px #4C77BD center)">安装</p>
</n-flex>
<!-- 闪光效果 -->
<div class="flash"></div>
</n-flex>
<!-- 可卸载状态 -->
<n-flex v-else vertical justify="center" align="center" :size="8" class="box colorful">
<svg class="size-38px color-#555"><use :href="`#${plugin.iconActive || plugin.icon}`"></use></svg>
<p class="text-(12px #666)">{{ plugin.title }}</p>
<n-flex
v-if="plugin.state === PluginEnum.UNINSTALLING"
class="relative rounded-22px border-(1px solid #c14053) bg-#f6dfe3 p-[4px_8px]">
<p class="text-(12px #c14053 center)">卸载中</p>
</n-flex>
<n-flex
v-else
class="relative rounded-22px border-(1px solid #4C77BD) bg-#e0e9fc size-fit p-[4px_8px]">
<p class="text-(12px #4C77BD center)">{{ plugin.version }}</p>
</n-flex>
<!-- 闪光效果 -->
<div class="flash"></div>
<!-- 插件操作 -->
<n-popover
v-model:show="plugin.isAction"
style="padding: 0"
:show-arrow="false"
trigger="click"
placement="bottom-end">
<template #trigger>
<svg class="absolute color-#666 right-0 top-0 size-18px rotate-90"><use href="#more"></use></svg>
</template>
<div @click.stop="plugin.isAction = false" class="action-item">
<div class="menu-list">
<div @click="handleAdd(index)" class="menu-item">
<svg class="color-#4C77BD"><use href="#add"></use></svg>
<p class="text-#4C77BD">添加侧边栏</p>
</div>
<div @click="handleDelete(index)" class="menu-item">
<svg class="color-#c14053"><use href="#reduce"></use></svg>
<p class="text-#c14053">删除</p>
</div>
<div @click="handleUnload(index)" class="menu-item">
<svg><use href="#delete"></use></svg>
<p>卸载</p>
</div>
</div>
</div>
</n-popover>
</n-flex>
</Transition>
</n-flex>
</template>
</n-flex>
</n-scrollbar>
</div>
</n-modal>
</template>
<script setup lang="ts">
import { type } from '@tauri-apps/plugin-os'
import { PluginEnum } from '@/enums'
import { itemsTop } from '../config.tsx'
// todo: OPT.L.Common[]
type Plugins = {
title: string
state: PluginEnum
version: string
isAction: boolean
icon: string
iconActive: string
}
const props = defineProps<{
show: boolean
}>()
const { show } = toRefs(props)
const isShow = ref(show.value)
const plugins = ref<Plugins[]>([
{
title: 'HuLa云音乐',
state: PluginEnum.NOT_INSTALLED,
version: 'v1.0.0-Bate',
isAction: false,
icon: 'Music'
},
{
title: 'HuLa AI',
state: PluginEnum.NOT_INSTALLED,
version: 'v2.0.0-Bate',
isAction: false,
icon: 'robot',
icon_active: 'robot-action'
}
])
const emits = defineEmits(['close'])
watch(show, (newVal) => {
isShow.value = newVal
})
const handleClose = () => {
isShow.value = false
emits('close', isShow.value)
}
const handleState = (plugin) => {
if (plugin.state === PluginEnum.INSTALLED) return
plugin.state = PluginEnum.DOWNLOADING
plugin.progress = 0
const interval = setInterval(() => {
if (plugin.progress < 100) {
plugin.progress += 10
} else {
clearInterval(interval)
plugin.state = PluginEnum.INSTALLED
}
}, 500)
}
const handleUnload = (index: number) => {
plugins.value.filter((item, i) => {
if (i === index) {
item.state = PluginEnum.UNINSTALLING
setTimeout(() => {
handleDelete(index)
item.state = PluginEnum.NOT_INSTALLED
}, 2000)
}
})
}
const handleDelete = (index) => {
plugins.value.filter((item, i) => {
if (i === index) {
// itemsTop item.icon
const itemIndex = itemsTop.value.findIndex((topItem) => topItem.icon === item.icon)
if (itemIndex !== -1) {
itemsTop.value.splice(itemIndex, 1)
}
}
})
}
const handleAdd = (index) => {
plugins.value.filter((item, i) => {
if (i === index) {
// itemsTop
const itemIndex = itemsTop.value.findIndex((topItem) => topItem.icon === item.icon)
if (itemIndex !== -1) {
return
}
itemsTop.value.push(item)
}
})
}
</script>
<style scoped lang="scss">
.box {
@apply relative select-none custom-shadow cursor-pointer size-fit w-100px h-100px rounded-8px overflow-hidden;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.2));
transition: all 0.2s;
.flash {
position: absolute;
left: -130%;
top: 0;
width: 100px;
height: 100px;
background-image: linear-gradient(90deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0));
transform: skew(-30deg);
pointer-events: none;
}
&:hover .flash {
left: 130%;
transition: all 0.8s ease-in-out;
}
}
.downloading {
width: 80px;
background: #f1f1f1;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.action-item {
@include menu-item-style();
left: -80px;
@include menu-list();
}
.colorful {
background: linear-gradient(45deg, #fdcbf1 0%, #fdcbf1 1%, #e6dee9 100%);
}
</style>

View File

@ -2,8 +2,21 @@
<n-modal v-model:show="editInfo.show" :mask-closable="false" class="rounded-8px" transform-origin="center">
<div class="bg-[--bg-edit] w-480px h-fit box-border flex flex-col">
<n-flex :size="6" vertical>
<div
v-if="type() === 'macos'"
@click="editInfo.show = false"
class="mac-close size-13px shadow-inner bg-#ed6a5eff rounded-50% mt-6px select-none absolute left-6px">
<svg class="hidden size-7px color-#000 font-bold select-none absolute top-3px left-3px">
<use href="#close"></use>
</svg>
</div>
<n-flex class="text-(14px --text-color) select-none pt-6px" justify="center">编辑资料</n-flex>
<svg class="size-14px ml-a cursor-pointer pt-6px select-none absolute right-6px" @click="editInfo.show = false">
<svg
v-if="type() === 'windows'"
class="size-14px cursor-pointer pt-6px select-none absolute right-6px"
@click="editInfo.show = false">
<use href="#close"></use>
</svg>
<span class="h-1px w-full bg-[--line-color]"></span>
@ -88,6 +101,7 @@ import { IsYetEnum, MittEnum } from '@/enums'
import { leftHook } from '@/layout/left/hook.ts'
import Mitt from '@/utils/Bus.ts'
import apis from '@/services/apis.ts'
import { type } from '@tauri-apps/plugin-os'
const { login, editInfo, currentBadge, saveEditInfo, toggleWarningBadge, countGraphemes } = leftHook()
@ -117,4 +131,9 @@ onMounted(() => {
@apply opacity-100;
}
}
.mac-close:hover {
svg {
display: block;
}
}
</style>

View File

@ -7,8 +7,8 @@
trigger="click">
<template #trigger>
<!-- 头像 -->
<div class="relative size-36px rounded-50% cursor-pointer">
<n-avatar :color="'#fff'" :size="36" :src="login.accountInfo.avatar" fallback-src="/logo.png" round />
<div class="relative size-34px rounded-50% cursor-pointer">
<n-avatar :color="'#fff'" :size="34" :src="login.accountInfo.avatar" fallback-src="/logo.png" round />
<div
class="bg-[--bg-avatar] text-10px rounded-50% size-12px absolute bottom--2px right--2px border-(2px solid [--bg-avatar])"

View File

@ -16,6 +16,7 @@ const { logout } = useLogin()
* @param size
* @param window
*/
/* todo: 这里需要存入到localStore中, 需要区分固定的和不固定的,固定时有会话和用户列表的,其他都为动态添加 */
const itemsTop = ref<OPT.L.Common[]>([
{
url: 'message',
@ -47,6 +48,7 @@ const itemsTop = ref<OPT.L.Common[]>([
iconAction: 'robot-action',
tip: '机器人新功能在开发中',
size: {
minWidth: 780,
width: 980,
height: 800
},

View File

@ -171,13 +171,21 @@ export const leftHook = () => {
const pageJumps = (
url: string,
title?: string,
size?: { width: number; height: number },
size?: { width: number; height: number; minWidth?: number },
window?: { resizable: boolean }
) => {
// 判断url是否等于isNewWindows.value数组中的值如果是则创建新的窗口
if (isNewWindows.value.includes(url)) {
delay(async () => {
await createWebviewWindow(title!, url, <number>size?.width, <number>size?.height, '', window?.resizable)
await createWebviewWindow(
title!,
url,
<number>size?.width,
<number>size?.height,
'',
window?.resizable,
<number>size?.minWidth
)
}, 300)
} else {
activeUrl.value = url

View File

@ -1,7 +1,8 @@
<template>
<main
class="left rounded-l-8px min-w-60px h-full p-[30px_6px_15px] box-border flex-col-center select-none"
class="left rounded-l-8px min-w-64px h-full p-[30px_6px_15px] box-border flex-col-center select-none"
data-tauri-drag-region>
<p class="text-(16px [--text-color]) cursor-default select-none m-[4px_0_16px_0]">HuLa</p>
<!-- 头像模块 -->
<LeftAvatar />
<!-- 导航选项按钮模块 -->

View File

@ -19,6 +19,8 @@ import { emit } from '@tauri-apps/api/event'
import { EventEnum } from '@/enums'
import pkg from '~/package.json'
import { handRelativeTime } from '@/utils/Day.ts'
import './style.scss'
import { type } from '@tauri-apps/plugin-os'
const formRef = ref<FormInst | null>()
const formValue = ref({
@ -62,9 +64,19 @@ export const LockScreen = defineComponent(() => {
return () => (
<NModal v-model:show={lock.value.modalShow} maskClosable={false} class="w-350px border-rd-8px">
<div class="bg-[--bg-popover] w-360px h-full p-6px box-border flex flex-col">
<svg onClick={() => (lock.value.modalShow = false)} class="w-12px h-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
</svg>
{type() === 'macos' ? (
<div
onClick={() => (lock.value.modalShow = false)}
class="mac-close relative size-13px shadow-inner bg-#ed6a5eff rounded-50% select-none">
<svg class="hidden size-7px color-#000 font-bold select-none absolute top-3px left-3px">
<use href="#close"></use>
</svg>
</div>
) : (
<svg onClick={() => (lock.value.modalShow = false)} class="w-12px h-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
</svg>
)}
<div class="flex flex-col gap-10px p-10px select-none">
<NFlex vertical justify="center" align="center" size={20}>
<span class="text-(14px center)"></span>
@ -196,11 +208,21 @@ export const CheckUpdate = defineComponent(() => {
return () => (
<NModal v-model:show={lock.value.modalShow} maskClosable={false} class="w-350px border-rd-8px">
<div class="bg-[--bg-popover] w-500px h-full p-6px box-border flex flex-col">
<svg onClick={() => (lock.value.modalShow = false)} class="w-12px h-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
</svg>
{type() === 'macos' ? (
<div
onClick={() => (lock.value.modalShow = false)}
class="mac-close relative size-13px shadow-inner bg-#ed6a5eff rounded-50% select-none">
<svg class="hidden size-7px color-#000 font-bold select-none absolute top-3px left-3px">
<use href="#close"></use>
</svg>
</div>
) : (
<svg onClick={() => (lock.value.modalShow = false)} class="w-12px h-12px ml-a cursor-pointer select-none">
<use href="#close"></use>
</svg>
)}
{loading.value ? (
<NFlex vertical justify={'center'} size={10}>
<NFlex vertical justify={'center'} size={10} class="mt-6px">
<NSkeleton text repeat={1} class="rounded-8px h-30px w-120px" />
<NSkeleton text repeat={1} class="rounded-8px h-300px" />
<NSkeleton text repeat={1} class="rounded-8px w-80px h-30px m-[0_0_0_auto]" />

View File

@ -32,6 +32,12 @@
@include menu-list();
}
.mac-close:hover {
svg {
display: block;
}
}
.item-hover {
@apply select-none hover:bg-[--info-hover] cursor-pointer w-fit rounded-10px p-4px;
transition: all 0.4s ease-in-out;

View File

@ -8,8 +8,8 @@
<!-- 聊天界面背景图标 -->
<div v-else class="flex-center size-full select-none">
<img v-if="imgTheme === ThemeEnum.DARK" class="w-130px h-100px" src="@/assets/img/hula_bg_dark.png" alt="" />
<img v-else class="w-130px h-100px" src="@/assets/img/hula_bg_light.png" alt="" />
<img v-if="imgTheme === ThemeEnum.DARK" class="w-110px h-100px" src="@/assets/img/hula_bg_d.png" alt="" />
<img v-else class="w-110px h-100px" src="@/assets/img/hula_bg_l.png" alt="" />
</div>
</main>
</template>

View File

@ -19,6 +19,11 @@ const routes: Array<RouteRecordRaw> = [
name: 'tray',
component: Tray
},
{
path: '/capture',
name: 'capture',
component: () => import('@/views/Capture.vue')
},
{
path: '/home',
name: 'home',

View File

@ -28,6 +28,12 @@
font-size: 14px;
}
.mac-close:hover {
svg {
display: block;
}
}
@keyframes slideIn {
from {
transform: translateX(100%);

View File

@ -47,6 +47,11 @@
background-color: rgb(195, 221, 216);
}
}
.mac-close:hover {
svg {
display: block;
}
}
/** 浮标达到最大值时候的样式 */
.max:hover {
background-color: #f5dce1;

View File

@ -1,14 +1,17 @@
@font-face {
font-family: 'AliFangYuan';
src: url('/src/assets/fonts/AlimamaFangYuanTiVF-Thin.ttf');
font-display: swap;
}
@font-face {
font-family: 'AliDaoLiTi';
src: url('/src/assets/fonts/AlimamaDaoLiTi.ttf');
font-display: swap;
}
@font-face {
font-family: 'AliDongFangDaKai';
src: url('/src/assets/fonts/AlimamaDongFangDaKai-Regular.ttf');
font-display: swap;
}

View File

@ -2,11 +2,13 @@
* {
font-family: var(--font-family), sans-serif !important;
font-weight: 500 !important;
caret-color: #13987f !important; /* 改变输入框光标的颜色 */
}
html, body {
overscroll-behavior: none; // 禁止mac的触底反弹效果
}
/**! 主题变量 */
/* todo: 这里只需要定义单个颜色的变量即可无需重复定义组件的颜色 */
:root {
// 是否启用阴影
--shadow-enabled: 1;
@ -52,7 +54,7 @@ html, body {
--bg-bubble: rgb(253, 253, 253);
--bg-bubble-active: #dedede;
// 头像状态的背景颜色
--bg-avatar: #e3e3e3;
--bg-avatar: rgba(241, 241, 241, 1);
// 个人信息框背景颜色
--info-hover: #eee;
--info-text-color: #202020;
@ -82,6 +84,13 @@ html, body {
--chat-right-bg: #f1f1f1;
--chat-text-color: #505050;
--chat-hover-color: #e3e3e3;
// 搜索框内样式
--search-color: #eee;
// bate
--bate-color: #a789d9;
--bate-bg: #d9ccf7;
// 卡片hover样式
--card-bg: #13987f60;
}
html[data-theme='dark'] {
@ -124,7 +133,7 @@ html[data-theme='dark'] {
--bg-bubble: rgb(38, 38, 38);
--bg-bubble-active: #202020;
// 头像状态的背景颜色
--bg-avatar: #3f3f3f;
--bg-avatar: rgba(22, 22, 22, 1);
// 个人信息框背景颜色
--info-hover: #3b3b3b;
--info-text-color: #eee;
@ -154,6 +163,13 @@ html[data-theme='dark'] {
--chat-right-bg: #161616;
--chat-text-color: #b1b1b1;
--chat-hover-color: #262626;
// 搜索框内样式
--search-color: #333;
// bate
--bate-color: #a789d9;
--bate-bg: #403555;
// 卡片hover样式
--card-bg: #13987f20;
}
/**! end */
// 线性动画
@ -206,6 +222,7 @@ html[data-theme='dark'] {
bottom: 0;
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur($blur);
-webkit-backdrop-filter: blur($blur);
}
.n-modal-mask {
@include mask-style;
@ -241,6 +258,7 @@ html[data-theme='dark'] {
background: var(--bg-menu);
color: var(--text-color);
border-radius: 6px;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.08),
@ -284,3 +302,10 @@ html[data-theme='dark'] {
}
}
}
/** 修改默认的naive-ui的选择器样式 */
:deep(.n-base-selection) {
--n-box-shadow-active: 0 0 0 2px rgba(19, 152, 127, 0.2) !important;
--n-border-active: 1px solid #13987f !important;
--n-border-hover: 1px solid #13987f !important;
}

View File

@ -7,58 +7,60 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
ActionBar: typeof import('./../components/windows/ActionBar.vue')['default']
ChatBox: typeof import('./../components/rightBox/chatBox/index.vue')['default']
ChatFooter: typeof import('./../components/rightBox/chatBox/ChatFooter.vue')['default']
ChatHeader: typeof import('./../components/rightBox/chatBox/ChatHeader.vue')['default']
ChatMain: typeof import('./../components/rightBox/chatBox/ChatMain.vue')['default']
ChatSidebar: typeof import('./../components/rightBox/chatBox/ChatSidebar.vue')['default']
ContextMenu: typeof import('./../components/common/ContextMenu.vue')['default']
Details: typeof import('./../components/rightBox/Details.vue')['default']
Emoji: typeof import('./../components/rightBox/emoji/index.vue')['default']
Image: typeof import('./../components/rightBox/renderMessage/Image.vue')['default']
InfoPopover: typeof import('./../components/common/InfoPopover.vue')['default']
MsgInput: typeof import('./../components/rightBox/MsgInput.vue')['default']
NaiveProvider: typeof import('./../components/common/NaiveProvider.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NAvatar: typeof import('naive-ui')['NAvatar']
NAvatarGroup: typeof import('naive-ui')['NAvatarGroup']
NBadge: typeof import('naive-ui')['NBadge']
NButton: typeof import('naive-ui')['NButton']
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDropdown: typeof import('naive-ui')['NDropdown']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NFlex: typeof import('naive-ui')['NFlex']
NIcon: typeof import('naive-ui')['NIcon']
NIconWrapper: typeof import('naive-ui')['NIconWrapper']
NImage: typeof import('naive-ui')['NImage']
NImageGroup: typeof import('naive-ui')['NImageGroup']
NInput: typeof import('naive-ui')['NInput']
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NModalProvider: typeof import('naive-ui')['NModalProvider']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopover: typeof import('naive-ui')['NPopover']
NPopselect: typeof import('naive-ui')['NPopselect']
NQrCode: typeof import('naive-ui')['NQrCode']
NRadio: typeof import('naive-ui')['NRadio']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSkeleton: typeof import('naive-ui')['NSkeleton']
NSwitch: typeof import('naive-ui')['NSwitch']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NTooltip: typeof import('naive-ui')['NTooltip']
NVirtualList: typeof import('naive-ui')['NVirtualList']
RenderMessage: typeof import('./../components/rightBox/renderMessage/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Text: typeof import('./../components/rightBox/renderMessage/Text.vue')['default']
ActionBar: (typeof import('./../components/windows/ActionBar.vue'))['default']
ChatBox: (typeof import('./../components/rightBox/chatBox/index.vue'))['default']
ChatFooter: (typeof import('./../components/rightBox/chatBox/ChatFooter.vue'))['default']
ChatHeader: (typeof import('./../components/rightBox/chatBox/ChatHeader.vue'))['default']
ChatMain: (typeof import('./../components/rightBox/chatBox/ChatMain.vue'))['default']
ChatSidebar: (typeof import('./../components/rightBox/chatBox/ChatSidebar.vue'))['default']
ContextMenu: (typeof import('./../components/common/ContextMenu.vue'))['default']
Details: (typeof import('./../components/rightBox/Details.vue'))['default']
Emoji: (typeof import('./../components/rightBox/emoji/index.vue'))['default']
Image: (typeof import('./../components/rightBox/renderMessage/Image.vue'))['default']
InfoPopover: (typeof import('./../components/common/InfoPopover.vue'))['default']
MsgInput: (typeof import('./../components/rightBox/MsgInput.vue'))['default']
NaiveProvider: (typeof import('./../components/common/NaiveProvider.vue'))['default']
NAlert: (typeof import('naive-ui'))['NAlert']
NAvatar: (typeof import('naive-ui'))['NAvatar']
NAvatarGroup: (typeof import('naive-ui'))['NAvatarGroup']
NBadge: (typeof import('naive-ui'))['NBadge']
NButton: (typeof import('naive-ui'))['NButton']
NButtonGroup: (typeof import('naive-ui'))['NButtonGroup']
NCheckbox: (typeof import('naive-ui'))['NCheckbox']
NCollapse: (typeof import('naive-ui'))['NCollapse']
NCollapseItem: (typeof import('naive-ui'))['NCollapseItem']
NConfigProvider: (typeof import('naive-ui'))['NConfigProvider']
NDialogProvider: (typeof import('naive-ui'))['NDialogProvider']
NDropdown: (typeof import('naive-ui'))['NDropdown']
NEllipsis: (typeof import('naive-ui'))['NEllipsis']
NFlex: (typeof import('naive-ui'))['NFlex']
NIcon: (typeof import('naive-ui'))['NIcon']
NIconWrapper: (typeof import('naive-ui'))['NIconWrapper']
NImage: (typeof import('naive-ui'))['NImage']
NImageGroup: (typeof import('naive-ui'))['NImageGroup']
NInput: (typeof import('naive-ui'))['NInput']
NLoadingBarProvider: (typeof import('naive-ui'))['NLoadingBarProvider']
NMessageProvider: (typeof import('naive-ui'))['NMessageProvider']
NModal: (typeof import('naive-ui'))['NModal']
NModalProvider: (typeof import('naive-ui'))['NModalProvider']
NNotificationProvider: (typeof import('naive-ui'))['NNotificationProvider']
NPopconfirm: (typeof import('naive-ui'))['NPopconfirm']
NPopover: (typeof import('naive-ui'))['NPopover']
NPopselect: (typeof import('naive-ui'))['NPopselect']
NQrCode: (typeof import('naive-ui'))['NQrCode']
NRadio: (typeof import('naive-ui'))['NRadio']
NScrollbar: (typeof import('naive-ui'))['NScrollbar']
NSelect: (typeof import('naive-ui'))['NSelect']
NSkeleton: (typeof import('naive-ui'))['NSkeleton']
NSwitch: (typeof import('naive-ui'))['NSwitch']
NTabPane: (typeof import('naive-ui'))['NTabPane']
NTabs: (typeof import('naive-ui'))['NTabs']
NTooltip: (typeof import('naive-ui'))['NTooltip']
NVirtualList: (typeof import('naive-ui'))['NVirtualList']
RenderMessage: (typeof import('./../components/rightBox/renderMessage/index.vue'))['default']
RouterLink: (typeof import('vue-router'))['RouterLink']
RouterView: (typeof import('vue-router'))['RouterView']
Screenshot: (typeof import('./../components/common/Screenshot.vue'))['default']
Text: (typeof import('./../components/rightBox/renderMessage/Text.vue'))['default']
}
}

View File

@ -11,6 +11,7 @@ declare namespace OPT {
badge?: number
tip?: string
size?: {
minWidth?: number
width: number
height: number
}

18
src/views/Capture.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<Screenshot :is-capturing="isCapturing" />
</template>
<script setup lang="ts">
import { listen } from '@tauri-apps/api/event'
const isCapturing = ref(false)
watchEffect(() => {
listen('capture', (e) => {
nextTick(() => {
isCapturing.value = e.payload as boolean
})
})
})
</script>
<style scoped lang="scss"></style>

Some files were not shown because too many files have changed in this diff Show More