45
CHANGELOG.md
@ -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
@ -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.
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
BIN
public/logo.png
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 35 KiB |
@ -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 |
@ -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 |
@ -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://*:*"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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://*:*"}]}]}}
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.1 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 85 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 109 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
After Width: | Height: | Size: 273 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
After Width: | Height: | Size: 19 KiB |
@ -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");
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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": {
|
||||
|
BIN
src/assets/img/dispersion-bg.png
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
src/assets/img/hula_bg_d.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 3.4 KiB |
BIN
src/assets/img/hula_bg_l.png
Normal file
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 3.4 KiB |
@ -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 |
@ -174,6 +174,7 @@ const handleAfterEnter = (el: any) => {
|
||||
}
|
||||
}
|
||||
.menu-list {
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
486
src/components/common/Screenshot.vue
Normal 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>
|
@ -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 } =
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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(() => {
|
||||
|
@ -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
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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 () => {
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/** 定义一个函数,在鼠标拖动时调用 */
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
.add-item {
|
||||
@include menu-item-style(absolute);
|
||||
top: 60px;
|
||||
right: 30px;
|
||||
top: 36px;
|
||||
right: 10px;
|
||||
@include menu-list();
|
||||
}
|
||||
|
@ -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>
|
||||
|
283
src/layout/left/components/DefinePlugins.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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])"
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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 />
|
||||
<!-- 导航选项按钮模块 -->
|
||||
|
@ -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]" />
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
@ -28,6 +28,12 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mac-close:hover {
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
|
@ -47,6 +47,11 @@
|
||||
background-color: rgb(195, 221, 216);
|
||||
}
|
||||
}
|
||||
.mac-close:hover {
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
/** 浮标达到最大值时候的样式 */
|
||||
.max:hover {
|
||||
background-color: #f5dce1;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
108
src/typings/components.d.ts
vendored
@ -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']
|
||||
}
|
||||
}
|
||||
|
1
src/typings/options.d.ts
vendored
@ -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
@ -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>
|