新增 pc-client:畅联 PC 端工程(Electron+React,按确认效果图改造,B-59)
- 基于 openim-electron-demo 改造:工号+密码登录(自研账号服务)、会话列表/聊天窗/通讯录界面重做
- 文字/语音/文件/图片消息、文件拖拽发送、表情面板、一对一语音通话(LiveKit)
- RTC 令牌对接账号服务带鉴权版 /api/rtc_token(Bearer imToken + {room,identity})
- account-service: CORS Allow-Headers 补 authorization(浏览器/渲染进程跨域调 rtc_token 需要)
- 附本地 mock 账号服务(scripts/mock-account-server.js)与截图联调脚本
@@ -1,10 +1,6 @@
|
||||
# 运行时生成,不入库
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
components/
|
||||
.selftest/
|
||||
data/
|
||||
|
||||
# Flutter 手机端构建产物
|
||||
mobile/.dart_tool/
|
||||
mobile/build/
|
||||
mobile/.flutter-plugins*
|
||||
|
||||
@@ -179,7 +179,7 @@ function send(res, status, obj) {
|
||||
res.writeHead(status, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, admin-token',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, admin-token, authorization',
|
||||
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
|
||||
});
|
||||
res.end(body);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# 内网测试环境(192.168.200.11,见仓库根目录 README 端口表)
|
||||
VITE_WS_URL=ws://192.168.200.11:10001
|
||||
VITE_API_URL=http://192.168.200.11:10002
|
||||
# 自研公司账号服务(登录 + 通话令牌)。本地联调可建 .env.local 覆盖为 mock:http://127.0.0.1:11010
|
||||
VITE_ACCOUNT_URL=http://192.168.200.11:10010
|
||||
# LiveKit 语音通话(宿主端口已改映射为 17880)
|
||||
VITE_LIVEKIT_URL=ws://192.168.200.11:17880
|
||||
@@ -0,0 +1,36 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# 依赖与构建产物
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
dist-electron
|
||||
package
|
||||
release
|
||||
|
||||
# 本地环境覆盖(.env 由仓库根 .gitignore 忽略)
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# 测试产物(截图等)
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
e2e/screenshots/
|
||||
|
||||
# sdk core
|
||||
open-im-sdk-core.*
|
||||
OpenIM_*
|
||||
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
@@ -0,0 +1,48 @@
|
||||
# 畅联 PC 端(pc-client)
|
||||
|
||||
公司内部通讯工具「畅联」的 PC 客户端,基于 OpenIM 官方 Electron 样板(openim-electron-demo,Electron + React 18 + TypeScript + antd 5 + zustand)改造:
|
||||
|
||||
- 登录改为**工号 + 密码**,走自研账号服务(不部署 OpenIM 官方 chat 业务服务 :10008)
|
||||
- 用户信息、音视频通话令牌等原走 chat 服务的调用,已全部改道 OpenIM SDK 或自研账号服务
|
||||
- 界面文案全中文,默认语言简体中文
|
||||
|
||||
## 依赖的服务端
|
||||
|
||||
见仓库根目录 `README.md`:OpenIM 服务端(REST :10002 / WS :10001)、自研账号服务(:10010,接口见 `account-service/README.md`)、LiveKit(:17880)。客户端连接地址在 `.env` 里配置(`VITE_ACCOUNT_URL`、`VITE_WS_URL`、`VITE_API_URL` 等)。
|
||||
|
||||
## 本地开发
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- `npm install` 时注意:本仓库用 allow-scripts 白名单管理依赖安装脚本,需批准 `electron` / `esbuild` / `koffi` 等包的 postinstall(package.json 里已配好 `allowScripts` 字段)。
|
||||
- `npm run dev` 启动 Vite 开发服务器(默认同时拉起 Electron 窗口)。
|
||||
|
||||
### 本地 mock 登录服务(联调用)
|
||||
|
||||
真实员工账号由管理员在服务器上开通。本地开发没有真实账号时,可以起一个 mock 账号服务:
|
||||
|
||||
```bash
|
||||
npm run mock:account
|
||||
```
|
||||
|
||||
然后新建 `.env.local`(已被 gitignore)写入 `VITE_ACCOUNT_URL=http://127.0.0.1:11010` 覆盖 `.env` 里的真实地址,用 `test001 / test123`(或 `test002 / test123`)登录。mock 服务实现了与真实账号服务相同的 `/api/login` 和 `/api/rtc_token` 接口(登录时会真实调用 OpenIM 管理接口换取 imToken;`/api/rtc_token` 同样要求 `Authorization: Bearer <imToken>`)。
|
||||
|
||||
> ⚠️ mock 服务内置测试环境默认密钥,仅供本地开发,禁止打包、禁止用于正式环境。
|
||||
|
||||
## 打包
|
||||
|
||||
先 `npm run build`(vite build,产物在 `dist/`),再用 electron-builder 打包安装包。打包用的脚本在 `package_electron.json` 里(打包前按原样板说明用它替换 `package.json`):
|
||||
|
||||
```bash
|
||||
npm run build:win # Windows 安装包
|
||||
```
|
||||
|
||||
如果只想验证构建不产出安装包:`npm run build && npx electron-builder`(此命令未验证,以 `package_electron.json` 里的脚本为准)。
|
||||
|
||||
## 其他
|
||||
|
||||
- 本项目基于 openim-electron-demo(AGPL-3.0 附加条款),仅作公司内部使用。
|
||||
- 原样板说明(英文、功能清单等)见上游仓库 <https://github.com/openimsdk/openim-electron-demo>。
|
||||
@@ -0,0 +1,132 @@
|
||||
module.exports = {
|
||||
parserPreset: "conventional-changelog-conventionalcommits",
|
||||
rules: {
|
||||
"body-leading-blank": [1, "always"],
|
||||
"body-max-line-length": [2, "always", 100],
|
||||
"footer-leading-blank": [1, "always"],
|
||||
"footer-max-line-length": [2, "always", 100],
|
||||
"header-max-length": [2, "always", 100],
|
||||
"subject-case": [
|
||||
2,
|
||||
"never",
|
||||
["sentence-case", "start-case", "pascal-case", "upper-case"],
|
||||
],
|
||||
"subject-empty": [2, "never"],
|
||||
"subject-full-stop": [2, "never", "."],
|
||||
"type-case": [2, "always", "lower-case"],
|
||||
"type-empty": [2, "never"],
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"build",
|
||||
"chore",
|
||||
"ci",
|
||||
"docs",
|
||||
"feat",
|
||||
"fix",
|
||||
"perf",
|
||||
"refactor",
|
||||
"revert",
|
||||
"style",
|
||||
"test",
|
||||
],
|
||||
],
|
||||
},
|
||||
prompt: {
|
||||
questions: {
|
||||
type: {
|
||||
description: "Select the type of change that you're committing",
|
||||
enum: {
|
||||
feat: {
|
||||
description: "A new feature",
|
||||
title: "Features",
|
||||
emoji: "✨",
|
||||
},
|
||||
fix: {
|
||||
description: "A bug fix",
|
||||
title: "Bug Fixes",
|
||||
emoji: "🐛",
|
||||
},
|
||||
docs: {
|
||||
description: "Documentation only changes",
|
||||
title: "Documentation",
|
||||
emoji: "📚",
|
||||
},
|
||||
style: {
|
||||
description:
|
||||
"Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)",
|
||||
title: "Styles",
|
||||
emoji: "💎",
|
||||
},
|
||||
refactor: {
|
||||
description: "A code change that neither fixes a bug nor adds a feature",
|
||||
title: "Code Refactoring",
|
||||
emoji: "📦",
|
||||
},
|
||||
perf: {
|
||||
description: "A code change that improves performance",
|
||||
title: "Performance Improvements",
|
||||
emoji: "🚀",
|
||||
},
|
||||
test: {
|
||||
description: "Adding missing tests or correcting existing tests",
|
||||
title: "Tests",
|
||||
emoji: "🚨",
|
||||
},
|
||||
build: {
|
||||
description:
|
||||
"Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)",
|
||||
title: "Builds",
|
||||
emoji: "🛠",
|
||||
},
|
||||
ci: {
|
||||
description:
|
||||
"Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)",
|
||||
title: "Continuous Integrations",
|
||||
emoji: "⚙️",
|
||||
},
|
||||
chore: {
|
||||
description: "Other changes that don't modify src or test files",
|
||||
title: "Chores",
|
||||
emoji: "♻️",
|
||||
},
|
||||
revert: {
|
||||
description: "Reverts a previous commit",
|
||||
title: "Reverts",
|
||||
emoji: "🗑",
|
||||
},
|
||||
},
|
||||
},
|
||||
scope: {
|
||||
description: "What is the scope of this change (e.g. component or file name)",
|
||||
},
|
||||
subject: {
|
||||
description: "Write a short, imperative tense description of the change",
|
||||
},
|
||||
body: {
|
||||
description: "Provide a longer description of the change",
|
||||
},
|
||||
isBreaking: {
|
||||
description: "Are there any breaking changes?",
|
||||
},
|
||||
breakingBody: {
|
||||
description:
|
||||
"A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself",
|
||||
},
|
||||
breaking: {
|
||||
description: "Describe the breaking changes",
|
||||
},
|
||||
isIssueAffected: {
|
||||
description: "Does this change affect any open issues?",
|
||||
},
|
||||
issuesBody: {
|
||||
description:
|
||||
"If issues are closed, the commit requires a body. Please enter a longer description of the commit itself",
|
||||
},
|
||||
issues: {
|
||||
description: 'Add issue references (e.g. "fix #123", "re #123".)',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 496 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 149 KiB |
@@ -0,0 +1,21 @@
|
||||
const { chromium } = require("@playwright/test");
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({
|
||||
executablePath:
|
||||
"C:/Users/MoBai/AppData/Local/ms-playwright/chromium_headless_shell-1228/chrome-headless-shell-win64/chrome-headless-shell.exe",
|
||||
});
|
||||
const page = await browser.newPage({ viewport: { width: 1200, height: 800 } });
|
||||
page.on("console", (msg) => console.log("[" + msg.type() + "]", msg.text().slice(0, 300)));
|
||||
page.on("pageerror", (err) => console.log("[pageerror]", String(err).slice(0, 500)));
|
||||
|
||||
await page.goto("http://localhost:5173/");
|
||||
await page.waitForTimeout(6000);
|
||||
const html = await page.content();
|
||||
console.log("body length:", html.length);
|
||||
console.log(html.slice(0, 800));
|
||||
await browser.close();
|
||||
})().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { test, expect, _electron as electron } from "@playwright/test";
|
||||
|
||||
test("homepage has title and links to intro page", async () => {
|
||||
const app = await electron.launch({ args: [".", "--no-sandbox"] });
|
||||
const page = await app.firstWindow();
|
||||
expect(await page.title()).toBe("OpenCorp");
|
||||
await page.screenshot({ path: "e2e/screenshots/example.png" });
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/* 临时截图脚本:mock 登录后截取登录页/主界面/通讯录 */
|
||||
const { chromium } = require("@playwright/test");
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({
|
||||
executablePath:
|
||||
"C:/Users/MoBai/AppData/Local/ms-playwright/chromium_headless_shell-1228/chrome-headless-shell-win64/chrome-headless-shell.exe",
|
||||
});
|
||||
const page = await browser.newPage({ viewport: { width: 1200, height: 800 } });
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") console.log("[console.error]", msg.text().slice(0, 200));
|
||||
});
|
||||
|
||||
await page.goto("http://localhost:5173/", { waitUntil: "networkidle" });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({ path: "e2e/screenshots/01-login.png" });
|
||||
|
||||
await page.fill('input[placeholder="请输入工号"]', "test001");
|
||||
await page.fill('input[placeholder="请输入您的密码"]', "test123");
|
||||
await page.click('button:has-text("登录")');
|
||||
|
||||
// 等 SDK 登录 + 同步
|
||||
await page.waitForTimeout(18000);
|
||||
await page.screenshot({ path: "e2e/screenshots/02-main.png" });
|
||||
|
||||
// 进入会话
|
||||
await page.click('text=测试二号');
|
||||
await page.waitForTimeout(4000);
|
||||
await page.screenshot({ path: "e2e/screenshots/04-chat.png" });
|
||||
|
||||
// 发一条文字消息看气泡
|
||||
await page.fill("textarea", "好,我开完会就看,下午给你回复");
|
||||
await page.keyboard.press("Enter");
|
||||
await page.waitForTimeout(2500);
|
||||
await page.screenshot({ path: "e2e/screenshots/05-chat-sent.png" });
|
||||
|
||||
await page.goto("http://localhost:5173/#/contact");
|
||||
await page.waitForTimeout(4000);
|
||||
await page.screenshot({ path: "e2e/screenshots/03-contact.png" });
|
||||
|
||||
await browser.close();
|
||||
console.log("done");
|
||||
})().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
const { chromium } = require("@playwright/test");
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({
|
||||
executablePath:
|
||||
"C:/Users/MoBai/AppData/Local/ms-playwright/chromium_headless_shell-1228/chrome-headless-shell-win64/chrome-headless-shell.exe",
|
||||
});
|
||||
const page = await browser.newPage({ viewport: { width: 1200, height: 800 } });
|
||||
|
||||
await page.goto("http://localhost:5173/");
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const hasLogin = await page.$('input[placeholder="请输入工号"]');
|
||||
if (hasLogin) {
|
||||
await page.fill('input[placeholder="请输入工号"]', "test001");
|
||||
await page.fill('input[placeholder="请输入您的密码"]', "test123");
|
||||
await page.click('button:has-text("登录")');
|
||||
await page.waitForTimeout(15000);
|
||||
} else {
|
||||
await page.waitForTimeout(12000);
|
||||
}
|
||||
|
||||
await page.click("text=测试二号");
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 「…」菜单
|
||||
await page.click(".anticon-more");
|
||||
await page.waitForTimeout(800);
|
||||
await page.screenshot({ path: "e2e/screenshots/06-header-menu.png" });
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// 表情面板
|
||||
await page.click(".anticon-smile");
|
||||
await page.waitForTimeout(800);
|
||||
await page.screenshot({ path: "e2e/screenshots/07-emoji.png" });
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await browser.close();
|
||||
console.log("done");
|
||||
})().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @see https://www.electron.build/configuration/configuration
|
||||
*/
|
||||
{
|
||||
appId: "com.changlian.desktop",
|
||||
productName: "畅联",
|
||||
asar: true,
|
||||
asarUnpack: ["**/*.node"],
|
||||
extends: null,
|
||||
directories: {
|
||||
output: "release/Base/${version}",
|
||||
},
|
||||
files: [
|
||||
"dist",
|
||||
"!dist/*.wasm",
|
||||
"dist-electron",
|
||||
"!node_modules/rxjs/**/*",
|
||||
"!node_modules/koffi/src/**/*",
|
||||
"!node_modules/koffi/build/**/*",
|
||||
"!node_modules/koffi/vendor/**/*",
|
||||
"!node_modules/@openim/wasm-client-sdk/assets/**/*",
|
||||
"!node_modules/@openim/electron-client-sdk/assets/**/*",
|
||||
],
|
||||
extraResources: [
|
||||
{
|
||||
from: "extraResources",
|
||||
to: "extraResources",
|
||||
},
|
||||
{
|
||||
from: "node_modules/rxjs",
|
||||
to: "app.asar.unpacked/node_modules/rxjs",
|
||||
},
|
||||
],
|
||||
mac: {
|
||||
artifactName: "${productName}_${version}_${arch}.${ext}",
|
||||
target: ["dmg"],
|
||||
icon: "./dist/icons/mac_icon.png",
|
||||
extraResources: [
|
||||
{
|
||||
from: "node_modules/@openim/electron-client-sdk/assets/mac_${arch}/",
|
||||
to: "app.asar.unpacked/node_modules/@openim/electron-client-sdk/assets/mac_${arch}",
|
||||
},
|
||||
{
|
||||
from: "node_modules/koffi/build/koffi/darwin_${arch}/",
|
||||
to: "koffi/darwin_${arch}",
|
||||
},
|
||||
],
|
||||
},
|
||||
win: {
|
||||
target: [
|
||||
{
|
||||
target: "nsis",
|
||||
},
|
||||
],
|
||||
artifactName: "${productName}_${version}.${ext}",
|
||||
icon: "./dist/icons/icon.ico",
|
||||
extraResources: [
|
||||
{
|
||||
from: "node_modules/@openim/electron-client-sdk/assets/win_${arch}/",
|
||||
to: "app.asar.unpacked/node_modules/@openim/electron-client-sdk/assets/win_${arch}",
|
||||
},
|
||||
{
|
||||
from: "node_modules/koffi/build/koffi/win32_${arch}/",
|
||||
to: "koffi/win32_${arch}",
|
||||
},
|
||||
],
|
||||
},
|
||||
linux: {
|
||||
icon: "./dist/icons/icon.png",
|
||||
target: "deb",
|
||||
maintainer: "opencorp-base",
|
||||
artifactName: "${productName}_${version}_${arch}.${ext}",
|
||||
extraResources: [
|
||||
{
|
||||
from: "node_modules/@openim/electron-client-sdk/assets/linux_${arch}/",
|
||||
to: "app.asar.unpacked/node_modules/@openim/electron-client-sdk/assets/linux_${arch}",
|
||||
},
|
||||
{
|
||||
from: "node_modules/koffi/build/koffi/linux_${arch}/",
|
||||
to: "koffi/linux_${arch}",
|
||||
},
|
||||
],
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
perMachine: true,
|
||||
allowElevation: true,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
createDesktopShortcut: true,
|
||||
createStartMenuShortcut: true,
|
||||
deleteAppDataOnUninstall: true,
|
||||
shortcutName: "OpenCorp-Base",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const IpcMainToRender = {
|
||||
appResume: "appResume",
|
||||
};
|
||||
|
||||
export const IpcRenderToMain = {
|
||||
showMainWindow: "showMainWindow",
|
||||
clearSession: "clearSession",
|
||||
minimizeWindow: "minimizeWindow",
|
||||
maxmizeWindow: "maxmizeWindow",
|
||||
closeWindow: "closeWindow",
|
||||
showMessageBox: "showMessageBox",
|
||||
setKeyStore: "setKeyStore",
|
||||
getKeyStore: "getKeyStore",
|
||||
getKeyStoreSync: "getKeyStoreSync",
|
||||
showInputContextMenu: "showInputContextMenu",
|
||||
getDataPath: "getDataPath",
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite-electron-plugin/electron-env" />
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
VSCODE_DEBUG?: "true";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import i18n from "i18next";
|
||||
|
||||
import { getStore } from "../main/storeManage";
|
||||
import { app } from "electron";
|
||||
|
||||
import translation_en from "./resources/en-US";
|
||||
import translation_zh from "./resources/zh-CN";
|
||||
|
||||
const store = getStore();
|
||||
|
||||
export const initI18n = () => {
|
||||
const systemLanguage = app.getLocale();
|
||||
const language = store.get("language", systemLanguage) as string;
|
||||
|
||||
const resources = {
|
||||
"en-US": {
|
||||
translation: translation_en,
|
||||
},
|
||||
"zh-CN": {
|
||||
translation: translation_zh,
|
||||
},
|
||||
zh: {
|
||||
translation: translation_zh,
|
||||
},
|
||||
};
|
||||
|
||||
i18n.init(
|
||||
{
|
||||
resources,
|
||||
lng: language,
|
||||
fallbackLng: "zh-CN",
|
||||
},
|
||||
(err) => {
|
||||
if (err) return console.error("Error loading i18n resources:", err);
|
||||
console.log("i18n resources loaded successfully");
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const changeLanguage = i18n.changeLanguage;
|
||||
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
system: {
|
||||
showWindow: "ShowWindow",
|
||||
hideWindow: "HideWindow",
|
||||
hide: "Hide",
|
||||
about: "About",
|
||||
quit: "Quit",
|
||||
window: "Window",
|
||||
toggleDevTools: "ToggleDevTools",
|
||||
minimize: "Minimize",
|
||||
close: "Close",
|
||||
copy: "Copy",
|
||||
paste: "Paste",
|
||||
cut: "Cut",
|
||||
undo: "Undo",
|
||||
redo: "Redo",
|
||||
selectAll: "SelectAll",
|
||||
fastKeys: "FastKeys",
|
||||
magnifier_position_label:"Position"
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
system: {
|
||||
showWindow: "显示",
|
||||
hideWindow: "隐藏",
|
||||
hide: "隐藏",
|
||||
about: "关于",
|
||||
quit: "退出",
|
||||
window: "窗口",
|
||||
toggleDevTools: "调试",
|
||||
minimize: "最小化",
|
||||
close: "关闭",
|
||||
copy: "复制",
|
||||
paste: "粘贴",
|
||||
cut: "剪切",
|
||||
undo: "撤销",
|
||||
redo: "重做",
|
||||
selectAll: "全选",
|
||||
fastKeys: "快键键",
|
||||
magnifier_position_label: "坐标",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { app, powerMonitor } from "electron";
|
||||
import { isExistMainWindow, sendEvent, showWindow } from "./windowManage";
|
||||
import { join } from "node:path";
|
||||
import fs from "fs";
|
||||
import { isMac, isProd, isWin } from "../utils";
|
||||
import { getStore } from "./storeManage";
|
||||
import { IpcMainToRender } from "../constants";
|
||||
import { logger } from ".";
|
||||
|
||||
const store = getStore();
|
||||
|
||||
export const setSingleInstance = () => {
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
app.on("second-instance", () => {
|
||||
showWindow();
|
||||
});
|
||||
};
|
||||
|
||||
export const setAppListener = (startApp: () => void) => {
|
||||
app.on("activate", () => {
|
||||
if (isExistMainWindow()) {
|
||||
showWindow();
|
||||
} else {
|
||||
startApp();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (isMac && !getIsForceQuit()) return;
|
||||
|
||||
app.quit();
|
||||
});
|
||||
|
||||
powerMonitor.on("suspend", () => {
|
||||
logger.debug("app suspend");
|
||||
});
|
||||
|
||||
powerMonitor.on("resume", () => {
|
||||
logger.debug("app resume");
|
||||
sendEvent(IpcMainToRender.appResume);
|
||||
});
|
||||
};
|
||||
|
||||
export const setAppGlobalData = () => {
|
||||
const electronDistPath = join(__dirname, "../");
|
||||
const distPath = join(electronDistPath, "../dist");
|
||||
const publicPath = isProd ? distPath : join(electronDistPath, "../public");
|
||||
const asarPath = process.resourcesPath;
|
||||
|
||||
global.pathConfig = {
|
||||
electronDistPath,
|
||||
distPath,
|
||||
publicPath,
|
||||
asarPath,
|
||||
logsPath: join(app.getPath("userData"), `/OpenIMData/logs`),
|
||||
sdkResourcesPath: join(app.getPath("userData"), `/OpenIMData/sdkResources`),
|
||||
imsdkLibPath: isProd
|
||||
? join(
|
||||
asarPath,
|
||||
"/app.asar.unpacked/node_modules/@openim/electron-client-sdk/assets",
|
||||
)
|
||||
: join(__dirname, "../../node_modules/@openim/electron-client-sdk/assets"),
|
||||
trayIcon: join(publicPath, `/icons/${isWin ? "icon.ico" : "tray.png"}`),
|
||||
emptyTrayIcon: join(publicPath, `/icons/${"empty_tray.png"}`),
|
||||
indexHtml: join(distPath, "index.html"),
|
||||
splashHtml: join(distPath, "splash.html"),
|
||||
preload: join(__dirname, "../preload/index.js"),
|
||||
};
|
||||
|
||||
if (isProd) {
|
||||
fs.promises
|
||||
.readdir(global.pathConfig.logsPath)
|
||||
.catch(
|
||||
(err) =>
|
||||
err.code === "ENOENT" &&
|
||||
fs.promises.mkdir(global.pathConfig.logsPath, { recursive: true }),
|
||||
);
|
||||
fs.promises
|
||||
.readdir(global.pathConfig.sdkResourcesPath)
|
||||
.catch(
|
||||
(err) =>
|
||||
err.code === "ENOENT" &&
|
||||
fs.promises.mkdir(global.pathConfig.sdkResourcesPath, { recursive: true }),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getIsForceQuit = () =>
|
||||
store.get("closeAction") === "quit" || global.forceQuit;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { app } from "electron";
|
||||
import { join } from "node:path";
|
||||
import { createMainWindow } from "./windowManage";
|
||||
import { createTray } from "./trayManage";
|
||||
import { setIpcMainListener } from "./ipcHandlerManage";
|
||||
import { setAppGlobalData, setAppListener, setSingleInstance } from "./appManage";
|
||||
import createAppMenu from "./menuManage";
|
||||
import { isLinux } from "../utils";
|
||||
import { getLogger } from "../utils/log";
|
||||
import { initI18n } from "../i18n";
|
||||
|
||||
export const logger = getLogger(join(app.getPath("userData"), `/OpenIMData/logs`));
|
||||
|
||||
const init = () => {
|
||||
initI18n();
|
||||
createMainWindow();
|
||||
createAppMenu();
|
||||
createTray();
|
||||
};
|
||||
|
||||
setAppGlobalData();
|
||||
setIpcMainListener();
|
||||
setSingleInstance();
|
||||
setAppListener(init);
|
||||
|
||||
app.whenReady().then(() => {
|
||||
isLinux ? setTimeout(init, 300) : init();
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { BrowserWindow, Menu, app, dialog, ipcMain } from "electron";
|
||||
import {
|
||||
clearCache,
|
||||
closeWindow,
|
||||
minimize,
|
||||
showWindow,
|
||||
splashEnd,
|
||||
updateMaximize,
|
||||
} from "./windowManage";
|
||||
import { t } from "i18next";
|
||||
import { IpcRenderToMain } from "../constants";
|
||||
import { getStore } from "./storeManage";
|
||||
import { changeLanguage } from "../i18n";
|
||||
|
||||
const store = getStore();
|
||||
|
||||
export const setIpcMainListener = () => {
|
||||
ipcMain.handle(IpcRenderToMain.clearSession, () => {
|
||||
clearCache();
|
||||
});
|
||||
|
||||
// window manage
|
||||
ipcMain.handle("changeLanguage", (_, locale) => {
|
||||
store.set("language", locale);
|
||||
changeLanguage(locale).then(() => {
|
||||
app.relaunch();
|
||||
app.exit(0);
|
||||
});
|
||||
});
|
||||
ipcMain.handle("main-win-ready", () => {
|
||||
splashEnd();
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.showMainWindow, () => {
|
||||
showWindow();
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.minimizeWindow, () => {
|
||||
minimize();
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.maxmizeWindow, () => {
|
||||
updateMaximize();
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.closeWindow, () => {
|
||||
closeWindow();
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.showMessageBox, (_, options) => {
|
||||
return dialog
|
||||
.showMessageBox(BrowserWindow.getFocusedWindow(), options)
|
||||
.then((res) => res.response);
|
||||
});
|
||||
|
||||
// data transfer
|
||||
ipcMain.handle(IpcRenderToMain.setKeyStore, (_, { key, data }) => {
|
||||
store.set(key, data);
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.getKeyStore, (_, { key }) => {
|
||||
return store.get(key);
|
||||
});
|
||||
ipcMain.on(IpcRenderToMain.getKeyStoreSync, (e, { key }) => {
|
||||
e.returnValue = store.get(key);
|
||||
});
|
||||
ipcMain.handle(IpcRenderToMain.showInputContextMenu, () => {
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: t("system.copy"),
|
||||
type: "normal",
|
||||
role: "copy",
|
||||
accelerator: "CommandOrControl+c",
|
||||
},
|
||||
{
|
||||
label: t("system.paste"),
|
||||
type: "normal",
|
||||
role: "paste",
|
||||
accelerator: "CommandOrControl+v",
|
||||
},
|
||||
{
|
||||
label: t("system.selectAll"),
|
||||
type: "normal",
|
||||
role: "selectAll",
|
||||
accelerator: "CommandOrControl+a",
|
||||
},
|
||||
]);
|
||||
menu.popup({
|
||||
window: BrowserWindow.getFocusedWindow()!,
|
||||
});
|
||||
});
|
||||
ipcMain.on(IpcRenderToMain.getDataPath, (e, key: string) => {
|
||||
switch (key) {
|
||||
case "public":
|
||||
e.returnValue = global.pathConfig.publicPath;
|
||||
break;
|
||||
case "sdkResources":
|
||||
e.returnValue = global.pathConfig.sdkResourcesPath;
|
||||
break;
|
||||
case "logsPath":
|
||||
e.returnValue = global.pathConfig.logsPath;
|
||||
break;
|
||||
default:
|
||||
e.returnValue = global.pathConfig.publicPath;
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { app, Menu } from "electron";
|
||||
import { t } from "i18next";
|
||||
import { isMac } from "../utils";
|
||||
|
||||
const createAppMenu = () => {
|
||||
if (isMac) {
|
||||
const template: Electron.MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: app.getName(),
|
||||
submenu: [
|
||||
{ label: t("system.about"), role: "about" },
|
||||
{ type: "separator" },
|
||||
{ label: t("system.hide"), role: "hide" },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: t("system.quit"),
|
||||
click: () => {
|
||||
global.forceQuit = true;
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("system.fastKeys"),
|
||||
submenu: [
|
||||
{ label: t("system.copy"), role: "copy", accelerator: "CmdOrCtrl+C" },
|
||||
{ label: t("system.paste"), role: "paste", accelerator: "CmdOrCtrl+V" },
|
||||
{ label: t("system.cut"), role: "cut", accelerator: "CmdOrCtrl+X" },
|
||||
{ label: t("system.undo"), role: "undo", accelerator: "CmdOrCtrl+Z" },
|
||||
{ label: t("system.redo"), role: "redo", accelerator: "CmdOrCtrl+Y" },
|
||||
{
|
||||
label: t("system.selectAll"),
|
||||
role: "selectAll",
|
||||
accelerator: "CmdOrCtrl+A",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t("system.window"),
|
||||
role: "window",
|
||||
submenu: [
|
||||
{ label: t("system.minimize"), role: "minimize", accelerator: "CmdOrCtrl+W" },
|
||||
{ label: t("system.close"), role: "close" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
||||
} else {
|
||||
Menu.setApplicationMenu(null);
|
||||
}
|
||||
};
|
||||
|
||||
export default createAppMenu;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { globalShortcut } from "electron";
|
||||
import { toggleDevTools } from "./windowManage";
|
||||
|
||||
export const registerShortcuts = () => {
|
||||
globalShortcut.register("CmdOrCtrl+F12", toggleDevTools);
|
||||
};
|
||||
|
||||
export const unregisterShortcuts = () => {
|
||||
globalShortcut.unregisterAll();
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import Store from "electron-store";
|
||||
|
||||
let store: Store;
|
||||
|
||||
export const getStore = () => {
|
||||
if (!store) {
|
||||
store = new Store();
|
||||
}
|
||||
return store;
|
||||
};
|
||||
|
||||
export { Store };
|
||||
@@ -0,0 +1,41 @@
|
||||
import { app, Menu, Tray } from "electron";
|
||||
import { t } from "i18next";
|
||||
import { hideWindow, showWindow } from "./windowManage";
|
||||
|
||||
let appTray: Tray;
|
||||
|
||||
export const createTray = () => {
|
||||
const trayMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: t("system.showWindow"),
|
||||
click: showWindow,
|
||||
},
|
||||
{
|
||||
label: t("system.hideWindow"),
|
||||
click: hideWindow,
|
||||
},
|
||||
{
|
||||
label: t("system.toggleDevTools"),
|
||||
role: "toggleDevTools",
|
||||
},
|
||||
{
|
||||
label: t("system.quit"),
|
||||
click: () => {
|
||||
global.forceQuit = true;
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
appTray = new Tray(global.pathConfig.trayIcon);
|
||||
appTray.setToolTip(app.getName());
|
||||
appTray.setIgnoreDoubleClickEvents(true);
|
||||
appTray.on("click", showWindow);
|
||||
|
||||
appTray.setContextMenu(trayMenu);
|
||||
};
|
||||
|
||||
export const destroyTray = () => {
|
||||
if (!appTray || appTray.isDestroyed()) return;
|
||||
appTray.destroy();
|
||||
appTray = null;
|
||||
};
|
||||
@@ -0,0 +1,259 @@
|
||||
import { join } from "node:path";
|
||||
import { BrowserWindow, dialog, shell } from "electron";
|
||||
import { isLinux, isMac, isWin } from "../utils";
|
||||
import { destroyTray } from "./trayManage";
|
||||
import { getIsForceQuit } from "./appManage";
|
||||
import { registerShortcuts, unregisterShortcuts } from "./shortcutManage";
|
||||
import { initIMSDK } from "../utils/imsdk";
|
||||
import OpenIMSDKMain from "@openim/electron-client-sdk";
|
||||
|
||||
const url = process.env.VITE_DEV_SERVER_URL;
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let splashWindow: BrowserWindow | null = null;
|
||||
let sdkInstance: OpenIMSDKMain | null = null;
|
||||
|
||||
function createSplashWindow() {
|
||||
splashWindow = new BrowserWindow({
|
||||
frame: false,
|
||||
width: 200,
|
||||
height: 200,
|
||||
resizable: false,
|
||||
transparent: true,
|
||||
});
|
||||
splashWindow.loadFile(global.pathConfig.splashHtml);
|
||||
splashWindow.on("closed", () => {
|
||||
splashWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
export function createMainWindow() {
|
||||
createSplashWindow();
|
||||
mainWindow = new BrowserWindow({
|
||||
title: "畅联",
|
||||
icon: join(global.pathConfig.publicPath, "favicon.ico"),
|
||||
frame: false,
|
||||
show: false,
|
||||
width: 1200,
|
||||
height: 800,
|
||||
minWidth: 1024,
|
||||
minHeight: 726,
|
||||
titleBarStyle: "hiddenInset",
|
||||
webPreferences: {
|
||||
preload: global.pathConfig.preload,
|
||||
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
|
||||
// Consider using contextBridge.exposeInMainWorld
|
||||
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: false,
|
||||
devTools: true,
|
||||
webSecurity: false,
|
||||
},
|
||||
});
|
||||
|
||||
sdkInstance = initIMSDK(mainWindow.webContents);
|
||||
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
// Open devTool if the app is not packaged
|
||||
mainWindow.loadURL(url);
|
||||
} else {
|
||||
mainWindow.loadFile(global.pathConfig.indexHtml);
|
||||
}
|
||||
|
||||
// Test actively push message to the Electron-Renderer
|
||||
mainWindow.webContents.on("did-finish-load", () => {
|
||||
mainWindow?.webContents.send("main-process-message", new Date().toLocaleString());
|
||||
});
|
||||
|
||||
// // Make all links open with the browser, not with the application
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (url.startsWith("https:") || url.startsWith("http:")) shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
mainWindow.on("focus", () => {
|
||||
mainWindow?.flashFrame(false);
|
||||
registerShortcuts();
|
||||
});
|
||||
|
||||
mainWindow.on("blur", () => {
|
||||
unregisterShortcuts();
|
||||
});
|
||||
|
||||
mainWindow.on("close", (e) => {
|
||||
if (getIsForceQuit() || !mainWindow.isVisible()) {
|
||||
mainWindow = null;
|
||||
destroyTray();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
if (isMac && mainWindow.isFullScreen()) {
|
||||
mainWindow.setFullScreen(false);
|
||||
}
|
||||
mainWindow?.hide();
|
||||
}
|
||||
});
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
export function splashEnd() {
|
||||
splashWindow?.close();
|
||||
mainWindow?.show();
|
||||
}
|
||||
|
||||
// utils
|
||||
export const isExistMainWindow = (): boolean =>
|
||||
!!mainWindow && !mainWindow?.isDestroyed();
|
||||
export const isShowMainWindow = (): boolean => {
|
||||
if (!mainWindow) return false;
|
||||
return mainWindow.isVisible() && (isWin ? true : mainWindow.isFocused());
|
||||
};
|
||||
|
||||
export const closeWindow = () => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.close();
|
||||
};
|
||||
|
||||
export const hotReload = () => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.reload();
|
||||
};
|
||||
|
||||
export const sendEvent = (name: string, ...args: any[]) => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.webContents.send(name, ...args);
|
||||
};
|
||||
|
||||
export const showSelectDialog = async (options: Electron.OpenDialogOptions) => {
|
||||
if (!mainWindow) throw new Error("main window is undefined");
|
||||
return await dialog.showOpenDialog(mainWindow, options);
|
||||
};
|
||||
export const showDialog = ({
|
||||
type,
|
||||
message,
|
||||
detail,
|
||||
}: Electron.MessageBoxSyncOptions) => {
|
||||
if (!mainWindow) return;
|
||||
dialog.showMessageBoxSync(mainWindow, {
|
||||
type,
|
||||
message,
|
||||
detail,
|
||||
});
|
||||
};
|
||||
export const showSaveDialog = async (options: Electron.SaveDialogOptions) => {
|
||||
if (!mainWindow) throw new Error("main window is undefined");
|
||||
return await dialog.showSaveDialog(mainWindow, options);
|
||||
};
|
||||
export const minimize = () => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.minimize();
|
||||
};
|
||||
export const updateMaximize = () => {
|
||||
if (!mainWindow) return;
|
||||
if (mainWindow.isMaximized()) {
|
||||
mainWindow.unmaximize();
|
||||
} else {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
};
|
||||
export const toggleHide = () => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
|
||||
};
|
||||
export const toggleMinimize = () => {
|
||||
if (!mainWindow) return;
|
||||
if (mainWindow.isMinimized()) {
|
||||
if (!mainWindow.isVisible()) {
|
||||
mainWindow.show();
|
||||
}
|
||||
mainWindow.restore();
|
||||
mainWindow.focus();
|
||||
} else {
|
||||
mainWindow.minimize();
|
||||
}
|
||||
};
|
||||
export const showWindow = () => {
|
||||
if (!mainWindow) return;
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.focus();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
}
|
||||
};
|
||||
export const hideWindow = () => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.hide();
|
||||
};
|
||||
export const toggleWindowVisible = (visible: boolean) => {
|
||||
if (!mainWindow) return;
|
||||
const opacity = mainWindow.getOpacity() ? 0 : 1;
|
||||
if (Boolean(opacity) !== visible) return;
|
||||
mainWindow.setOpacity(opacity);
|
||||
};
|
||||
export const setProgressBar = (
|
||||
progress: number,
|
||||
options?: Electron.ProgressBarOptions,
|
||||
) => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.setProgressBar(progress, options);
|
||||
};
|
||||
export const taskFlicker = () => {
|
||||
if (
|
||||
isMac ||
|
||||
(mainWindow.isVisible() && mainWindow.isFocused() && !isExistMainWindow())
|
||||
)
|
||||
return;
|
||||
mainWindow?.flashFrame(true);
|
||||
};
|
||||
export const setIgnoreMouseEvents = (
|
||||
ignore: boolean,
|
||||
options?: Electron.IgnoreMouseEventsOptions,
|
||||
) => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.setIgnoreMouseEvents(ignore, options);
|
||||
};
|
||||
export const toggleDevTools = () => {
|
||||
if (!mainWindow) return;
|
||||
if (mainWindow.webContents.isDevToolsOpened()) {
|
||||
mainWindow.webContents.closeDevTools();
|
||||
} else {
|
||||
mainWindow.webContents.openDevTools({
|
||||
mode: "detach",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const setFullScreen = (isFullscreen: boolean): boolean => {
|
||||
if (!mainWindow) return false;
|
||||
if (isLinux) {
|
||||
// linux It needs to be resizable before it can be full screen
|
||||
if (isFullscreen) {
|
||||
mainWindow.setResizable(isFullscreen);
|
||||
mainWindow.setFullScreen(isFullscreen);
|
||||
} else {
|
||||
mainWindow.setFullScreen(isFullscreen);
|
||||
mainWindow.setResizable(isFullscreen);
|
||||
}
|
||||
} else {
|
||||
mainWindow.setFullScreen(isFullscreen);
|
||||
}
|
||||
return isFullscreen;
|
||||
};
|
||||
|
||||
export const clearCache = async () => {
|
||||
if (!mainWindow) throw new Error("main window is undefined");
|
||||
await mainWindow.webContents.session.clearCache();
|
||||
await mainWindow.webContents.session.clearStorageData();
|
||||
};
|
||||
|
||||
export const getCacheSize = async () => {
|
||||
if (!mainWindow) throw new Error("main window is undefined");
|
||||
return await mainWindow.webContents.session.getCacheSize();
|
||||
};
|
||||
|
||||
export const getWebContents = (): Electron.WebContents => {
|
||||
if (!mainWindow) throw new Error("main window is undefined");
|
||||
return mainWindow.webContents;
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { DataPath, IElectronAPI } from "./../../src/types/globalExpose.d";
|
||||
import { contextBridge, ipcRenderer } from "electron";
|
||||
import { isProd } from "../utils";
|
||||
import "@openim/electron-client-sdk/lib/preload";
|
||||
import { Platform } from "@openim/wasm-client-sdk";
|
||||
|
||||
const getPlatform = () => {
|
||||
if (process.platform === "darwin") {
|
||||
return Platform.MacOSX;
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
return Platform.Windows;
|
||||
}
|
||||
return Platform.Linux;
|
||||
};
|
||||
|
||||
const getDataPath = (key: DataPath) => {
|
||||
switch (key) {
|
||||
case "public":
|
||||
return isProd ? ipcRenderer.sendSync("getDataPath", "public") : "";
|
||||
case "sdkResources":
|
||||
return isProd ? ipcRenderer.sendSync("getDataPath", "sdkResources") : "";
|
||||
case "logsPath":
|
||||
return isProd ? ipcRenderer.sendSync("getDataPath", "logsPath") : "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const subscribe = (channel: string, callback: (...args: any[]) => void) => {
|
||||
const subscription = (_, ...args) => callback(...args);
|
||||
ipcRenderer.on(channel, subscription);
|
||||
return () => ipcRenderer.removeListener(channel, subscription);
|
||||
};
|
||||
|
||||
const subscribeOnce = (channel: string, callback: (...args: any[]) => void) => {
|
||||
ipcRenderer.once(channel, (_, ...args) => callback(...args));
|
||||
};
|
||||
|
||||
const unsubscribeAll = (channel: string) => {
|
||||
ipcRenderer.removeAllListeners(channel);
|
||||
};
|
||||
|
||||
const ipcInvoke = (channel: string, ...arg: any) => {
|
||||
return ipcRenderer.invoke(channel, ...arg);
|
||||
};
|
||||
|
||||
const ipcSendSync = (channel: string, ...arg: any) => {
|
||||
return ipcRenderer.sendSync(channel, ...arg);
|
||||
};
|
||||
|
||||
const getUniqueSavePath = (originalPath: string) => {
|
||||
let counter = 0;
|
||||
let savePath = originalPath;
|
||||
let fileDir = path.dirname(originalPath);
|
||||
let fileName = path.basename(originalPath);
|
||||
let fileExt = path.extname(originalPath);
|
||||
let baseName = path.basename(fileName, fileExt);
|
||||
|
||||
while (fs.existsSync(savePath)) {
|
||||
counter++;
|
||||
fileName = `${baseName}(${counter})${fileExt}`;
|
||||
savePath = path.join(fileDir, fileName);
|
||||
}
|
||||
|
||||
return savePath;
|
||||
};
|
||||
|
||||
const getFileByPath = async (filePath: string) => {
|
||||
try {
|
||||
const filename = path.basename(filePath);
|
||||
const data = await fs.promises.readFile(filePath);
|
||||
return new File([data], filename);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const saveFileToDisk = async ({
|
||||
file,
|
||||
sync,
|
||||
}: {
|
||||
file: File;
|
||||
sync?: boolean;
|
||||
}): Promise<string> => {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const saveDir = ipcRenderer.sendSync("getDataPath", "sdkResources");
|
||||
const savePath = path.join(saveDir, file.name);
|
||||
const uniqueSavePath = getUniqueSavePath(savePath);
|
||||
if (!fs.existsSync(saveDir)) {
|
||||
fs.mkdirSync(saveDir, { recursive: true });
|
||||
}
|
||||
if (sync) {
|
||||
await fs.promises.writeFile(uniqueSavePath, Buffer.from(arrayBuffer));
|
||||
} else {
|
||||
fs.promises.writeFile(uniqueSavePath, Buffer.from(arrayBuffer));
|
||||
}
|
||||
return uniqueSavePath;
|
||||
};
|
||||
|
||||
const Api: IElectronAPI = {
|
||||
getDataPath,
|
||||
getVersion: () => process.version,
|
||||
getPlatform,
|
||||
getSystemVersion: process.getSystemVersion,
|
||||
subscribe,
|
||||
subscribeOnce,
|
||||
unsubscribeAll,
|
||||
ipcInvoke,
|
||||
ipcSendSync,
|
||||
getFileByPath,
|
||||
saveFileToDisk,
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld("electronAPI", Api);
|
||||
@@ -0,0 +1,22 @@
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import OpenIMSDKMain from "@openim/electron-client-sdk";
|
||||
import { WebContents } from "electron";
|
||||
|
||||
export const getLibSuffix = () => {
|
||||
const platform = process.platform;
|
||||
const arch = os.arch();
|
||||
if (platform === "darwin") {
|
||||
return path.join(`mac_${arch === "arm64" ? "arm64" : "x64"}`, "libopenimsdk.dylib");
|
||||
}
|
||||
if (platform === "win32") {
|
||||
return path.join(`win_${arch === "ia32" ? "ia32" : "x64"}`, "libopenimsdk.dll");
|
||||
}
|
||||
return path.join(`linux_${arch === "arm64" ? "arm64" : "x64"}`, "libopenimsdk.so");
|
||||
};
|
||||
|
||||
export const initIMSDK = (webContents: WebContents) =>
|
||||
new OpenIMSDKMain(
|
||||
path.join(global.pathConfig.imsdkLibPath, getLibSuffix()),
|
||||
webContents,
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
export const isLinux = process.platform == "linux";
|
||||
export const isWin = process.platform == "win32";
|
||||
export const isMac = process.platform == "darwin";
|
||||
export const isProd = !process.env.VITE_DEV_SERVER_URL;
|
||||
@@ -0,0 +1,17 @@
|
||||
import log from "electron-log/main";
|
||||
import { join } from "node:path";
|
||||
import fs from "fs";
|
||||
|
||||
const getLogger = (logsPath: string) => {
|
||||
log.transports.file.level = "debug";
|
||||
log.transports.file.maxSize = 104857600; // max size 100M
|
||||
log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}";
|
||||
let date = new Date();
|
||||
// let dateStr = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
|
||||
// log.transports.file.resolvePathFn = () => join(logsPath, `log${dateStr}.log`);
|
||||
log.transports.file.resolvePathFn = () => join(logsPath, `OpenIM.log`);
|
||||
log.initialize({ preload: true });
|
||||
return log.scope("ipcMain");
|
||||
};
|
||||
|
||||
export { getLogger };
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- <meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="script-src 'self' 'unsafe-inline';"
|
||||
/> -->
|
||||
<title>畅联</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script src="./wasm_exec.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"name": "changlian-pc",
|
||||
"version": "1.0.0",
|
||||
"main": "dist-electron/main/index.js",
|
||||
"description": "OpenIM PC Client.",
|
||||
"author": "blooming",
|
||||
"private": true,
|
||||
"debug": {
|
||||
"env": {
|
||||
"VITE_DEV_SERVER_URL": "http://127.0.0.1:7777/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --force --host",
|
||||
"mock:account": "node scripts/mock-account-server.js",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"pree2e": "vite build --mode=test",
|
||||
"e2e": "playwright test",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint --ext .js,.jsx,.ts,.tsx src",
|
||||
"lint:fix": "eslint --ext .js,.jsx,.ts,.tsx --fix --quiet src",
|
||||
"prepare": "husky install",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.4.4",
|
||||
"@commitlint/config-conventional": "^17.4.4",
|
||||
"@playwright/test": "^1.31.0",
|
||||
"@types/md5": "^2.3.2",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
||||
"@typescript-eslint/parser": "^5.53.0",
|
||||
"@vitejs/plugin-legacy": "^4.1.0",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"electron": "^22.3.27",
|
||||
"electron-builder": "^23.6.0",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.1.2",
|
||||
"patch-package": "^8.0.0",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-nesting": "^12.0.2",
|
||||
"prettier": "^2.8.4",
|
||||
"prettier-plugin-tailwindcss": "^0.2.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.58.3",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"terser": "^5.19.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.1.4",
|
||||
"vite-electron-plugin": "^0.8.2",
|
||||
"vite-plugin-electron": "^0.11.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{tsx,ts}": [
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
],
|
||||
"*.{json,html,css,scss,xml,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.1.4",
|
||||
"@ckeditor/ckeditor5-react": "^8.0.0",
|
||||
"@livekit/components-react": "2.6.9",
|
||||
"@livekit/components-styles": "1.1.4",
|
||||
"@openim/electron-client-sdk": "^3.8.3-patch.10",
|
||||
"@openim/wasm-client-sdk": "^3.8.3-patch.10",
|
||||
"adm-zip": "^0.5.10",
|
||||
"ahooks": "^3.7.7",
|
||||
"antd": "5.10.0",
|
||||
"axios": "^1.4.0",
|
||||
"ckeditor5": "^43.0.0",
|
||||
"clsx": "^1.2.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"electron-log": "^5.0.0",
|
||||
"electron-screenshots": "^0.5.27",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^5.3.0",
|
||||
"emoji-picker-element": "1.21.3",
|
||||
"i18next": "^22.5.0",
|
||||
"i18next-browser-languagedetector": "^7.0.2",
|
||||
"i18next-fs-backend": "^2.2.0",
|
||||
"livekit-client": "2.7.0",
|
||||
"localforage": "^1.10.0",
|
||||
"md5": "^2.3.0",
|
||||
"mitt": "^3.0.0",
|
||||
"react-draggable": "^4.4.5",
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-image-file-resizer": "^0.4.8",
|
||||
"react-query": "^3.39.3",
|
||||
"react-resizable-panels": "^2.0.2",
|
||||
"react-router-dom": "^6.11.1",
|
||||
"react-use": "^17.4.0",
|
||||
"react-virtuoso": "4.3.8",
|
||||
"sudo-prompt": "^9.2.1",
|
||||
"twemoji": "14.0.1",
|
||||
"uuid": "^9.0.1",
|
||||
"xgplayer": "^3.0.5",
|
||||
"zustand": "^4.3.3"
|
||||
},
|
||||
"allowScripts": {
|
||||
"@parcel/watcher@2.6.0": true,
|
||||
"core-js@3.50.0": true,
|
||||
"electron@22.3.27": true,
|
||||
"es5-ext@0.10.64": true,
|
||||
"esbuild@0.18.20": true,
|
||||
"koffi@2.8.0": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"name": "OpenCorp-Base",
|
||||
"version": "1.0.0",
|
||||
"main": "dist-electron/main/index.js",
|
||||
"description": "OpenIM PC Client.",
|
||||
"author": "blooming",
|
||||
"private": true,
|
||||
"debug": {
|
||||
"env": {
|
||||
"VITE_DEV_SERVER_URL": "http://127.0.0.1:7777/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --force --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"pree2e": "vite build --mode=test",
|
||||
"e2e": "playwright test",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint --ext .js,.jsx,.ts,.tsx src",
|
||||
"lint:fix": "eslint --ext .js,.jsx,.ts,.tsx --fix --quiet src",
|
||||
"prepare": "husky install",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.4.4",
|
||||
"@commitlint/config-conventional": "^17.4.4",
|
||||
"@playwright/test": "^1.31.0",
|
||||
"@types/md5": "^2.3.2",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
||||
"@typescript-eslint/parser": "^5.53.0",
|
||||
"@vitejs/plugin-legacy": "^4.1.0",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"electron": "^22.3.27",
|
||||
"electron-builder": "^23.6.0",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.1.2",
|
||||
"patch-package": "^8.0.0",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-nesting": "^12.0.2",
|
||||
"prettier": "^2.8.4",
|
||||
"prettier-plugin-tailwindcss": "^0.2.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.58.3",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"terser": "^5.19.0",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.1.4",
|
||||
"vite-electron-plugin": "^0.8.2",
|
||||
"vite-plugin-electron": "^0.11.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{tsx,ts}": ["prettier --write", "eslint --fix"],
|
||||
"*.{json,html,css,scss,xml,md}": ["prettier --write"]
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.1.4",
|
||||
"@ckeditor/ckeditor5-react": "^8.0.0",
|
||||
"@livekit/components-react": "^2.6.9",
|
||||
"@livekit/components-styles": "^1.1.4",
|
||||
"@openim/electron-client-sdk": "^3.8.3-patch.3",
|
||||
"@openim/wasm-client-sdk": "^3.8.3-patch.3",
|
||||
"adm-zip": "^0.5.10",
|
||||
"ahooks": "^3.7.7",
|
||||
"antd": "5.10.0",
|
||||
"axios": "^1.4.0",
|
||||
"ckeditor5": "^43.0.0",
|
||||
"clsx": "^1.2.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"electron-log": "^5.0.0",
|
||||
"electron-screenshots": "^0.5.23",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^5.3.0",
|
||||
"emoji-picker-element": "1.21.3",
|
||||
"i18next": "^22.5.0",
|
||||
"i18next-browser-languagedetector": "^7.0.2",
|
||||
"i18next-fs-backend": "^2.2.0",
|
||||
"livekit-client": "2.7.0",
|
||||
"localforage": "^1.10.0",
|
||||
"md5": "^2.3.0",
|
||||
"mitt": "^3.0.0",
|
||||
"react-draggable": "^4.4.5",
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-image-file-resizer": "^0.4.8",
|
||||
"react-query": "^3.39.3",
|
||||
"react-resizable-panels": "^2.0.2",
|
||||
"react-router-dom": "^6.11.1",
|
||||
"react-use": "^17.4.0",
|
||||
"react-virtuoso": "4.3.8",
|
||||
"sudo-prompt": "^9.2.1",
|
||||
"twemoji": "14.0.1",
|
||||
"uuid": "^9.0.1",
|
||||
"xgplayer": "^3.0.5",
|
||||
"zustand": "^4.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "OpenCorp-Base",
|
||||
"version": "3.8.3",
|
||||
"main": "dist-electron/main/index.js",
|
||||
"description": "OpenIM PC Client.",
|
||||
"author": "blooming",
|
||||
"private": true,
|
||||
"debug": {
|
||||
"env": {
|
||||
"VITE_DEV_SERVER_URL": "http://127.0.0.1:7777/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"build:mac": "vite build && electron-builder --macos --x64",
|
||||
"build:mac-arm": "vite build && electron-builder --macos --arm64",
|
||||
"build:win": "vite build && electron-builder --win --x64",
|
||||
"build:win-arm": "vite build && electron-builder --win --arm64",
|
||||
"build:linux": "vite build && electron-builder --linux --x64",
|
||||
"build:linux-arm": "vite build && electron-builder --linux --arm64",
|
||||
"build:all": "vite build && electron-builder --macos --x64 && electron-builder --macos --arm64 && electron-builder --win --x64 && electron-builder --linux --x64 && electron-builder --linux --arm64",
|
||||
"pree2e": "vite build --mode=test",
|
||||
"e2e": "playwright test",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint --ext .js,.jsx,.ts,.tsx src",
|
||||
"lint:fix": "eslint --ext .js,.jsx,.ts,.tsx --fix --quiet src",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{tsx,ts}": ["prettier --write", "eslint --fix"],
|
||||
"*.{json,html,css,scss,xml,md}": ["prettier --write"]
|
||||
},
|
||||
"dependencies": {
|
||||
"@openim/electron-client-sdk": "^3.8.3-patch.3",
|
||||
"@openim/wasm-client-sdk": "^3.8.3-patch.3",
|
||||
"electron-log": "^5.0.0",
|
||||
"electron-screenshots": "^0.5.23",
|
||||
"electron-store": "^8.1.0",
|
||||
"adm-zip": "^0.5.10",
|
||||
"i18next": "^22.5.0",
|
||||
"sudo-prompt": "^9.2.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
diff --git a/node_modules/@ckeditor/ckeditor5-ui/src/editorui/poweredby.js b/node_modules/@ckeditor/ckeditor5-ui/src/editorui/poweredby.js
|
||||
index 191aba1..324f6d2 100644
|
||||
--- a/node_modules/@ckeditor/ckeditor5-ui/src/editorui/poweredby.js
|
||||
+++ b/node_modules/@ckeditor/ckeditor5-ui/src/editorui/poweredby.js
|
||||
@@ -112,12 +112,9 @@ export default class PoweredBy extends /* #__PURE__ */ DomEmitterMixin() {
|
||||
if (!this._balloonView) {
|
||||
this._createBalloonView();
|
||||
}
|
||||
- this._balloonView.pin(attachOptions);
|
||||
+ // this._balloonView.pin(attachOptions);
|
||||
}
|
||||
}
|
||||
- /**
|
||||
- * Hides the "powered by" balloon if already visible.
|
||||
- */
|
||||
_hideBalloon() {
|
||||
if (this._balloonView) {
|
||||
this._balloonView.unpin();
|
||||
diff --git a/node_modules/@ckeditor/ckeditor5-ui/src/icon/iconview.js b/node_modules/@ckeditor/ckeditor5-ui/src/icon/iconview.js
|
||||
index dd6d2ed..7af8e61 100644
|
||||
--- a/node_modules/@ckeditor/ckeditor5-ui/src/icon/iconview.js
|
||||
+++ b/node_modules/@ckeditor/ckeditor5-ui/src/icon/iconview.js
|
||||
@@ -42,17 +42,14 @@ class IconView extends View {
|
||||
}
|
||||
});
|
||||
}
|
||||
- /**
|
||||
- * @inheritDoc
|
||||
- */
|
||||
render() {
|
||||
super.render();
|
||||
- this._updateXMLContent();
|
||||
+ // this._updateXMLContent();
|
||||
this._colorFillPaths();
|
||||
// This is a hack for lack of innerHTML binding.
|
||||
// See: https://github.com/ckeditor/ckeditor5-ui/issues/99.
|
||||
this.on('change:content', () => {
|
||||
- this._updateXMLContent();
|
||||
+ // this._updateXMLContent();
|
||||
this._colorFillPaths();
|
||||
});
|
||||
this.on('change:fillColor', () => {
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { PlaywrightTestConfig } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: "./e2e",
|
||||
/* Maximum time one test can run for. */
|
||||
timeout: 30 * 1000,
|
||||
expect: {
|
||||
/**
|
||||
* Maximum time expect() should wait for the condition to be met.
|
||||
* For example in `await expect(locator).toHaveText();`
|
||||
*/
|
||||
timeout: 5000,
|
||||
},
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
|
||||
actionTimeout: 0,
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
|
||||
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
|
||||
// outputDir: 'test-results/',
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// port: 3000,
|
||||
// },
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'tailwindcss/nesting': 'postcss-nesting',
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
},
|
||||
};
|
||||
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 342 B |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,70 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.letter {
|
||||
opacity: 0;
|
||||
color: lightgreen;
|
||||
}
|
||||
|
||||
.fade-in-animation {
|
||||
animation: fade-in 4s infinite;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
color: lightgreen;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
color: lightblue;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="logo">
|
||||
<span class="letter">O</span>
|
||||
<span class="letter">p</span>
|
||||
<span class="letter">e</span>
|
||||
<span class="letter">n</span>
|
||||
<span class="letter">I</span>
|
||||
<span class="letter">M</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const letters = document.querySelectorAll(".letter");
|
||||
|
||||
function animateLetters() {
|
||||
letters.forEach((letter, index) => {
|
||||
setTimeout(() => {
|
||||
letter.classList.add("fade-in-animation");
|
||||
}, index * 300);
|
||||
});
|
||||
}
|
||||
|
||||
animateLetters();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,575 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
"use strict";
|
||||
|
||||
(() => {
|
||||
const enosys = () => {
|
||||
const err = new Error("not implemented");
|
||||
err.code = "ENOSYS";
|
||||
return err;
|
||||
};
|
||||
|
||||
if (!globalThis.fs) {
|
||||
let outputBuf = "";
|
||||
globalThis.fs = {
|
||||
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
|
||||
writeSync(fd, buf) {
|
||||
outputBuf += decoder.decode(buf);
|
||||
const nl = outputBuf.lastIndexOf("\n");
|
||||
if (nl != -1) {
|
||||
console.log(outputBuf.substring(0, nl));
|
||||
outputBuf = outputBuf.substring(nl + 1);
|
||||
}
|
||||
return buf.length;
|
||||
},
|
||||
write(fd, buf, offset, length, position, callback) {
|
||||
if (offset !== 0 || length !== buf.length || position !== null) {
|
||||
callback(enosys());
|
||||
return;
|
||||
}
|
||||
const n = this.writeSync(fd, buf);
|
||||
callback(null, n);
|
||||
},
|
||||
chmod(path, mode, callback) { callback(enosys()); },
|
||||
chown(path, uid, gid, callback) { callback(enosys()); },
|
||||
close(fd, callback) { callback(enosys()); },
|
||||
fchmod(fd, mode, callback) { callback(enosys()); },
|
||||
fchown(fd, uid, gid, callback) { callback(enosys()); },
|
||||
fstat(fd, callback) { callback(enosys()); },
|
||||
fsync(fd, callback) { callback(null); },
|
||||
ftruncate(fd, length, callback) { callback(enosys()); },
|
||||
lchown(path, uid, gid, callback) { callback(enosys()); },
|
||||
link(path, link, callback) { callback(enosys()); },
|
||||
lstat(path, callback) { callback(enosys()); },
|
||||
mkdir(path, perm, callback) { callback(enosys()); },
|
||||
open(path, flags, mode, callback) { callback(enosys()); },
|
||||
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
|
||||
readdir(path, callback) { callback(enosys()); },
|
||||
readlink(path, callback) { callback(enosys()); },
|
||||
rename(from, to, callback) { callback(enosys()); },
|
||||
rmdir(path, callback) { callback(enosys()); },
|
||||
stat(path, callback) { callback(enosys()); },
|
||||
symlink(path, link, callback) { callback(enosys()); },
|
||||
truncate(path, length, callback) { callback(enosys()); },
|
||||
unlink(path, callback) { callback(enosys()); },
|
||||
utimes(path, atime, mtime, callback) { callback(enosys()); },
|
||||
};
|
||||
}
|
||||
|
||||
if (!globalThis.process) {
|
||||
globalThis.process = {
|
||||
getuid() { return -1; },
|
||||
getgid() { return -1; },
|
||||
geteuid() { return -1; },
|
||||
getegid() { return -1; },
|
||||
getgroups() { throw enosys(); },
|
||||
pid: -1,
|
||||
ppid: -1,
|
||||
umask() { throw enosys(); },
|
||||
cwd() { throw enosys(); },
|
||||
chdir() { throw enosys(); },
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.path) {
|
||||
globalThis.path = {
|
||||
resolve(...pathSegments) {
|
||||
return pathSegments.join("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.crypto) {
|
||||
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
|
||||
}
|
||||
|
||||
if (!globalThis.performance) {
|
||||
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
|
||||
}
|
||||
|
||||
if (!globalThis.TextEncoder) {
|
||||
throw new Error("globalThis.TextEncoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
if (!globalThis.TextDecoder) {
|
||||
throw new Error("globalThis.TextDecoder is not available, polyfill required");
|
||||
}
|
||||
|
||||
const encoder = new TextEncoder("utf-8");
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
|
||||
globalThis.Go = class {
|
||||
constructor() {
|
||||
this.argv = ["js"];
|
||||
this.env = {};
|
||||
this.exit = (code) => {
|
||||
if (code !== 0) {
|
||||
console.warn("exit code:", code);
|
||||
}
|
||||
};
|
||||
this._exitPromise = new Promise((resolve) => {
|
||||
this._resolveExitPromise = resolve;
|
||||
});
|
||||
this._pendingEvent = null;
|
||||
this._scheduledTimeouts = new Map();
|
||||
this._nextCallbackTimeoutID = 1;
|
||||
|
||||
const setInt64 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
|
||||
}
|
||||
|
||||
const setInt32 = (addr, v) => {
|
||||
this.mem.setUint32(addr + 0, v, true);
|
||||
}
|
||||
|
||||
const getInt64 = (addr) => {
|
||||
const low = this.mem.getUint32(addr + 0, true);
|
||||
const high = this.mem.getInt32(addr + 4, true);
|
||||
return low + high * 4294967296;
|
||||
}
|
||||
|
||||
const loadValue = (addr) => {
|
||||
const f = this.mem.getFloat64(addr, true);
|
||||
if (f === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isNaN(f)) {
|
||||
return f;
|
||||
}
|
||||
|
||||
const id = this.mem.getUint32(addr, true);
|
||||
return this._values[id];
|
||||
}
|
||||
|
||||
const storeValue = (addr, v) => {
|
||||
const nanHead = 0x7FF80000;
|
||||
|
||||
if (typeof v === "number" && v !== 0) {
|
||||
if (isNaN(v)) {
|
||||
this.mem.setUint32(addr + 4, nanHead, true);
|
||||
this.mem.setUint32(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
this.mem.setFloat64(addr, v, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (v === undefined) {
|
||||
this.mem.setFloat64(addr, 0, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let id = this._ids.get(v);
|
||||
if (id === undefined) {
|
||||
id = this._idPool.pop();
|
||||
if (id === undefined) {
|
||||
id = this._values.length;
|
||||
}
|
||||
this._values[id] = v;
|
||||
this._goRefCounts[id] = 0;
|
||||
this._ids.set(v, id);
|
||||
}
|
||||
this._goRefCounts[id]++;
|
||||
let typeFlag = 0;
|
||||
switch (typeof v) {
|
||||
case "object":
|
||||
if (v !== null) {
|
||||
typeFlag = 1;
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
typeFlag = 2;
|
||||
break;
|
||||
case "symbol":
|
||||
typeFlag = 3;
|
||||
break;
|
||||
case "function":
|
||||
typeFlag = 4;
|
||||
break;
|
||||
}
|
||||
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
|
||||
this.mem.setUint32(addr, id, true);
|
||||
}
|
||||
|
||||
const loadSlice = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
|
||||
}
|
||||
|
||||
const loadSliceOfValues = (addr) => {
|
||||
const array = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
const a = new Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
a[i] = loadValue(array + i * 8);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
const loadString = (addr) => {
|
||||
const saddr = getInt64(addr + 0);
|
||||
const len = getInt64(addr + 8);
|
||||
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
|
||||
}
|
||||
|
||||
const testCallExport = (a, b) => {
|
||||
this._inst.exports.testExport0();
|
||||
return this._inst.exports.testExport(a, b);
|
||||
}
|
||||
|
||||
const timeOrigin = Date.now() - performance.now();
|
||||
this.importObject = {
|
||||
_gotest: {
|
||||
add: (a, b) => a + b,
|
||||
callExport: testCallExport,
|
||||
},
|
||||
gojs: {
|
||||
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
|
||||
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
|
||||
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
|
||||
// This changes the SP, thus we have to update the SP used by the imported function.
|
||||
|
||||
// func wasmExit(code int32)
|
||||
"runtime.wasmExit": (sp) => {
|
||||
sp >>>= 0;
|
||||
const code = this.mem.getInt32(sp + 8, true);
|
||||
this.exited = true;
|
||||
delete this._inst;
|
||||
delete this._values;
|
||||
delete this._goRefCounts;
|
||||
delete this._ids;
|
||||
delete this._idPool;
|
||||
this.exit(code);
|
||||
},
|
||||
|
||||
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
|
||||
"runtime.wasmWrite": (sp) => {
|
||||
sp >>>= 0;
|
||||
const fd = getInt64(sp + 8);
|
||||
const p = getInt64(sp + 16);
|
||||
const n = this.mem.getInt32(sp + 24, true);
|
||||
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
|
||||
},
|
||||
|
||||
// func resetMemoryDataView()
|
||||
"runtime.resetMemoryDataView": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
},
|
||||
|
||||
// func nanotime1() int64
|
||||
"runtime.nanotime1": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
|
||||
},
|
||||
|
||||
// func walltime() (sec int64, nsec int32)
|
||||
"runtime.walltime": (sp) => {
|
||||
sp >>>= 0;
|
||||
const msec = (new Date).getTime();
|
||||
setInt64(sp + 8, msec / 1000);
|
||||
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
|
||||
},
|
||||
|
||||
// func scheduleTimeoutEvent(delay int64) int32
|
||||
"runtime.scheduleTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this._nextCallbackTimeoutID;
|
||||
this._nextCallbackTimeoutID++;
|
||||
this._scheduledTimeouts.set(id, setTimeout(
|
||||
() => {
|
||||
this._resume();
|
||||
while (this._scheduledTimeouts.has(id)) {
|
||||
// for some reason Go failed to register the timeout event, log and try again
|
||||
// (temporary workaround for https://github.com/golang/go/issues/28975)
|
||||
console.warn("scheduleTimeoutEvent: missed timeout event");
|
||||
this._resume();
|
||||
}
|
||||
},
|
||||
getInt64(sp + 8),
|
||||
));
|
||||
this.mem.setInt32(sp + 16, id, true);
|
||||
},
|
||||
|
||||
// func clearTimeoutEvent(id int32)
|
||||
"runtime.clearTimeoutEvent": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getInt32(sp + 8, true);
|
||||
clearTimeout(this._scheduledTimeouts.get(id));
|
||||
this._scheduledTimeouts.delete(id);
|
||||
},
|
||||
|
||||
// func getRandomData(r []byte)
|
||||
"runtime.getRandomData": (sp) => {
|
||||
sp >>>= 0;
|
||||
crypto.getRandomValues(loadSlice(sp + 8));
|
||||
},
|
||||
|
||||
// func finalizeRef(v ref)
|
||||
"syscall/js.finalizeRef": (sp) => {
|
||||
sp >>>= 0;
|
||||
const id = this.mem.getUint32(sp + 8, true);
|
||||
this._goRefCounts[id]--;
|
||||
if (this._goRefCounts[id] === 0) {
|
||||
const v = this._values[id];
|
||||
this._values[id] = null;
|
||||
this._ids.delete(v);
|
||||
this._idPool.push(id);
|
||||
}
|
||||
},
|
||||
|
||||
// func stringVal(value string) ref
|
||||
"syscall/js.stringVal": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, loadString(sp + 8));
|
||||
},
|
||||
|
||||
// func valueGet(v ref, p string) ref
|
||||
"syscall/js.valueGet": (sp) => {
|
||||
sp >>>= 0;
|
||||
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 32, result);
|
||||
},
|
||||
|
||||
// func valueSet(v ref, p string, x ref)
|
||||
"syscall/js.valueSet": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
|
||||
},
|
||||
|
||||
// func valueDelete(v ref, p string)
|
||||
"syscall/js.valueDelete": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
|
||||
},
|
||||
|
||||
// func valueIndex(v ref, i int) ref
|
||||
"syscall/js.valueIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
|
||||
},
|
||||
|
||||
// valueSetIndex(v ref, i int, x ref)
|
||||
"syscall/js.valueSetIndex": (sp) => {
|
||||
sp >>>= 0;
|
||||
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
|
||||
},
|
||||
|
||||
// func valueCall(v ref, m string, args []ref) (ref, bool)
|
||||
"syscall/js.valueCall": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const m = Reflect.get(v, loadString(sp + 16));
|
||||
const args = loadSliceOfValues(sp + 32);
|
||||
const result = Reflect.apply(m, v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, result);
|
||||
this.mem.setUint8(sp + 64, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 56, err);
|
||||
this.mem.setUint8(sp + 64, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueInvoke(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueInvoke": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.apply(v, undefined, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueNew(v ref, args []ref) (ref, bool)
|
||||
"syscall/js.valueNew": (sp) => {
|
||||
sp >>>= 0;
|
||||
try {
|
||||
const v = loadValue(sp + 8);
|
||||
const args = loadSliceOfValues(sp + 16);
|
||||
const result = Reflect.construct(v, args);
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, result);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
} catch (err) {
|
||||
sp = this._inst.exports.getsp() >>> 0; // see comment above
|
||||
storeValue(sp + 40, err);
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
}
|
||||
},
|
||||
|
||||
// func valueLength(v ref) int
|
||||
"syscall/js.valueLength": (sp) => {
|
||||
sp >>>= 0;
|
||||
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
|
||||
},
|
||||
|
||||
// valuePrepareString(v ref) (ref, int)
|
||||
"syscall/js.valuePrepareString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = encoder.encode(String(loadValue(sp + 8)));
|
||||
storeValue(sp + 16, str);
|
||||
setInt64(sp + 24, str.length);
|
||||
},
|
||||
|
||||
// valueLoadString(v ref, b []byte)
|
||||
"syscall/js.valueLoadString": (sp) => {
|
||||
sp >>>= 0;
|
||||
const str = loadValue(sp + 8);
|
||||
loadSlice(sp + 16).set(str);
|
||||
},
|
||||
|
||||
// func valueInstanceOf(v ref, t ref) bool
|
||||
"syscall/js.valueInstanceOf": (sp) => {
|
||||
sp >>>= 0;
|
||||
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
|
||||
},
|
||||
|
||||
// func copyBytesToGo(dst []byte, src ref) (int, bool)
|
||||
"syscall/js.copyBytesToGo": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadSlice(sp + 8);
|
||||
const src = loadValue(sp + 32);
|
||||
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
// func copyBytesToJS(dst ref, src []byte) (int, bool)
|
||||
"syscall/js.copyBytesToJS": (sp) => {
|
||||
sp >>>= 0;
|
||||
const dst = loadValue(sp + 8);
|
||||
const src = loadSlice(sp + 16);
|
||||
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
|
||||
this.mem.setUint8(sp + 48, 0);
|
||||
return;
|
||||
}
|
||||
const toCopy = src.subarray(0, dst.length);
|
||||
dst.set(toCopy);
|
||||
setInt64(sp + 40, toCopy.length);
|
||||
this.mem.setUint8(sp + 48, 1);
|
||||
},
|
||||
|
||||
"debug": (value) => {
|
||||
console.log(value);
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async run(instance) {
|
||||
if (!(instance instanceof WebAssembly.Instance)) {
|
||||
throw new Error("Go.run: WebAssembly.Instance expected");
|
||||
}
|
||||
this._inst = instance;
|
||||
this.mem = new DataView(this._inst.exports.mem.buffer);
|
||||
this._values = [ // JS values that Go currently has references to, indexed by reference id
|
||||
NaN,
|
||||
0,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
globalThis,
|
||||
this,
|
||||
];
|
||||
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
|
||||
this._ids = new Map([ // mapping from JS values to reference ids
|
||||
[0, 1],
|
||||
[null, 2],
|
||||
[true, 3],
|
||||
[false, 4],
|
||||
[globalThis, 5],
|
||||
[this, 6],
|
||||
]);
|
||||
this._idPool = []; // unused ids that have been garbage collected
|
||||
this.exited = false; // whether the Go program has exited
|
||||
|
||||
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
|
||||
let offset = 4096;
|
||||
|
||||
const strPtr = (str) => {
|
||||
const ptr = offset;
|
||||
const bytes = encoder.encode(str + "\0");
|
||||
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
|
||||
offset += bytes.length;
|
||||
if (offset % 8 !== 0) {
|
||||
offset += 8 - (offset % 8);
|
||||
}
|
||||
return ptr;
|
||||
};
|
||||
|
||||
const argc = this.argv.length;
|
||||
|
||||
const argvPtrs = [];
|
||||
this.argv.forEach((arg) => {
|
||||
argvPtrs.push(strPtr(arg));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const keys = Object.keys(this.env).sort();
|
||||
keys.forEach((key) => {
|
||||
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
|
||||
});
|
||||
argvPtrs.push(0);
|
||||
|
||||
const argv = offset;
|
||||
argvPtrs.forEach((ptr) => {
|
||||
this.mem.setUint32(offset, ptr, true);
|
||||
this.mem.setUint32(offset + 4, 0, true);
|
||||
offset += 8;
|
||||
});
|
||||
|
||||
// The linker guarantees global data starts from at least wasmMinDataAddr.
|
||||
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
|
||||
const wasmMinDataAddr = 4096 + 8192;
|
||||
if (offset >= wasmMinDataAddr) {
|
||||
throw new Error("total length of command line and environment variables exceeds limit");
|
||||
}
|
||||
|
||||
this._inst.exports.run(argc, argv);
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
await this._exitPromise;
|
||||
}
|
||||
|
||||
_resume() {
|
||||
if (this.exited) {
|
||||
throw new Error("Go program has already exited");
|
||||
}
|
||||
this._inst.exports.resume();
|
||||
if (this.exited) {
|
||||
this._resolveExitPromise();
|
||||
}
|
||||
}
|
||||
|
||||
_makeFuncWrapper(id) {
|
||||
const go = this;
|
||||
return function () {
|
||||
const event = { id: id, this: this, args: arguments };
|
||||
go._pendingEvent = event;
|
||||
go._resume();
|
||||
return event.result;
|
||||
};
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 本地开发用 mock 账号服务(畅联 PC 端联调用)
|
||||
*
|
||||
* ⚠️ 仅供本地开发联调!
|
||||
* - 内置测试环境默认密钥(OpenIM secret / LiveKit key),都是内网测试环境的公开默认值
|
||||
* - 禁止打包进客户端、禁止用于正式环境
|
||||
* - 真实员工账号由管理员在服务器上的账号服务(:10010)开通
|
||||
*
|
||||
* 用法:
|
||||
* 1. node scripts/mock-account-server.js(或 npm run mock:account)
|
||||
* 2. 把 .env 里的 VITE_ACCOUNT_URL 临时指向 http://127.0.0.1:11010
|
||||
* 3. 用 test001 / test123(或 test002 / test123)登录
|
||||
*
|
||||
* 接口形状与真实账号服务完全一致:POST /api/login、POST /api/rtc_token。
|
||||
* 零依赖,Node >= 18(用到全局 fetch)。
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const PORT = 11010;
|
||||
const OPENIM_API_URL = 'http://192.168.200.11:10002';
|
||||
const OPENIM_SECRET = 'openIM123';
|
||||
const LIVEKIT_API_KEY = 'openimLKkey';
|
||||
const LIVEKIT_API_SECRET = 'lk-secret-9f2c7a41d5e8b063f1a4c795e2d8b3a6';
|
||||
|
||||
// 本地测试账号(密码均为 test123)
|
||||
const USERS = {
|
||||
test001: { password: 'test123', nickname: '测试一号' },
|
||||
test002: { password: 'test123', nickname: '测试二号' },
|
||||
};
|
||||
|
||||
// ---------------- OpenIM 管理接口 ----------------
|
||||
|
||||
async function imApi(route, body, token) {
|
||||
const res = await fetch(OPENIM_API_URL + route, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
operationID: crypto.randomUUID(),
|
||||
...(token ? { token } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function getAdminToken() {
|
||||
const r = await imApi('/auth/get_admin_token', {
|
||||
secret: OPENIM_SECRET,
|
||||
userID: 'imAdmin',
|
||||
});
|
||||
if (r.errCode !== 0) throw new Error('OpenIM 管理员令牌获取失败: ' + (r.errMsg || r.errCode));
|
||||
return r.data.token;
|
||||
}
|
||||
|
||||
/** 确保 OpenIM 里存在该用户(不存在则注册,尽力而为) */
|
||||
async function ensureImUser(userID, nickname, admin) {
|
||||
try {
|
||||
await imApi('/user/user_register', {
|
||||
users: [{ userID, nickname, faceURL: '' }],
|
||||
}, admin);
|
||||
} catch { /* 忽略,后面取令牌时再报错 */ }
|
||||
}
|
||||
|
||||
async function getImUserToken(userID, platformID) {
|
||||
const admin = await getAdminToken();
|
||||
await ensureImUser(userID, USERS[userID].nickname, admin);
|
||||
const r = await imApi('/auth/get_user_token', { secret: OPENIM_SECRET, platformID, userID }, admin);
|
||||
if (r.errCode !== 0) throw new Error('OpenIM 用户令牌获取失败: ' + (r.errMsg || r.errCode));
|
||||
return r.data;
|
||||
}
|
||||
|
||||
// ---------------- LiveKit RTC 令牌签发 ----------------
|
||||
|
||||
const b64url = (s) => Buffer.from(s).toString('base64url');
|
||||
|
||||
function signLivekitToken(userID, room) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const header = b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
|
||||
const payload = b64url(JSON.stringify({
|
||||
iss: LIVEKIT_API_KEY,
|
||||
sub: userID,
|
||||
exp: now + 3600,
|
||||
nbf: now - 10,
|
||||
video: { roomJoin: true, room },
|
||||
metadata: '',
|
||||
}));
|
||||
const sig = crypto.createHmac('sha256', LIVEKIT_API_SECRET).update(`${header}.${payload}`).digest('base64url');
|
||||
return `${header}.${payload}.${sig}`;
|
||||
}
|
||||
|
||||
// ---------------- HTTP 服务 ----------------
|
||||
|
||||
function send(res, status, obj) {
|
||||
res.writeHead(status, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, authorization',
|
||||
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
|
||||
});
|
||||
res.end(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
const ok = (res, data) => send(res, 200, { code: 0, msg: '', data });
|
||||
const fail = (res, msg, httpStatus = 200) => send(res, httpStatus, { code: 1, msg });
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buf = '';
|
||||
req.on('data', (c) => { buf += c; });
|
||||
req.on('end', () => {
|
||||
if (!buf) return resolve({});
|
||||
try { resolve(JSON.parse(buf)); } catch { reject(new Error('请求体不是合法 JSON')); }
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
if (req.method === 'OPTIONS') return send(res, 204, {});
|
||||
|
||||
if (req.method === 'POST' && url.pathname === '/api/login') {
|
||||
const body = await readBody(req);
|
||||
const { staffNo, password, platformID } = body;
|
||||
if (!staffNo || !password) return fail(res, '工号和密码不能为空');
|
||||
const user = USERS[staffNo];
|
||||
if (!user || user.password !== String(password)) return fail(res, '工号或密码错误');
|
||||
const t = await getImUserToken(staffNo, Number.isInteger(platformID) ? platformID : 3);
|
||||
return ok(res, {
|
||||
userID: staffNo,
|
||||
nickname: user.nickname,
|
||||
imToken: t.token,
|
||||
expireTimeSeconds: t.expireTimeSeconds,
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && url.pathname === '/api/rtc_token') {
|
||||
// 与真实账号服务一致:Authorization: Bearer <imToken> + {room, identity},返回 {token}
|
||||
const m = /^Bearer\s+(.+)$/.exec(String(req.headers.authorization || ''));
|
||||
if (!m) return fail(res, '未登录或登录已过期', 401);
|
||||
const body = await readBody(req);
|
||||
const { room, identity } = body;
|
||||
if (!room || !identity) return fail(res, '房间号或用户标识不能为空');
|
||||
return ok(res, { token: signLivekitToken(String(identity), String(room)) });
|
||||
}
|
||||
|
||||
return fail(res, '接口不存在', 404);
|
||||
} catch (e) {
|
||||
return fail(res, '服务内部错误: ' + e.message, 500);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, '127.0.0.1', () => {
|
||||
console.log(`mock-account-server listening on http://127.0.0.1:${PORT}`);
|
||||
console.log('测试账号:test001 / test123,test002 / test123');
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { App } from "antd";
|
||||
import type { MessageInstance } from "antd/es/message/interface";
|
||||
import type { ModalStaticFunctions } from "antd/es/modal/confirm";
|
||||
import type { NotificationInstance } from "antd/es/notification/interface";
|
||||
|
||||
let message: MessageInstance;
|
||||
let notification: NotificationInstance;
|
||||
let modal: Omit<ModalStaticFunctions, "warn">;
|
||||
|
||||
export default () => {
|
||||
const staticFunction = App.useApp();
|
||||
message = staticFunction.message;
|
||||
modal = staticFunction.modal;
|
||||
notification = staticFunction.notification;
|
||||
return null;
|
||||
};
|
||||
|
||||
export { message, modal, notification };
|
||||
@@ -0,0 +1,44 @@
|
||||
import { App as AntdApp, ConfigProvider, theme } from "antd";
|
||||
import enUS from "antd/locale/en_US";
|
||||
import zhCN from "antd/locale/zh_CN";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
import { ReactQueryDevtools } from "react-query/devtools";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
|
||||
import AntdGlobalComp from "./AntdGlobalComp";
|
||||
import router from "./routes";
|
||||
import { useUserStore } from "./store";
|
||||
|
||||
function App() {
|
||||
const locale = useUserStore((state) => state.appSettings.locale);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
autoInsertSpaceInButton={false}
|
||||
locale={locale === "zh-CN" ? zhCN : enUS}
|
||||
theme={{
|
||||
token: { colorPrimary: "#0073D9" },
|
||||
}}
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Suspense fallback={<div>loading...</div>}>
|
||||
<AntdApp>
|
||||
<AntdGlobalComp />
|
||||
<RouterProvider router={router} />
|
||||
</AntdApp>
|
||||
</Suspense>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { message } from "@/AntdGlobalComp";
|
||||
import { ErrCodeMap } from "@/constants";
|
||||
|
||||
interface ErrorData {
|
||||
errCode: number;
|
||||
errMsg?: string;
|
||||
}
|
||||
|
||||
export const errorHandle = (err: unknown) => {
|
||||
const errData = err as ErrorData;
|
||||
if (errData.errMsg) {
|
||||
message.error(ErrCodeMap[errData.errCode] || errData.errMsg);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import axios from "axios";
|
||||
|
||||
import { getIMToken } from "@/utils/storage";
|
||||
|
||||
// RTC(音视频通话)令牌改由自研账号服务签发(原 OpenIM chat 服务 :10008 已下线)。
|
||||
// 账号服务 /api/rtc_token 用登录拿到的 IM token 鉴权(Authorization: Bearer),
|
||||
// 返回形状保持 { data: { serverUrl, token } },RtcCallModal 调用处无需改动。
|
||||
export const getRtcConnectData = async (room: string, identity: string) => {
|
||||
const imToken = await getIMToken();
|
||||
if (!imToken) {
|
||||
return Promise.reject(new Error("登录已过期,请重新登录"));
|
||||
}
|
||||
const { data: res } = await axios.post<{
|
||||
code: number;
|
||||
msg: string;
|
||||
data: { token: string };
|
||||
}>(
|
||||
`${import.meta.env.VITE_ACCOUNT_URL}/api/rtc_token`,
|
||||
{ room, identity },
|
||||
{ headers: { Authorization: `Bearer ${imToken}` } },
|
||||
);
|
||||
if (res.code !== 0) {
|
||||
return Promise.reject(new Error(res.msg || "获取通话令牌失败"));
|
||||
}
|
||||
return {
|
||||
data: {
|
||||
serverUrl: import.meta.env.VITE_LIVEKIT_URL as string,
|
||||
token: res.data.token,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { MessageReceiveOptType } from "@openim/wasm-client-sdk";
|
||||
|
||||
import { IMSDK } from "@/layout/MainContentWrap";
|
||||
|
||||
// 原 OpenIM chat 业务服务(:10008)已下线,用户信息相关接口全部改走 OpenIM SDK。
|
||||
// 以下导出保持原有函数签名和返回形状({ data: { users } } 等),调用处无需大改。
|
||||
|
||||
export interface BusinessUserInfo {
|
||||
userID: string;
|
||||
password: string;
|
||||
account: string;
|
||||
phoneNumber: string;
|
||||
areaCode: string;
|
||||
email: string;
|
||||
nickname: string;
|
||||
faceURL: string;
|
||||
gender: number;
|
||||
level: number;
|
||||
birth: number;
|
||||
allowAddFriend: BusinessAllowType;
|
||||
allowBeep: BusinessAllowType;
|
||||
allowVibration: BusinessAllowType;
|
||||
globalRecvMsgOpt: MessageReceiveOptType;
|
||||
}
|
||||
|
||||
export enum BusinessAllowType {
|
||||
Allow = 1,
|
||||
NotAllow = 2,
|
||||
}
|
||||
|
||||
// 批量获取用户信息(原 chat 服务 /user/find/full)
|
||||
// SDK 只提供公开资料(userID/nickname/faceURL/ex),手机号、邮箱等原业务字段不再有值。
|
||||
export const getBusinessUserInfo = async (userIDs: string[]) => {
|
||||
const { data } = await IMSDK.getUsersInfo(userIDs);
|
||||
return {
|
||||
data: {
|
||||
users: data as unknown as BusinessUserInfo[],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// 按工号精确查找用户(原 chat 服务 /user/search/full)
|
||||
export const searchBusinessUserInfo = async (keyword: string) => {
|
||||
try {
|
||||
const { data } = await IMSDK.getUsersInfo([keyword]);
|
||||
return {
|
||||
data: {
|
||||
total: data.length,
|
||||
users: data as unknown as BusinessUserInfo[],
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
// 查不到(用户不存在等)统一按无结果处理,由调用处提示“未搜索到相关结果”
|
||||
return {
|
||||
data: {
|
||||
total: 0,
|
||||
users: [] as BusinessUserInfo[],
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
interface UpdateBusinessUserInfoParams {
|
||||
email: string;
|
||||
nickname: string;
|
||||
faceURL: string;
|
||||
gender: number;
|
||||
birth: number;
|
||||
allowAddFriend: number;
|
||||
allowBeep: number;
|
||||
allowVibration: number;
|
||||
globalRecvMsgOpt: number;
|
||||
}
|
||||
|
||||
// 更新个人资料(原 chat 服务 /user/update)
|
||||
// OpenIM 只支持同步昵称/头像等字段,邮箱、性别、生日等业务字段仅更新本地,不会落服务端。
|
||||
export const updateBusinessUserInfo = async (
|
||||
params: Partial<UpdateBusinessUserInfoParams>,
|
||||
) => {
|
||||
const imFields: { nickname?: string; faceURL?: string; globalRecvMsgOpt?: MessageReceiveOptType } =
|
||||
{};
|
||||
if (params.nickname !== undefined) imFields.nickname = params.nickname;
|
||||
if (params.faceURL !== undefined) imFields.faceURL = params.faceURL;
|
||||
if (params.globalRecvMsgOpt !== undefined) {
|
||||
imFields.globalRecvMsgOpt = params.globalRecvMsgOpt as MessageReceiveOptType;
|
||||
}
|
||||
await IMSDK.setSelfInfo(imFields);
|
||||
return { data: {} };
|
||||
};
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 961 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 669 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 649 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 894 B |
|
After Width: | Height: | Size: 884 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 719 B |
|
After Width: | Height: | Size: 761 B |
|
After Width: | Height: | Size: 1007 B |
|
After Width: | Height: | Size: 624 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 855 B |
|
After Width: | Height: | Size: 907 B |
|
After Width: | Height: | Size: 807 B |
|
After Width: | Height: | Size: 795 B |
|
After Width: | Height: | Size: 792 B |
|
After Width: | Height: | Size: 928 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 695 B |
|
After Width: | Height: | Size: 360 B |
|
After Width: | Height: | Size: 900 B |
|
After Width: | Height: | Size: 499 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 3.3 KiB |