提交之GitHub
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* text=auto
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
### Example user template template
|
||||
### Example user template
|
||||
|
||||
# IntelliJ project files
|
||||
.idea
|
||||
*.iml
|
||||
out
|
||||
gen
|
||||
/.vscode/
|
||||
/data/
|
||||
/dist/LocalChatClient/
|
||||
/dist/LocalChatServer/
|
||||
/dist/Output/
|
||||
/lib/
|
||||
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 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 General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is 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. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
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.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
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 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. Use with the GNU Affero General Public License.
|
||||
|
||||
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 Affero 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 special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU 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 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 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 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 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU 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 the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
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 GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
# LocalChatApp
|
||||
|
||||
The second small project of the "Brick and Tile" team: A local LAN chat application based on Java Swing and Socket.
|
||||
|
||||
## Introduction
|
||||
|
||||
This project is a C/S architecture instant messaging software that supports user registration, login, group chat, group management, and other functions.
|
||||
The project is developed in Java, using Swing as the GUI framework, and the underlying layer communicates in full duplex via TCP Socket.
|
||||
|
||||
## Software Architecture
|
||||
|
||||
The project is mainly divided into three core packages: `client`, `server`, and `global`/`util` (common components).
|
||||
|
||||
### Directory Structure
|
||||
|
||||
- **Client**
|
||||
- Entry file: [`src/client/Client.java`](src/client/Client.java)
|
||||
- Business Logic: [`src/client/service/`](src/client/service/)
|
||||
- `ChatReceiver.java`: Responsible for receiving server messages and driving UI updates
|
||||
- `ChatSender.java`: Responsible for sending messages to the server
|
||||
- `LocalData.java`: Client local data cache
|
||||
- Interface Display: [`src/client/view/`](src/client/view/)
|
||||
- `LoginPage.java`: Login/Registration main container
|
||||
- `MainPage.java`: Chat main interface container
|
||||
- `login/`: Specific view components for login and registration
|
||||
- `main/`: Specific view components such as chat windows and group lists
|
||||
|
||||
- **Server**
|
||||
- Entry file: [`src/server/Server.java`](src/server/Server.java)
|
||||
- Business Service: [`src/server/serveice/`](src/server/serveice/)
|
||||
- `ClientChatThread.java`: Connection thread handling a single client
|
||||
- `Wrapper.java`: Communication protocol data packet definition
|
||||
- Data Model: [`src/server/data/`](src/server/data/)
|
||||
|
||||
- **Common Components**
|
||||
- Global Constants: [`src/global/global.java`](src/global/global.java)
|
||||
- Utilities: [`src/util/`](src/util/)
|
||||
- `MsgUtil.java`: Message processing utility
|
||||
- `SocketUtil.java`: Socket related utility
|
||||
- `FileUtil.java`: File operation utility
|
||||
|
||||
## Core Features
|
||||
|
||||
1. **User System**
|
||||
- Login and Registration
|
||||
- Personal Information Maintenance
|
||||
2. **Instant Messaging**
|
||||
- Real-time Group Chat
|
||||
- Message Receiving and Sending
|
||||
3. **Group Management**
|
||||
- Group List Display
|
||||
- Group Information Update and Maintenance
|
||||
|
||||
## UI Component Introduction
|
||||
|
||||
This project adopts a design pattern of dynamically replacing UI components. For example, on the login interface, when the user clicks the "Register" button, the login panel (SignIn) will be replaced by the registration panel (SignUp).
|
||||
|
||||
- **Login Page (`LoginPage`)**
|
||||
- Contains `SignInView`: Login form component
|
||||
- Contains `SignUpView`: Registration form component
|
||||
- *Replacement Logic*: Implement panel switching through `CardLayout` or removing/adding components.
|
||||
|
||||
- **Main Interface (`MainPage`)**
|
||||
- **Left Navigation (`SideOptionView`)**: Function menu and group list
|
||||
- **Middle Content (`ContentView`)**: Chat window and message display
|
||||
- **Secondary Options (`SecondaryOptionView`)**: Auxiliary function entry. *(Update: Added clear top and bottom dividers to the top title bar to enhance the visual hierarchy with the chat list and window title)*
|
||||
|
||||
- **UI Toolkit (`util`)**
|
||||
- Contains custom UI elements (such as `BlueOutlineWhiteBtn`, `CircleCharIcon2`)
|
||||
- `DesignToken`: Uniformly defines design specifications such as colors and fonts to ensure UI style consistency.
|
||||
|
||||
## Project Packaging
|
||||
|
||||
This project supports generating independent client installation packages.
|
||||
|
||||
1. **Compile and Package**:
|
||||
Use Maven to compile. `pom.xml` is configured with `maven-resources-plugin` to automatically copy dependencies to `target/lib`.
|
||||
```bash
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
2. **Generate Client Image**:
|
||||
Use `jpackage` to generate an independent client containing JRE.
|
||||
```bash
|
||||
jpackage --type app-image --input target --main-jar LocalChatRoom-1.0-SNAPSHOT.jar --main-class client.Client --dest dist --name LocalChatClient --java-options "-Dfile.encoding=UTF-8"
|
||||
```
|
||||
|
||||
3. **Generate Installer**:
|
||||
Use Inno Setup to compile the `dist/setup.iss` script, finally generating `dist/Output/LocalChatApp_Setup.exe`.
|
||||
|
||||
## Extended Features
|
||||
|
||||
This project reserves relevant interfaces and logic for the friend system. Although the current version mainly focuses on group chat, the following functions can be expanded in the future:
|
||||
|
||||
- **Friend Management**: Add and delete friends
|
||||
- **Private Chat Function**: Establish point-to-point private chat sessions
|
||||
- **Status Synchronization**: Friend online/offline status display
|
||||
|
||||
*Note: At this stage, these functions are optional items and are not forcibly required to be implemented.*
|
||||
|
||||
## Development Environment
|
||||
|
||||
- **JDK**: Java 11 (Recommended) / Java 8
|
||||
- **Build Tool**: Maven
|
||||
- **UI Framework**: Java Swing / JavaFX
|
||||
- **Dependencies**: The project has built-in required dependency libraries (located in the `lib/` directory), and can be built without additional Maven repository configuration.
|
||||
|
||||
## How to Run
|
||||
|
||||
1. **Start Server**
|
||||
Run the `main` method in [`src/server/Server.java`](src/server/Server.java).
|
||||
|
||||
2. **Start Client**
|
||||
Run the `main` method in [`src/client/Client.java`](src/client/Client.java).
|
||||
*Note: Please ensure the server is started, otherwise the client cannot connect.*
|
||||
|
||||
## Contribution Guide
|
||||
|
||||
1. Fork this repository
|
||||
2. Create `feature/xxx` branch
|
||||
3. Commit your code
|
||||
4. Create Pull Request
|
||||
@@ -0,0 +1,135 @@
|
||||
# LocalChatApp (本地网络聊天室)
|
||||
|
||||
添砖加瓦小组的第二次小项目:一个基于 Java Swing 和 Socket 的本地局域网聊天应用。
|
||||
|
||||
## 简介
|
||||
|
||||
本项目是一个 C/S 架构的即时通讯软件,支持用户注册、登录、群组聊天、群组管理等功能。
|
||||
项目采用 Java 语言开发,使用 Swing 作为 GUI 框架,底层通过 TCP Socket 进行全双工通信。
|
||||
|
||||
## 软件架构
|
||||
|
||||
项目主要分为三个核心包:`client`(客户端)、`server`(服务端)和 `global`/`util`(公共组件)。
|
||||
|
||||
### 目录结构说明
|
||||
|
||||
- **客户端 (Client)**
|
||||
- 入口文件: [`src/client/Client.java`](src/client/Client.java)
|
||||
- 业务逻辑: [`src/client/service/`](src/client/service/)
|
||||
- `ChatReceiver.java`: 负责接收服务端消息,驱动 UI 更新
|
||||
- `ChatSender.java`: 负责发送消息给服务端
|
||||
- `LocalData.java`: 客户端本地数据缓存
|
||||
- 界面展示: [`src/client/view/`](src/client/view/)
|
||||
- `LoginPage.java`: 登录/注册主容器
|
||||
- `MainPage.java`: 聊天主界面容器
|
||||
- `login/`: 登录与注册的具体视图组件
|
||||
- `main/`: 聊天窗口、群组列表等具体视图组件
|
||||
|
||||
- **服务端 (Server)**
|
||||
- 入口文件: [`src/server/Server.java`](src/server/Server.java)
|
||||
- 业务服务: [`src/server/serveice/`](src/server/serveice/)
|
||||
- `ClientChatThread.java`: 处理单个客户端的连接线程
|
||||
- `Wrapper.java`: 通信协议数据包定义
|
||||
- 数据模型: [`src/server/data/`](src/server/data/)
|
||||
|
||||
- **公共组件**
|
||||
- 全局常量: [`src/global/global.java`](src/global/global.java)
|
||||
- 工具类: [`src/util/`](src/util/)
|
||||
- `MsgUtil.java`: 消息处理工具
|
||||
- `SocketUtil.java`: Socket 相关工具
|
||||
- `FileUtil.java`: 文件操作工具
|
||||
|
||||
## 核心功能
|
||||
|
||||
1. **用户系统**
|
||||
- 登录与注册:支持新用户注册和账号登录验证
|
||||
- 个人信息维护:支持修改头像、签名、生日等个人信息
|
||||
2. **即时通讯**
|
||||
- **群组聊天**:支持创建群组、加入群组、群内实时消息广播
|
||||
- **私聊功能**:支持好友间点对点私聊,消息实时推送
|
||||
- **混合消息列表**:统一展示群聊和私聊会话,直观管理所有聊天
|
||||
3. **好友与关系链**
|
||||
- **好友管理**:支持查找用户、发送好友请求、同意/拒绝好友申请
|
||||
- **状态同步**:实时感知好友在线/离线状态
|
||||
4. **数据持久化**
|
||||
- 服务端自动保存群组信息、用户信息及聊天记录
|
||||
- 客户端本地缓存最近的聊天数据和用户配置
|
||||
|
||||
## 拓展功能
|
||||
|
||||
*注:以下功能为项目进阶特性,已在当前版本中完全实现。*
|
||||
|
||||
- **好友系统深度集成**:不仅支持基础的增删好友,还实现了好友状态的实时同步和私聊消息的独立存储。
|
||||
- **动态 UI 交互**:
|
||||
- 登录/注册面板平滑切换
|
||||
- 消息列表根据消息类型(群聊/私聊)自动展示不同图标和状态
|
||||
- 聊天窗口自适应调整,支持显示历史消息记录
|
||||
|
||||
## UI 组件介绍
|
||||
|
||||
本项目存在动态替换 UI 组件的设计模式。例如在登录界面,当用户点击“注册”按钮时,登录面板(SignIn)会被替换为注册面板(SignUp)。
|
||||
|
||||
- **登录页面 (`LoginPage`)**
|
||||
- 包含 `SignInView`: 登录表单组件
|
||||
- 包含 `SignUpView`: 注册表单组件
|
||||
- *替换逻辑*: 通过 `CardLayout` 或移除/添加组件的方式实现面板切换。
|
||||
|
||||
- **主界面 (`MainPage`)**
|
||||
- **左侧导航 (`SideOptionView`)**: 功能菜单与群组列表
|
||||
- **中间内容 (`ContentView`)**: 聊天窗口与消息展示
|
||||
- **二级选项 (`SecondaryOptionView`)**: 辅助功能入口。*(更新:顶部标题栏添加了明显的上下分割线,增强了与聊天列表及窗口标题的视觉层次感)*
|
||||
|
||||
- **UI 工具包 (`util`)**
|
||||
- 包含自定义 UI 元素(如 `BlueOutlineWhiteBtn`、`CircleCharIcon2`)
|
||||
- `DesignToken`: 统一定义颜色、字体等设计规范,确保 UI 风格一致性。
|
||||
|
||||
## 项目打包
|
||||
|
||||
本项目支持生成包含客户端和服务端的独立安装包。
|
||||
|
||||
1. **编译打包**:
|
||||
使用 Maven 进行编译,`pom.xml` 已配置 `maven-resources-plugin` 自动复制依赖到 `target/lib`。
|
||||
```bash
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
2. **生成客户端和服务端映像**:
|
||||
使用 `jpackage` 生成包含 JRE 的独立可执行程序。
|
||||
|
||||
* **生成客户端**:
|
||||
```bash
|
||||
jpackage --type app-image --input target --main-jar LocalChatRoom-1.0-SNAPSHOT.jar --main-class client.Client --dest dist --name LocalChatClient --java-options "-Dfile.encoding=UTF-8"
|
||||
```
|
||||
|
||||
* **生成服务端**:
|
||||
```bash
|
||||
jpackage --type app-image --input target --main-jar LocalChatRoom-1.0-SNAPSHOT.jar --main-class server.Server --dest dist --name LocalChatServer --java-options "-Dfile.encoding=UTF-8"
|
||||
```
|
||||
|
||||
3. **生成安装包**:
|
||||
使用 Inno Setup 编译 `dist/setup.iss` 脚本,最终生成 `dist/Output/LocalChatApp_Setup.exe`。
|
||||
安装包将同时包含客户端和服务端程序。
|
||||
|
||||
|
||||
## 开发环境
|
||||
|
||||
- **JDK**: Java 11 (推荐) / Java 8
|
||||
- **Build Tool**: Maven
|
||||
- **UI Framework**: Java Swing / JavaFX
|
||||
- **Dependencies**: 项目已内置所需依赖库(位于 `lib/` 目录),无需额外配置 Maven 仓库即可构建。
|
||||
|
||||
## 如何运行
|
||||
|
||||
1. **启动服务端**
|
||||
运行 [`src/server/Server.java`](src/server/Server.java) 中的 `main` 方法。
|
||||
|
||||
2. **启动客户端**
|
||||
运行 [`src/client/Client.java`](src/client/Client.java) 中的 `main` 方法。
|
||||
*注意:请确保服务端已启动,否则客户端无法连接。*
|
||||
|
||||
## 贡献指南
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 新建 `feature/xxx` 分支
|
||||
3. 提交代码
|
||||
4. 新建 Pull Request
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
; Script generated by Inno Setup Script Wizard.
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppName "LocalChatApp"
|
||||
#define MyAppVersion "1.0"
|
||||
#define MyAppPublisher "TzJava"
|
||||
#define MyAppURL "http://www.example.com/"
|
||||
#define MyAppClientExeName "LocalChatClient.exe"
|
||||
#define MyAppServerExeName "LocalChatServer.exe"
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
AppId={{8B86927C-1234-4567-89AB-CDEF01234567}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
;AppVerName={#MyAppName} {#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
AppSupportURL={#MyAppURL}
|
||||
AppUpdatesURL={#MyAppURL}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
DisableProgramGroupPage=yes
|
||||
; Uncomment the following line to run in non administrative install mode (install for current user only.)
|
||||
;PrivilegesRequired=lowest
|
||||
OutputDir=Output
|
||||
OutputBaseFilename=LocalChatApp_Setup
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
SetupIconFile=LocalChat.ico
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Files]
|
||||
; Client Files
|
||||
Source: "LocalChatClient\*"; DestDir: "{app}\LocalChatClient"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "\app\maven-status,\app\surefire-reports,\app\test-classes,\app\maven-archiver"
|
||||
; Server Files
|
||||
Source: "LocalChatServer\*"; DestDir: "{app}\LocalChatServer"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "\app\maven-status,\app\surefire-reports,\app\test-classes,\app\maven-archiver"
|
||||
; Icon File
|
||||
Source: "LocalChat.ico"; DestDir: "{app}"; Flags: ignoreversion
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\LocalChatClient\{#MyAppClientExeName}"; IconFilename: "{app}\LocalChat.ico"
|
||||
Name: "{autoprograms}\{#MyAppName} Server"; Filename: "{app}\LocalChatServer\{#MyAppServerExeName}"; IconFilename: "{app}\LocalChat.ico"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\LocalChatClient\{#MyAppClientExeName}"; Tasks: desktopicon; IconFilename: "{app}\LocalChat.ico"
|
||||
Name: "{autodesktop}\{#MyAppName} Server"; Filename: "{app}\LocalChatServer\{#MyAppServerExeName}"; Tasks: desktopicon; IconFilename: "{app}\LocalChat.ico"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\LocalChatClient\{#MyAppClientExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: shellexec postinstall skipifsilent
|
||||
@@ -0,0 +1,169 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.example</groupId>
|
||||
<artifactId>LocalChatRoom</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>LocalChatRoom</name>
|
||||
<url>http://www.example.com</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>5.7.1</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/junit-jupiter-engine-5.7.1.jar</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-params</artifactId>
|
||||
<version>5.7.1</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/junit-jupiter-params-5.7.1.jar</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
<artifactId>junit-platform-engine</artifactId>
|
||||
<version>1.7.1</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/junit-platform-engine-1.7.1.jar</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.platform</groupId>
|
||||
<artifactId>junit-platform-commons</artifactId>
|
||||
<version>1.7.1</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/junit-platform-commons-1.7.1.jar</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.7.1</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/junit-jupiter-api-5.7.1.jar</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apiguardian</groupId>
|
||||
<artifactId>apiguardian-api</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/apiguardian-api-1.1.0.jar</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.opentest4j</groupId>
|
||||
<artifactId>opentest4j</artifactId>
|
||||
<version>1.2.0</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/opentest4j-1.2.0.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<!-- Source: https://mvnrepository.com/artifact/org.openjfx/javafx-swing -->
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-swing</artifactId>
|
||||
<version>26-ea+22</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/javafx-swing-26-ea+22.jar</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-graphics</artifactId>
|
||||
<version>26-ea+22</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/javafx-graphics-26-ea+22.jar</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-base</artifactId>
|
||||
<version>26-ea+22</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/lib/javafx-base-26-ea+22.jar</systemPath>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.formdev</groupId>
|
||||
<artifactId>flatlaf</artifactId>
|
||||
<version>3.2.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>src</sourceDirectory>
|
||||
<testSourceDirectory>src/test</testSourceDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src</directory>
|
||||
<excludes>
|
||||
<exclude>test/**</exclude>
|
||||
<exclude>**/*.java</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addClasspath>true</addClasspath>
|
||||
<classpathPrefix>lib/</classpathPrefix>
|
||||
<mainClass>client.Client</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<excludes>
|
||||
<exclude>test/**</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-lib</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/lib</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${project.basedir}/lib</directory>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M5</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>11</source>
|
||||
<target>11</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,119 @@
|
||||
package client;
|
||||
|
||||
import com.formdev.flatlaf.FlatLightLaf;
|
||||
import client.service.*;
|
||||
import client.view.LoginPage;
|
||||
import global.global;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
public class Client {
|
||||
private static boolean isConnected = false; // 是否连接成功
|
||||
|
||||
// 接收信息线程
|
||||
private ChatReceiver chatReceiver;
|
||||
// 发送信息线程
|
||||
private ChatSender chatSender;
|
||||
// 信息处理队列
|
||||
private BlockingQueue<Wrapper> messageQueue;
|
||||
|
||||
// 用于分配动态端口的计数器
|
||||
private static int dynamicPortCounter = 10000;
|
||||
private static final int MAX_DYNAMIC_PORT = 20000;
|
||||
|
||||
// 程序入口
|
||||
public static void main(String[] args) {
|
||||
// 设置 FlatLaf 主题,使界面更现代化,符合当前审美
|
||||
try {
|
||||
FlatLightLaf.setup();
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to initialize FlatLaf");
|
||||
}
|
||||
|
||||
Client client = new Client();
|
||||
client.startClient();
|
||||
|
||||
System.out.println("客户端启动成功,本地端口:" +
|
||||
(dynamicPortCounter > 10000 ? dynamicPortCounter - 1 : "未分配"));
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
LoginPage.get().setVisible(true);
|
||||
});
|
||||
}
|
||||
|
||||
// 启动客户端,连接服务端,初始化相关内容
|
||||
public void startClient() {
|
||||
messageQueue = new ArrayBlockingQueue<>(40);
|
||||
// 连接服务器 创建 clientSocket
|
||||
try {
|
||||
int localPort = findAvailablePort(getNextDynamicPort());
|
||||
Socket socket;
|
||||
if (localPort > 0) {
|
||||
socket = new Socket(
|
||||
global.LOCAL_HOST,
|
||||
global.SERVER_PORT,
|
||||
null,
|
||||
localPort);
|
||||
} else {
|
||||
socket = new Socket(
|
||||
global.LOCAL_HOST,
|
||||
global.SERVER_PORT);
|
||||
}
|
||||
|
||||
chatSender = new ChatSender(socket, messageQueue);
|
||||
chatReceiver = new ChatReceiver(socket, messageQueue);
|
||||
|
||||
chatSender.start();
|
||||
Thread.sleep(200);
|
||||
chatReceiver.start();
|
||||
isConnected = true;
|
||||
} catch (IOException e) {
|
||||
isConnected = false;
|
||||
e.printStackTrace();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取动态分配的本地端口
|
||||
private synchronized int getNextDynamicPort() {
|
||||
int port = dynamicPortCounter;
|
||||
dynamicPortCounter++;
|
||||
|
||||
// 如果超出范围,重置到起始端口
|
||||
if (dynamicPortCounter >= MAX_DYNAMIC_PORT) {
|
||||
dynamicPortCounter = 10000;
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
// 寻找可用端口
|
||||
private int findAvailablePort(int startPort) {
|
||||
int port = startPort;
|
||||
|
||||
while (port < MAX_DYNAMIC_PORT) {
|
||||
try (ServerSocket serverSocket = new ServerSocket(port)) {
|
||||
// 如果能成功创建ServerSocket,说明端口可用
|
||||
serverSocket.close();
|
||||
return port;
|
||||
} catch (IOException e) {
|
||||
// 端口被占用,尝试下一个
|
||||
port++;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没找到可用端口,使用0让系统自动分配
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static boolean isConnected() {
|
||||
return isConnected;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
package client.service;
|
||||
|
||||
import client.view.LoginPage;
|
||||
import client.view.MainPage;
|
||||
import client.view.main.*;
|
||||
import global.global;
|
||||
import server.data.GroupData;
|
||||
import server.data.UserData;
|
||||
import server.serveice.Wrapper;
|
||||
import util.MsgUtil;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
/**
|
||||
* 客户端消息接收服务线程
|
||||
* <p>
|
||||
* 该类负责维护与服务端的长连接,持续监听并接收服务端推送的消息包(Wrapper)。
|
||||
* 作为客户端的“下行数据通道”,它将接收到的原始数据根据操作码(Operation Code)进行解析和分发,
|
||||
* 驱动本地数据更新(LocalData)及 UI 界面刷新(UIUpdate)。
|
||||
* <p>
|
||||
* 核心功能包括:
|
||||
* 1. 登录/注册响应处理
|
||||
* 2. 实时聊天消息接收与存储
|
||||
* 3. 群组创建、邀请及变更通知
|
||||
* 4. 异常捕获与日志记录
|
||||
*/
|
||||
public class ChatReceiver extends Thread {
|
||||
// 客户端本地Socket,用于与服务端通信
|
||||
private final Socket localSocket;
|
||||
ObjectInputStream ois;
|
||||
private static boolean isRunning;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* <p>
|
||||
* 初始化接收线程,绑定到已连接的客户端Socket。
|
||||
* 创建对象输入流,用于从Socket读取服务端消息。
|
||||
*
|
||||
* @param localSocket 已连接的客户端Socket
|
||||
* @param messageQueue 消息处理队列
|
||||
* @throws IOException 如果获取输入流失败
|
||||
*/
|
||||
public ChatReceiver(Socket localSocket, BlockingQueue<Wrapper> messageQueue) {
|
||||
this.localSocket = localSocket;
|
||||
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 线程主循环:持续阻塞读取服务端消息并分发处理
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
ois = new ObjectInputStream(localSocket.getInputStream());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
while (isRunning) {
|
||||
try {
|
||||
// 阻塞式读取服务端发送的 Wrapper 对象
|
||||
Wrapper message = (Wrapper) ois.readObject();
|
||||
|
||||
System.out.println("收到消息:" + message.getOperation());
|
||||
|
||||
handleMessage(message);
|
||||
}
|
||||
// 捕获 IO 异常(如连接中断)
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
// 连接中断,退出循环
|
||||
break;
|
||||
}
|
||||
// 捕获反序列化异常
|
||||
catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopRunning() {
|
||||
ChatReceiver.isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息处理核心方法:根据操作码分发业务逻辑
|
||||
* 1. 登录/注册响应处理
|
||||
* 2. 实时聊天消息接收与存储
|
||||
* 3. 群组创建、邀请及变更通知
|
||||
* 4. 异常捕获与日志记录
|
||||
* 5. 服务器关闭通知
|
||||
* 6. 好友添加响应处理
|
||||
* 7. 好友删除响应处理
|
||||
* 8. 群组解散响应处理
|
||||
* 9. 好友添加请求处理
|
||||
* 10. 好友删除请求处理
|
||||
* 11. 群组解散请求处理
|
||||
* 12. 好友添加响应处理
|
||||
* 13. 好友删除响应处理
|
||||
* 14. 群组解散响应处理
|
||||
*
|
||||
* @param msg 服务端下发的消息包
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleMessage(Wrapper msg) {
|
||||
int opt = msg.getOperation();
|
||||
switch (opt) {
|
||||
case global.OPT_QUEST_WRONG:
|
||||
// 通用异常响应:直接展示服务端返回的错误信息
|
||||
System.out.println("出现未知异常");
|
||||
break;
|
||||
|
||||
case global.OPT_REGISTER_FAILED_ACC:
|
||||
LoginPage.get().showMsgDialog("账号已存在");
|
||||
break;
|
||||
|
||||
case global.OPT_REGISTER_SUCCESS:
|
||||
case global.OPT_LOGIN_SUCCESS:
|
||||
System.out.println("登录/注册成功");
|
||||
LoginPage.get().openMainPage();
|
||||
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_USER));
|
||||
// 新增:请求初始化所有用户详细信息
|
||||
ChatSender.addMsg(Wrapper.simpleRequest(LocalData.get().getId(), null, global.OPT_INIT_USER_DETAIL));
|
||||
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_GROUP));
|
||||
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_CHAT));
|
||||
break;
|
||||
|
||||
case global.OPT_LOGIN_FAILED_ACC:
|
||||
LoginPage.get().showMsgDialog("账号不存在");
|
||||
break;
|
||||
|
||||
case global.OPT_LOGIN_FAILED_PWD:
|
||||
LoginPage.get().showMsgDialog("密码错误");
|
||||
break;
|
||||
|
||||
case global.OPT_LOGIN_FAILED_REPEATED:
|
||||
LoginPage.get().showMsgDialog("账户已登录,请勿重复登录");
|
||||
break;
|
||||
|
||||
case global.OPT_ERROR_NOT_LOGIN:
|
||||
System.out.println("未知的登录问题?");
|
||||
break;
|
||||
|
||||
case global.OPT_LOGOUT:
|
||||
MainPage.get().openLogInPage();
|
||||
break;
|
||||
|
||||
case global.OPT_DELETE_ACCOUNT:
|
||||
MainPage.get().showMsgDialog("账号已被删除");
|
||||
MainPage.get().openLogInPage();
|
||||
break;
|
||||
|
||||
case global.OPT_UPDATE_NICKNAME:
|
||||
LocalData.get().setUserName(msg.getSenderId(), (String) msg.getData());
|
||||
break;
|
||||
|
||||
case global.OPT_UPDATE_PASSWORD:
|
||||
MainPage.get().showMsgDialog("密码已经更新");
|
||||
break;
|
||||
|
||||
case global.OPT_USER_UPDATE_NAME_FAILED:
|
||||
MainPage.get().showMsgDialog("昵称修改失败");
|
||||
break;
|
||||
|
||||
case global.OPT_USER_UPDATE_PASSWORD_FAILED:
|
||||
MainPage.get().showMsgDialog("密码修改失败");
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_CREATE_SUCCESS:
|
||||
MainPage.get().showMsgDialog("群组创建成功");
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_INVITE:
|
||||
// 群组变更(新建/被拉入):更新本地群组列表并刷新左侧导航栏
|
||||
MainPage.get().showGroupInviteRequestDialog(
|
||||
msg.getSenderId(),
|
||||
LocalData.get().getUserName(msg.getSenderId()),
|
||||
(String) msg.getData(),
|
||||
msg.getGroupId());
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_INVITE_REFUSE:
|
||||
MainPage.get().showMsgDialog("对方拒绝了你的邀请");
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_INVITE_OFFLINE:
|
||||
MainPage.get().showMsgDialog("邀请的用户并不在线");
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_QUIT:
|
||||
handleGroupQuit(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_DISBAND:
|
||||
MainPage.get().showMsgDialog("群聊已被解散");
|
||||
break;
|
||||
|
||||
case global.OPT_CHAT:
|
||||
handleChatRequest(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_PRIVATE_CHAT:
|
||||
handlePrivateChatRequest(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_UPDATE_NAME:
|
||||
// 群信息变更:更新群名显示
|
||||
LocalData.get().setGroupName(msg.getGroupId(), (String) msg.getData());
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_UPDATE_OWNER:
|
||||
LocalData.get().getGroupData(msg.getGroupId()).setGroupOwner((String) msg.getData());
|
||||
break;
|
||||
|
||||
case global.OPT_USER_UPDATE_NAME_FAILED_WRONG_FORMAT:
|
||||
MainPage.get().showMsgDialog("用户名格式错误,请重新输入");
|
||||
break;
|
||||
|
||||
case global.OPT_GROUP_JOIN_FAILED:
|
||||
MainPage.get().showMsgDialog("加入群聊失败:群组不存在");
|
||||
break;
|
||||
|
||||
case global.OPT_FRIEND_ADD_SUCCESS:
|
||||
handleFriendAddSuccess(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_FRIEND_ADD_FAILED:
|
||||
MainPage.get().showMsgDialog("添加好友失败:用户不存在");
|
||||
break;
|
||||
|
||||
case global.OPT_INIT_USER_DETAIL:
|
||||
// 初始化所有用户详细信息
|
||||
Map<String, UserData> userDetails = (Map<String, UserData>) msg.getData();
|
||||
LocalData.get().setUserDetails(userDetails);
|
||||
break;
|
||||
|
||||
case global.OPT_UPDATE_USER_DETAIL:
|
||||
// 更新单个用户详细信息
|
||||
UserData updatedUser = (UserData) msg.getData();
|
||||
LocalData.get().updateUserDetails(updatedUser);
|
||||
// 刷新界面(如果当前正显示该用户的资料)
|
||||
// 由于目前没有全局事件总线,这里简单起见,可以刷新整个 MainView 或者依赖用户重新点击
|
||||
// 实际上,如果正在查看好友资料,可能需要实时刷新。
|
||||
// 简单实现:不主动刷新UI,下次进入界面时读取最新数据即可。
|
||||
// 如果在“设置-个人信息”界面,且更新的是自己,界面通常已经由用户操作更新了,或者可以刷新一下。
|
||||
break;
|
||||
|
||||
case global.OPT_EXIT:
|
||||
// 服务器关闭通知
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(MainPage.get(), "服务器已关闭,程序将退出。", "系统通知", JOptionPane.WARNING_MESSAGE);
|
||||
System.exit(0);
|
||||
});
|
||||
break;
|
||||
|
||||
case global.OPT_FRIEND_ADD:
|
||||
handleFriendAddRequest(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_FRIEND_ADD_REFUSE:
|
||||
MainPage.get().showMsgDialog("对方拒绝了你的好友请求");
|
||||
break;
|
||||
|
||||
case global.OPT_INIT_CHAT:
|
||||
handleChatInit(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_INIT_USER:
|
||||
handleUserInit(msg);
|
||||
break;
|
||||
|
||||
case global.OPT_INIT_GROUP:
|
||||
handleGroupInit(msg);
|
||||
break;
|
||||
|
||||
case global.SERVER_MESSAGE:
|
||||
MainPage.get().showMsgDialog((String) msg.getData());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理收到的好友请求
|
||||
* 弹出对话框询问用户是否同意
|
||||
*/
|
||||
private void handleFriendAddRequest(Wrapper msg) {
|
||||
String senderId = msg.getSenderId();
|
||||
String senderName = (String) msg.getData();
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
int option = JOptionPane.showConfirmDialog(
|
||||
MainPage.get(),
|
||||
"收到来自 " + senderName + " (" + senderId + ") 的好友请求,是否同意?",
|
||||
"好友请求",
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
|
||||
if (option == JOptionPane.YES_OPTION) {
|
||||
ChatSender.addMsg(Wrapper.friendAddAgree(LocalData.get().getId(), senderId));
|
||||
} else {
|
||||
ChatSender.addMsg(Wrapper.friendAddRefuse(LocalData.get().getId(), senderId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理好友添加成功消息
|
||||
*
|
||||
* @param msg 好友添加成功消息
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleFriendAddSuccess(Wrapper msg) {
|
||||
Map<String, String> friendInfo = (Map<String, String>) msg.getData();
|
||||
if (friendInfo != null) {
|
||||
for (Map.Entry<String, String> entry : friendInfo.entrySet()) {
|
||||
LocalData.get().addFriend(entry.getKey(), entry.getValue());
|
||||
}
|
||||
// 刷新好友列表 UI
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
SecondaryOptionView.get().exchangeToFriendList();
|
||||
MainPage.get().showMsgDialog("添加好友成功");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理群聊退出消息
|
||||
*
|
||||
* @param msg 群聊退出消息
|
||||
*/
|
||||
private void handleGroupQuit(Wrapper msg) {
|
||||
LocalData.get().removeGroupChatMsg(msg.getGroupId());
|
||||
LocalData.get().setCurrentChatId(null);
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
SecondaryOptionView.get().removeGroupListItem(msg.getGroupId());
|
||||
MainPage.get().exchangeToBlankContent();
|
||||
MainPage.get().revalidate();
|
||||
MainPage.get().repaint();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理群聊初始化消息
|
||||
* 初始化群聊消息记录
|
||||
*
|
||||
* @param msg 群聊初始化消息
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleChatInit(Wrapper msg) {
|
||||
List<String> chatHistory = (List<String>) msg.getData();
|
||||
|
||||
// 初始化群聊消息记录
|
||||
LocalData.get().addChatMsg(msg.getGroupId(), chatHistory);
|
||||
|
||||
// 如果当前正好停留在该群聊界面,刷新消息
|
||||
if (msg.getGroupId().equals(LocalData.get().getCurrentChatId())) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
ChatInfoView.get().setChatInfo(msg.getGroupId());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户初始化消息
|
||||
*
|
||||
* @param msg 用户初始化消息
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void handleUserInit(Wrapper msg) {
|
||||
Map<String, String> groupMap = (Map<String, String>) msg.getData();
|
||||
for (Map.Entry<String, String> entry : groupMap.entrySet()) {
|
||||
LocalData.get().addUserId_name(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理群聊初始化消息
|
||||
*
|
||||
* @param msg 群聊初始化消息
|
||||
*/
|
||||
private void handleGroupInit(Wrapper msg) {
|
||||
GroupData newGroupData = (GroupData) msg.getData();
|
||||
|
||||
// 打印群聊初始化信息
|
||||
System.out.println(
|
||||
"handleGroupInit: " + newGroupData.getGroupId() + ", members: " + newGroupData.getMembers().size());
|
||||
|
||||
// 添加群聊数据到本地存储
|
||||
LocalData.get().addGroup(newGroupData.getGroupId(), newGroupData);
|
||||
|
||||
// 更新群聊列表 UI
|
||||
SecondaryOptionView.get().updateGroupList(
|
||||
newGroupData.getGroupId(),
|
||||
newGroupData.getGroupName(),
|
||||
0);
|
||||
|
||||
// 如果当前处于群聊模式,刷新群聊列表以显示新加入的群组
|
||||
SecondaryOptionView.get().refreshIfInGroupMode();
|
||||
|
||||
// 更新当前群组信息(如果当前正打开该群)
|
||||
String currentChatId = LocalData.get().getCurrentChatId();
|
||||
// SecondaryOptionView.get().updateGroupList(newGroupData.getGroupId(),
|
||||
// newGroupData.getGroupName(), 0);
|
||||
|
||||
// 检查是否需要更新当前显示的群组信息(如果当前正打开该群)
|
||||
if (newGroupData.getGroupId().equals(currentChatId)) {
|
||||
System.out.println("Updating GroupInfoView...");
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
GroupInfoView.get().updateInfo();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理群聊请求消息
|
||||
*
|
||||
* @param msg 群聊请求消息
|
||||
*/
|
||||
private void handleChatRequest(Wrapper msg) {
|
||||
String content = (String) msg.getData();
|
||||
String[] split = MsgUtil.splitMsg(content);
|
||||
|
||||
LocalData.get().addChatMsg(msg.getGroupId(), content);
|
||||
// 如果就在当前这个面板,立即更新面板
|
||||
if (LocalData.get().getCurrentChatId().equals(msg.getGroupId())) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
ChatInfoView.get().addOtherUserMessage(split[1], split[2]);
|
||||
});
|
||||
} else {
|
||||
SecondaryOptionView.get().updateGroupList(
|
||||
msg.getGroupId(),
|
||||
LocalData.get().getGroupName(msg.getGroupId()),
|
||||
1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理私聊请求消息
|
||||
*/
|
||||
private void handlePrivateChatRequest(Wrapper msg) {
|
||||
String senderId = msg.getSenderId();
|
||||
String text = (String) msg.getData();
|
||||
|
||||
// 获取发送者名称
|
||||
String senderName = LocalData.get().getFriends().get(senderId);
|
||||
if (senderName == null) {
|
||||
senderName = LocalData.get().getUserName(senderId);
|
||||
if (senderName == null)
|
||||
senderName = senderId;
|
||||
}
|
||||
|
||||
// 组合成标准消息格式: id|name|text
|
||||
String combinedMsg = MsgUtil.combineMsg(senderId, senderName, text);
|
||||
|
||||
// 存储消息,使用senderId作为chatId
|
||||
LocalData.get().addChatMsg(senderId, combinedMsg);
|
||||
|
||||
// 如果就在当前这个面板,立即更新面板
|
||||
if (senderId.equals(LocalData.get().getCurrentChatId())) {
|
||||
String finalSenderName = senderName;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
ChatInfoView.get().addOtherUserMessage(finalSenderName, text);
|
||||
});
|
||||
} else {
|
||||
// 更新消息列表
|
||||
SecondaryOptionView.get().updateMessageList(
|
||||
senderId,
|
||||
senderName,
|
||||
text,
|
||||
1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package client.service;
|
||||
|
||||
import client.view.MainPage;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
public class ChatSender extends Thread {
|
||||
|
||||
private static BlockingQueue<Wrapper> messageQueue;
|
||||
|
||||
public static void addMsg(Wrapper msg) {
|
||||
messageQueue.add(msg);
|
||||
}
|
||||
|
||||
private ObjectOutputStream oos;
|
||||
private final Socket clientSocket;
|
||||
|
||||
private static boolean isRunning;
|
||||
|
||||
public static void stopRunning() {
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
public ChatSender(Socket clientSocket, BlockingQueue<Wrapper> messageQueue) {
|
||||
ChatSender.messageQueue = messageQueue;
|
||||
this.clientSocket = clientSocket;
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
oos = new ObjectOutputStream(clientSocket.getOutputStream());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Wrapper msg = null;
|
||||
while (isRunning) {
|
||||
try {
|
||||
msg = messageQueue.take();
|
||||
oos.writeObject(msg);
|
||||
oos.flush();
|
||||
System.out.println("信息已发出:" + msg.getOperation());
|
||||
} catch (InterruptedException e) {
|
||||
System.out.println(LocalData.get().getId() + ": 消息队列被中断");
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
if (isConnectionClosed(e)) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(MainPage.get(), "服务器连接已断开,请重新登录。", "连接断开",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
System.exit(0);
|
||||
});
|
||||
System.out.println("服务器连接已断开");
|
||||
break;
|
||||
} else {
|
||||
System.out.println("发送消息时发生IO异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConnectionClosed(IOException e) {
|
||||
// 根据异常类型判断连接是否断开
|
||||
return e instanceof SocketException ||
|
||||
e.getMessage() != null && (e.getMessage().contains("Connection reset") ||
|
||||
e.getMessage().contains("Broken pipe") ||
|
||||
e.getMessage().contains("Connection refused") ||
|
||||
e.getMessage().contains("Software caused connection abort"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package client.service;
|
||||
|
||||
import server.data.GroupData;
|
||||
import server.data.UserData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
// 这个类用于存储本地的数据,便于之后的UI更新操作。
|
||||
public class LocalData {
|
||||
private static final LocalData INSTANCE = new LocalData();
|
||||
|
||||
public static LocalData get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private String id;
|
||||
|
||||
// 当前的所在的群聊id,辅助UI更新
|
||||
private String currentChatId;
|
||||
|
||||
// 使用 ConcurrentHashMap 替代 HashMap
|
||||
private ConcurrentMap<String, String> userIdNameMap;
|
||||
private ConcurrentMap<String, GroupData> groupDataMap;
|
||||
private ConcurrentMap<String, List<String>> groupChatMap;
|
||||
|
||||
// 好友列表
|
||||
private ConcurrentMap<String, String> friendMap;
|
||||
|
||||
// 存储所有用户的详细信息
|
||||
private ConcurrentMap<String, UserData> userDetailsMap;
|
||||
|
||||
private LocalData() {
|
||||
userIdNameMap = new ConcurrentHashMap<>();
|
||||
groupDataMap = new ConcurrentHashMap<>();
|
||||
groupChatMap = new ConcurrentHashMap<>();
|
||||
friendMap = new ConcurrentHashMap<>();
|
||||
userDetailsMap = new ConcurrentHashMap<>();
|
||||
|
||||
id = "";
|
||||
currentChatId = "";
|
||||
}
|
||||
|
||||
public Map<String, String> getFriends() {
|
||||
return friendMap;
|
||||
}
|
||||
|
||||
public void addFriend(String id, String name) {
|
||||
friendMap.put(id, name);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getCurrentChatName() {
|
||||
return groupDataMap.get(currentChatId).getGroupName();
|
||||
}
|
||||
|
||||
public synchronized void setCurrentChatId(String id) {
|
||||
this.currentChatId = id;
|
||||
}
|
||||
|
||||
public synchronized String getCurrentChatId() {
|
||||
return currentChatId;
|
||||
}
|
||||
|
||||
public void addUserId_name(String id, String name) {
|
||||
userIdNameMap.put(id, name);
|
||||
}
|
||||
|
||||
public String getGroupName(String groupId) {
|
||||
return groupDataMap.get(groupId).getGroupName();
|
||||
}
|
||||
|
||||
public void addChatMsg(String groupId, List<String> messages) {
|
||||
// 覆盖模式:用于初始化加载历史消息,避免重复添加
|
||||
groupChatMap.put(groupId, new ArrayList<>(messages));
|
||||
}
|
||||
|
||||
public void addChatMsg(String groupId, String message) {
|
||||
if (groupChatMap.containsKey(groupId)) {
|
||||
groupChatMap.get(groupId).add(message);
|
||||
} else {
|
||||
groupChatMap.put(groupId, new ArrayList<>());
|
||||
groupChatMap.get(groupId).add(message);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getChatMsg(String groupId) {
|
||||
if (groupChatMap.containsKey(groupId)) {
|
||||
return groupChatMap.get(groupId);
|
||||
} else {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public void setUserDetails(Map<String, UserData> userDetails) {
|
||||
userDetailsMap.clear();
|
||||
userDetailsMap.putAll(userDetails);
|
||||
// 同时更新 userIdNameMap,确保一致性
|
||||
userDetails.forEach((id, userData) -> {
|
||||
userIdNameMap.put(id, userData.getNickname());
|
||||
});
|
||||
}
|
||||
|
||||
public void updateUserDetails(UserData userData) {
|
||||
userDetailsMap.put(userData.getUserId(), userData);
|
||||
userIdNameMap.put(userData.getUserId(), userData.getNickname());
|
||||
}
|
||||
|
||||
public UserData getUserDetail(String userId) {
|
||||
return userDetailsMap.get(userId);
|
||||
}
|
||||
|
||||
public void removeGroupChatMsg(String groupId) {
|
||||
if (groupChatMap.containsKey(groupId)) {
|
||||
groupChatMap.remove(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
public String getUserName(String userId) {
|
||||
if (userIdNameMap.containsKey(userId))
|
||||
return userIdNameMap.get(userId);
|
||||
else
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserName(String suerId, String name) {
|
||||
userIdNameMap.put(suerId, name);
|
||||
}
|
||||
|
||||
public synchronized void addGroup(String groupId, GroupData groupData) {
|
||||
groupDataMap.put(groupId, groupData); // HashMap的put方法会自动替换已有的键值对
|
||||
}
|
||||
|
||||
public void setGroupName(String groupId, String name) {
|
||||
groupDataMap.get(groupId).setGroupName(name);
|
||||
}
|
||||
|
||||
public GroupData getGroupData(String groupId) {
|
||||
return groupDataMap.get(groupId);
|
||||
}
|
||||
|
||||
public List<GroupData> getAllGroups() {
|
||||
return new ArrayList<>(groupDataMap.values());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package client.view;
|
||||
|
||||
import client.Client;
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.login.*;
|
||||
import client.view.util.DesignToken;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
|
||||
/**
|
||||
* 登录页面,用于完成用户账户登录的功能,作为一个独立的页面,登录完成之后将自动关闭,并开启主界面
|
||||
*/
|
||||
public class LoginPage extends JFrame {
|
||||
private static volatile LoginPage INSTANCE;
|
||||
|
||||
// 获取登录页面实例,使用单例模式确保全局唯一性
|
||||
public static LoginPage get() {
|
||||
if (INSTANCE == null) {
|
||||
synchronized (LoginPage.class) {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new LoginPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
// 交替展示两个窗口,分别用于进行注册和登录操作。
|
||||
private SignInView signInView;
|
||||
private SignUpView signUpView;
|
||||
|
||||
private LoginPage() {
|
||||
setTitle("欢迎来到本地网聊天室!");
|
||||
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
setSize(DesignToken.LOGIN_WIDTH, DesignToken.LOGIN_HEIGHT);
|
||||
setLocationRelativeTo(null);
|
||||
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
// 如果链接上了,发出退出信息
|
||||
if (Client.isConnected()) {
|
||||
ChatSender.addMsg(Wrapper.logoutRequest(LocalData.get().getId()));
|
||||
}
|
||||
super.windowClosing(e);
|
||||
// 如果未登录,则直接退出。
|
||||
if (LocalData.get().getId().length() == 0) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
signInView = new SignInView(this);
|
||||
signUpView = new SignUpView(this);
|
||||
|
||||
// 默认为登录
|
||||
exchangeToSignInView();
|
||||
}
|
||||
|
||||
// 更换到注册界面
|
||||
public void exchangeToSignUpView() {
|
||||
this.remove(signInView);
|
||||
this.add(signUpView, BorderLayout.CENTER);
|
||||
this.validate();
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
// 更换到登录界面
|
||||
public void exchangeToSignInView() {
|
||||
this.remove(signUpView);
|
||||
this.add(signInView, BorderLayout.CENTER);
|
||||
this.validate();
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
// 打开主界面,并关闭本界面
|
||||
public void openMainPage() {
|
||||
// 创建新窗口
|
||||
MainPage.get().setVisible(true);
|
||||
|
||||
// 关闭当前窗口
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
public void showMsgDialog(String text) {
|
||||
JDialog inviteDialog = new JDialog(this, "信息", true);
|
||||
inviteDialog.setSize(300, 200);
|
||||
inviteDialog.setLocationRelativeTo(this);
|
||||
|
||||
// 设置对话框内容
|
||||
String htmlText = "<html><body style='width: 210px; padding: 10px;'>" + text + "</body></html>";
|
||||
JLabel label = new JLabel(htmlText, SwingConstants.CENTER);
|
||||
JButton closeBtn = new JButton("确定");
|
||||
|
||||
closeBtn.addActionListener(e -> inviteDialog.dispose());
|
||||
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
JPanel centerPanel = new JPanel(new FlowLayout());
|
||||
centerPanel.add(label);
|
||||
|
||||
panel.add(centerPanel, BorderLayout.CENTER);
|
||||
panel.add(closeBtn, BorderLayout.SOUTH);
|
||||
inviteDialog.add(panel);
|
||||
|
||||
// 显示对话框(会阻塞主窗口交互)
|
||||
inviteDialog.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
package client.view;
|
||||
|
||||
import client.Client;
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.main.*;
|
||||
import global.global;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ComponentAdapter;
|
||||
import java.awt.event.ComponentEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
|
||||
import static client.view.util.DesignToken.*;
|
||||
|
||||
/**
|
||||
* 主界面。
|
||||
*/
|
||||
public class MainPage extends JFrame {
|
||||
private volatile static MainPage instance;
|
||||
|
||||
// 获取主界面
|
||||
public static MainPage get() {
|
||||
if (instance == null) {
|
||||
synchronized (LoginPage.class) {
|
||||
if (instance == null) {
|
||||
instance = new MainPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 可以左右的移动大小的分割界面
|
||||
private final JSplitPane splitPane;
|
||||
private final SideOptionView sideOptionView;
|
||||
private final SecondaryOptionView secondaryOptionView;
|
||||
private final ContentView contentView;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* 初始化主界面组件,包括侧边栏、二级选项栏、详细内容区域等。
|
||||
* 设置主界面的标题、关闭操作、大小、位置等属性。
|
||||
* 同时添加窗口关闭监听器,在窗口关闭时发送登出请求。
|
||||
*/
|
||||
private MainPage() {
|
||||
Dimension size = new Dimension(WINDOW_ORI_WIDTH, WINDOW_ORI_HEIGHT);
|
||||
|
||||
setTitle("本地网络聊天室");
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
setSize(size);
|
||||
setLocationRelativeTo(null);
|
||||
|
||||
// 侧边栏
|
||||
sideOptionView = new SideOptionView();
|
||||
sideOptionView.setMinimumSize(new Dimension(SIDE_PANEL_WIDTH, WINDOW_ORI_HEIGHT));
|
||||
sideOptionView.setMaximumSize(new Dimension(SIDE_PANEL_WIDTH, Integer.MAX_VALUE));
|
||||
sideOptionView.setPreferredSize(new Dimension(SIDE_PANEL_WIDTH, WINDOW_ORI_HEIGHT));
|
||||
|
||||
// 二级选项栏
|
||||
secondaryOptionView = SecondaryOptionView.get();
|
||||
// 详细内容
|
||||
contentView = new ContentView();
|
||||
|
||||
splitPane = new JSplitPane(
|
||||
JSplitPane.HORIZONTAL_SPLIT,
|
||||
secondaryOptionView,
|
||||
contentView);
|
||||
|
||||
splitPane.setDividerSize(2);
|
||||
splitPane.setOneTouchExpandable(true);
|
||||
splitPane.setResizeWeight(0.5);
|
||||
splitPane.setContinuousLayout(true);
|
||||
splitPane.setMinimumSize(new Dimension(SECONDARY_PANEL_WIDTH_MIN + CONTENT_PANEL_WIDTH_MIN, size.height));
|
||||
splitPane.setPreferredSize(new Dimension(SECONDARY_PANEL_WIDTH_MIN + CONTENT_PANEL_WIDTH_MIN, size.height));
|
||||
|
||||
addDividerConstraintListener();
|
||||
|
||||
this.add(sideOptionView, BorderLayout.WEST);
|
||||
this.add(splitPane, BorderLayout.CENTER);
|
||||
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
// 如果没有连接上,则直接退出
|
||||
if (Client.isConnected()) {
|
||||
ChatSender.addMsg(Wrapper.logoutRequest(LocalData.get().getId()));
|
||||
}
|
||||
// 否则,窗口关闭的时候发送登出信息
|
||||
super.windowClosing(e);
|
||||
}
|
||||
});
|
||||
|
||||
// 发送初始化请求
|
||||
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_USER));
|
||||
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_GROUP));
|
||||
ChatSender.addMsg(Wrapper.initRequest(LocalData.get().getId(), global.OPT_INIT_CHAT));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加分隔条约束监听器
|
||||
* 监听分隔条位置变化事件,确保分隔条不会超出最小和最大允许位置范围。
|
||||
* 当窗口大小改变或分隔条位置改变时,调用constrainDividerLocation方法重新限制分隔条位置。
|
||||
*/
|
||||
private void addDividerConstraintListener() {
|
||||
splitPane.addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
// 窗口大小改变时重新计算限制
|
||||
constrainDividerLocation();
|
||||
}
|
||||
});
|
||||
|
||||
splitPane.getLeftComponent().addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
constrainDividerLocation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 限制分隔条位置
|
||||
* 确保分隔条不会超出最小和最大允许位置范围。
|
||||
* 如果当前位置小于最小位置,将分隔条位置设置为最小位置。
|
||||
* 如果当前位置大于最大位置,将分隔条位置设置为最大位置。
|
||||
*/
|
||||
private void constrainDividerLocation() {
|
||||
int totalWidth = splitPane.getWidth();
|
||||
int dividerSize = splitPane.getDividerSize();
|
||||
int currentLocation = splitPane.getDividerLocation();
|
||||
|
||||
// 计算有效位置范围
|
||||
int minLocation = SECONDARY_PANEL_WIDTH_MIN;
|
||||
int maxLocation = totalWidth - dividerSize - SECONDARY_PANEL_WIDTH_MIN;
|
||||
|
||||
// 限制分隔条位置
|
||||
if (currentLocation < minLocation) {
|
||||
splitPane.setDividerLocation(minLocation);
|
||||
} else if (currentLocation > maxLocation) {
|
||||
splitPane.setDividerLocation(maxLocation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到设置界面,当前还没做具体实现
|
||||
*/
|
||||
public void exchangeToSettingPage() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示消息对话框
|
||||
* 创建一个对话框,显示text内容。
|
||||
* 对话框标题为"信息",大小为300x200,居中显示在主窗口上。
|
||||
* 对话框内容为text,居中对齐,宽度为210px, padding为10px。
|
||||
* 对话框包含一个确定按钮,点击后关闭对话框。
|
||||
*/
|
||||
public void showMsgDialog(String text) {
|
||||
JDialog inviteDialog = new JDialog(MainPage.get(), "信息", true);
|
||||
inviteDialog.setSize(300, 200);
|
||||
inviteDialog.setLocationRelativeTo(MainPage.get());
|
||||
|
||||
// 设置对话框内容
|
||||
String htmlText = "<html><body style='width: 210px; padding: 10px;'>" + text + "</body></html>";
|
||||
JLabel label = new JLabel(htmlText, SwingConstants.CENTER);
|
||||
JButton closeBtn = new JButton("确定");
|
||||
|
||||
closeBtn.addActionListener(e -> inviteDialog.dispose());
|
||||
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
JPanel centerPanel = new JPanel(new FlowLayout());
|
||||
centerPanel.add(label);
|
||||
|
||||
panel.add(centerPanel, BorderLayout.CENTER);
|
||||
panel.add(closeBtn, BorderLayout.SOUTH);
|
||||
inviteDialog.add(panel);
|
||||
|
||||
// 显示对话框(会阻塞主窗口交互)
|
||||
inviteDialog.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到登录界面
|
||||
* 隐藏当前主窗口,显示登录界面。
|
||||
*/
|
||||
public void openLogInPage() {
|
||||
LoginPage.get().setVisible(true);
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到空白内容界面
|
||||
* 清空当前内容区域,显示一个空白界面。
|
||||
*/
|
||||
public void exchangeToBlankContent() {
|
||||
contentView.exchangeToBlank();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到聊天房间界面
|
||||
* 清空当前内容区域,显示聊天房间界面。
|
||||
* 聊天房间界面显示与groupId相关的聊天内容。
|
||||
*/
|
||||
public void exchangeToChatRoom(String groupId) {
|
||||
contentView.exchangeToChatRoom(groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到好友个人信息界面
|
||||
* 清空当前内容区域,显示好友个人信息界面。
|
||||
* 好友个人信息界面显示与userId相关的好友信息。
|
||||
*/
|
||||
public void exchangeToFriendProfile(String userId, String userName) {
|
||||
contentView.exchangeToFriendProfile(userId, userName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到设置界面
|
||||
* 清空当前内容区域,显示设置界面。
|
||||
* 设置界面根据type显示不同的设置选项。
|
||||
*/
|
||||
public void exchangeToSettings(String type) {
|
||||
contentView.exchangeToSettings(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到群聊邀请请求界面
|
||||
* 清空当前内容区域,显示群聊邀请请求界面。
|
||||
* 群聊邀请请求界面显示与inviterId相关的群聊邀请请求,包括邀请者姓名、群聊名称等。
|
||||
*/
|
||||
public void showGroupInviteRequestDialog(String inviterId, String inviterName, String groupName, String groupId) {
|
||||
JDialog inviteDialog = new JDialog(MainPage.get(), "群聊邀请", true);
|
||||
inviteDialog.setSize(300, 200);
|
||||
inviteDialog.setLocationRelativeTo(MainPage.get());
|
||||
|
||||
// 设置对话框内容
|
||||
JLabel label = new JLabel(
|
||||
"用户" + inviterName + "邀请你加入:" + groupName,
|
||||
SwingConstants.CENTER);
|
||||
JButton confirmBtn = new JButton("接受");
|
||||
JButton closeBtn = new JButton("拒绝");
|
||||
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
JPanel centerPanel = new JPanel(new FlowLayout());
|
||||
centerPanel.add(label);
|
||||
|
||||
JPanel bottomPanel = new JPanel();
|
||||
bottomPanel.add(confirmBtn);
|
||||
bottomPanel.add(closeBtn);
|
||||
|
||||
panel.add(centerPanel, BorderLayout.CENTER);
|
||||
panel.add(bottomPanel, BorderLayout.SOUTH);
|
||||
inviteDialog.add(panel);
|
||||
|
||||
confirmBtn.addActionListener(
|
||||
e -> {
|
||||
ChatSender.addMsg(
|
||||
new Wrapper(inviterId, LocalData.get().getId(), groupId, global.OPT_GROUP_INVITE_AGREE));
|
||||
inviteDialog.dispose();
|
||||
});
|
||||
|
||||
closeBtn.addActionListener(
|
||||
e -> {
|
||||
ChatSender.addMsg(
|
||||
new Wrapper(inviterId, LocalData.get().getId(), groupId, global.OPT_GROUP_INVITE_REFUSE));
|
||||
inviteDialog.dispose();
|
||||
});
|
||||
|
||||
// 显示对话框(会阻塞主窗口交互)
|
||||
inviteDialog.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到邀请好友界面
|
||||
* 清空当前内容区域,显示邀请好友界面。
|
||||
* 邀请好友界面允许用户输入好友ID,邀请好友加入当前聊天房间。
|
||||
*/
|
||||
public void showGroupInviteDialog() {
|
||||
JDialog inviteDialog = new JDialog(MainPage.get(), "邀请好友", true);
|
||||
inviteDialog.setSize(300, 200);
|
||||
inviteDialog.setLocationRelativeTo(MainPage.get());
|
||||
|
||||
// 创建主面板
|
||||
JPanel mainPanel = new JPanel();
|
||||
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
|
||||
|
||||
// 创建ID面板
|
||||
JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
idPanel.add(new JLabel("好友ID :"));
|
||||
JTextField idField = new JTextField(15);
|
||||
idPanel.add(idField);
|
||||
mainPanel.add(idPanel);
|
||||
|
||||
// 添加间隔
|
||||
mainPanel.add(Box.createVerticalStrut(20));
|
||||
|
||||
// 添加按钮面板
|
||||
JPanel buttonPanel = new JPanel();
|
||||
JButton confirmButton = new JButton("确定");
|
||||
|
||||
confirmButton.addActionListener(e -> {
|
||||
String userId = idField.getText().trim();
|
||||
|
||||
if (userId.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(inviteDialog, "好友不能为空", "警告", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// 这里添加创建群聊的逻辑
|
||||
ChatSender.addMsg(Wrapper.groupInviteRequest(
|
||||
userId,
|
||||
LocalData.get().getId(),
|
||||
LocalData.get().getCurrentChatId()));
|
||||
inviteDialog.dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(confirmButton);
|
||||
mainPanel.add(buttonPanel);
|
||||
|
||||
inviteDialog.add(mainPanel);
|
||||
inviteDialog.getRootPane().setDefaultButton(confirmButton);
|
||||
inviteDialog.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到加入群聊界面
|
||||
* 清空当前内容区域,显示加入群聊界面。
|
||||
* 加入群聊界面允许用户输入群聊ID,申请加入群聊。
|
||||
*/
|
||||
public void showJoinGroupDialog() {
|
||||
JDialog dialog = new JDialog(MainPage.get(), "加入群聊", true);
|
||||
dialog.setSize(300, 200);
|
||||
dialog.setLocationRelativeTo(MainPage.get());
|
||||
|
||||
// 创建主面板
|
||||
JPanel mainPanel = new JPanel();
|
||||
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
|
||||
|
||||
// 创建ID面板
|
||||
JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
idPanel.add(new JLabel("群聊ID:"));
|
||||
JTextField idField = new JTextField(15);
|
||||
idPanel.add(idField);
|
||||
mainPanel.add(idPanel);
|
||||
|
||||
// 添加间隔
|
||||
mainPanel.add(Box.createVerticalStrut(20));
|
||||
|
||||
// 添加按钮面板
|
||||
JPanel buttonPanel = new JPanel();
|
||||
JButton confirmButton = new JButton("确定");
|
||||
|
||||
confirmButton.addActionListener(e -> {
|
||||
String groupId = idField.getText().trim();
|
||||
|
||||
if (groupId.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(dialog, "群聊ID不能为空", "警告", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送加入群聊请求
|
||||
ChatSender.addMsg(new Wrapper(null, LocalData.get().getId(), groupId, global.OPT_GROUP_JOIN));
|
||||
dialog.dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(confirmButton);
|
||||
mainPanel.add(buttonPanel);
|
||||
|
||||
dialog.add(mainPanel);
|
||||
dialog.getRootPane().setDefaultButton(confirmButton);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到添加好友界面
|
||||
* 清空当前内容区域,显示添加好友界面。
|
||||
* 添加好友界面允许用户输入好友ID,申请添加好友。
|
||||
*/
|
||||
public void showAddFriendDialog() {
|
||||
JDialog dialog = new JDialog(MainPage.get(), "添加好友", true);
|
||||
dialog.setSize(300, 200);
|
||||
dialog.setLocationRelativeTo(MainPage.get());
|
||||
|
||||
// 创建主面板
|
||||
JPanel mainPanel = new JPanel();
|
||||
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
|
||||
|
||||
// 创建ID面板
|
||||
JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
idPanel.add(new JLabel("好友ID:"));
|
||||
JTextField idField = new JTextField(15);
|
||||
idPanel.add(idField);
|
||||
mainPanel.add(idPanel);
|
||||
|
||||
// 添加间隔
|
||||
mainPanel.add(Box.createVerticalStrut(20));
|
||||
|
||||
// 添加按钮面板
|
||||
JPanel buttonPanel = new JPanel();
|
||||
JButton confirmButton = new JButton("确定");
|
||||
|
||||
confirmButton.addActionListener(e -> {
|
||||
String friendId = idField.getText().trim();
|
||||
|
||||
if (friendId.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(dialog, "好友ID不能为空", "警告", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (friendId.equals(LocalData.get().getId())) {
|
||||
JOptionPane.showMessageDialog(dialog, "不能添加自己为好友", "警告", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送添加好友请求
|
||||
ChatSender.addMsg(new Wrapper(friendId, LocalData.get().getId(), null, global.OPT_FRIEND_ADD));
|
||||
dialog.dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(confirmButton);
|
||||
mainPanel.add(buttonPanel);
|
||||
|
||||
dialog.add(mainPanel);
|
||||
dialog.getRootPane().setDefaultButton(confirmButton);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到创建群聊界面
|
||||
* 清空当前内容区域,显示创建群聊界面。
|
||||
* 创建群聊界面允许用户输入群聊ID和名称,创建一个新的群聊房间。
|
||||
*/
|
||||
public void showGroupCreateDialog() {
|
||||
JDialog inviteDialog = new JDialog(MainPage.get(), "创建群聊", true);
|
||||
inviteDialog.setSize(300, 200);
|
||||
inviteDialog.setLocationRelativeTo(MainPage.get());
|
||||
|
||||
// 创建主面板
|
||||
JPanel mainPanel = new JPanel();
|
||||
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
|
||||
|
||||
// 创建ID面板
|
||||
JPanel idPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
idPanel.add(new JLabel("群聊ID :"));
|
||||
JTextField idField = new JTextField(15);
|
||||
idPanel.add(idField);
|
||||
mainPanel.add(idPanel);
|
||||
|
||||
// 创建名称面板
|
||||
JPanel namePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
namePanel.add(new JLabel("群聊名称:"));
|
||||
JTextField nameField = new JTextField(15);
|
||||
namePanel.add(nameField);
|
||||
mainPanel.add(namePanel);
|
||||
|
||||
// 添加间隔
|
||||
mainPanel.add(Box.createVerticalStrut(20));
|
||||
|
||||
// 添加按钮面板
|
||||
JPanel buttonPanel = new JPanel();
|
||||
JButton confirmButton = new JButton("确定");
|
||||
|
||||
confirmButton.addActionListener(e -> {
|
||||
String groupId = idField.getText().trim();
|
||||
String groupName = nameField.getText().trim();
|
||||
|
||||
if (groupId.isEmpty() || groupName.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(inviteDialog, "群聊ID和名称不能为空", "警告", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupId.length() < 6 || groupId.length() > 10 || !groupId.matches("[a-zA-Z0-9_]+")) {
|
||||
JOptionPane.showMessageDialog(inviteDialog, "群聊ID只能包含字母大小写和数字下划线,且长度不得小于6,大于10", "警告",
|
||||
JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (groupName.contains(" ")) {
|
||||
JOptionPane.showMessageDialog(inviteDialog, "群聊名称不能包含空格", "警告", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加创建群聊的逻辑
|
||||
ChatSender.addMsg(Wrapper.createGroupRequest(LocalData.get().getId(), groupName, groupId));
|
||||
inviteDialog.dispose();
|
||||
});
|
||||
|
||||
buttonPanel.add(confirmButton);
|
||||
mainPanel.add(buttonPanel);
|
||||
|
||||
inviteDialog.add(mainPanel);
|
||||
inviteDialog.getRootPane().setDefaultButton(confirmButton);
|
||||
inviteDialog.setVisible(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package client.view.login;
|
||||
|
||||
|
||||
import client.Client;
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.LoginPage;
|
||||
import client.view.util.DesignToken;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
// 登录界面
|
||||
public class SignInView extends JPanel {
|
||||
private LoginPage loginPage;
|
||||
|
||||
JTextField account;
|
||||
JTextField password;
|
||||
|
||||
/**
|
||||
* 生成一个登录界面
|
||||
* 按照原型图中设计,创建两个文本输入框,用于让用户输入id和密码。
|
||||
* 创建一个按钮用于登录,为按钮添加SignIncheck的按下事件
|
||||
* 创建一个注册选项按钮,按下后该界面更换为注册界面。
|
||||
*/
|
||||
public SignInView(LoginPage loginPage) {
|
||||
this.loginPage = loginPage;
|
||||
|
||||
//面板
|
||||
this.setLayout(null);
|
||||
this.setSize(DesignToken.LOGIN_WIDTH, DesignToken.LOGIN_HEIGHT);
|
||||
this.setVisible(true);
|
||||
|
||||
//文本展示
|
||||
JLabel ja1 = new JLabel("登录");
|
||||
ja1.setBounds(165, 10, 80, 80);
|
||||
this.add(ja1);
|
||||
|
||||
|
||||
JLabel ja2 = new JLabel("账户:");
|
||||
ja2.setBounds(50, 80, 100, 30);
|
||||
this.add(ja2);
|
||||
|
||||
|
||||
JLabel ja3 = new JLabel("密码:");
|
||||
ja3.setBounds(50, 120, 100, 30);
|
||||
this.add(ja3);
|
||||
|
||||
|
||||
//按钮设计
|
||||
JButton jb1 = new JButton("注册");
|
||||
jb1.setBounds(230, 190, 59, 20);
|
||||
jb1.setBorderPainted(false);
|
||||
jb1.addActionListener(e -> {
|
||||
this.loginPage.exchangeToSignUpView();
|
||||
});
|
||||
this.add(jb1);
|
||||
|
||||
// JButton jb2=new JButton("忘记密码");
|
||||
// jb2.setBounds(290,190,90,20);
|
||||
// jb2.setBorderPainted(false);
|
||||
// jb2.addActionListener(e -> {
|
||||
//
|
||||
// });
|
||||
// add(jb2);
|
||||
|
||||
|
||||
JButton jb3 = new JButton("登录");
|
||||
jb3.setBounds(150, 160, 59, 50);
|
||||
jb3.addActionListener(e -> {
|
||||
signIncheck();
|
||||
});
|
||||
this.add(jb3);
|
||||
|
||||
//建立文本域
|
||||
account = new JTextField(DesignToken.MAX_FONT_SIZE);
|
||||
account.setBounds(80, 80, 220, 30);
|
||||
this.add(account);
|
||||
|
||||
password = new JPasswordField(DesignToken.MAX_FONT_SIZE);
|
||||
password.setBounds(80, 120, 220, 30);
|
||||
this.add(password);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前页面的用户id和密码
|
||||
* 如果当前服务器未在线,则直接向用户说明
|
||||
* 否则,发出登录请求
|
||||
*/
|
||||
private void signIncheck() {
|
||||
if (!Client.isConnected()) {
|
||||
loginPage.showMsgDialog("当前服务器未在线,请稍后再试");
|
||||
return;
|
||||
}
|
||||
|
||||
LocalData.get().setId(account.getText());
|
||||
ChatSender.addMsg(
|
||||
Wrapper.loginRequest(LocalData.get().getId(), password.getText())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package client.view.login;
|
||||
|
||||
import client.Client;
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.LoginPage;
|
||||
import client.view.util.DesignToken;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public class SignUpView extends JPanel {
|
||||
private LoginPage loginPage;
|
||||
|
||||
private JTextField account;
|
||||
private JTextField password;
|
||||
private JTextField name;
|
||||
|
||||
/**
|
||||
* 生成一个注册界面
|
||||
* 按照原型图中设计,创建三个文本输入框,用于让用户输入id,密码,用户名
|
||||
* 创建一个按钮用于注册
|
||||
* 创建一个登录选项按钮,按下后该界面更换为登录界面。
|
||||
*/
|
||||
public SignUpView(LoginPage loginPage) {
|
||||
this.loginPage = loginPage;
|
||||
|
||||
//功能面板的建立
|
||||
this.setLayout(null);
|
||||
this.setSize(DesignToken.LOGIN_WIDTH, DesignToken.LOGIN_HEIGHT);
|
||||
this.setVisible(true);
|
||||
|
||||
//文本
|
||||
JLabel ja1 = new JLabel("注册");
|
||||
ja1.setBounds(165, 10, 80, 80);
|
||||
this.add(ja1);
|
||||
|
||||
JLabel ja2 = new JLabel("账户:");
|
||||
ja2.setBounds(50, 80, 100, 30);
|
||||
this.add(ja2);
|
||||
|
||||
JLabel ja3 = new JLabel("密码:");
|
||||
ja3.setBounds(50, 120, 100, 30);
|
||||
this.add(ja3);
|
||||
|
||||
JLabel ja4 = new JLabel("名字:");
|
||||
ja4.setBounds(50, 160, 100, 30);
|
||||
this.add(ja4);
|
||||
|
||||
|
||||
//监听按钮
|
||||
JButton jb1 = new JButton("注册");
|
||||
jb1.setBounds(150, 200, 59, 50);
|
||||
this.add(jb1);
|
||||
jb1.addActionListener(e -> {
|
||||
this.SignUpcheck();
|
||||
});
|
||||
|
||||
JButton jb2 = new JButton("已有账号?去登录");
|
||||
jb2.setBounds(220, 230, 150, 20);
|
||||
jb2.setBorderPainted(false);
|
||||
jb2.addActionListener(e -> {
|
||||
this.loginPage.exchangeToSignInView();
|
||||
});
|
||||
this.add(jb2);
|
||||
|
||||
//文本域
|
||||
account = new JTextField(DesignToken.MAX_FONT_SIZE);
|
||||
account.setBounds(80, 80, 220, 30);
|
||||
this.add(account);
|
||||
|
||||
password = new JTextField(DesignToken.MAX_FONT_SIZE);
|
||||
password.setBounds(80, 120, 220, 30);
|
||||
this.add(password);
|
||||
|
||||
name = new JTextField(DesignToken.MAX_FONT_SIZE);
|
||||
name.setBounds(80, 160, 220, 30);
|
||||
this.add(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前界面的文本的内容
|
||||
* 检查服务器是否链接
|
||||
* 检查注册的id,name,password是否合法
|
||||
* 都合适的话,发送注册请求。
|
||||
*/
|
||||
private void SignUpcheck() {
|
||||
if (!Client.isConnected()) {
|
||||
loginPage.showMsgDialog("当前服务器未在线,请稍后再试");
|
||||
return;
|
||||
}
|
||||
|
||||
String userId = account.getText();
|
||||
String userName = name.getText();
|
||||
String userPassword = password.getText();
|
||||
|
||||
if (userId.length() < 6 || userId.length() > 10 || !userId.matches("[a-zA-Z0-9_]+")) {
|
||||
loginPage.showMsgDialog("用户id只能包含字母大小写和数字下划线,且长度不得小于6,大于10");
|
||||
return;
|
||||
}
|
||||
|
||||
if (userName.contains(" ")) {
|
||||
loginPage.showMsgDialog("用户名不能包含空格");
|
||||
return;
|
||||
}
|
||||
|
||||
if (userPassword.contains(" ")) {
|
||||
loginPage.showMsgDialog("密码不能包含空格");
|
||||
return;
|
||||
}
|
||||
|
||||
LocalData.get().setId(userId);
|
||||
LocalData.get().setUserName(userId, userName);
|
||||
|
||||
ChatSender.addMsg(Wrapper.registerRequest(
|
||||
LocalData.get().getId(),
|
||||
password.getText(),
|
||||
name.getText()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.util.CircleCharIcon2;
|
||||
import client.view.util.DesignToken;
|
||||
import server.serveice.Wrapper;
|
||||
import util.MsgUtil;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.event.AncestorEvent;
|
||||
import javax.swing.event.AncestorListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import static client.view.util.DesignToken.*;
|
||||
|
||||
// 群聊信息组件,包含底部的打字框和滚动的信息聊天信息
|
||||
public class ChatInfoView extends JPanel {
|
||||
|
||||
private static volatile ChatInfoView instance;
|
||||
|
||||
public static ChatInfoView get() {
|
||||
if (instance == null) {
|
||||
synchronized (ChatInfoView.class) {
|
||||
if (instance == null) {
|
||||
instance = new ChatInfoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 界面组件
|
||||
private JPanel messagePanel; // 使用JPanel来承载消息,可以自定义布局
|
||||
private List<JPanel> bubbles;
|
||||
private JScrollPane messageScrollPane;
|
||||
private JTextArea inputArea;
|
||||
private JScrollPane inputScrollPane;
|
||||
private JButton sendButton;
|
||||
private JPanel inputPanel;
|
||||
private JPanel buttonPanel;
|
||||
|
||||
// 样式相关
|
||||
private SimpleDateFormat timeFormat;
|
||||
private Color userColor = Color.decode(DesignToken.BUBBLE_COLOR_GREEN); // 用户消息气泡颜色
|
||||
private Color otherColor = Color.decode(DesignToken.BUBBLE_COLOR_WHITE); // 他人消息气泡颜色(白色)
|
||||
private Color systemColor = Color.decode(DesignToken.BACKGROUND_COLOR); // 系统消息背景色
|
||||
|
||||
// 头像颜色数组,用于不同用户的头像显示
|
||||
private final Color[] avatarColors = {
|
||||
Color.decode(DesignToken.BUBBLE_COLOR_BLUE),
|
||||
Color.decode(DesignToken.BUBBLE_COLOR_GRAY),
|
||||
Color.decode(DesignToken.BUBBLE_COLOR_RED),
|
||||
Color.decode(DesignToken.BUBBLE_COLOR_YELLOW),
|
||||
Color.decode(DesignToken.BUBBLE_COLOR_WHITE),
|
||||
};
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* 初始化界面组件和布局
|
||||
*/
|
||||
private ChatInfoView() {
|
||||
setLayout(new BorderLayout(0, 0));
|
||||
|
||||
// 初始化时间格式
|
||||
timeFormat = new SimpleDateFormat("HH:mm");
|
||||
|
||||
// 初始化消息显示区域
|
||||
initMessagePanel();
|
||||
|
||||
// 创建输入区域
|
||||
initInputPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化消息面板
|
||||
* 包含滚动条和消息气泡容器
|
||||
*/
|
||||
private void initMessagePanel() {
|
||||
bubbles = new ArrayList<>();
|
||||
|
||||
// 创建消息面板,使用垂直箱式布局
|
||||
messagePanel = new JPanel();
|
||||
messagePanel.setLayout(new BoxLayout(messagePanel, BoxLayout.Y_AXIS));
|
||||
messagePanel.setBackground(systemColor);
|
||||
messagePanel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
|
||||
// 添加一个弹性空间,让新消息从底部开始
|
||||
messagePanel.add(Box.createVerticalGlue());
|
||||
|
||||
// 添加滚动条
|
||||
messageScrollPane = new JScrollPane(messagePanel);
|
||||
messageScrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
|
||||
messageScrollPane.getVerticalScrollBar().setUnitIncrement(16);
|
||||
messageScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
|
||||
// 设置视口的背景色
|
||||
JViewport viewport = messageScrollPane.getViewport();
|
||||
viewport.setBackground(systemColor);
|
||||
|
||||
// 添加到主面板
|
||||
this.add(messageScrollPane, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化输入面板
|
||||
* 包含输入文本框和发送按钮
|
||||
*/
|
||||
private void initInputPanel() {
|
||||
// 创建输入面板
|
||||
inputPanel = new JPanel(new BorderLayout(5, 5));
|
||||
inputPanel.setBackground(systemColor);
|
||||
inputPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
|
||||
// 创建输入文本框
|
||||
inputArea = new JTextArea(3, 20);
|
||||
inputArea.setFont(new Font(DEFAULT_FONT, Font.PLAIN, DesignToken.FONT_SIZE));
|
||||
inputArea.setLineWrap(true);
|
||||
inputArea.setWrapStyleWord(true);
|
||||
inputArea.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(Color.decode(DesignToken.EDGE_COLOR), 1),
|
||||
BorderFactory.createEmptyBorder(8, 8, 8, 8)));
|
||||
|
||||
// 设置提示文本
|
||||
inputArea.setToolTipText("输入消息,按Enter发送,Ctrl+Enter换行");
|
||||
|
||||
// 添加滚动条到输入框
|
||||
inputScrollPane = new JScrollPane(inputArea);
|
||||
inputScrollPane.setBorder(null);
|
||||
|
||||
// 创建发送按钮
|
||||
sendButton = createSendButton();
|
||||
|
||||
// 创建按钮面板
|
||||
buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 5));
|
||||
buttonPanel.setBackground(systemColor);
|
||||
buttonPanel.add(sendButton);
|
||||
setupListeners();
|
||||
|
||||
// 添加组件到输入面板
|
||||
inputPanel.add(inputScrollPane, BorderLayout.CENTER);
|
||||
inputPanel.add(buttonPanel, BorderLayout.SOUTH);
|
||||
|
||||
// 添加到主面板
|
||||
this.add(inputPanel, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新主题
|
||||
* 当主题变化时调用,用于更新所有组件的颜色
|
||||
*/
|
||||
public void updateTheme() {
|
||||
// 更新颜色变量
|
||||
userColor = Color.decode(DesignToken.BUBBLE_COLOR_GREEN);
|
||||
otherColor = Color.decode(DesignToken.BUBBLE_COLOR_WHITE);
|
||||
systemColor = Color.decode(DesignToken.BACKGROUND_COLOR);
|
||||
|
||||
// 更新组件背景
|
||||
if (messagePanel != null)
|
||||
messagePanel.setBackground(systemColor);
|
||||
if (messageScrollPane != null && messageScrollPane.getViewport() != null) {
|
||||
messageScrollPane.getViewport().setBackground(systemColor);
|
||||
}
|
||||
if (inputPanel != null)
|
||||
inputPanel.setBackground(systemColor);
|
||||
if (buttonPanel != null)
|
||||
buttonPanel.setBackground(systemColor);
|
||||
if (sendButton != null)
|
||||
sendButton.setBackground(userColor);
|
||||
if (inputArea != null) {
|
||||
inputArea.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(Color.decode(DesignToken.EDGE_COLOR), 1),
|
||||
BorderFactory.createEmptyBorder(8, 8, 8, 8)));
|
||||
}
|
||||
|
||||
// 刷新当前聊天记录
|
||||
if (LocalData.get().getCurrentChatId() != null) {
|
||||
init(LocalData.get().getCurrentChatId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建发送按钮
|
||||
* 按钮文本为“发送”,字体为加粗,大小为 DesignToken.FONT_SIZE
|
||||
* 背景颜色为 DesignToken.BUBBLE_COLOR_GREEN,前景颜色为黑色
|
||||
* 点击时背景颜色为 DesignToken.BUBBLE_COLOR_GREEN,松开时恢复为 DesignToken.BUBBLE_COLOR_GREEN
|
||||
* 鼠标悬停时背景颜色为 DesignToken.BUBBLE_COLOR_GREEN
|
||||
*
|
||||
* @return 发送按钮
|
||||
*/
|
||||
private JButton createSendButton() {
|
||||
JButton button = new JButton("发送");
|
||||
button.setFont(new Font(DEFAULT_FONT, Font.BOLD, DesignToken.FONT_SIZE));
|
||||
button.setBackground(userColor);
|
||||
button.setForeground(Color.BLACK); // 强制设置字体颜色为黑色,确保在绿色背景下清晰可见
|
||||
button.setFocusPainted(false);
|
||||
button.setBorder(BorderFactory.createEmptyBorder(10, 25, 10, 25));
|
||||
button.setCursor(new Cursor(Cursor.HAND_CURSOR));
|
||||
|
||||
// 鼠标悬停效果
|
||||
button.addMouseListener(new java.awt.event.MouseAdapter() {
|
||||
public void mouseEntered(java.awt.event.MouseEvent evt) {
|
||||
button.setBackground(Color.decode(DesignToken.BUBBLE_COLOR_GREEN));
|
||||
}
|
||||
|
||||
public void mouseExited(java.awt.event.MouseEvent evt) {
|
||||
button.setBackground(userColor);
|
||||
}
|
||||
|
||||
public void mousePressed(java.awt.event.MouseEvent evt) {
|
||||
button.setBackground(userColor);
|
||||
}
|
||||
|
||||
public void mouseReleased(java.awt.event.MouseEvent evt) {
|
||||
button.setBackground(userColor);
|
||||
}
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息发送事件
|
||||
* 点击发送按钮或按下Enter键发送消息
|
||||
*/
|
||||
private void setupListeners() {
|
||||
// 发送按钮点击事件
|
||||
sendButton.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// 回车发送消息,Ctrl+Enter换行
|
||||
inputArea.addKeyListener(new java.awt.event.KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(java.awt.event.KeyEvent e) {
|
||||
if (e.getKeyCode() == java.awt.event.KeyEvent.VK_ENTER) {
|
||||
if (e.isControlDown()) {
|
||||
// Ctrl+Enter 换行
|
||||
inputArea.append("\n");
|
||||
} else {
|
||||
// Enter 发送消息
|
||||
e.consume(); // 防止默认的换行行为
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 窗口显示时自动聚焦到输入框
|
||||
addAncestorListener(new AncestorListener() {
|
||||
@Override
|
||||
public void ancestorAdded(AncestorEvent e) {
|
||||
inputArea.requestFocusInWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ancestorRemoved(AncestorEvent event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ancestorMoved(AncestorEvent event) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送信息
|
||||
* 先调用使用DataManager进行信息发送操作
|
||||
* 如果发送成功,则更新界面,添加信息到消息历史上
|
||||
* 如果发送未成功,则忽略这个操作(发射未成功表示程序出现了问题,在控制态输出问题)
|
||||
*/
|
||||
private void sendMessage() {
|
||||
String text = inputArea.getText().trim();
|
||||
if (!text.isEmpty()) {
|
||||
|
||||
String id = LocalData.get().getId();
|
||||
String currentChatId = LocalData.get().getCurrentChatId();
|
||||
String message = MsgUtil.combineMsg(id, LocalData.get().getUserName(id), text);
|
||||
|
||||
// 检查是群聊还是私聊
|
||||
if (LocalData.get().getGroupData(currentChatId) != null) {
|
||||
// 群聊
|
||||
ChatSender.addMsg(Wrapper.groupChat(message, id, currentChatId));
|
||||
} else {
|
||||
// 私聊:发送纯文本
|
||||
ChatSender.addMsg(Wrapper.privateChat(text, id, currentChatId));
|
||||
}
|
||||
|
||||
// 暂时保存消息
|
||||
LocalData.get().addChatMsg(
|
||||
LocalData.get().getCurrentChatId(),
|
||||
message);
|
||||
|
||||
// 添加用户消息
|
||||
addUserMessage(text);
|
||||
|
||||
// 清空输入框
|
||||
inputArea.setText("");
|
||||
|
||||
// 滚动到底部
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 聚焦回输入框
|
||||
inputArea.requestFocusInWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加用户消息
|
||||
* 用户消息右对齐
|
||||
*
|
||||
* @param content 消息内容
|
||||
*/
|
||||
public void addUserMessage(String content) {
|
||||
// 用户消息右对齐
|
||||
addMessageBubble(true, "我", content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加他人消息
|
||||
* 他人消息左对齐
|
||||
*
|
||||
* @param senderName 发送者名称
|
||||
* @param content 消息内容
|
||||
*/
|
||||
public void addOtherUserMessage(String senderName, String content) {
|
||||
// 他人消息左对齐
|
||||
addMessageBubble(false, senderName, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前聊天信息
|
||||
* 从本地数据中获取聊天记录,根据发送者ID判断是用户消息还是他人消息
|
||||
*
|
||||
* @param chatId 聊天ID
|
||||
*/
|
||||
public void setChatInfo(String chatId) {
|
||||
// 清空当前消息
|
||||
messagePanel.removeAll();
|
||||
messagePanel.add(Box.createVerticalGlue());
|
||||
bubbles.clear();
|
||||
|
||||
// 从本地数据中获取聊天记录
|
||||
List<String> messages = LocalData.get().getChatMsg(chatId);
|
||||
if (messages != null) {
|
||||
String myId = LocalData.get().getId();
|
||||
for (String msg : messages) {
|
||||
String[] split = MsgUtil.splitMsg(msg);
|
||||
// split[0] = senderId, split[1] = senderName, split[2] = content
|
||||
if (split.length >= 3) {
|
||||
if (split[0].equals(myId)) {
|
||||
addUserMessage(split[2]);
|
||||
} else {
|
||||
addOtherUserMessage(split[1], split[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新消息面板
|
||||
messagePanel.revalidate();
|
||||
messagePanel.repaint();
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加系统消息
|
||||
* 系统消息居中显示,字体为斜体,颜色为灰色
|
||||
*
|
||||
* @param content 系统消息内容
|
||||
*/
|
||||
public void addSystemMessage(String content) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// 系统消息
|
||||
JPanel systemPanel = new JPanel();
|
||||
systemPanel.setLayout(new BorderLayout());
|
||||
systemPanel.setBackground(new Color(236, 236, 236));
|
||||
systemPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 40));
|
||||
|
||||
JLabel systemLabel = new JLabel(content);
|
||||
systemLabel.setFont(new Font(DEFAULT_FONT, Font.ITALIC, DesignToken.FONT_SIZE_SMALL));
|
||||
systemLabel.setForeground(Color.GRAY);
|
||||
systemLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
|
||||
systemPanel.add(systemLabel, BorderLayout.CENTER);
|
||||
|
||||
// 添加到消息面板顶部
|
||||
messagePanel.add(systemPanel, 0);
|
||||
messagePanel.revalidate();
|
||||
messagePanel.repaint();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息气泡
|
||||
* 根据是否是用户消息,创建不同的消息气泡样式
|
||||
*
|
||||
* @param isSelf 是否是用户消息
|
||||
* @param senderName 发送者名称
|
||||
* @param content 消息内容
|
||||
*/
|
||||
public void addMessageBubble(boolean isSelf, String senderName, String content) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// 创建消息气泡面板
|
||||
JPanel messageBubblePanel = new JPanel();
|
||||
messageBubblePanel.setLayout(new BorderLayout(8, 0));
|
||||
messageBubblePanel.setBackground(systemColor);
|
||||
messageBubblePanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 200));
|
||||
|
||||
// 添加头像
|
||||
JLabel avatarPanel = new JLabel(new CircleCharIcon2(
|
||||
avatarColors[Math.abs(senderName.hashCode()) % avatarColors.length],
|
||||
Color.WHITE,
|
||||
senderName.substring(0, 1).toUpperCase(),
|
||||
40));
|
||||
|
||||
// 创建消息内容面板
|
||||
JPanel contentPanel = new JPanel();
|
||||
contentPanel.setLayout(new BorderLayout(0, 5));
|
||||
contentPanel.setOpaque(false);
|
||||
|
||||
// 创建发送者标签和时间标签
|
||||
String time = timeFormat.format(new Date());
|
||||
JLabel infoLabel = new JLabel(senderName + " " + time);
|
||||
infoLabel.setFont(new Font(DEFAULT_FONT, Font.PLAIN, FONT_SIZE_SMALL));
|
||||
infoLabel.setForeground(Color.GRAY);
|
||||
|
||||
// 创建消息气泡
|
||||
JTextArea messageLabel = new JTextArea(content);
|
||||
messageLabel.setEditable(false);
|
||||
messageLabel.setLineWrap(true);
|
||||
messageLabel.setWrapStyleWord(true);
|
||||
messageLabel.setFont(new Font(DEFAULT_FONT, Font.PLAIN, FONT_SIZE));
|
||||
messageLabel.setBorder(BorderFactory.createEmptyBorder(10, 15, 10, 15));
|
||||
|
||||
// 设置气泡颜色和样式
|
||||
if (isSelf) {
|
||||
// 用户消息:右对齐,绿色气泡
|
||||
messageLabel.setBackground(userColor);
|
||||
messageLabel.setForeground(Color.WHITE);
|
||||
infoLabel.setHorizontalAlignment(SwingConstants.RIGHT);
|
||||
contentPanel.add(infoLabel, BorderLayout.NORTH);
|
||||
contentPanel.add(messageLabel, BorderLayout.CENTER);
|
||||
|
||||
// 右对齐布局
|
||||
messageBubblePanel.add(contentPanel, BorderLayout.CENTER);
|
||||
messageBubblePanel.add(avatarPanel, BorderLayout.EAST);
|
||||
} else {
|
||||
// 他人消息:左对齐,白色气泡
|
||||
messageLabel.setBackground(otherColor);
|
||||
messageLabel.setForeground(Color.decode(DesignToken.COLOR_FONT_BLACK));
|
||||
messageLabel.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(Color.decode(EDGE_COLOR), 1),
|
||||
BorderFactory.createEmptyBorder(10, 15, 10, 15)));
|
||||
infoLabel.setHorizontalAlignment(SwingConstants.LEFT);
|
||||
contentPanel.add(infoLabel, BorderLayout.NORTH);
|
||||
contentPanel.add(messageLabel, BorderLayout.CENTER);
|
||||
|
||||
// 左对齐布局
|
||||
messageBubblePanel.add(avatarPanel, BorderLayout.WEST);
|
||||
messageBubblePanel.add(contentPanel, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
// 设置消息气泡的最大宽度(防止过宽)
|
||||
int maxBubbleWidth = 350;
|
||||
messageLabel.setSize(new Dimension(maxBubbleWidth, Integer.MAX_VALUE));
|
||||
int preferredHeight = messageLabel.getPreferredSize().height;
|
||||
messageLabel.setPreferredSize(new Dimension(maxBubbleWidth, preferredHeight));
|
||||
|
||||
// 将其添加到消息列表当中
|
||||
bubbles.add(messageBubblePanel);
|
||||
|
||||
// 添加到消息面板顶部(新消息在顶部显示)
|
||||
messagePanel.add(messageBubblePanel);
|
||||
messagePanel.revalidate();
|
||||
messagePanel.repaint();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除所有消息气泡(bubbles)
|
||||
* 清空输入栏的内容
|
||||
*/
|
||||
public void removeAllMessageBubble() {
|
||||
for (JPanel panel : bubbles) {
|
||||
messagePanel.remove(panel);
|
||||
}
|
||||
bubbles.clear();
|
||||
inputArea.setText("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据群聊id在DataManager中选择并加载群聊信息
|
||||
*
|
||||
* @param groupId 群聊id
|
||||
*/
|
||||
public void init(String groupId) {
|
||||
this.removeAllMessageBubble();
|
||||
List<String> messages = LocalData.get().getChatMsg(groupId);
|
||||
for (String text : messages) {
|
||||
String[] msgs = MsgUtil.splitMsg(text);
|
||||
|
||||
if (msgs[0].equals(LocalData.get().getId())) {
|
||||
addUserMessage(msgs[2]);
|
||||
} else {
|
||||
addMessageBubble(msgs[0].equals(LocalData.get().getId()), msgs[1], msgs[2]);
|
||||
}
|
||||
}
|
||||
|
||||
messagePanel.revalidate();
|
||||
messagePanel.repaint();
|
||||
}
|
||||
|
||||
private void scrollToBottom() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JScrollBar vertical = messageScrollPane.getVerticalScrollBar();
|
||||
vertical.setValue(vertical.getMaximum());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.service.LocalData;
|
||||
import client.view.util.DesignToken;
|
||||
import client.view.util.LimitSizePanel;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
import static client.view.util.DesignToken.*;
|
||||
|
||||
/**
|
||||
* 内容界面组件
|
||||
* 用于显示聊天、群聊、好友个人信息等内容。
|
||||
* 点击不同选项可以切换到对应的功能界面。
|
||||
*/
|
||||
public class ContentView extends LimitSizePanel {
|
||||
private ChatInfoView chatInfoView;
|
||||
private GroupInfoView groupInfoView;
|
||||
|
||||
public ContentView() {
|
||||
super(CONTENT_PANEL_WIDTH_MIN);
|
||||
this.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
|
||||
this.setLayout(new BorderLayout());
|
||||
|
||||
chatInfoView = ChatInfoView.get();
|
||||
chatInfoView.setPreferredSize(new Dimension(GROUP_CHAT_PANEL_WIDTH, this.getHeight()));
|
||||
chatInfoView.setMinimumSize(new Dimension(GROUP_CHAT_PANEL_WIDTH, this.getHeight()));
|
||||
|
||||
groupInfoView = GroupInfoView.get();
|
||||
groupInfoView.setPreferredSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, this.getHeight()));
|
||||
|
||||
exchangeToBlank();
|
||||
}
|
||||
|
||||
public void exchangeToBlank() {
|
||||
this.removeAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内容组件更改为群聊组件
|
||||
* 依据groupId来从DataManager中获取群聊信息
|
||||
* 更新 chatInfoView, groupInfoView两个组件
|
||||
*/
|
||||
public void exchangeToChatRoom(String groupId) {
|
||||
this.removeAll();
|
||||
chatInfoView.init(groupId);
|
||||
this.add(chatInfoView, BorderLayout.CENTER);
|
||||
|
||||
if (LocalData.get().getGroupData(groupId) != null) {
|
||||
groupInfoView.updateInfo();
|
||||
this.add(groupInfoView, BorderLayout.EAST);
|
||||
}
|
||||
|
||||
this.revalidate();
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内容组件更改为好友个人信息组件
|
||||
* 依据userId和userName来创建FriendProfileView组件
|
||||
* 更新 chatInfoView, groupInfoView两个组件
|
||||
*/
|
||||
public void exchangeToFriendProfile(String userId, String userName) {
|
||||
this.removeAll();
|
||||
FriendProfileView profileView = new FriendProfileView(userId, userName);
|
||||
this.add(profileView, BorderLayout.CENTER);
|
||||
this.revalidate();
|
||||
this.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内容组件更改为设置组件
|
||||
* 依据type来创建SettingsView组件
|
||||
* 更新 chatInfoView, groupInfoView两个组件
|
||||
*/
|
||||
public void exchangeToSettings(String type) {
|
||||
this.removeAll();
|
||||
SettingsView settingsView = new SettingsView(type);
|
||||
this.add(settingsView, BorderLayout.CENTER);
|
||||
this.revalidate();
|
||||
this.repaint();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.view.MainPage;
|
||||
import client.view.util.CircleCharIcon2;
|
||||
import client.view.util.DesignToken;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
/**
|
||||
* 好友列表项组件
|
||||
* 用于显示好友列表中的每个好友项,包括好友头像、好友名称等。
|
||||
* 点击好友项可以进入好友个人信息界面。
|
||||
*/
|
||||
public class FriendListItem extends JPanel {
|
||||
private final String userId;
|
||||
private final String userName;
|
||||
|
||||
JLabel icon;
|
||||
JPanel centerPanel;
|
||||
JLabel titleLabel;
|
||||
|
||||
/**
|
||||
* 构造好友列表项组件
|
||||
*
|
||||
* @param userId 好友用户ID
|
||||
* @param userName 好友用户名
|
||||
*/
|
||||
public FriendListItem(String userId, String userName) {
|
||||
this.userId = userId;
|
||||
this.userName = userName;
|
||||
|
||||
// 设置布局管理器
|
||||
setLayout(new BorderLayout(10, 0));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 10, 8, 10));
|
||||
setMaximumSize(new Dimension(Integer.MAX_VALUE, 70));
|
||||
setPreferredSize(new Dimension(200, 70));
|
||||
|
||||
// 左侧图标
|
||||
icon = new JLabel(new CircleCharIcon2(Color.LIGHT_GRAY, Color.WHITE,
|
||||
userName.substring(0, 1).toUpperCase(), 40));
|
||||
icon.setPreferredSize(new Dimension(40, 40));
|
||||
|
||||
// 中间区域 - 好友名称
|
||||
centerPanel = new JPanel(new GridLayout(1, 1));
|
||||
titleLabel = new JLabel(userName);
|
||||
titleLabel.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 14));
|
||||
centerPanel.add(titleLabel);
|
||||
|
||||
// 组装
|
||||
this.add(icon, BorderLayout.WEST);
|
||||
this.add(centerPanel, BorderLayout.CENTER);
|
||||
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
enterFriendProfile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e) {
|
||||
setBackground(UIManager.getColor("List.selectionBackground")); // 悬停效果
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
setBackground(UIManager.getColor("Panel.background")); // 恢复原背景
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
setBackground(UIManager.getColor("List.selectionInactiveBackground")); // 点击效果
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (contains(e.getPoint())) {
|
||||
setBackground(UIManager.getColor("List.selectionBackground"));
|
||||
} else {
|
||||
setBackground(UIManager.getColor("Panel.background"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入好友个人信息界面
|
||||
* 点击好友项时,切换到好友个人信息界面,显示与该好友相关的个人信息。
|
||||
*/
|
||||
public void enterFriendProfile() {
|
||||
MainPage.get().exchangeToFriendProfile(userId, userName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.service.LocalData;
|
||||
import client.view.util.CircleCharIcon2;
|
||||
import client.view.util.DesignToken;
|
||||
import server.data.UserData;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 好友个人信息视图
|
||||
* 显示好友的头像、名称和ID,以及操作按钮(发送消息、删除好友)
|
||||
* 好友头像:显示好友的圆形头像
|
||||
* 好友名称:显示好友的名称,字体为默认字体,大小为24号,加粗
|
||||
* 用户ID:显示好友的唯一标识符,字体为默认字体,大小为16号,颜色为灰色
|
||||
* 发送消息按钮:点击后可以发送消息给好友
|
||||
* 删除好友按钮:点击后可以删除好友关系
|
||||
*/
|
||||
public class FriendProfileView extends JPanel {
|
||||
private String userId;
|
||||
private String userName;
|
||||
|
||||
public FriendProfileView(String userId, String userName) {
|
||||
this.userId = userId;
|
||||
this.userName = userName;
|
||||
initUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化UI组件
|
||||
* 设置布局为网格Bag布局,背景颜色为白色
|
||||
* 好友头像:显示好友的圆形头像,大小为80x80
|
||||
* 好友名称:显示好友的名称,字体为默认字体,大小为24号,加粗
|
||||
* 用户ID:显示好友的唯一标识符,字体为默认字体,大小为16号,颜色为灰色
|
||||
* 发送消息按钮:点击后可以发送消息给好友,大小为120x40,背景颜色为蓝色,文字颜色为白色
|
||||
* 删除好友按钮:点击后可以删除好友关系,大小为120x40,背景颜色为红色,文字颜色为白色
|
||||
*/
|
||||
private void initUI() {
|
||||
setLayout(new GridBagLayout());
|
||||
setBackground(Color.WHITE);
|
||||
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
gbc.insets = new Insets(10, 10, 10, 10);
|
||||
gbc.anchor = GridBagConstraints.CENTER;
|
||||
|
||||
// 好友头像
|
||||
JLabel icon = new JLabel(new CircleCharIcon2(Color.ORANGE, Color.WHITE,
|
||||
userName.substring(0, 1).toUpperCase(), 80));
|
||||
add(icon, gbc);
|
||||
|
||||
// 好友名称
|
||||
gbc.gridy++;
|
||||
JLabel nameLabel = new JLabel(userName);
|
||||
nameLabel.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 24));
|
||||
add(nameLabel, gbc);
|
||||
|
||||
// 用户ID
|
||||
gbc.gridy++;
|
||||
JLabel idLabel = new JLabel("ID: " + userId);
|
||||
idLabel.setForeground(Color.GRAY);
|
||||
add(idLabel, gbc);
|
||||
|
||||
// 获取并显示详细信息
|
||||
UserData friendData = LocalData.get().getUserDetail(userId);
|
||||
if (friendData != null) {
|
||||
addInfoLabel(friendData.getEmail(), gbc);
|
||||
addInfoLabel(friendData.getBirthday(), gbc);
|
||||
addInfoLabel(friendData.getAddress(), gbc);
|
||||
addInfoLabel(friendData.getSignature(), gbc);
|
||||
}
|
||||
|
||||
// 发送消息按钮
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(30, 10, 10, 10);
|
||||
JButton sendBtn = new JButton("发送消息");
|
||||
sendBtn.setPreferredSize(new Dimension(120, 40));
|
||||
sendBtn.setBackground(new Color(0, 122, 255));
|
||||
sendBtn.setForeground(Color.WHITE);
|
||||
sendBtn.setFocusPainted(false);
|
||||
sendBtn.addActionListener(e -> {
|
||||
// 更新消息列表
|
||||
SecondaryOptionView.get().updateMessageList(userId, userName, "", 0);
|
||||
|
||||
// 更新当前聊天ID
|
||||
client.service.LocalData.get().setCurrentChatId(userId);
|
||||
|
||||
// 切换到聊天房间
|
||||
client.view.MainPage.get().exchangeToChatRoom(userId);
|
||||
});
|
||||
add(sendBtn, gbc);
|
||||
|
||||
// 好友操作按钮
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(10, 10, 10, 10);
|
||||
JButton deleteBtn = new JButton("删除好友");
|
||||
deleteBtn.setPreferredSize(new Dimension(120, 40));
|
||||
deleteBtn.setBackground(new Color(220, 53, 69));
|
||||
deleteBtn.setForeground(Color.WHITE);
|
||||
deleteBtn.setFocusPainted(false);
|
||||
deleteBtn.addActionListener(e -> {
|
||||
JOptionPane.showMessageDialog(this, "删除好友功能开发中...");
|
||||
});
|
||||
add(deleteBtn, gbc);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加详细信息标签
|
||||
* 检查文本是否为空,如果不为空则添加到面板中
|
||||
* 标签字体为默认字体,大小为14号,颜色为深灰色
|
||||
*
|
||||
* @param text 要添加的详细信息文本
|
||||
* @param gbc 网格BagConstraints对象,用于布局
|
||||
*/
|
||||
private void addInfoLabel(String text, GridBagConstraints gbc) {
|
||||
if (text != null && !text.isEmpty()) {
|
||||
gbc.gridy++;
|
||||
gbc.insets = new Insets(2, 10, 2, 10);
|
||||
JLabel label = new JLabel(text);
|
||||
label.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 14));
|
||||
label.setForeground(Color.DARK_GRAY);
|
||||
add(label, gbc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.MainPage;
|
||||
import client.view.util.DesignToken;
|
||||
import server.data.GroupData;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 聊天信息组件,用于展示当前聊天室的信息
|
||||
*/
|
||||
public class GroupInfoView extends JScrollPane {
|
||||
private static volatile GroupInfoView instance;
|
||||
|
||||
public static GroupInfoView get() {
|
||||
if (instance == null) {
|
||||
synchronized (GroupInfoView.class) {
|
||||
if (instance == null) {
|
||||
instance = new GroupInfoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final JPanel mainPanel;
|
||||
private final JPanel groupInfoPanel;
|
||||
private final JPanel groupMemberPanel;
|
||||
|
||||
public GroupInfoView() {
|
||||
mainPanel = new JPanel();
|
||||
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); // 垂直布局
|
||||
|
||||
setPreferredSize(new Dimension(DesignToken.INFO_PANEL_WIDTH, DesignToken.WINDOW_ORI_HEIGHT));
|
||||
setBackground(Color.GRAY);
|
||||
|
||||
// 设置滚动策略
|
||||
setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
|
||||
setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
|
||||
groupInfoPanel = createGroupInfoPanel();
|
||||
groupMemberPanel = createGroupMemberPanel();
|
||||
|
||||
mainPanel.add(groupInfoPanel);
|
||||
|
||||
mainPanel.add(groupInfoPanel);
|
||||
mainPanel.add(createInviteButton());
|
||||
mainPanel.add(createExitButton());
|
||||
mainPanel.add(groupMemberPanel);
|
||||
|
||||
mainPanel.add(groupMemberPanel);
|
||||
|
||||
mainPanel.setMinimumSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, 0));
|
||||
mainPanel.setPreferredSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, DesignToken.WINDOW_ORI_HEIGHT));
|
||||
|
||||
this.setViewportView(mainPanel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建聊天成员信息面板
|
||||
*/
|
||||
public JPanel createGroupMemberPanel() {
|
||||
JPanel memberInfoPanel = new JPanel();
|
||||
memberInfoPanel.setPreferredSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, DesignToken.WINDOW_ORI_HEIGHT - 50));
|
||||
|
||||
memberInfoPanel.setMinimumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, 0));
|
||||
|
||||
memberInfoPanel.setLayout(new BoxLayout(memberInfoPanel, BoxLayout.Y_AXIS));
|
||||
|
||||
// 添加成员标题
|
||||
JLabel memberTitle = new JLabel("群成员");
|
||||
memberTitle.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
memberTitle.setFont(new Font("微软雅黑", Font.BOLD, 16));
|
||||
memberTitle.setForeground(Color.WHITE);
|
||||
memberTitle.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0));
|
||||
memberInfoPanel.add(memberTitle);
|
||||
|
||||
// 添加分隔线
|
||||
JSeparator separator = new JSeparator();
|
||||
separator.setForeground(Color.DARK_GRAY);
|
||||
separator.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
separator.setMaximumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 1));
|
||||
|
||||
memberInfoPanel.add(separator);
|
||||
|
||||
return memberInfoPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建群信息组件
|
||||
*/
|
||||
public JPanel createGroupInfoPanel() {
|
||||
JPanel groupInfoPanel = new JPanel();
|
||||
groupInfoPanel.setPreferredSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, DesignToken.WINDOW_ORI_HEIGHT - 50));
|
||||
groupInfoPanel.setMinimumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, 0));
|
||||
groupInfoPanel.setLayout(new BoxLayout(groupInfoPanel, BoxLayout.Y_AXIS));
|
||||
|
||||
// 添加标题
|
||||
JLabel titleLabel = new JLabel("群聊信息");
|
||||
titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 18));
|
||||
titleLabel.setBorder(BorderFactory.createEmptyBorder(10, 0, 15, 0));
|
||||
groupInfoPanel.add(titleLabel);
|
||||
|
||||
return groupInfoPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个邀请成员的按钮
|
||||
*/
|
||||
public JButton createInviteButton() {
|
||||
JButton inviteButton = new JButton("邀请");
|
||||
inviteButton.setAlignmentX(Component.CENTER_ALIGNMENT); // 居中对齐
|
||||
inviteButton.setPreferredSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 30));
|
||||
inviteButton.setMaximumSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 30));
|
||||
inviteButton.setMargin(
|
||||
new Insets(5, 10, 5, 10));
|
||||
|
||||
inviteButton.addActionListener(e -> MainPage.get().showGroupInviteDialog());
|
||||
return inviteButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个退出群聊的按钮
|
||||
*/
|
||||
public JButton createExitButton() {
|
||||
JButton exitButton = new JButton("退出");
|
||||
exitButton.setAlignmentX(Component.CENTER_ALIGNMENT); // 居中对齐
|
||||
exitButton.setPreferredSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 30));
|
||||
exitButton.setMaximumSize(
|
||||
new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 30));
|
||||
exitButton.setMargin(
|
||||
new Insets(5, 10, 5, 10));
|
||||
|
||||
exitButton.addActionListener(e -> {
|
||||
ChatSender.addMsg(Wrapper.groupQuitRequest(
|
||||
LocalData.get().getId(),
|
||||
LocalData.get().getCurrentChatId()));
|
||||
});
|
||||
return exitButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 groupInfoPanel 和 groupMemberPanel 这两个组件
|
||||
*/
|
||||
public void updateInfo() {
|
||||
groupInfoPanel.removeAll();
|
||||
groupMemberPanel.removeAll();
|
||||
|
||||
String currentChatId = LocalData.get().getCurrentChatId();
|
||||
if (currentChatId == null || currentChatId.isEmpty()) {
|
||||
// 如果没有当前群聊,显示提示信息
|
||||
JLabel noGroupLabel = new JLabel("未选择群聊");
|
||||
noGroupLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
noGroupLabel.setForeground(Color.GRAY);
|
||||
groupInfoPanel.add(noGroupLabel);
|
||||
|
||||
groupInfoPanel.revalidate();
|
||||
groupInfoPanel.repaint();
|
||||
groupMemberPanel.revalidate();
|
||||
groupMemberPanel.repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
GroupData groupData = LocalData.get().getGroupData(currentChatId);
|
||||
if (groupData == null) {
|
||||
JLabel errorLabel = new JLabel("群聊数据不存在");
|
||||
errorLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
errorLabel.setForeground(Color.RED);
|
||||
groupInfoPanel.add(errorLabel);
|
||||
|
||||
groupInfoPanel.revalidate();
|
||||
groupInfoPanel.repaint();
|
||||
groupMemberPanel.revalidate();
|
||||
groupMemberPanel.repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新群聊信息面板
|
||||
updateGroupInfoPanel(groupData);
|
||||
|
||||
// 更新群成员面板
|
||||
updateGroupMemberPanel(groupData);
|
||||
|
||||
groupInfoPanel.revalidate();
|
||||
groupInfoPanel.repaint();
|
||||
groupMemberPanel.revalidate();
|
||||
groupMemberPanel.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新群聊信息面板内容
|
||||
*/
|
||||
private void updateGroupInfoPanel(GroupData groupData) {
|
||||
// 添加标题
|
||||
JLabel titleLabel = new JLabel("群聊信息");
|
||||
titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 18));
|
||||
titleLabel.setBorder(BorderFactory.createEmptyBorder(10, 0, 15, 0));
|
||||
groupInfoPanel.add(titleLabel);
|
||||
|
||||
// 群聊名称
|
||||
JLabel nameLabel = new JLabel("群名: " + groupData.getGroupName());
|
||||
nameLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
|
||||
nameLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
|
||||
groupInfoPanel.add(nameLabel);
|
||||
|
||||
// 群聊ID
|
||||
JLabel idLabel = new JLabel("群ID: " + groupData.getGroupId());
|
||||
idLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
idLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
|
||||
idLabel.setForeground(Color.DARK_GRAY);
|
||||
idLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 5, 0));
|
||||
groupInfoPanel.add(idLabel);
|
||||
|
||||
// 成员数量
|
||||
JLabel memberCountLabel = new JLabel("成员: " + (groupData.getMembers() != null ? groupData.getMembers().size() : 0) + "人");
|
||||
memberCountLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
memberCountLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12));
|
||||
memberCountLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
|
||||
groupInfoPanel.add(memberCountLabel);
|
||||
|
||||
// 添加分隔线
|
||||
JSeparator separator = new JSeparator();
|
||||
separator.setForeground(Color.GRAY);
|
||||
separator.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
separator.setMaximumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 30, 1));
|
||||
groupInfoPanel.add(separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新群成员面板内容
|
||||
*/
|
||||
private void updateGroupMemberPanel(GroupData groupData) {
|
||||
// 添加成员标题
|
||||
JLabel memberTitle = new JLabel("群成员 (" + (groupData.getMembers() != null ? groupData.getMembers().size() : 0) + ")");
|
||||
memberTitle.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
memberTitle.setFont(new Font("微软雅黑", Font.BOLD, 16));
|
||||
memberTitle.setForeground(Color.WHITE);
|
||||
memberTitle.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0));
|
||||
groupMemberPanel.add(memberTitle);
|
||||
|
||||
// 添加分隔线
|
||||
JSeparator separator = new JSeparator();
|
||||
separator.setForeground(Color.DARK_GRAY);
|
||||
separator.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
separator.setMaximumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH - 20, 1));
|
||||
groupMemberPanel.add(separator);
|
||||
|
||||
if (groupData.getMembers() == null || groupData.getMembers().isEmpty()) {
|
||||
JLabel noMemberLabel = new JLabel("暂无成员");
|
||||
noMemberLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
noMemberLabel.setForeground(Color.LIGHT_GRAY);
|
||||
noMemberLabel.setBorder(BorderFactory.createEmptyBorder(20, 0, 0, 0));
|
||||
groupMemberPanel.add(noMemberLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加成员列表
|
||||
for (GroupData.GroupMember memberId : groupData.getMembers()) {
|
||||
JPanel memberItemPanel =
|
||||
createMemberItemPanel(memberId.id);
|
||||
groupMemberPanel.add(memberItemPanel);
|
||||
}
|
||||
|
||||
// 添加底部空白,确保内容居中
|
||||
groupMemberPanel.add(Box.createVerticalGlue());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单个成员信息面板
|
||||
*/
|
||||
private JPanel createMemberItemPanel(String memberId) {
|
||||
JPanel memberItemPanel = new JPanel();
|
||||
memberItemPanel.setLayout(new BoxLayout(memberItemPanel, BoxLayout.X_AXIS));
|
||||
memberItemPanel.setBackground(new Color(40, 40, 40));
|
||||
memberItemPanel.setBorder(BorderFactory.createEmptyBorder(8, 15, 8, 15));
|
||||
memberItemPanel.setMaximumSize(new Dimension(DesignToken.GROUP_INFO_PANEL_WIDTH, 50));
|
||||
memberItemPanel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
|
||||
// 成员头像(使用圆形标签模拟)
|
||||
JLabel avatarLabel = new JLabel();
|
||||
avatarLabel.setOpaque(true);
|
||||
avatarLabel.setBackground(getMemberColor(memberId));
|
||||
avatarLabel.setPreferredSize(new Dimension(30, 30));
|
||||
avatarLabel.setMinimumSize(new Dimension(30, 30));
|
||||
avatarLabel.setMaximumSize(new Dimension(30, 30));
|
||||
avatarLabel.setBorder(BorderFactory.createLineBorder(Color.WHITE, 1));
|
||||
|
||||
// 设置圆形头像
|
||||
avatarLabel.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(Color.WHITE, 1),
|
||||
BorderFactory.createEmptyBorder(2, 2, 2, 2)
|
||||
));
|
||||
|
||||
// 成员信息
|
||||
JPanel infoPanel = new JPanel();
|
||||
infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.Y_AXIS));
|
||||
infoPanel.setBackground(new Color(40, 40, 40));
|
||||
infoPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 0));
|
||||
|
||||
// 成员名称
|
||||
String memberName = LocalData.get().getUserName(memberId);
|
||||
if (memberName == null) {
|
||||
memberName = "用户" + memberId.substring(0, Math.min(6, memberId.length()));
|
||||
}
|
||||
|
||||
JLabel nameLabel = new JLabel(memberName);
|
||||
nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
|
||||
nameLabel.setForeground(Color.WHITE);
|
||||
nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
// 成员ID
|
||||
JLabel idLabel = new JLabel("ID: " + memberId.substring(0, Math.min(10, memberId.length())));
|
||||
idLabel.setFont(new Font("微软雅黑", Font.PLAIN, 10));
|
||||
idLabel.setForeground(Color.LIGHT_GRAY);
|
||||
idLabel.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
infoPanel.add(nameLabel);
|
||||
infoPanel.add(idLabel);
|
||||
|
||||
memberItemPanel.add(avatarLabel);
|
||||
memberItemPanel.add(infoPanel);
|
||||
memberItemPanel.add(Box.createHorizontalGlue());
|
||||
|
||||
// 如果是当前用户,添加标识
|
||||
if (memberId.equals(LocalData.get().getId())) {
|
||||
JLabel meLabel = new JLabel("(我)");
|
||||
meLabel.setFont(new Font("微软雅黑", Font.ITALIC, 11));
|
||||
meLabel.setForeground(new Color(100, 150, 255));
|
||||
memberItemPanel.add(meLabel);
|
||||
}
|
||||
|
||||
return memberItemPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID生成固定颜色(用于头像背景)
|
||||
*/
|
||||
private Color getMemberColor(String memberId) {
|
||||
// 简单的哈希算法生成固定颜色
|
||||
int hash = memberId.hashCode();
|
||||
int r = (hash & 0xFF0000) >> 16;
|
||||
int g = (hash & 0x00FF00) >> 8;
|
||||
int b = hash & 0x0000FF;
|
||||
|
||||
// 确保颜色不太暗
|
||||
r = Math.max(r, 50);
|
||||
g = Math.max(g, 50);
|
||||
b = Math.max(b, 50);
|
||||
|
||||
return new Color(r, g, b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.service.LocalData;
|
||||
import client.view.MainPage;
|
||||
import client.view.util.CircleCharIcon2;
|
||||
import client.view.util.DesignToken;
|
||||
import client.view.util.RoundedRectCharIcon;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
// 群聊列表项组件
|
||||
public class GroupListItem extends JPanel implements Comparable<String> {
|
||||
private int unread;
|
||||
private final String groupId;
|
||||
|
||||
JLabel icon;
|
||||
JLabel badge;
|
||||
JPanel centerPanel;
|
||||
JLabel titleLabel;
|
||||
JPanel rightPanel; // 用于放置徽章
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param groupId 群聊ID
|
||||
* @param title 群聊标题
|
||||
* @param unread 未读消息数量
|
||||
*/
|
||||
public GroupListItem(String groupId, String title, int unread) {
|
||||
this.groupId = groupId;
|
||||
this.unread = unread;
|
||||
|
||||
// 设置布局管理器
|
||||
setLayout(new BorderLayout(10, 0));
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 10, 8, 10));
|
||||
setMaximumSize(new Dimension(Integer.MAX_VALUE, 70));
|
||||
setPreferredSize(new Dimension(200, 70));
|
||||
|
||||
// 左侧图标
|
||||
boolean isGroup = LocalData.get().getGroupData(groupId) != null;
|
||||
if (isGroup) {
|
||||
icon = new JLabel(new RoundedRectCharIcon(Color.decode(DesignToken.BUBBLE_COLOR_BLUE), Color.WHITE,
|
||||
title.substring(0, 1).toUpperCase(), 40));
|
||||
} else {
|
||||
icon = new JLabel(new CircleCharIcon2(Color.ORANGE, Color.WHITE,
|
||||
title.substring(0, 1).toUpperCase(), 40));
|
||||
}
|
||||
icon.setPreferredSize(new Dimension(40, 40));
|
||||
|
||||
// 中间区域 - 群聊标题
|
||||
centerPanel = new JPanel(new GridLayout(1, 1));
|
||||
titleLabel = new JLabel(title);
|
||||
titleLabel.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 14));
|
||||
centerPanel.add(titleLabel);
|
||||
|
||||
// 右侧区域 - 未读消息徽章
|
||||
rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0));
|
||||
rightPanel.setOpaque(false);
|
||||
|
||||
badge = new JLabel();
|
||||
badge.setForeground(Color.WHITE);
|
||||
badge.setBackground(Color.RED);
|
||||
badge.setOpaque(true);
|
||||
badge.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
badge.setFont(new Font("Microsoft YaHei", Font.BOLD, 10));
|
||||
badge.setBorder(BorderFactory.createEmptyBorder(2, 6, 2, 6));
|
||||
badge.setPreferredSize(new Dimension(20, 20));
|
||||
|
||||
updateBadge(); // 初始化徽章显示状态
|
||||
rightPanel.add(badge);
|
||||
|
||||
// 组装
|
||||
this.add(icon, BorderLayout.WEST);
|
||||
this.add(centerPanel, BorderLayout.CENTER);
|
||||
this.add(rightPanel, BorderLayout.EAST);
|
||||
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
enterGroup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e) {
|
||||
setBackground(new Color(240, 240, 240)); // 悬停效果
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
setBackground(UIManager.getColor("Panel.background")); // 恢复原背景
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
setBackground(new Color(220, 220, 220)); // 点击效果
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
if (contains(e.getPoint())) {
|
||||
setBackground(new Color(240, 240, 240));
|
||||
} else {
|
||||
setBackground(UIManager.getColor("Panel.background"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新徽章显示
|
||||
*/
|
||||
private void updateBadge() {
|
||||
if (unread > 0) {
|
||||
String badgeText = unread > 99 ? "99+" : String.valueOf(unread);
|
||||
badge.setText(badgeText);
|
||||
badge.setVisible(true);
|
||||
|
||||
// 根据文本长度调整徽章大小
|
||||
FontMetrics fm = badge.getFontMetrics(badge.getFont());
|
||||
int width = fm.stringWidth(badgeText) + 12;
|
||||
badge.setPreferredSize(new Dimension(width, 20));
|
||||
} else {
|
||||
badge.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新组件
|
||||
* 将组件上对应的信息修改。
|
||||
* newUnread 是加在原有的unread上的
|
||||
*/
|
||||
public void updateUI(String name, int newUnread) {
|
||||
titleLabel.setText(name);
|
||||
|
||||
// 更新图标的首字母显示
|
||||
boolean isGroup = LocalData.get().getGroupData(groupId) != null;
|
||||
if (isGroup) {
|
||||
icon.setIcon(new RoundedRectCharIcon(Color.decode(DesignToken.BUBBLE_COLOR_BLUE), Color.WHITE,
|
||||
name.substring(0, 1).toUpperCase(), 40));
|
||||
} else {
|
||||
icon.setIcon(new CircleCharIcon2(Color.ORANGE, Color.WHITE,
|
||||
name.substring(0, 1).toUpperCase(), 40));
|
||||
}
|
||||
|
||||
unread += newUnread;
|
||||
updateBadge();
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击事件
|
||||
* 当点击之后,使用UIUpdater来更新ContentView的UI,
|
||||
* 使得其加载新的群聊信息
|
||||
* 清空unread为0
|
||||
* UIUpdater在ContentView展示当前群聊信息的时候不会更新这个群聊的未读信息数量。
|
||||
*/
|
||||
public void enterGroup() {
|
||||
unread = 0;
|
||||
LocalData.get().setCurrentChatId(groupId);
|
||||
|
||||
updateBadge(); // 更新徽章显示
|
||||
|
||||
String name;
|
||||
if (LocalData.get().getFriends().containsKey(groupId)) {
|
||||
name = LocalData.get().getFriends().get(groupId);
|
||||
} else {
|
||||
name = LocalData.get().getGroupName(groupId);
|
||||
}
|
||||
|
||||
updateUI(name, 0);
|
||||
|
||||
MainPage.get().exchangeToChatRoom(groupId);
|
||||
|
||||
if (LocalData.get().getGroupData(groupId) != null) {
|
||||
System.out.println(
|
||||
"进入群聊:" + groupId +
|
||||
",人数:" + LocalData.get().getGroupData(groupId).getMemberCount() +
|
||||
", 信息数量: " + LocalData.get().getChatMsg(groupId).size());
|
||||
} else {
|
||||
System.out.println("进入私聊:" + groupId);
|
||||
}
|
||||
|
||||
MainPage.get().revalidate(); // 重新计算布局
|
||||
MainPage.get().repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未读消息数量
|
||||
*/
|
||||
public int getUnread() {
|
||||
return unread;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群组ID
|
||||
*/
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现Comparable接口,用于排序
|
||||
* 按照群组ID进行排序
|
||||
*
|
||||
* @param o 要比较的对象
|
||||
* @return 比较结果
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(String o) {
|
||||
return this.groupId.compareTo(o);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.view.MainPage;
|
||||
import client.view.util.LimitSizePanel;
|
||||
|
||||
import client.service.LocalData;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static client.view.util.DesignToken.SECONDARY_PANEL_WIDTH_MIN;
|
||||
|
||||
/**
|
||||
* 二级菜单栏组件
|
||||
* 用于显示聊天、好友、设置等二级选项。
|
||||
* 点击不同选项可以切换到对应的功能界面。
|
||||
*/
|
||||
public class SecondaryOptionView extends LimitSizePanel {
|
||||
private static volatile SecondaryOptionView instance;
|
||||
|
||||
public static SecondaryOptionView get() {
|
||||
if (instance == null) {
|
||||
synchronized (SecondaryOptionView.class) {
|
||||
if (instance == null) {
|
||||
instance = new SecondaryOptionView();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 二级选项模式
|
||||
private enum Mode {
|
||||
MESSAGE, FRIEND, GROUP, SETTING
|
||||
}
|
||||
|
||||
// 当前二级选项模式
|
||||
private Mode currentMode = Mode.MESSAGE;
|
||||
// 这是一个群聊ID,组件的映射
|
||||
private Map<String, GroupListItem> listItems;
|
||||
// 关于好友ID,组件的映射
|
||||
private JPanel chatContainer;
|
||||
// 好友列表滚动面板
|
||||
private JScrollPane scrollPane;
|
||||
// 创建群聊按钮
|
||||
private JButton createGroupButton;
|
||||
// 二级选项标题标签
|
||||
private JLabel titleLabel;
|
||||
|
||||
private SecondaryOptionView() {
|
||||
super(SECONDARY_PANEL_WIDTH_MIN);
|
||||
|
||||
init();
|
||||
|
||||
listItems = new LinkedHashMap<>();
|
||||
|
||||
// 添加右侧边框分割线
|
||||
this.setBorder(BorderFactory.createMatteBorder(0, 0, 0, 1, UIManager.getColor("Component.borderColor")));
|
||||
|
||||
setLayout(new BorderLayout());
|
||||
|
||||
// 创建顶部面板
|
||||
JPanel topPanel = new JPanel(new BorderLayout());
|
||||
topPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); // 增加内边距
|
||||
|
||||
titleLabel = new JLabel("消息"); // 或者 "群聊"/"好友",根据当前视图动态变化更好,这里先用通用标题
|
||||
titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
|
||||
|
||||
// 调整“+”按钮样式,使其更像一个功能图标
|
||||
createGroupButton.setMargin(new Insets(2, 6, 2, 6));
|
||||
createGroupButton.setFocusPainted(false);
|
||||
|
||||
topPanel.add(titleLabel, BorderLayout.WEST);
|
||||
topPanel.add(createGroupButton, BorderLayout.EAST);
|
||||
|
||||
// 创建群聊容器
|
||||
chatContainer = new JPanel();
|
||||
chatContainer.setLayout(new BoxLayout(chatContainer, BoxLayout.Y_AXIS));
|
||||
|
||||
// 创建滚动面板
|
||||
scrollPane = new JScrollPane(chatContainer);
|
||||
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
scrollPane.setBorder(BorderFactory.createEmptyBorder()); // 移除默认边框
|
||||
|
||||
// 自定义滚动条UI
|
||||
JScrollBar verticalScrollBar = scrollPane.getVerticalScrollBar();
|
||||
verticalScrollBar.setUnitIncrement(16);
|
||||
verticalScrollBar.setPreferredSize(new Dimension(10, 0));
|
||||
|
||||
// 组装界面
|
||||
JPanel headerContainer = new JPanel(new BorderLayout());
|
||||
headerContainer.add(topPanel, BorderLayout.CENTER);
|
||||
// 添加底部分割线,同时为了更好的层次感,也可以考虑添加顶部分割线(如果需要与标题栏分隔)
|
||||
// 这里我们给上下都添加分割线,确保 headerContainer 与上面的 Window Title 和下面的 List 都有分隔线
|
||||
headerContainer
|
||||
.setBorder(BorderFactory.createMatteBorder(1, 0, 1, 0, UIManager.getColor("Component.borderColor")));
|
||||
|
||||
this.add(headerContainer, BorderLayout.NORTH);
|
||||
this.add(scrollPane, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个用于创建群聊的按钮组件
|
||||
* 按下这个组件后弹出一个创建群聊的对话框,填写完成后进行创建群聊的操作(调用DataManager的createGroupChat方法)
|
||||
* 如果失败,则放弃创建群聊。
|
||||
*
|
||||
* @return 群聊创建按钮组件
|
||||
*/
|
||||
private JButton createGroupCreateBtn() {
|
||||
JButton groupCreateBtn = new JButton("+");
|
||||
return groupCreateBtn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化所有组件
|
||||
* 读取DataManager的信息,而后群聊信息创建为GroupListItem,加入到类中对应的列表中
|
||||
* 并将其添加到chatContainer中
|
||||
* 同样,好友信息创建为FriendListItem,加入到类中对应的列表中
|
||||
* 并添加到friendContainer中
|
||||
* 最后使用exchangeToGroupChat将群聊列表加入到聊天项容器中
|
||||
* 使用createGroupCreateBtn创建群聊创建按钮,并添加到chatContainer中
|
||||
*/
|
||||
public void init() {
|
||||
createGroupButton = createGroupCreateBtn();
|
||||
createGroupButton.addActionListener(e -> {
|
||||
showAddMenu(createGroupButton);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示添加菜单
|
||||
* 根据当前模式(群聊/好友/设置),显示不同的添加选项
|
||||
* 例如,在群聊模式下,显示创建群聊和加入群聊选项
|
||||
* 在好友模式下,显示添加好友选项
|
||||
* 在设置模式下,显示不同的设置选项
|
||||
*/
|
||||
private void showAddMenu(Component invoker) {
|
||||
JPopupMenu popupMenu = new JPopupMenu();
|
||||
|
||||
if (currentMode == Mode.GROUP) {
|
||||
JMenuItem createGroupItem = new JMenuItem("创建群聊");
|
||||
createGroupItem.addActionListener(e -> MainPage.get().showGroupCreateDialog());
|
||||
popupMenu.add(createGroupItem);
|
||||
|
||||
JMenuItem joinGroupItem = new JMenuItem("加入群聊");
|
||||
joinGroupItem.addActionListener(e -> {
|
||||
MainPage.get().showJoinGroupDialog();
|
||||
});
|
||||
popupMenu.add(joinGroupItem);
|
||||
|
||||
} else if (currentMode == Mode.FRIEND) {
|
||||
JMenuItem addFriendItem = new JMenuItem("添加好友");
|
||||
addFriendItem.addActionListener(e -> {
|
||||
MainPage.get().showAddFriendDialog();
|
||||
});
|
||||
popupMenu.add(addFriendItem);
|
||||
}
|
||||
|
||||
popupMenu.show(invoker, 0, invoker.getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息列表的UI
|
||||
* 根据传入的groupId,更新对应groupListItems的群聊列表项的UI
|
||||
* 这里需要先删除createGroupButton按钮,而后进行更新操作后再添加回来。
|
||||
* 如果没有对应的id,则创建新的GroupListItem,并添加到groupListItems中
|
||||
*/
|
||||
public void updateGroupList(String groupId, String title, int unreadCount) {
|
||||
if (listItems.containsKey(groupId)) {
|
||||
listItems.get(groupId).updateUI(title, unreadCount);
|
||||
} else {
|
||||
GroupListItem item = new GroupListItem(groupId, title, 0);
|
||||
listItems.put(groupId, item);
|
||||
}
|
||||
|
||||
// 只有在 MESSAGE 模式下才更新 UI 容器
|
||||
if (currentMode == Mode.MESSAGE) {
|
||||
// 简单处理:重新加载所有 item 保证顺序,或者只添加新的
|
||||
// 为了简单,如果它不在容器里,加进去
|
||||
GroupListItem item = listItems.get(groupId);
|
||||
boolean alreadyIn = false;
|
||||
for (Component c : chatContainer.getComponents()) {
|
||||
if (c == item) {
|
||||
alreadyIn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!alreadyIn) {
|
||||
chatContainer.add(item);
|
||||
}
|
||||
chatContainer.revalidate();
|
||||
chatContainer.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息列表(兼容群聊和私聊)
|
||||
*/
|
||||
public void updateMessageList(String id, String name, String content, int unread) {
|
||||
updateGroupList(id, name, unread);
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果当前处于群聊模式,刷新群聊列表
|
||||
* 用于处理新加入群聊时的列表更新
|
||||
*/
|
||||
public void refreshIfInGroupMode() {
|
||||
if (currentMode == Mode.GROUP) {
|
||||
exchangeToGroupList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定的群聊列表
|
||||
* 如果当前模式是CHAT,且groupId存在于listItems中,
|
||||
* 则从chatContainer中移除对应的GroupListItem组件,
|
||||
* 并从listItems中删除该条目。
|
||||
* 最后调用chatContainer的revalidate和repaint方法更新UI。
|
||||
*/
|
||||
public void removeGroupListItem(String groupId) {
|
||||
if (listItems.containsKey(groupId)) {
|
||||
GroupListItem item = listItems.get(groupId);
|
||||
listItems.remove(groupId);
|
||||
if (currentMode == Mode.MESSAGE || currentMode == Mode.GROUP) {
|
||||
chatContainer.remove(item);
|
||||
chatContainer.revalidate();
|
||||
chatContainer.repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到设置列表模式
|
||||
* 将当前模式设置为SETTING,更新标题为"设置",隐藏创建群聊按钮,清空聊天项容器。
|
||||
* 然后添加个人信息和关于软件的设置项按钮到聊天项容器中。
|
||||
* 最后调用revalidate和repaint方法更新UI。
|
||||
*/
|
||||
public void exchangeToSettingList() {
|
||||
currentMode = Mode.SETTING;
|
||||
titleLabel.setText("设置");
|
||||
createGroupButton.setVisible(false);
|
||||
chatContainer.removeAll();
|
||||
|
||||
// 添加设置项
|
||||
chatContainer.add(createSettingItem("个人信息", "info"));
|
||||
chatContainer.add(createSettingItem("关于软件", "about"));
|
||||
|
||||
chatContainer.revalidate();
|
||||
chatContainer.repaint();
|
||||
|
||||
MainPage.get().exchangeToBlankContent(); // 右侧清空或显示默认页
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个设置项按钮
|
||||
* 按钮的文本为text,类型为type。
|
||||
* 按钮的对齐方式为居中对齐,最大宽度为Integer.MAX_VALUE,高度为50。
|
||||
* 点击按钮时,调用MainPage的exchangeToSettings方法,传入type参数。
|
||||
*
|
||||
* @param text 按钮的文本
|
||||
* @param type 按钮的类型
|
||||
* @return 一个设置项按钮组件
|
||||
*/
|
||||
private JButton createSettingItem(String text, String type) {
|
||||
JButton btn = new JButton(text);
|
||||
btn.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||
btn.setMaximumSize(new Dimension(Integer.MAX_VALUE, 50));
|
||||
btn.setFocusPainted(false);
|
||||
btn.setBackground(Color.WHITE);
|
||||
btn.addActionListener(e -> MainPage.get().exchangeToSettings(type));
|
||||
return btn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到消息列表模式
|
||||
* 将当前模式设置为MESSAGE,更新标题为"消息",隐藏创建群聊按钮,清空聊天项容器。
|
||||
* 然后遍历listItems中的所有GroupListItem组件,添加到聊天项容器中。
|
||||
* 最后调用chatContainer的revalidate和repaint方法更新UI。
|
||||
*/
|
||||
public void exchangeToMessageList() {
|
||||
currentMode = Mode.MESSAGE;
|
||||
titleLabel.setText("消息");
|
||||
createGroupButton.setVisible(false);
|
||||
chatContainer.removeAll();
|
||||
|
||||
// 恢复消息列表
|
||||
for (GroupListItem item : listItems.values()) {
|
||||
chatContainer.add(item);
|
||||
}
|
||||
|
||||
chatContainer.revalidate();
|
||||
chatContainer.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到群聊列表模式
|
||||
* 将当前模式设置为GROUP,更新标题为"群聊",显示创建群聊按钮,清空聊天项容器。
|
||||
* 然后遍历LocalData中的所有群聊,创建GroupListItem组件并添加到聊天项容器中。
|
||||
* 最后调用chatContainer的revalidate和repaint方法更新UI。
|
||||
*/
|
||||
public void exchangeToGroupList() {
|
||||
currentMode = Mode.GROUP;
|
||||
titleLabel.setText("群聊");
|
||||
createGroupButton.setVisible(true);
|
||||
chatContainer.removeAll();
|
||||
|
||||
java.util.List<server.data.GroupData> groups = LocalData.get().getAllGroups();
|
||||
if (groups != null) {
|
||||
for (server.data.GroupData group : groups) {
|
||||
// 这里我们复用GroupListItem,未读数设为0
|
||||
GroupListItem item = new GroupListItem(group.getGroupId(), group.getGroupName(), 0);
|
||||
chatContainer.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
chatContainer.revalidate();
|
||||
chatContainer.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到好友列表模式
|
||||
* 将当前模式设置为FRIEND,更新标题为"好友",隐藏创建群聊按钮,清空聊天项容器。
|
||||
* 然后遍历LocalData中的好友列表,创建FriendListItem组件并添加到聊天项容器中。
|
||||
* 最后调用chatContainer的revalidate和repaint方法更新UI。
|
||||
*/
|
||||
public void exchangeToFriendList() {
|
||||
currentMode = Mode.FRIEND;
|
||||
titleLabel.setText("好友");
|
||||
createGroupButton.setVisible(true); // 显示加号按钮
|
||||
chatContainer.removeAll();
|
||||
|
||||
Map<String, String> friends = LocalData.get().getFriends();
|
||||
if (friends != null) {
|
||||
for (Map.Entry<String, String> entry : friends.entrySet()) {
|
||||
FriendListItem item = new FriendListItem(entry.getKey(), entry.getValue());
|
||||
chatContainer.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
chatContainer.revalidate();
|
||||
chatContainer.repaint();
|
||||
|
||||
MainPage.get().exchangeToBlankContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package client.view.main;
|
||||
|
||||
import client.service.ChatSender;
|
||||
import client.service.LocalData;
|
||||
import client.view.MainPage;
|
||||
import client.view.util.DesignToken;
|
||||
import server.data.UserData;
|
||||
import server.serveice.Wrapper;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 设置界面组件
|
||||
* 用于显示用户个人信息和关于 LocalChatApp 的设置选项。
|
||||
* 包括用户ID、用户名、退出登录等功能。
|
||||
*/
|
||||
public class SettingsView extends JPanel {
|
||||
|
||||
public SettingsView(String type) {
|
||||
initUI(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化设置界面组件
|
||||
* 用于根据界面类型显示不同的设置内容。
|
||||
* 如果是 "info" 类型,显示用户个人信息,包括用户ID和用户名。
|
||||
* 如果是 "about" 类型,显示关于 LocalChatApp 的信息,包括版本号等。
|
||||
* @param type 界面类型,"info" 显示个人信息,"about" 显示关于 LocalChatApp 的信息
|
||||
*/
|
||||
private void initUI(String type) {
|
||||
setLayout(new BorderLayout());
|
||||
setBackground(Color.WHITE);
|
||||
|
||||
JPanel contentPanel = new JPanel();
|
||||
contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
|
||||
contentPanel.setBorder(BorderFactory.createEmptyBorder(40, 40, 40, 40));
|
||||
contentPanel.setBackground(Color.WHITE);
|
||||
|
||||
if ("info".equals(type)) {
|
||||
addInfoContent(contentPanel);
|
||||
} else if ("about".equals(type)) {
|
||||
addAboutContent(contentPanel);
|
||||
}
|
||||
|
||||
add(contentPanel, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加个人信息内容到设置界面
|
||||
* 包括用户ID、用户名、邮箱、生日、地址、签名等。
|
||||
* @param panel 用于添加组件的面板
|
||||
*/
|
||||
private void addInfoContent(JPanel panel) {
|
||||
JLabel title = new JLabel("个人信息");
|
||||
title.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 24));
|
||||
title.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
panel.add(title);
|
||||
panel.add(Box.createVerticalStrut(20));
|
||||
|
||||
String myId = LocalData.get().getId();
|
||||
UserData myData = LocalData.get().getUserDetail(myId);
|
||||
if (myData == null) {
|
||||
// 如果数据尚未同步,使用本地基本信息创建一个临时对象
|
||||
myData = new UserData(LocalData.get().getUserName(myId), myId, null);
|
||||
}
|
||||
|
||||
addLabel(panel, "用户ID:", myId);
|
||||
addLabel(panel, "用户名:", myData.getNickname());
|
||||
|
||||
// 可编辑字段
|
||||
JTextField emailField = addEditableField(panel, "邮箱:", myData.getEmail());
|
||||
JTextField birthdayField = addEditableField(panel, "生日:", myData.getBirthday());
|
||||
JTextField addressField = addEditableField(panel, "地址:", myData.getAddress());
|
||||
JTextField signatureField = addEditableField(panel, "个性签名:", myData.getSignature());
|
||||
|
||||
panel.add(Box.createVerticalStrut(30));
|
||||
|
||||
// 保存修改按钮
|
||||
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
btnPanel.setBackground(Color.WHITE);
|
||||
btnPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
// 保存修改按钮
|
||||
JButton saveBtn = new JButton("保存修改");
|
||||
final UserData currentData = myData;
|
||||
saveBtn.addActionListener(e -> {
|
||||
// 更新本地对象字段
|
||||
currentData.setEmail(emailField.getText().trim());
|
||||
currentData.setBirthday(birthdayField.getText().trim());
|
||||
currentData.setAddress(addressField.getText().trim());
|
||||
currentData.setSignature(signatureField.getText().trim());
|
||||
|
||||
// 发送更新请求
|
||||
ChatSender.addMsg(Wrapper.updateUserDetailRequest(myId, currentData));
|
||||
|
||||
// 更新本地缓存(虽然服务器会广播回来,但本地先更新体验更好)
|
||||
LocalData.get().updateUserDetails(currentData);
|
||||
|
||||
JOptionPane.showMessageDialog(this, "个人信息已保存", "提示", JOptionPane.INFORMATION_MESSAGE);
|
||||
});
|
||||
|
||||
// 退出登录按钮
|
||||
JButton logoutBtn = new JButton("退出登录");
|
||||
logoutBtn.addActionListener(e -> {
|
||||
int confirm = JOptionPane.showConfirmDialog(this, "确定要退出登录吗?", "提示", JOptionPane.YES_NO_OPTION);
|
||||
if (confirm == JOptionPane.YES_OPTION) {
|
||||
MainPage.get().openLogInPage();
|
||||
}
|
||||
});
|
||||
|
||||
// 添加按钮到面板
|
||||
btnPanel.add(saveBtn);
|
||||
btnPanel.add(logoutBtn);
|
||||
panel.add(btnPanel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加可编辑文本字段到设置界面
|
||||
* 用于用户输入个人信息的编辑。
|
||||
* @param panel 用于添加组件的面板
|
||||
* @param labelText 标签文本,描述字段的作用
|
||||
* @param value 初始文本字段值
|
||||
* @return 新创建的 JTextField 对象
|
||||
*/
|
||||
private JTextField addEditableField(JPanel panel, String labelText, String value) {
|
||||
JPanel fieldPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
fieldPanel.setBackground(Color.WHITE);
|
||||
fieldPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
JLabel label = new JLabel(labelText);
|
||||
label.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 16));
|
||||
label.setPreferredSize(new Dimension(80, 30));
|
||||
|
||||
JTextField textField = new JTextField(value, 20);
|
||||
textField.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 14));
|
||||
|
||||
fieldPanel.add(label);
|
||||
fieldPanel.add(textField);
|
||||
panel.add(fieldPanel);
|
||||
return textField;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加关于 LocalChatApp 的内容到设置界面
|
||||
* 包括版本号、开发团队、应用描述等。
|
||||
* @param panel 用于添加组件的面板
|
||||
*/
|
||||
private void addAboutContent(JPanel panel) {
|
||||
JLabel title = new JLabel("关于 LocalChatApp");
|
||||
title.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 24));
|
||||
title.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
panel.add(title);
|
||||
panel.add(Box.createVerticalStrut(30));
|
||||
|
||||
JLabel version = new JLabel("版本: v1.0.0");
|
||||
version.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 16));
|
||||
panel.add(version);
|
||||
|
||||
panel.add(Box.createVerticalStrut(10));
|
||||
JLabel author = new JLabel("开发团队: 添砖加瓦小组");
|
||||
author.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 16));
|
||||
panel.add(author);
|
||||
|
||||
panel.add(Box.createVerticalStrut(10));
|
||||
JLabel desc = new JLabel("<html><body><p style='width:300px'>基于Java Swing和Socket开发的本地局域网聊天室。</p></body></html>");
|
||||
desc.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 14));
|
||||
panel.add(desc);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加标签到设置界面
|
||||
* 用于显示键值对信息,例如用户ID和用户名。
|
||||
* @param panel 用于添加组件的面板
|
||||
* @param key 键,例如 "用户ID:"
|
||||
* @param value 值,例如 "10086"
|
||||
*/
|
||||
private void addLabel(JPanel panel, String key, String value) {
|
||||
// 创建一个行面板,用于添加键值对标签
|
||||
JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
row.setBackground(Color.WHITE);
|
||||
row.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
|
||||
// 键标签
|
||||
JLabel k = new JLabel(key);
|
||||
k.setFont(new Font(DesignToken.DEFAULT_FONT, Font.BOLD, 14));
|
||||
k.setPreferredSize(new Dimension(80, 30));
|
||||
|
||||
// 值标签
|
||||
JLabel v = new JLabel(value);
|
||||
v.setFont(new Font(DesignToken.DEFAULT_FONT, Font.PLAIN, 14));
|
||||
|
||||
// 添加键值对标签到行面板
|
||||
row.add(k);
|
||||
row.add(v);
|
||||
panel.add(row);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package client.view.main;
|
||||
|
||||
import com.formdev.flatlaf.FlatDarkLaf;
|
||||
import com.formdev.flatlaf.FlatLaf;
|
||||
import com.formdev.flatlaf.FlatLightLaf;
|
||||
import client.view.util.CircleCharIcon2;
|
||||
import client.view.util.DesignToken;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
/**
|
||||
* 左侧侧边栏组件
|
||||
* 包含消息、好友、群聊、设置、黑暗模式切换按钮
|
||||
*/
|
||||
public class SideOptionView extends JPanel {
|
||||
private static volatile SideOptionView instance;
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*
|
||||
* @return 单例实例
|
||||
*/
|
||||
public static SideOptionView get() {
|
||||
if (instance == null) {
|
||||
synchronized (SideOptionView.class) {
|
||||
if (instance == null) {
|
||||
instance = new SideOptionView();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照原型图,生成三个按钮组件:群聊,好友,设置组件
|
||||
* 每个组件都设置一个图标
|
||||
* 为每一个按钮配置一个事件
|
||||
* 群聊:exchangeToChatPage()/UIUpdate
|
||||
* 设置: exchangeToSettingPage()/UIUpdate。这个当前还没做,提示用户正在制作中。
|
||||
* 现在暂时没有设置相关功能
|
||||
*/
|
||||
public SideOptionView() {
|
||||
this.setLayout(new GridLayout(5, 1));
|
||||
|
||||
// this.setBackground(Color.GRAY);
|
||||
// 添加右侧边框分割线
|
||||
this.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createMatteBorder(0, 0, 0, 1, UIManager.getColor("Component.borderColor")),
|
||||
BorderFactory.createEmptyBorder(5, 5, 5, 5)));
|
||||
|
||||
// 初始化五个核心按钮:消息、好友、群聊、设置、黑暗模式切换按钮
|
||||
initMessageButton();
|
||||
initFriendButton();
|
||||
initGroupButton();
|
||||
initSettingButton();
|
||||
initDarkModeButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化消息按钮
|
||||
* 点击后切换到消息页面
|
||||
*/
|
||||
private void initMessageButton() {
|
||||
JButton messageBtn = createIconButton("消", Color.LIGHT_GRAY);
|
||||
messageBtn.setToolTipText("消息");
|
||||
messageBtn.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
SecondaryOptionView.get().exchangeToMessageList();
|
||||
}
|
||||
});
|
||||
this.add(messageBtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化好友按钮
|
||||
* 点击后切换到好友页面
|
||||
*/
|
||||
private void initFriendButton() {
|
||||
JButton friendBtn = createIconButton("友", Color.LIGHT_GRAY);
|
||||
friendBtn.setToolTipText("好友");
|
||||
friendBtn.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
SecondaryOptionView.get().exchangeToFriendList();
|
||||
}
|
||||
});
|
||||
this.add(friendBtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化群聊按钮
|
||||
* 点击后切换到群聊页面
|
||||
*/
|
||||
private void initGroupButton() {
|
||||
JButton groupBtn = createIconButton("群", Color.LIGHT_GRAY);
|
||||
groupBtn.setToolTipText("群聊");
|
||||
groupBtn.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
SecondaryOptionView.get().exchangeToGroupList();
|
||||
}
|
||||
});
|
||||
this.add(groupBtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化设置按钮
|
||||
* 点击后切换到设置页面
|
||||
*/
|
||||
private void initSettingButton() {
|
||||
JButton settingBtn = createIconButton("设", Color.LIGHT_GRAY);
|
||||
settingBtn.setToolTipText("设置");
|
||||
settingBtn.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
SecondaryOptionView.get().exchangeToSettingList();
|
||||
}
|
||||
});
|
||||
this.add(settingBtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化黑暗模式切换按钮
|
||||
*/
|
||||
private void initDarkModeButton() {
|
||||
JButton darkModeBtn = createIconButton("黑", Color.DARK_GRAY);
|
||||
darkModeBtn.setToolTipText("切换模式");
|
||||
darkModeBtn.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (FlatLaf.isLafDark()) {
|
||||
try {
|
||||
DesignToken.setDarkMode(false);
|
||||
FlatLightLaf.setup();
|
||||
FlatLaf.updateUI();
|
||||
ChatInfoView.get().updateTheme();
|
||||
// 切换图标
|
||||
darkModeBtn.setIcon(new CircleCharIcon2(Color.DARK_GRAY, Color.WHITE, "黑", 36));
|
||||
darkModeBtn.setToolTipText("切换到黑暗模式");
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
DesignToken.setDarkMode(true);
|
||||
FlatDarkLaf.setup();
|
||||
FlatLaf.updateUI();
|
||||
ChatInfoView.get().updateTheme();
|
||||
// 切换图标
|
||||
darkModeBtn.setIcon(new CircleCharIcon2(Color.LIGHT_GRAY, Color.BLACK, "白", 36));
|
||||
darkModeBtn.setToolTipText("切换到明亮模式");
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.add(darkModeBtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带圆形图标的按钮
|
||||
*/
|
||||
private JButton createIconButton(String text, Color bgColor) {
|
||||
JButton button = new JButton();
|
||||
button.setPreferredSize(new Dimension(40, 40));
|
||||
button.setIcon(new CircleCharIcon2(bgColor, Color.WHITE, text, 36));
|
||||
button.setBorderPainted(false);
|
||||
button.setContentAreaFilled(false);
|
||||
button.setFocusPainted(false);
|
||||
return button;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package client.view.util;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
public class CircleCharIcon2 implements Icon {
|
||||
private Color circleColor;
|
||||
private Color textColor;
|
||||
private String character;
|
||||
private int size;
|
||||
|
||||
public CircleCharIcon2(Color circleColor, Color textColor, String character, int size) {
|
||||
this.circleColor = circleColor;
|
||||
this.textColor = textColor;
|
||||
this.character = character;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintIcon(Component c, Graphics g, int x, int y) {
|
||||
Graphics2D g2d = (Graphics2D) g;
|
||||
|
||||
// 开启抗锯齿
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 绘制圆形
|
||||
g2d.setColor(circleColor);
|
||||
g2d.fillOval(x, y, size - 1, size - 1);
|
||||
|
||||
// 绘制字符
|
||||
g2d.setColor(textColor);
|
||||
g2d.setFont(new Font("Microsoft YaHei", Font.BOLD, size / 2));
|
||||
FontMetrics fm = g2d.getFontMetrics();
|
||||
int textWidth = fm.stringWidth(character);
|
||||
int textHeight = fm.getAscent();
|
||||
int textX = x + (size - textWidth) / 2;
|
||||
int textY = y + (size - textHeight) / 2 + (int) (fm.getAscent() / 1.5);
|
||||
g2d.drawString(character, textX, textY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconWidth() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconHeight() {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package client.view.util;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 用于设定 UI元素的各种变量
|
||||
*/
|
||||
public class DesignToken implements Serializable {
|
||||
private static final long serialVersionUID = -3791869536619757015L;
|
||||
|
||||
//============================窗口大小==============================
|
||||
// 主界面大小
|
||||
public final static int WINDOW_ORI_WIDTH = 1200;
|
||||
public final static int WINDOW_ORI_HEIGHT = 600;
|
||||
public final static int WINDOW_MIN_WIDTH = 800;
|
||||
public final static int WINDOW_MIN_HEIGHT = 600;
|
||||
// 登录界面大小
|
||||
public final static int LOGIN_WIDTH = 400;
|
||||
public final static int LOGIN_HEIGHT = 300;
|
||||
|
||||
// 主界面组件大小
|
||||
public final static int SIDE_PANEL_WIDTH = 50;
|
||||
public final static int SECONDARY_PANEL_WIDTH = 200;
|
||||
public final static int SECONDARY_PANEL_WIDTH_MIN = 150;
|
||||
public final static int CONTENT_PANEL_WIDTH = 570;
|
||||
|
||||
// 内容组件大小
|
||||
public final static int CHAT_PANEL_WIDTH = 400;
|
||||
public final static int INPUT_AREA_HEIGHT = 100;
|
||||
public final static int MSG_BUBBLE_WIDTH = 50;
|
||||
public final static int INFO_PANEL_WIDTH = 150;
|
||||
|
||||
public final static int CONTENT_PANEL_WIDTH_MIN = 300;
|
||||
public final static int GROUP_INFO_PANEL_WIDTH = 240;
|
||||
public final static int GROUP_CHAT_PANEL_WIDTH = 210;
|
||||
|
||||
//============================字体==================================
|
||||
public static int FONT_SIZE = 14;
|
||||
public static int FONT_SIZE_SMALL = 12;
|
||||
public static int FONT_SIZE_TITLE = 20;
|
||||
public static int FONT_SIZE_TITLE_MIN = 18;
|
||||
|
||||
public static String DEFAULT_FONT = "微软雅黑";
|
||||
|
||||
//=============================颜色=================================
|
||||
|
||||
// public static String COLOR_BACKGROUND = "#ddebee";
|
||||
|
||||
public static String BUBBLE_COLOR_GREEN = "#5aca58";
|
||||
public static String BUBBLE_COLOR_WHITE = "#c8cdcd";
|
||||
public static String BUBBLE_COLOR_GRAY = "#8a8a8a";
|
||||
public static String BUBBLE_COLOR_RED = "#ff4d4d";
|
||||
public static String BUBBLE_COLOR_BLUE = "#4097bc";
|
||||
public static String BUBBLE_COLOR_YELLOW = "#f7b500";
|
||||
|
||||
public static String BACKGROUND_COLOR = "#c7d9d9";
|
||||
public static String EDGE_COLOR = "#aab7b7";
|
||||
|
||||
public static String COLOR_FONT_BLUE = "#4097bc";// blue
|
||||
public static String COLOR_FONT_BLACK = "#000000";// black
|
||||
public static String COLOR_FONT_WHITE = "#ffffff";// white
|
||||
public static String COLOR_FONT_GRAY = "#8a8a8a";// gray
|
||||
public static String COLOR_FONT_GREEN = "#008000";// green
|
||||
public static String COLOR_FONT_RED = "#ff0000";// red
|
||||
public static String COLOR_FONT_YELLOW = "#ffff00";// yellow
|
||||
public static String COLOR_FONT_ORANGE = "#ff8c00";// orange
|
||||
public static String COLOR_FONT_PURPLE = "#800080";// purple
|
||||
|
||||
public static String COLOR_HEAD_BLACK = "#000000"; // black
|
||||
public static String COLOR_HEAD_GRAY = "#8a8a8a"; // gray
|
||||
public static String COLOR_HEAD_WHITE = "#ffffff"; // white
|
||||
public static String COLOR_HEAD_BLUE = "#4097bc"; // blue
|
||||
public static String COLOR_HEAD_GREEN = "#008000"; // green
|
||||
public static String COLOR_HEAD_RED = "#ff0000"; // red
|
||||
public static String COLOR_HEAD_YELLOW = "#ffff00"; // yellow
|
||||
public static String COLOR_HEAD_ORANGE = "#ff8c00"; // orange
|
||||
public static String COLOR_HEAD_PURPLE = "#800080"; // purple
|
||||
|
||||
public static void setDarkMode(boolean isDark) {
|
||||
if (isDark) {
|
||||
BACKGROUND_COLOR = "#3c3f41";
|
||||
EDGE_COLOR = "#5e6060";
|
||||
BUBBLE_COLOR_WHITE = "#505050"; // 白色气泡颜色(其他用户)
|
||||
COLOR_FONT_BLACK = "#dddddd"; // 黑色字体颜色(其他用户)
|
||||
BUBBLE_COLOR_GREEN = "#2e7d32"; // 绿色气泡颜色(自己)
|
||||
} else {
|
||||
BACKGROUND_COLOR = "#c7d9d9";
|
||||
EDGE_COLOR = "#aab7b7";
|
||||
BUBBLE_COLOR_WHITE = "#c8cdcd";
|
||||
COLOR_FONT_BLACK = "#000000";
|
||||
BUBBLE_COLOR_GREEN = "#5aca58";
|
||||
}
|
||||
}
|
||||
|
||||
//=============================文本域输入字数限制=================================
|
||||
public static int MAX_FONT_SIZE = 20;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package client.view.util;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 一个可以限制最小宽度的面板,由于splitPane中
|
||||
*/
|
||||
public class LimitSizePanel extends JPanel {
|
||||
private int minWidth;
|
||||
|
||||
public LimitSizePanel(int minWidth) {
|
||||
super();
|
||||
this.minWidth = minWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getMinimumSize() {
|
||||
// 设置最小尺寸
|
||||
Dimension dim = super.getMinimumSize();
|
||||
dim.width = Math.max(dim.width, minWidth);
|
||||
return dim;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
// 设置首选尺寸
|
||||
Dimension dim = super.getPreferredSize();
|
||||
dim.width = Math.max(dim.width, minWidth * 2);
|
||||
return dim;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package client.view.util;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 圆角矩形字符图标
|
||||
*/
|
||||
public class RoundedRectCharIcon implements Icon {
|
||||
private Color bgColor; // 背景颜色
|
||||
private Color textColor; // 字体颜色
|
||||
private String character; // 显示的字符
|
||||
private int size; // 图标大小
|
||||
private int arc; // 圆角大小
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param bgColor 背景颜色
|
||||
* @param textColor 字体颜色
|
||||
* @param character 显示的字符
|
||||
* @param size 图标大小
|
||||
*/
|
||||
public RoundedRectCharIcon(Color bgColor, Color textColor, String character, int size) {
|
||||
this.bgColor = bgColor;
|
||||
this.textColor = textColor;
|
||||
this.character = character;
|
||||
this.size = size;
|
||||
this.arc = size / 3; // 圆角大小
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制图标
|
||||
*
|
||||
* @param c 组件
|
||||
* @param g 图形上下文
|
||||
* @param x 图标左上角的 x 坐标
|
||||
* @param y 图标左上角的 y 坐标
|
||||
*/
|
||||
@Override
|
||||
public void paintIcon(Component c, Graphics g, int x, int y) {
|
||||
Graphics2D g2d = (Graphics2D) g;
|
||||
|
||||
// 开启抗锯齿
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 绘制圆角矩形
|
||||
g2d.setColor(bgColor);
|
||||
g2d.fillRoundRect(x, y, size - 1, size - 1, arc, arc);
|
||||
|
||||
// 绘制字符
|
||||
g2d.setColor(textColor);
|
||||
g2d.setFont(new Font("Microsoft YaHei", Font.BOLD, size / 2));
|
||||
FontMetrics fm = g2d.getFontMetrics();
|
||||
int textWidth = fm.stringWidth(character);
|
||||
int textHeight = fm.getAscent();
|
||||
int textX = x + (size - textWidth) / 2;
|
||||
int textY = y + (size - textHeight) / 2 + (int) (fm.getAscent() / 1.5);
|
||||
g2d.drawString(character, textX, textY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图标宽度
|
||||
*
|
||||
* @return 图标宽度
|
||||
*/
|
||||
@Override
|
||||
public int getIconWidth() {
|
||||
return size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图标高度
|
||||
*
|
||||
* @return 图标高度
|
||||
*/
|
||||
@Override
|
||||
public int getIconHeight() {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package global;
|
||||
|
||||
/**
|
||||
* 全局配置类
|
||||
* 全局变量均在此处,不要随意更改!!!局部变量命名不要有冲突
|
||||
*/
|
||||
public class global {
|
||||
// ====================== 1. 网络与基础配置 ======================
|
||||
public static final int SERVER_PORT = 1145; // 服务端监听端口,两端统一
|
||||
public static final String LOCAL_HOST = "127.0.0.1"; // 本地IP,客户端连接服务端用
|
||||
public static final String SERVER_ACCOUNT = "admin"; // 服务端账户名
|
||||
public static final int AUTO_SAVE_PERIOD = 1200; // 自动保存周期(单位:秒)
|
||||
public static final String DATA_SPLIT = "\uE000"; // 数据分隔符
|
||||
|
||||
// ====================== 2. 客户端请求操作码 (OPT) ======================
|
||||
// 注:常量值需保持唯一,避免冲突
|
||||
|
||||
// ------ 账户注册与登录 (1-4) ------
|
||||
public static final int OPT_REGISTER = 1; // 注册
|
||||
public static final int OPT_REGISTER_SUCCESS = 11; // 注册成功
|
||||
public static final int OPT_REGISTER_FAILED_ACC = 12; // 注册失败:账号存在
|
||||
public static final int OPT_REGISTER_FAILED_FORMAT = 13;// 注册失败:格式错误
|
||||
|
||||
public static final int OPT_LOGIN = 2; // 登录
|
||||
public static final int OPT_LOGIN_SUCCESS = 25; // 登录成功
|
||||
public static final int OPT_ERROR_NOT_LOGIN = 21; // 错误:还未登录
|
||||
public static final int OPT_LOGIN_FAILED_PWD = 22; // 登录失败:密码错误
|
||||
public static final int OPT_LOGIN_FAILED_ACC = 23; // 登录失败:账号错误
|
||||
public static final int OPT_LOGIN_FAILED_REPEATED = 24; // 登录失败:重复登录
|
||||
|
||||
public static final int OPT_LOGOUT = 3; // 登出
|
||||
public static final int OPT_DELETE_ACCOUNT = 4; // 注销/删除账号
|
||||
|
||||
// ------ 用户信息维护 (41-49) ------
|
||||
public static final int OPT_UPDATE_NICKNAME = 41; // 更新昵称
|
||||
public static final int OPT_UPDATE_PASSWORD = 42; // 更新密码
|
||||
public static final int OPT_USER_UPDATE_NAME_FAILED = 411; // 更新昵称失败
|
||||
public static final int OPT_USER_UPDATE_PASSWORD_FAILED = 412; // 更新密码失败
|
||||
public static final int OPT_USER_UPDATE_NAME_FAILED_WRONG_FORMAT = 413; // 更新昵称失败:格式错误
|
||||
|
||||
// ------ 群组功能 (5-7) ------
|
||||
public static final int OPT_GROUP_CREATE = 5; // 创建群聊
|
||||
public static final int OPT_GROUP_CREATE_SUCCESS = 51; // 创建群聊成功
|
||||
|
||||
public static final int OPT_GROUP_INVITE = 6; // 邀请加入群聊
|
||||
public static final int OPT_GROUP_INVITE_AGREE = 61; // 同意加入群聊
|
||||
public static final int OPT_GROUP_INVITE_REFUSE = 62; // 拒绝加入群聊
|
||||
public static final int OPT_GROUP_INVITE_OFFLINE = 63; // 邀请加入群聊失败:用户离线
|
||||
|
||||
public static final int OPT_GROUP_JOIN = 64; // 申请加入群聊
|
||||
public static final int OPT_GROUP_JOIN_SUCCESS = 65; // 申请加入群聊成功
|
||||
public static final int OPT_GROUP_JOIN_FAILED = 66; // 申请加入群聊失败
|
||||
|
||||
public static final int OPT_GROUP_QUIT = 7; // 退出群聊
|
||||
public static final int OPT_GROUP_DISBAND = 71; // 解散群聊
|
||||
public static final int OPT_GROUP_UPDATE_NAME = 72; // 更新群聊名字
|
||||
public static final int OPT_GROUP_UPDATE_OWNER = 73; // 更新群聊拥有者
|
||||
|
||||
// ------ 好友功能 (67-69) ------
|
||||
public static final int OPT_FRIEND_ADD = 67; // 申请添加好友
|
||||
public static final int OPT_FRIEND_ADD_SUCCESS = 68; // 申请添加好友成功
|
||||
public static final int OPT_FRIEND_ADD_FAILED = 69; // 申请添加好友失败
|
||||
public static final int OPT_FRIEND_ADD_AGREE = 681; // 同意添加好友
|
||||
public static final int OPT_FRIEND_ADD_REFUSE = 682; // 拒绝添加好友
|
||||
|
||||
// ------ 聊天消息 (8) ------
|
||||
public static final int OPT_CHAT = 8; // 群聊消息
|
||||
public static final int OPT_PRIVATE_CHAT = 81; // 私聊消息
|
||||
|
||||
// ------ 数据初始化与同步 (9) ------
|
||||
public static final int OPT_INIT_CHAT = 9; // 初始化:群聊历史消息
|
||||
public static final int OPT_INIT_USER = 91; // 初始化:在线用户列表
|
||||
public static final int OPT_INIT_GROUP = 92; // 初始化:群组列表
|
||||
public static final int SERVER_MESSAGE = 93; // 服务器系统消息
|
||||
|
||||
// 扩展用户信息 (v2)
|
||||
public static final int OPT_INIT_USER_DETAIL = 94; // 初始化:用户详细信息
|
||||
public static final int OPT_UPDATE_USER_DETAIL = 95; // 更新:用户详细信息
|
||||
|
||||
// ------ 系统控制 ------
|
||||
public static final int OPT_EXIT = 999; // 服务器关闭通知
|
||||
public static final int OPT_QUEST_WRONG = 404; // 请求错误
|
||||
|
||||
// ====================== 3. 响应提示信息 (MSG) ======================
|
||||
// 配合结果码,客户端直接展示给用户
|
||||
public static final String MSG_SUCCESS = "操作成功";
|
||||
public static final String MSG_ACCOUNT_EXIST = "注册失败:该账户已存在(账户唯一)";
|
||||
public static final String MSG_ACCOUNT_NOT_EXIST = "登录失败:该账户不存在";
|
||||
public static final String MSG_PWD_ERROR = "登录失败:密码与账户不匹配";
|
||||
public static final String MSG_UNKNOWN_OPT = "请求失败:未知的操作类型";
|
||||
public static final String MSG_DATA_ERROR = "请求失败:数据格式错误";
|
||||
|
||||
// ====================== 4. 聊天应用配置 ======================
|
||||
public static final int MAX_MSG_SEND_GAP = 10; // 消息最大发送事件间隔/秒
|
||||
public static final int DISCONNECT_TIMEOUT = 30; // 断开连接超时时间/秒
|
||||
|
||||
public static final String DEFAULT_GROUP_ID = "group_default"; // 默认群ID
|
||||
public static final String CHAT_MSG_PREFIX = "【系统消息】"; // 系统消息前缀
|
||||
|
||||
// ====================== 5. 运行时状态标识 ======================
|
||||
public final static boolean IS_ONLINE = true;
|
||||
public final static boolean IS_OFFLINE = false;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package server;
|
||||
|
||||
import server.data.ServerData;
|
||||
import util.FileUtil;
|
||||
|
||||
import java.util.Scanner;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class Server {
|
||||
|
||||
public static void main(String[] args) {
|
||||
//启动服务器
|
||||
ServerMainThread serverThread = new ServerMainThread();
|
||||
serverThread.start();
|
||||
|
||||
//启动定时任务:保存服务器数据
|
||||
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setDaemon(true); // 设置为守护线程
|
||||
return t;
|
||||
});
|
||||
|
||||
// 每隔半小时执行一次
|
||||
scheduler.scheduleAtFixedRate(() -> {
|
||||
System.out.println("保存数据中");
|
||||
FileUtil.saveServerData();
|
||||
}, 1200, 1200, TimeUnit.SECONDS);
|
||||
|
||||
Scanner sc = new Scanner(System.in);
|
||||
while (true) {
|
||||
// System.out.print("SERVER_CMD>>");
|
||||
String cmd = sc.nextLine();
|
||||
switch (cmd) {
|
||||
case "shutdown":
|
||||
serverThread.shutdown();
|
||||
System.out.println("=======服务器已关闭=======");
|
||||
System.exit(0);
|
||||
break;
|
||||
case "groupInfo":
|
||||
System.out.println("=======群聊列表=======");
|
||||
ServerData.getInstance().getServerGroups().values().forEach(
|
||||
group -> System.out.println(group.getGroupId() + " " + group.getGroupName())
|
||||
);
|
||||
break;
|
||||
case "chatThreadStatus":
|
||||
System.out.println("=======聊天线程状态=======");
|
||||
System.out.println(ServerMainThread.getChatThreadPoolStatus());
|
||||
break;
|
||||
case "receiveThreadStatus":
|
||||
System.out.println("=======接收线程状态=======");
|
||||
System.out.println(ServerMainThread.getReceiveThreadPoolStatus());
|
||||
break;
|
||||
case "blockingQueueStatus":
|
||||
System.out.println("=======阻塞队列状态=======");
|
||||
System.out.println(ServerMainThread.getBlockingQueueStatus());
|
||||
break;
|
||||
default:
|
||||
System.out.println("无效指令");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package server;
|
||||
|
||||
import server.serveice.*;
|
||||
import util.FileUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import static global.global.SERVER_PORT;
|
||||
|
||||
public class ServerMainThread extends Thread {
|
||||
// 服务器运行状态
|
||||
private static volatile boolean running = true;
|
||||
// 线程池:用于处理聊天消息的线程池
|
||||
private static ExecutorService chatThreadPool;
|
||||
// 线程池:用于接收客户端消息的线程池
|
||||
private static ExecutorService receiveThreadPool;
|
||||
// 用于存储每个客户端的消息队列
|
||||
private static ConcurrentHashMap<Socket, ArrayBlockingQueue<Wrapper>> msgQueues;
|
||||
|
||||
// 核心:启动服务、监听端口、循环接收客户端连接
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println("加载本地数据成功");
|
||||
// 初始化推送信息线程池
|
||||
chatThreadPool = new ThreadPoolExecutor(
|
||||
10, // 核心线程数
|
||||
50, // 最大线程数
|
||||
60L, TimeUnit.SECONDS, // 空闲线程存活时间
|
||||
new SynchronousQueue<>(), // 直接提交队列
|
||||
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
|
||||
);
|
||||
receiveThreadPool = new ThreadPoolExecutor(
|
||||
10, // 核心线程数
|
||||
50, // 最大线程数
|
||||
60L, TimeUnit.SECONDS, // 空闲线程存活时间
|
||||
new SynchronousQueue<>(), // 直接提交队列
|
||||
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
|
||||
);
|
||||
|
||||
// 初始化消息队列
|
||||
msgQueues = new ConcurrentHashMap<>();
|
||||
|
||||
System.out.println("初始化信息线程池成功");
|
||||
System.out.println("服务器启动成功");
|
||||
try {
|
||||
ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
|
||||
|
||||
while (running) {
|
||||
|
||||
// 扫描端口,接收链接请求,如果有链接请求,则尝试链接
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
System.out.println("有新的用户端连接: " + clientSocket.getPort());
|
||||
|
||||
// 创建线程,处理客户端请求
|
||||
ArrayBlockingQueue<Wrapper> threadQueue = new ArrayBlockingQueue<>(40);
|
||||
msgQueues.put(clientSocket, threadQueue);
|
||||
|
||||
ClientChatThread clientChatThread = new ClientChatThread(clientSocket, threadQueue);
|
||||
chatThreadPool.submit(clientChatThread);
|
||||
Thread.sleep(200);
|
||||
ClientReceiveThread clientReceiveThread = new ClientReceiveThread(clientSocket, threadQueue);
|
||||
receiveThreadPool.submit(clientReceiveThread);
|
||||
System.out.println("创建线程成功");
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭服务器
|
||||
public void shutdown() {
|
||||
FileUtil.saveServerData();
|
||||
// 向所有用户发送服务器关闭信息。
|
||||
if (msgQueues != null) {
|
||||
Wrapper exitMsg = new Wrapper(global.global.OPT_EXIT);
|
||||
for (ArrayBlockingQueue<Wrapper> queue : msgQueues.values()) {
|
||||
try {
|
||||
queue.put(exitMsg);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
running = false;
|
||||
}
|
||||
|
||||
// 检查服务器是否运行
|
||||
public static boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
// 检查chatThreadPool状态
|
||||
public static Map<String, Object> getChatThreadPoolStatus() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
|
||||
if (chatThreadPool == null) {
|
||||
status.put("error", "线程池未初始化");
|
||||
return status;
|
||||
}
|
||||
|
||||
// 检查是否为 ThreadPoolExecutor
|
||||
if (chatThreadPool instanceof ThreadPoolExecutor) {
|
||||
ThreadPoolExecutor tpe = (ThreadPoolExecutor) chatThreadPool;
|
||||
|
||||
status.put("poolSize", tpe.getPoolSize());
|
||||
status.put("activeCount", tpe.getActiveCount());
|
||||
status.put("corePoolSize", tpe.getCorePoolSize());
|
||||
status.put("maximumPoolSize", tpe.getMaximumPoolSize());
|
||||
status.put("largestPoolSize", tpe.getLargestPoolSize());
|
||||
status.put("queueSize", tpe.getQueue().size());
|
||||
status.put("completedTaskCount", tpe.getCompletedTaskCount());
|
||||
status.put("taskCount", tpe.getTaskCount());
|
||||
status.put("isShutdown", tpe.isShutdown());
|
||||
status.put("isTerminated", tpe.isTerminated());
|
||||
}
|
||||
// 检查是否为 ForkJoinPool
|
||||
else if (chatThreadPool instanceof ForkJoinPool) {
|
||||
ForkJoinPool fjp = (ForkJoinPool) chatThreadPool;
|
||||
|
||||
status.put("poolSize", fjp.getPoolSize());
|
||||
status.put("activeCount", fjp.getActiveThreadCount());
|
||||
status.put("parallelism", fjp.getParallelism());
|
||||
status.put("runningThreadCount", fjp.getRunningThreadCount());
|
||||
status.put("queuedTaskCount", fjp.getQueuedTaskCount());
|
||||
status.put("queuedSubmissionCount", fjp.getQueuedSubmissionCount());
|
||||
status.put("stealCount", fjp.getStealCount());
|
||||
}
|
||||
// 其他类型的 ExecutorService
|
||||
else {
|
||||
// 使用反射尝试获取信息
|
||||
status.put("type", chatThreadPool.getClass().getName());
|
||||
status.put("info", "无法直接获取详细状态");
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
// 检查receiveThreadPool的状态
|
||||
public static Map<String, Object> getReceiveThreadPoolStatus() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
|
||||
if (receiveThreadPool == null) {
|
||||
status.put("error", "线程池未初始化");
|
||||
return status;
|
||||
}
|
||||
|
||||
// 检查是否为 ThreadPoolExecutor
|
||||
if (receiveThreadPool instanceof ThreadPoolExecutor) {
|
||||
ThreadPoolExecutor tpe = (ThreadPoolExecutor) receiveThreadPool;
|
||||
|
||||
status.put("poolSize", tpe.getPoolSize());
|
||||
status.put("activeCount", tpe.getActiveCount());
|
||||
status.put("corePoolSize", tpe.getCorePoolSize());
|
||||
status.put("maximumPoolSize", tpe.getMaximumPoolSize());
|
||||
status.put("largestPoolSize", tpe.getLargestPoolSize());
|
||||
status.put("queueSize", tpe.getQueue().size());
|
||||
status.put("completedTaskCount", tpe.getCompletedTaskCount());
|
||||
status.put("taskCount", tpe.getTaskCount());
|
||||
status.put("isShutdown", tpe.isShutdown());
|
||||
status.put("isTerminated", tpe.isTerminated());
|
||||
}
|
||||
// 检查是否为 ForkJoinPool
|
||||
else if (receiveThreadPool instanceof ForkJoinPool) {
|
||||
ForkJoinPool fjp = (ForkJoinPool) receiveThreadPool;
|
||||
|
||||
status.put("poolSize", fjp.getPoolSize());
|
||||
status.put("activeCount", fjp.getActiveThreadCount());
|
||||
status.put("parallelism", fjp.getParallelism());
|
||||
status.put("runningThreadCount", fjp.getRunningThreadCount());
|
||||
status.put("queuedTaskCount", fjp.getQueuedTaskCount());
|
||||
status.put("queuedSubmissionCount", fjp.getQueuedSubmissionCount());
|
||||
status.put("stealCount", fjp.getStealCount());
|
||||
}
|
||||
// 其他类型的 ExecutorService
|
||||
else {
|
||||
// 使用反射尝试获取信息
|
||||
status.put("type", receiveThreadPool.getClass().getName());
|
||||
status.put("info", "无法直接获取详细状态");
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
// 检查阻塞队列状态
|
||||
public static List<String> getBlockingQueueStatus() {
|
||||
List<String> queueStatus = new ArrayList<>();
|
||||
|
||||
if (msgQueues == null) {
|
||||
queueStatus.add("阻塞队列未初始化");
|
||||
return queueStatus;
|
||||
}
|
||||
|
||||
msgQueues.forEach(
|
||||
(key, value) -> queueStatus.add(key + ": " + value.size()));
|
||||
|
||||
return queueStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用于删除不需要的阻塞队列
|
||||
* 这里只能ClientChatThread调用
|
||||
*
|
||||
* @param socket 对应的客户端的socket
|
||||
*/
|
||||
public static void dropMsgQueue(Socket socket) {
|
||||
if (msgQueues == null) {
|
||||
System.out.println("意外:主线程为初始化的情况下调用了dropMsgQueue");
|
||||
return;
|
||||
}
|
||||
if (msgQueues.containsKey(socket)) {
|
||||
msgQueues.remove(socket);
|
||||
} else {
|
||||
System.out.println("意外:尝试删除不存在的阻塞队列");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package server.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* 群聊信息类,用于保存聊天室的具体信息。
|
||||
*/
|
||||
public class GroupData implements Serializable {
|
||||
private static final long serialVersionUID = 4303981922076715842L;
|
||||
|
||||
public class GroupMember implements Comparable<GroupMember>, Serializable {
|
||||
private static final long serialVersionUID = 585007162886079570L;
|
||||
|
||||
public String id;
|
||||
public boolean isOut;
|
||||
|
||||
public GroupMember(String id) {
|
||||
this.id = id;
|
||||
isOut = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(GroupMember o) {
|
||||
return this.id.compareTo(o.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 群聊id
|
||||
private String groupId;
|
||||
private String groupName;
|
||||
private GroupMember groupOwner;
|
||||
private TreeSet<GroupMember> members;
|
||||
|
||||
public GroupData(String groupId, String groupName, String groupOwner) {
|
||||
this.groupName = groupName;
|
||||
this.groupId = groupId;
|
||||
this.groupOwner = new GroupMember(groupOwner);
|
||||
members = new TreeSet<>();
|
||||
}
|
||||
|
||||
public GroupData(String groupId) {
|
||||
this.groupId = groupId;
|
||||
this.groupName = "TEMP_TEST";
|
||||
}
|
||||
|
||||
// 添加组员
|
||||
public void addMember(String id) {
|
||||
members.add(new GroupMember(id));
|
||||
}
|
||||
|
||||
// 移除组员
|
||||
public boolean removeMember(String id) {
|
||||
GroupMember temp = new GroupMember(id);
|
||||
if (members.contains(temp)) {
|
||||
members.remove(new GroupMember(id));
|
||||
return true;
|
||||
} else {
|
||||
// System.out.println("组员移除失败. groupId: " + groupId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public int getMemberCount() {
|
||||
return members.size();
|
||||
}
|
||||
|
||||
// 获取群聊id
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
// 获取群聊名称
|
||||
public String getGroupName() {
|
||||
return groupName;
|
||||
}
|
||||
|
||||
// 获取群主
|
||||
public GroupMember getGroupOwner() {
|
||||
return groupOwner;
|
||||
}
|
||||
|
||||
// 获取群成员
|
||||
public TreeSet<GroupMember> getMembers() {
|
||||
return members;
|
||||
}
|
||||
|
||||
// 设置群主
|
||||
public void setGroupOwner(String id) {
|
||||
this.groupOwner = new GroupMember(id);
|
||||
}
|
||||
|
||||
public void setGroupName(String groupName) {
|
||||
this.groupName = groupName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GroupData{" +
|
||||
"groupId='" + groupId + '\'' +
|
||||
", groupName='" + groupName + '\'' +
|
||||
", groupOwner=" + groupOwner.id +
|
||||
", members count=" + members.size() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package server.data;
|
||||
|
||||
import util.FileUtil;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
// 服务器数据,单个服务器仅对应一个服务器数据集合。
|
||||
// 允许存储,用于数据持久化
|
||||
// 辅助进行数据核验
|
||||
public class ServerData implements Serializable {
|
||||
private static final long serialVersionUID = 5016807647175865383L;
|
||||
|
||||
private static volatile ServerData instance = null;
|
||||
|
||||
// 获取唯一的serverData对象(线程安全的懒加载)
|
||||
public static ServerData getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (ServerData.class) {
|
||||
if (instance == null) {
|
||||
instance = new ServerData();
|
||||
instance.loadData();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
// 重置实例(用于测试或重新加载)
|
||||
public static void resetInstance() {
|
||||
synchronized (ServerData.class) {
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, UserData> serverUsers;
|
||||
private Map<String, GroupData> serverGroups;
|
||||
private transient boolean dataLoaded = false;
|
||||
|
||||
// 处理服务器信息的主类
|
||||
public ServerData() {
|
||||
// 初始化空数据
|
||||
serverUsers = new ConcurrentHashMap<>();
|
||||
serverGroups = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
// 显式加载数据的方法
|
||||
public void loadData() {
|
||||
if (!dataLoaded) {
|
||||
synchronized (this) {
|
||||
if (!dataLoaded) {
|
||||
ServerData loadedData = FileUtil.loadServerData();
|
||||
|
||||
if (loadedData != null) {
|
||||
// 如果加载到了数据,合并到当前实例
|
||||
if (loadedData.getServerUsers() != null) {
|
||||
this.serverUsers = loadedData.getServerUsers();
|
||||
}
|
||||
if (loadedData.getServerGroups() != null) {
|
||||
this.serverGroups = loadedData.getServerGroups();
|
||||
}
|
||||
|
||||
System.out.println("服务器数据加载成功,用户数: " +
|
||||
(serverUsers != null ? serverUsers.size() : 0) +
|
||||
", 群组数: " + (serverGroups != null ? serverGroups.size() : 0));
|
||||
} else {
|
||||
System.out.println("未找到数据文件或加载失败,使用初始化空数据");
|
||||
}
|
||||
|
||||
dataLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证数据是否已正确初始化
|
||||
private void validateData() {
|
||||
if (serverUsers == null) {
|
||||
serverUsers = new ConcurrentHashMap<>();
|
||||
}
|
||||
if (serverGroups == null) {
|
||||
serverGroups = new ConcurrentHashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
// 保存服务器数据到本地
|
||||
public void saveServerData() {
|
||||
validateData(); // 确保数据有效
|
||||
FileUtil.saveServerData();
|
||||
}
|
||||
|
||||
// 添加用户
|
||||
public void addUser(UserData userData) {
|
||||
serverUsers.put(userData.getUserId(), userData);
|
||||
}
|
||||
|
||||
// 移除用户
|
||||
public void removeUser(String userId) {
|
||||
serverUsers.remove(userId);
|
||||
serverGroups.values().forEach(groupData -> {
|
||||
groupData.removeMember(userId);
|
||||
if (groupData.getMemberCount() == 0) {
|
||||
removeGroup(groupData.getGroupId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 修改用户名字
|
||||
public void updateUserName(String userId, String newName) {
|
||||
serverUsers.get(userId).setNikename(newName);
|
||||
}
|
||||
|
||||
public void updateUserPwd(String userId, String newPwd) {
|
||||
serverUsers.get(userId).setPassword(newPwd);
|
||||
}
|
||||
|
||||
// 添加群聊
|
||||
public void addGroup(GroupData groupData) {
|
||||
serverGroups.put(groupData.getGroupId(), groupData);
|
||||
// 更新关联用户信息
|
||||
groupData.getMembers().forEach(member -> {
|
||||
addGroupToUser(member.id, groupData.getGroupId());
|
||||
});
|
||||
}
|
||||
|
||||
public void addGroupToUser(String userId, String groupId) {
|
||||
serverUsers.get(userId).addGroupId(groupId);
|
||||
}
|
||||
|
||||
// 移除群聊
|
||||
public void removeGroup(String groupId) {
|
||||
GroupData groupData = serverGroups.remove(groupId);
|
||||
groupData.getMembers().forEach(member -> {
|
||||
serverUsers.get(member.id).removeGroup(groupData.getGroupId());
|
||||
});
|
||||
}
|
||||
|
||||
// 更新群聊信息
|
||||
public void updateGroupName(String groupId, String newName) {
|
||||
serverGroups.get(groupId).setGroupName(newName);
|
||||
}
|
||||
|
||||
public void updateGroupOwner(String groupId, String newOwnerId) {
|
||||
serverGroups.get(groupId).setGroupOwner(newOwnerId);
|
||||
}
|
||||
|
||||
// 添加群聊成员
|
||||
public void addUserToGroup(String groupId, String userId) {
|
||||
serverGroups.get(groupId).addMember(userId);
|
||||
}
|
||||
|
||||
// 移除群聊成员
|
||||
public void removeUserFromGroup(String groupId, String userId) {
|
||||
serverGroups.values().forEach(groupData -> {
|
||||
if (groupData.getGroupId().equals(groupId)) {
|
||||
groupData.removeMember(userId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 移除成员的群聊
|
||||
public void removeGroupFromUser(String userId, String groupId) {
|
||||
serverUsers.get(userId).removeGroup(groupId);
|
||||
}
|
||||
|
||||
// 获取用户名字,不存在就回复id本身
|
||||
public String getUserName(String userId) {
|
||||
if (serverUsers.containsKey(userId))
|
||||
return serverUsers.get(userId).getNickname();
|
||||
else
|
||||
return userId;
|
||||
}
|
||||
|
||||
// 获取用户数据
|
||||
public UserData getUserData(String userId) {
|
||||
return serverUsers.get(userId);
|
||||
}
|
||||
|
||||
// 获取群聊名字
|
||||
public String getGroupName(String groupId) {
|
||||
return serverGroups.get(groupId).getGroupName();
|
||||
}
|
||||
|
||||
// 判断群聊是否存在
|
||||
public boolean containsGroup(String groupId) {
|
||||
return serverGroups.containsKey(groupId);
|
||||
}
|
||||
|
||||
// 获取用户群聊id组
|
||||
public TreeSet<String> getUserGroups(String userId) {
|
||||
if (userId == null) {
|
||||
return new TreeSet<>();
|
||||
} else {
|
||||
return serverUsers.get(userId).getGroupIds();
|
||||
}
|
||||
}
|
||||
|
||||
// 获取群聊的成员组
|
||||
public TreeSet<GroupData.GroupMember> getGroupUsers(String groupId) {
|
||||
return serverGroups.get(groupId).getMembers();
|
||||
}
|
||||
|
||||
// 获取群聊的成员id组
|
||||
public List<String> getGroupMembersId(String groupId) {
|
||||
List<String> members = new ArrayList<>();
|
||||
serverGroups.get(groupId).getMembers().forEach(groupMember -> {
|
||||
members.add(groupMember.id);
|
||||
});
|
||||
return members;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断账号是否存在
|
||||
*
|
||||
* @param userId 账号
|
||||
* @return true:存在 false:不存在
|
||||
*/
|
||||
public boolean IsAccountExist(String userId) {
|
||||
return serverUsers.containsKey(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验账号与密码是否匹配
|
||||
*
|
||||
* @param userId 待校验的用户账号
|
||||
* @param password 待校验的用户密码
|
||||
* @return 密码匹配返回true,不匹配返回false
|
||||
*/
|
||||
public boolean AccountAndPasswordIsMatch(String userId, String password) {
|
||||
if (serverUsers.get(userId).getPassword().equals(password)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务器用户数据
|
||||
*
|
||||
* @param serverUsers 服务器用户数据映射表
|
||||
*/
|
||||
public void setServerUsers(Map<String, UserData> serverUsers) {
|
||||
this.serverUsers = serverUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置服务器群聊数据
|
||||
*
|
||||
* @param serverGroups 服务器群聊数据映射表
|
||||
*/
|
||||
public void setServerGroups(Map<String, GroupData> serverGroups) {
|
||||
this.serverGroups = serverGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器用户数据映射表
|
||||
*
|
||||
* @return 服务器用户数据映射表
|
||||
*/
|
||||
public Map<String, UserData> getServerUsers() {
|
||||
return serverUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器用户id到用户名的映射表
|
||||
*
|
||||
* @return 服务器用户id到用户名的映射表
|
||||
*/
|
||||
public Map<String, String> getIdNameMap() {
|
||||
Map<String, String> idNameMap = new HashMap<>();
|
||||
serverUsers.values().forEach(user -> {
|
||||
idNameMap.put(user.getUserId(), user.getNickname());
|
||||
});
|
||||
return idNameMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器群聊数据映射表
|
||||
*
|
||||
* @return 服务器群聊数据映射表
|
||||
*/
|
||||
public Map<String, GroupData> getServerGroups() {
|
||||
return serverGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器群聊数据映射表
|
||||
*
|
||||
* @return 服务器群聊数据映射表
|
||||
*/
|
||||
public GroupData getGroupById(String groupId) {
|
||||
return serverGroups.get(groupId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package server.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* 用户信息类,用于保存用户的具体信息。
|
||||
*/
|
||||
public class UserData implements Serializable, Comparable<UserData> {
|
||||
// 序列化版本号,用于版本控制
|
||||
private static final long serialVersionUID = 2809761558436195616L;
|
||||
|
||||
// 用户昵称
|
||||
private String nikename;
|
||||
// 用户ID
|
||||
private String id;
|
||||
// 用户密码
|
||||
private String password;
|
||||
// 所属群聊ID集合
|
||||
private TreeSet<String> groupIds;
|
||||
// 好友ID集合
|
||||
private TreeSet<String> friendIds;
|
||||
// 用户邮箱
|
||||
private String email;
|
||||
// 用户生日
|
||||
private String birthday;
|
||||
// 用户地址
|
||||
private String address;
|
||||
// 用户签名
|
||||
private String signature;
|
||||
|
||||
public UserData(String nikename, String id, String password) {
|
||||
this.nikename = nikename;
|
||||
this.id = id;
|
||||
this.password = password;
|
||||
this.groupIds = new TreeSet<>();
|
||||
this.friendIds = new TreeSet<>();
|
||||
// 初始化扩展信息为空字符串,避免 null
|
||||
this.email = "";
|
||||
this.birthday = "";
|
||||
this.address = "";
|
||||
this.signature = "";
|
||||
}
|
||||
|
||||
// 获取安全的副本(不包含密码),用于网络传输
|
||||
public UserData getSafeCopy() {
|
||||
UserData copy = new UserData(this.nikename, this.id, null);
|
||||
copy.setGroupIds(new TreeSet<>(this.groupIds));
|
||||
copy.setFriendIds(new TreeSet<>(this.friendIds));
|
||||
copy.setEmail(this.email);
|
||||
copy.setBirthday(this.birthday);
|
||||
copy.setAddress(this.address);
|
||||
copy.setSignature(this.signature);
|
||||
return copy;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nikename;
|
||||
}
|
||||
|
||||
public void setNikename(String nikename) {
|
||||
this.nikename = nikename;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public TreeSet<String> getGroupIds() {
|
||||
return groupIds;
|
||||
}
|
||||
|
||||
public void setGroupIds(TreeSet<String> groupIds) {
|
||||
this.groupIds = groupIds;
|
||||
}
|
||||
|
||||
public TreeSet<String> getFriendIds() {
|
||||
return friendIds;
|
||||
}
|
||||
|
||||
public void setFriendIds(TreeSet<String> friendIds) {
|
||||
this.friendIds = friendIds;
|
||||
}
|
||||
|
||||
public void addFriend(String friendId) {
|
||||
this.friendIds.add(friendId);
|
||||
}
|
||||
|
||||
public boolean addGroupId(String groupId) {
|
||||
return this.groupIds.add(groupId);
|
||||
}
|
||||
|
||||
public boolean removeGroupId(Long groupId) {
|
||||
if (groupIds.contains(groupId.toString())) {
|
||||
groupIds.remove(groupId.toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(UserData o) {
|
||||
return this.id.compareTo(o.id);
|
||||
}
|
||||
|
||||
public void removeGroup(String groupId) {
|
||||
this.groupIds.remove(groupId);
|
||||
}
|
||||
|
||||
// 扩展信息的 Getter 和 Setter
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getBirthday() {
|
||||
return birthday;
|
||||
}
|
||||
|
||||
public void setBirthday(String birthday) {
|
||||
this.birthday = birthday;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public void setAddress(String address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public String getSignature() {
|
||||
return signature;
|
||||
}
|
||||
|
||||
public void setSignature(String signature) {
|
||||
this.signature = signature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,993 @@
|
||||
package server.serveice;
|
||||
|
||||
import global.global;
|
||||
import server.ServerMainThread;
|
||||
import server.data.*;
|
||||
import util.FileUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* 不可独立创建线程,这个会在内部自主创建线程。
|
||||
* 每个客户端连接对应一个线程,用于处理客户端的请求。
|
||||
* 线程启动后,会进入一个循环,等待服务端发送的消息。
|
||||
* 收到消息后,会根据消息的操作码,调用对应的处理方法。
|
||||
* 处理完成后,会将结果发送给客户端。
|
||||
* 线程会在以下情况下退出:
|
||||
* 1. 服务端关闭
|
||||
* 2. 客户端主动关闭
|
||||
* 3. 线程被中断
|
||||
*/
|
||||
public class ClientChatThread implements Runnable {
|
||||
// 群在线用户存储:全局静态、线程安全
|
||||
// key:用户id value:用户在线输出流
|
||||
private static final Map<String, ObjectOutputStream> USER_ONLINE_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
// 与客户端相连的套接字
|
||||
private final Socket clientSocket;
|
||||
// 是否登录
|
||||
private boolean isLogin = false;
|
||||
// 用户的账户
|
||||
private String userId;
|
||||
// 阻塞队列,用于进行线程的信息交流
|
||||
private BlockingQueue<Wrapper> messageQueue;
|
||||
|
||||
private ObjectOutputStream oos;
|
||||
|
||||
private volatile boolean isRunning = true;
|
||||
|
||||
/**
|
||||
* 创建一个数据发送线程,并且附带创建一个信息接收线程。
|
||||
* 由于两个线程总是同步创建和销毁的,因此不进行单独创建。
|
||||
*
|
||||
* @param clientSocket 与客户端相连的套接字
|
||||
*/
|
||||
public ClientChatThread(Socket clientSocket, BlockingQueue<Wrapper> threadQueue)
|
||||
throws IOException {
|
||||
this.clientSocket = clientSocket;
|
||||
this.isLogin = false;
|
||||
this.userId = null;
|
||||
|
||||
messageQueue = threadQueue;
|
||||
}
|
||||
|
||||
// 线程核心:聊天业务主流程
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
oos = new ObjectOutputStream(clientSocket.getOutputStream());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
while (ServerMainThread.isRunning() && this.isRunning) {
|
||||
try {
|
||||
Wrapper msg = messageQueue.take();
|
||||
handleReceiveMsg(msg);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理服务端接收的消息,按规则解析。在解析出结果后,调用下面的内容来处理数据体
|
||||
*
|
||||
* @param msg 服务端接收的消息
|
||||
*/
|
||||
private void handleReceiveMsg(Wrapper msg) {
|
||||
int opt = msg.getOperation();
|
||||
String senderId = msg.getSenderId();
|
||||
|
||||
switch (opt) {
|
||||
// 登录请求
|
||||
case global.OPT_REGISTER:
|
||||
handleRegisterRequest(msg);
|
||||
break;
|
||||
case global.OPT_LOGIN:
|
||||
handleLogInRequest(msg);
|
||||
break;
|
||||
case global.OPT_LOGOUT:
|
||||
handleLogOutRequest(senderId);
|
||||
break;
|
||||
case global.OPT_DELETE_ACCOUNT:
|
||||
handleDeleteUserRequest(senderId);
|
||||
break;
|
||||
case global.OPT_UPDATE_NICKNAME:
|
||||
handleUpdateUserNameRequest(msg);
|
||||
break;
|
||||
case global.OPT_UPDATE_PASSWORD:
|
||||
handleUpdateUserPwdRequest(msg);
|
||||
break;
|
||||
case global.OPT_GROUP_CREATE:
|
||||
handleCreateGroupRequest((String) msg.getData(), msg.getGroupId());
|
||||
break;
|
||||
case global.OPT_GROUP_INVITE:
|
||||
handleInviteRequest(msg);
|
||||
break;
|
||||
case global.OPT_GROUP_JOIN:
|
||||
handleGroupJoinRequest(msg);
|
||||
break;
|
||||
case global.OPT_FRIEND_ADD:
|
||||
handleFriendAddRequest(msg);
|
||||
break;
|
||||
case global.OPT_FRIEND_ADD_AGREE:
|
||||
handleFriendAddAgree(msg);
|
||||
break;
|
||||
case global.OPT_FRIEND_ADD_REFUSE:
|
||||
handleFriendAddRefuse(msg);
|
||||
break;
|
||||
case global.OPT_GROUP_INVITE_AGREE:
|
||||
// 用户加入群聊
|
||||
handleJoinGroupRequest(senderId, msg.getGroupId());
|
||||
break;
|
||||
case global.OPT_GROUP_INVITE_REFUSE:
|
||||
// 拒绝加入群聊
|
||||
sendToUser(Wrapper.serverResponse(global.OPT_GROUP_INVITE_REFUSE), (String) msg.getData());
|
||||
break;
|
||||
case global.OPT_GROUP_QUIT:
|
||||
handleQuitGroupRequest(senderId, msg.getGroupId());
|
||||
break;
|
||||
case global.OPT_GROUP_DISBAND:
|
||||
handleDeleteGroupRequest(msg.getGroupId());
|
||||
break;
|
||||
case global.OPT_CHAT:
|
||||
sendToGroupExceptSelf(msg, msg.getGroupId());
|
||||
FileUtil.addChatMessage(msg.getGroupId(), (String) msg.getData());
|
||||
break;
|
||||
case global.OPT_PRIVATE_CHAT:
|
||||
handlePrivateChatRequest(msg);
|
||||
break;
|
||||
case global.OPT_GROUP_UPDATE_NAME:
|
||||
handleUpdateGroupNameRequest(msg);
|
||||
break;
|
||||
case global.OPT_GROUP_UPDATE_OWNER:
|
||||
handleUpdateGroupOwnerRequest(msg);
|
||||
break;
|
||||
case global.OPT_INIT_CHAT:
|
||||
handleInitChatRequest();
|
||||
break;
|
||||
case global.OPT_INIT_GROUP:
|
||||
handleInitGroupRequest();
|
||||
break;
|
||||
case global.OPT_INIT_USER:
|
||||
// 发送用户 ID-Name 映射
|
||||
sendToSelf(Wrapper.initResponse(ServerData.getInstance().getIdNameMap()));
|
||||
break;
|
||||
case global.OPT_INIT_USER_DETAIL:
|
||||
// 发送所有用户详细信息
|
||||
handleInitUserDetailRequest();
|
||||
break;
|
||||
case global.OPT_UPDATE_USER_DETAIL:
|
||||
// 处理更新用户详细信息
|
||||
handleUpdateUserDetailRequest(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理注册请求
|
||||
* 1. 检查用户ID是否为空
|
||||
* 2. 检查用户名是否为空
|
||||
* 3. 检查密码是否为空
|
||||
* 4. 检查用户ID是否已存在
|
||||
* 5. 添加新用户到ServerData
|
||||
* 6. 发送注册成功消息给客户端
|
||||
*
|
||||
* @param msg 注册请求消息
|
||||
*/
|
||||
private void handleRegisterRequest(Wrapper msg) {
|
||||
String[] data = (String[]) msg.getData();
|
||||
|
||||
String userId = msg.getSenderId();
|
||||
String nikname = data[0];
|
||||
String password = data[1];
|
||||
|
||||
// 数据为空的情况
|
||||
if (userId == null || nikname == null || password == null) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_QUEST_WRONG));
|
||||
return;
|
||||
}
|
||||
// 账户存在的情况
|
||||
if (ServerData.getInstance().IsAccountExist(userId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_REGISTER_FAILED_ACC));
|
||||
return;
|
||||
}
|
||||
|
||||
ServerData.getInstance().addUser(new UserData(nikname, userId, password));
|
||||
|
||||
this.isLogin = true;
|
||||
this.userId = userId;
|
||||
|
||||
USER_ONLINE_MAP.values().forEach(oos -> {
|
||||
Map<String, String> newRegister = new HashMap<>();
|
||||
newRegister.put(userId, nikname);
|
||||
try {
|
||||
oos.writeObject(Wrapper.initResponse(newRegister));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加注册用户的id oos映射,初始化id,群id映射
|
||||
USER_ONLINE_MAP.put(this.userId, oos);
|
||||
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_REGISTER_SUCCESS));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录请求
|
||||
* 1. 检查用户ID是否为空
|
||||
* 2. 检查密码是否为空
|
||||
* 3. 检查用户ID是否已存在
|
||||
* 4. 添加新用户到ServerData
|
||||
* 5. 发送登录成功消息给客户端
|
||||
*
|
||||
* @param msg 登录请求消息
|
||||
*/
|
||||
private void handleLogInRequest(Wrapper msg) {
|
||||
String userId = msg.getSenderId();
|
||||
String password = (String) msg.getData();
|
||||
|
||||
// 检测Id是否存在
|
||||
if (!ServerData.getInstance().IsAccountExist(userId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_LOGIN_FAILED_ACC));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检测是否重复登录
|
||||
if (USER_ONLINE_MAP.containsKey(userId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_LOGIN_FAILED_REPEATED));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检测账户密码是否相对应
|
||||
else if (!ServerData.getInstance().AccountAndPasswordIsMatch(userId, password)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_LOGIN_FAILED_PWD));
|
||||
return;
|
||||
}
|
||||
// 修改数据
|
||||
this.isLogin = true;
|
||||
this.userId = userId;
|
||||
// 更新用户在线信息
|
||||
USER_ONLINE_MAP.put(this.userId, oos);
|
||||
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_LOGIN_SUCCESS));
|
||||
USER_ONLINE_MAP.put(this.userId, oos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否已登录
|
||||
*
|
||||
* @return 如果用户已登录且在在线映射中,则返回true;否则返回false
|
||||
*/
|
||||
private boolean YesLogin() {
|
||||
return isLogin && USER_ONLINE_MAP.containsKey(userId) && this.userId != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭当前用户链接,关闭这个线程(接受线程在接收关闭信息时已经结束了)
|
||||
*/
|
||||
private void closeClient() {
|
||||
// 优先结束阻塞队列
|
||||
ServerMainThread.dropMsgQueue(this.clientSocket);
|
||||
|
||||
if (!isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
isRunning = false;
|
||||
if (oos != null) {
|
||||
try {
|
||||
oos.close();
|
||||
} catch (IOException e) {
|
||||
System.err.println("关闭输出流异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
if (clientSocket != null && !clientSocket.isClosed()) {
|
||||
try {
|
||||
clientSocket.close();
|
||||
} catch (IOException e) {
|
||||
System.err.println("关闭socket异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登出请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 从在线用户映射中移除用户
|
||||
* 3. 设置用户状态为未登录
|
||||
* 4. 发送登出成功消息给客户端
|
||||
*
|
||||
* @param userId 要登出的用户ID
|
||||
*/
|
||||
private void handleLogOutRequest(String userId) {
|
||||
if (!YesLogin()) {
|
||||
// 出现错误,现在还未登录
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
USER_ONLINE_MAP.remove(this.userId);
|
||||
isLogin = false;
|
||||
this.userId = null;
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_LOGOUT));
|
||||
try {
|
||||
Thread.sleep(100); // 短暂延迟
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
closeClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理删除用户请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 从在线用户映射中移除用户
|
||||
* 3. 从服务器数据中删除用户
|
||||
* 4. 设置用户状态为未登录
|
||||
* 5. 发送删除成功消息给客户端
|
||||
*
|
||||
* @param userId 要删除的用户ID
|
||||
*/
|
||||
private void handleDeleteUserRequest(String userId) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
USER_ONLINE_MAP.remove(this.userId);
|
||||
ServerData.getInstance().removeUser(userId);
|
||||
isLogin = false;
|
||||
this.userId = null;
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_DELETE_ACCOUNT));
|
||||
try {
|
||||
Thread.sleep(100); // 短暂延迟
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
closeClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理初始化所有用户详细信息的请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 从服务器数据中获取所有用户的详细信息
|
||||
* 3. 发送安全的用户详细信息副本给客户端
|
||||
*/
|
||||
private void handleInitUserDetailRequest() {
|
||||
if (!YesLogin())
|
||||
return;
|
||||
|
||||
Map<String, UserData> allUsers = ServerData.getInstance().getServerUsers();
|
||||
Map<String, UserData> safeUsers = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, UserData> entry : allUsers.entrySet()) {
|
||||
// 只发送安全的副本(无密码)
|
||||
safeUsers.put(entry.getKey(), entry.getValue().getSafeCopy());
|
||||
}
|
||||
|
||||
sendToSelf(Wrapper.initUserDetailResponse(safeUsers));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理更新用户详细信息的请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查更新数据是否包含有效用户ID
|
||||
* 3. 从服务器数据中获取原始用户数据
|
||||
* 4. 更新服务器上的用户详细信息
|
||||
* 5. 广播更新给所有在线用户
|
||||
*
|
||||
* @param msg 包含更新用户详细信息的消息
|
||||
*/
|
||||
private void handleUpdateUserDetailRequest(Wrapper msg) {
|
||||
if (!YesLogin())
|
||||
return;
|
||||
|
||||
UserData updatedData = (UserData) msg.getData();
|
||||
if (updatedData == null || !updatedData.getUserId().equals(userId)) {
|
||||
return; // 安全校验:只能更新自己的信息
|
||||
}
|
||||
|
||||
// 获取服务器上的原始数据
|
||||
UserData serverData = ServerData.getInstance().getUserData(userId);
|
||||
if (serverData != null) {
|
||||
// 更新字段
|
||||
serverData.setEmail(updatedData.getEmail());
|
||||
serverData.setBirthday(updatedData.getBirthday());
|
||||
serverData.setAddress(updatedData.getAddress());
|
||||
serverData.setSignature(updatedData.getSignature());
|
||||
// 注意:不更新密码和昵称(有专门的接口),也不更新关系链
|
||||
|
||||
// 广播更新给所有在线用户
|
||||
UserData safeCopy = serverData.getSafeCopy();
|
||||
Wrapper updateMsg = Wrapper.updateUserDetailResponse(safeCopy);
|
||||
|
||||
for (ObjectOutputStream oos : USER_ONLINE_MAP.values()) {
|
||||
try {
|
||||
synchronized (oos) {
|
||||
oos.writeObject(updateMsg);
|
||||
oos.flush();
|
||||
oos.reset();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理申请加入群聊请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查群聊是否存在
|
||||
* 3. 检查用户是否已加入该群聊
|
||||
* 4. 加入群聊
|
||||
* 5. 通知群内其他成员有新人加入
|
||||
*
|
||||
* @param msg 包含群聊ID的消息
|
||||
*/
|
||||
private void handleGroupJoinRequest(Wrapper msg) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
String userId = msg.getSenderId();
|
||||
String groupId = msg.getGroupId();
|
||||
|
||||
GroupData group = ServerData.getInstance().getGroupById(groupId);
|
||||
if (group == null) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_GROUP_JOIN_FAILED));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已经是成员
|
||||
if (group.getMembers().contains(group.new GroupMember(userId))) {
|
||||
// 已经是成员,视为成功
|
||||
sendToSelf(Wrapper.initResponse(group));
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加成员
|
||||
group.addMember(userId);
|
||||
ServerData.getInstance().getUserData(userId).addGroupId(groupId);
|
||||
|
||||
// 1. 通知自己加入成功 (发送群组信息)
|
||||
sendToSelf(Wrapper.initResponse(group));
|
||||
|
||||
// 2. 通知群内其他成员有新人加入
|
||||
sendToGroupExceptSelf(Wrapper.initResponse(group), groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理添加好友请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查好友是否存在
|
||||
* 3. 检查是否已添加该好友
|
||||
* 4. 转发请求给目标用户(如果在线)
|
||||
*
|
||||
* @param msg 包含好友ID的消息
|
||||
*/
|
||||
private void handleFriendAddRequest(Wrapper msg) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
String userId = msg.getSenderId();
|
||||
String friendId = (String) msg.getData(); // 目标好友ID
|
||||
|
||||
if (userId.equals(friendId)) {
|
||||
// 不能添加自己
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_FRIEND_ADD_FAILED));
|
||||
return;
|
||||
}
|
||||
|
||||
UserData friendData = ServerData.getInstance().getUserData(friendId);
|
||||
if (friendData == null) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_FRIEND_ADD_FAILED));
|
||||
return;
|
||||
}
|
||||
|
||||
UserData myData = ServerData.getInstance().getUserData(userId);
|
||||
|
||||
// 检查是否已经是好友
|
||||
if (myData.getFriendIds().contains(friendId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "你们已经是好友了"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果对方在线,转发请求
|
||||
if (USER_ONLINE_MAP.containsKey(friendId)) {
|
||||
try {
|
||||
// 发送给目标用户:Sender=userId, Data=NickName
|
||||
USER_ONLINE_MAP.get(friendId)
|
||||
.writeObject(new Wrapper(myData.getNickname(), userId, null, global.OPT_FRIEND_ADD));
|
||||
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "好友请求已发送"));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "用户不在线,无法发送请求"));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFriendAddAgree(Wrapper msg) {
|
||||
String myId = msg.getSenderId();
|
||||
String friendId = (String) msg.getData(); // 发起者的ID
|
||||
|
||||
UserData myData = ServerData.getInstance().getUserData(myId);
|
||||
UserData friendData = ServerData.getInstance().getUserData(friendId);
|
||||
|
||||
if (friendData == null)
|
||||
return;
|
||||
|
||||
// 双向添加
|
||||
myData.addFriend(friendId);
|
||||
friendData.addFriend(myId);
|
||||
|
||||
// 通知自己成功
|
||||
Map<String, String> friendInfo = new HashMap<>();
|
||||
friendInfo.put(friendId, friendData.getNickname());
|
||||
sendToSelf(new Wrapper(friendInfo, global.SERVER_ACCOUNT, null, global.OPT_FRIEND_ADD_SUCCESS));
|
||||
|
||||
// 通知对方成功
|
||||
if (USER_ONLINE_MAP.containsKey(friendId)) {
|
||||
Map<String, String> myInfo = new HashMap<>();
|
||||
myInfo.put(myId, myData.getNickname());
|
||||
try {
|
||||
USER_ONLINE_MAP.get(friendId).writeObject(
|
||||
new Wrapper(myInfo, global.SERVER_ACCOUNT, null, global.OPT_FRIEND_ADD_SUCCESS));
|
||||
// 还可以发个系统消息提示
|
||||
USER_ONLINE_MAP.get(friendId).writeObject(
|
||||
Wrapper.serverResponse(global.SERVER_MESSAGE, myData.getNickname() + " 同意了你的好友请求"));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理好友添加拒绝请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查好友是否存在
|
||||
* 3. 通知拒绝者好友请求已被拒绝
|
||||
*
|
||||
* @param msg 包含拒绝好友ID的消息
|
||||
*/
|
||||
private void handleFriendAddRefuse(Wrapper msg) {
|
||||
String myId = msg.getSenderId();
|
||||
String friendId = (String) msg.getData(); // 发起者的ID
|
||||
UserData myData = ServerData.getInstance().getUserData(myId);
|
||||
|
||||
if (USER_ONLINE_MAP.containsKey(friendId)) {
|
||||
try {
|
||||
USER_ONLINE_MAP.get(friendId).writeObject(
|
||||
new Wrapper(myData.getNickname(), myId, null, global.OPT_FRIEND_ADD_REFUSE));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理私聊请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查接收者是否存在
|
||||
* 3. 检查接收者是否在线
|
||||
* 4. 转发消息给接收者
|
||||
*
|
||||
* @param msg 包含接收者ID和消息内容的消息
|
||||
*/
|
||||
private void handlePrivateChatRequest(Wrapper msg) {
|
||||
String receiverId = msg.getGroupId(); // 接收者ID
|
||||
|
||||
if (USER_ONLINE_MAP.containsKey(receiverId)) {
|
||||
try {
|
||||
// 转发消息给接收者
|
||||
USER_ONLINE_MAP.get(receiverId).writeObject(msg);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
// 对方不在线
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "对方不在线"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理更新用户信息请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 更新用户昵称
|
||||
* 3. 通知所有连接的客户端更新用户信息
|
||||
*
|
||||
* @param wrapper 包含新昵称的消息
|
||||
*/
|
||||
private void handleUpdateUserNameRequest(Wrapper wrapper) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
String newName = (String) wrapper.getData();
|
||||
ServerData.getInstance().updateUserName(this.userId, newName);
|
||||
|
||||
Map<String, String> idNameMap = new HashMap<>();
|
||||
idNameMap.put(this.userId, newName);
|
||||
|
||||
// OPT_INIT_USER
|
||||
sentToConnectedGroups(Wrapper.initResponse(idNameMap), this.userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理更新用户密码请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 更新用户密码
|
||||
* 3. 通知客户端密码更新成功
|
||||
*
|
||||
* @param wrapper 包含新密码的消息
|
||||
*/
|
||||
private void handleUpdateUserPwdRequest(Wrapper wrapper) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
String newPwd = (String) wrapper.getData();
|
||||
ServerData.getInstance().updateUserPwd(this.userId, newPwd);
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_UPDATE_PASSWORD));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理创建群组请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查群组名称是否为空
|
||||
* 3. 检查群组ID是否重复
|
||||
* 4. 创建新的群组
|
||||
* 5. 通知客户端群组创建成功
|
||||
*
|
||||
* @param groupName 群组名称
|
||||
* @param groupId 群组ID
|
||||
*/
|
||||
private void handleCreateGroupRequest(String groupName, String groupId) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果信息为空
|
||||
if (groupName == null) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_QUEST_WRONG));
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已经有对应的id的群聊
|
||||
if (ServerData.getInstance().containsGroup(groupId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "群聊已存在,id重复"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理数据调用serverData的addGroup
|
||||
GroupData groupData = new GroupData(groupId, groupName, userId);
|
||||
groupData.addMember(userId);
|
||||
ServerData.getInstance().addGroup(groupData);
|
||||
// 调用serverData的addUser
|
||||
ServerData.getInstance().addUserToGroup(groupId, userId);
|
||||
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_GROUP_CREATE_SUCCESS));
|
||||
sendToSelf(Wrapper.initResponse(groupData));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理邀请加入群组请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查邀请人是否是自己
|
||||
* 3. 检查用户是否已在群聊中
|
||||
* 4. 检查用户是否在线
|
||||
* 5. 通知被邀请人加入群组
|
||||
*
|
||||
* @param inviteMsg 包含被邀请人ID和群组ID的消息
|
||||
*/
|
||||
private void handleInviteRequest(Wrapper inviteMsg) {
|
||||
String inviteId = (String) inviteMsg.getData();
|
||||
String theGroupId = inviteMsg.getGroupId();
|
||||
String senderId = inviteMsg.getSenderId();
|
||||
|
||||
// 如果邀请人是自己
|
||||
if (senderId.equals(inviteId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "不能邀请自己加入群聊"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果邀请人已经在群聊中
|
||||
if (ServerData.getInstance().getGroupMembersId(theGroupId).contains(inviteId)) {
|
||||
sendToSelf(Wrapper.serverResponse(global.SERVER_MESSAGE, "该用户已在群聊中"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果用户不在线
|
||||
if (!USER_ONLINE_MAP.containsKey(inviteId)) {
|
||||
sendToSelf(Wrapper.serverResponse(
|
||||
global.SERVER_MESSAGE,
|
||||
"用户(" + ServerData.getInstance().getUserName(inviteId) + ")不在线!"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 通知被邀请人加入群组
|
||||
sendToUser(Wrapper.groupInviteRequest(
|
||||
ServerData.getInstance().getGroupName(theGroupId), senderId, theGroupId),
|
||||
inviteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理加入群组请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查用户是否已在群聊中
|
||||
* 3. 检查群组是否存在
|
||||
* 4. 加入群组
|
||||
* 5. 通知群组内所有人用户加入
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param groupId 群组ID
|
||||
*/
|
||||
private void handleJoinGroupRequest(String userId, String groupId) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
ServerData.getInstance().addUserToGroup(groupId, userId);
|
||||
ServerData.getInstance().addGroupToUser(userId, groupId);
|
||||
|
||||
Wrapper wrapper = Wrapper.initResponse(ServerData.getInstance().getGroupById(groupId));
|
||||
// 向组内所有人推送更新。(人员加入)
|
||||
sendToGroup(wrapper, groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理退出群组请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查用户是否已在群聊中
|
||||
* 3. 从群组中移除用户
|
||||
* 4. 从用户群组列表中移除群组
|
||||
* 5. 如果群组为空,则删除群组
|
||||
* 6. 通知群组内其他用户更新
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param groupId 群组ID
|
||||
*/
|
||||
private void handleQuitGroupRequest(String userId, String groupId) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
ServerData.getInstance().removeUserFromGroup(groupId, userId);
|
||||
ServerData.getInstance().removeGroupFromUser(userId, groupId);
|
||||
|
||||
if (ServerData.getInstance().getGroupById(groupId).getMemberCount() == 0) {
|
||||
// 没有人的群聊就删掉
|
||||
ServerData.getInstance().removeGroup(groupId);
|
||||
} else {
|
||||
// 不为空则需要推送更新
|
||||
Wrapper wrapper = Wrapper.initResponse(ServerData.getInstance().getGroupById(groupId));
|
||||
// 向组内其它所有人推送更新。
|
||||
sendToGroupExceptSelf(wrapper, groupId);
|
||||
}
|
||||
// 退出人也要进行更新
|
||||
sendToSelf(new Wrapper(null, null, groupId, global.OPT_GROUP_QUIT));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理删除群组请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查群组是否存在
|
||||
* 3. 检查用户是否是群组所有者
|
||||
* 4. 删除群组
|
||||
* 5. 通知所有连接的客户端群组已删除
|
||||
*
|
||||
* @param groupId 群组ID
|
||||
*/
|
||||
private void handleDeleteGroupRequest(String groupId) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
|
||||
Wrapper wrapper = new Wrapper(null, null, groupId, global.OPT_GROUP_QUIT);
|
||||
|
||||
// 向所有人推送更新。
|
||||
ServerData.getInstance().removeGroup(groupId);
|
||||
sendToGroup(wrapper, groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理更新群组名称请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 更新群组名称
|
||||
* 3. 通知所有连接的客户端更新群组名称
|
||||
*
|
||||
* @param groupUpdateMsg 包含群组ID和新名称的消息
|
||||
*/
|
||||
private void handleUpdateGroupNameRequest(Wrapper groupUpdateMsg) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
ServerData.getInstance().updateGroupName(groupUpdateMsg.getGroupId(),
|
||||
(String) groupUpdateMsg.getData());
|
||||
sendToGroup(groupUpdateMsg, groupUpdateMsg.getGroupId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理更新群组管理员请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 检查管理员是否是自己
|
||||
* 3. 更新群组管理员
|
||||
* 4. 通知群组内所有成员更新管理员
|
||||
*
|
||||
* @param groupUpdateMsg 包含新管理员ID和群组ID的消息
|
||||
*/
|
||||
private void handleUpdateGroupOwnerRequest(Wrapper groupUpdateMsg) {
|
||||
if (!YesLogin()) {
|
||||
sendToSelf(Wrapper.serverResponse(global.OPT_ERROR_NOT_LOGIN));
|
||||
return;
|
||||
}
|
||||
ServerData.getInstance().updateGroupOwner(groupUpdateMsg.getGroupId(), (String) groupUpdateMsg.getData());
|
||||
sendToGroup(groupUpdateMsg, groupUpdateMsg.getGroupId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理初始化聊天消息请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 遍历用户所属的所有群组
|
||||
* 3. 加载群组聊天记录
|
||||
* 4. 向用户发送群组聊天记录初始化消息
|
||||
*/
|
||||
private void handleInitChatRequest() {
|
||||
for (String groupId : ServerData.getInstance().getUserGroups(userId)) {
|
||||
Wrapper wrapper = Wrapper.initResponse(FileUtil.loadGroupChatMsg(groupId), groupId);
|
||||
sendToSelf(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理初始化群组请求
|
||||
* 1. 检查用户是否已登录
|
||||
* 2. 遍历用户所属的所有群组
|
||||
* 3. 加载群组信息
|
||||
* 4. 向用户发送群组初始化消息
|
||||
*/
|
||||
private void handleInitGroupRequest() {
|
||||
for (String groupId : ServerData.getInstance().getUserGroups(userId)) {
|
||||
Wrapper wrapper = Wrapper.initResponse(ServerData.getInstance().getGroupById(groupId));
|
||||
sendToSelf(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定用户广播消息
|
||||
* 1. 遍历用户ID列表
|
||||
* 2. 检查用户是否在线
|
||||
* 3. 向在线用户发送消息
|
||||
*
|
||||
* @param userIds 接收消息的用户ID列表
|
||||
* @param wrapper 要发送的消息包装对象
|
||||
*/
|
||||
public static void broadcastMsg(String[] userIds, Wrapper wrapper) {
|
||||
for (String userId : userIds) {
|
||||
ObjectOutputStream oos = USER_ONLINE_MAP.get(userId);
|
||||
if (oos == null)
|
||||
continue;
|
||||
try {
|
||||
synchronized (oos) {
|
||||
oos.writeObject(wrapper);
|
||||
oos.flush();
|
||||
oos.reset();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
System.err.println("send error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向用户发送消息
|
||||
* 1. 检查用户是否在线
|
||||
* 2. 向在线用户发送消息
|
||||
*
|
||||
* @param o 要发送的消息包装对象
|
||||
*/
|
||||
private void sendToSelf(Wrapper o) {
|
||||
try {
|
||||
synchronized (oos) {
|
||||
oos.writeObject(o);
|
||||
oos.flush();
|
||||
oos.reset();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
System.out.println("send error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定用户发送消息
|
||||
* 1. 检查用户是否在线
|
||||
* 2. 向在线用户发送消息
|
||||
*
|
||||
* @param wrapper 要发送的消息包装对象
|
||||
* @param userId 接收消息的用户ID
|
||||
*/
|
||||
private void sendToUser(Wrapper wrapper, String userId) {
|
||||
try {
|
||||
ObjectOutputStream oos = USER_ONLINE_MAP.get(userId);
|
||||
if (oos != null) {
|
||||
synchronized (oos) {
|
||||
oos.writeObject(wrapper);
|
||||
oos.flush();
|
||||
oos.reset();
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
System.err.println("send error: " + userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定群聊发送消息
|
||||
* 1. 遍历群聊成员ID列表
|
||||
* 2. 检查成员是否在线
|
||||
* 3. 向在线成员发送消息
|
||||
*
|
||||
* @param wrapper 要发送的消息包装对象
|
||||
* @param groupId 接收消息的群聊ID
|
||||
*/
|
||||
private void sendToGroup(Wrapper wrapper, String groupId) {
|
||||
List<String> members = ServerData.getInstance().getGroupMembersId(groupId);
|
||||
for (String member : members) {
|
||||
sendToUser(wrapper, member);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定群聊发送消息(排除发送者)
|
||||
* 1. 遍历群聊成员ID列表
|
||||
* 2. 检查成员是否在线
|
||||
* 3. 向在线成员发送消息(排除发送者)
|
||||
*
|
||||
* @param wrapper 要发送的消息包装对象
|
||||
* @param groupId 接收消息的群聊ID
|
||||
*/
|
||||
private void sendToGroupExceptSelf(Wrapper wrapper, String groupId) {
|
||||
List<String> members = ServerData.getInstance().getGroupMembersId(groupId);
|
||||
for (String member : members) {
|
||||
if (!member.equals(userId)) {
|
||||
sendToUser(wrapper, member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向用户所属的所有群聊发送消息
|
||||
* 1. 遍历用户所属的所有群组
|
||||
* 2. 向每个群组发送消息
|
||||
*
|
||||
* @param wrapper 要发送的消息包装对象
|
||||
* @param userId 接收消息的用户ID
|
||||
*/
|
||||
private void sentToConnectedGroups(Wrapper wrapper, String userId) {
|
||||
TreeSet<String> groups = ServerData.getInstance().getUserGroups(userId);
|
||||
groups.forEach(groupId -> sendToGroup(wrapper, groupId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package server.serveice;
|
||||
|
||||
import global.global;
|
||||
import server.ServerMainThread;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
public class ClientReceiveThread implements Runnable {
|
||||
private BlockingQueue<Wrapper> messageQueue;
|
||||
private volatile boolean isRunning;
|
||||
|
||||
// 接收线程专属资源:构造器传入,仅用于接收消息
|
||||
private final Socket clientSocket;
|
||||
ObjectInputStream ois;
|
||||
|
||||
// 构造器:初始化套接字资源
|
||||
public ClientReceiveThread(
|
||||
Socket clientSocket,
|
||||
BlockingQueue<Wrapper> messageQueue) throws IOException {
|
||||
|
||||
this.messageQueue = messageQueue;
|
||||
this.clientSocket = clientSocket;
|
||||
|
||||
isRunning = true;
|
||||
}
|
||||
|
||||
// 线程核心:循环接收服务端消息(历史/广播),打印展示
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
ois = new ObjectInputStream(clientSocket.getInputStream());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
// 当主线程和自身都在跑的时候
|
||||
while (ServerMainThread.isRunning() && this.isRunning) {
|
||||
// 接收服务端消息
|
||||
// 接收到后将其添加到阻塞队列中
|
||||
try {
|
||||
Wrapper msg = (Wrapper) ois.readObject();
|
||||
// 如果收到的是关闭信息,则这个循环结束后关闭自身
|
||||
if (msg.getOperation() == global.OPT_LOGOUT) {
|
||||
isRunning = false;
|
||||
System.out.println("接受线程已结束");
|
||||
}
|
||||
// 添加信息到阻塞队列。
|
||||
messageQueue.put(msg);
|
||||
} catch (InterruptedException e) {
|
||||
System.out.println("消息队列被中断");
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
if (isConnectionClosed(e)) {
|
||||
// 链接断开则结束链接
|
||||
// isRunning =false;
|
||||
System.out.println(clientSocket.getPort() + ": 连接已断开");
|
||||
break;
|
||||
} else {
|
||||
System.out.println("发送消息时发生IO异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConnectionClosed(IOException e) {
|
||||
// 根据异常类型判断连接是否断开
|
||||
return e instanceof SocketException ||
|
||||
e.getMessage() != null && (e.getMessage().contains("Connection reset") ||
|
||||
e.getMessage().contains("Broken pipe") ||
|
||||
e.getMessage().contains("Connection refused") ||
|
||||
e.getMessage().contains("Software caused connection abort"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package server.serveice;
|
||||
|
||||
import global.global;
|
||||
import server.data.GroupData;
|
||||
import server.data.UserData;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
// 打包类,用于将数据打包传输
|
||||
|
||||
/**
|
||||
* Wrapper类的构造方法这里设置为私有,需要时请使用/创建对应的静态构造方法来获取对应的信息载体。
|
||||
* 详情请查阅源代码。
|
||||
*/
|
||||
public class Wrapper implements Serializable {
|
||||
private static final long serialVersionUID = 7499350690768481854L;
|
||||
|
||||
private Object data;
|
||||
|
||||
private String senderId;
|
||||
private String groupId;
|
||||
private int operation;
|
||||
|
||||
public Wrapper(Object data, String senderId, String groupId, int operation) {
|
||||
|
||||
this.data = data;
|
||||
this.senderId = senderId;
|
||||
this.groupId = groupId;
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
public Object getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public String getSenderId() {
|
||||
return senderId;
|
||||
}
|
||||
|
||||
public String getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public int getOperation() {
|
||||
return operation;
|
||||
}
|
||||
|
||||
// 一下静态方法都是用于便捷创建消息体的方法。字如其名,当你需要创造相应的信息载体的时候,请使用对应的方法。
|
||||
// 如果有其它需要,请在这里手动添加新的静态方法用于创建对应数据类别。
|
||||
// 需要注意的时,有有一部分的类型的信息体是客户端和服务器端都可以发送的,但是他们发送具有不同的含义。
|
||||
|
||||
// 注册请求
|
||||
public static Wrapper registerRequest(String[] nickNameAndpwsd, String senderId) {
|
||||
return new Wrapper(nickNameAndpwsd, senderId, null, global.OPT_REGISTER);
|
||||
}
|
||||
|
||||
// 注册请求
|
||||
public static Wrapper registerRequest(String senderId, String password, String username) {
|
||||
String[] temp = new String[] { username, password };
|
||||
return new Wrapper(temp, senderId, null, global.OPT_REGISTER);
|
||||
}
|
||||
|
||||
// 登录请求
|
||||
public static Wrapper loginRequest(String senderId, String password) {
|
||||
return new Wrapper(password, senderId, null, global.OPT_LOGIN);
|
||||
}
|
||||
|
||||
// 服务器回复,用于回复一些简单的关于操作流的信息
|
||||
public static Wrapper serverResponse(int opt) {
|
||||
return new Wrapper(null, global.SERVER_ACCOUNT, null, opt);
|
||||
}
|
||||
|
||||
// 服务器回复,带文本信息
|
||||
public static Wrapper serverResponse(int opt, String msg) {
|
||||
return new Wrapper(msg, global.SERVER_ACCOUNT, null, opt);
|
||||
}
|
||||
|
||||
// 构造一个只包含操作码的消息(用于 OPT_EXIT 等)
|
||||
public Wrapper(int operation) {
|
||||
this.data = null;
|
||||
this.senderId = global.SERVER_ACCOUNT;
|
||||
this.groupId = null;
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
// 登出请求
|
||||
public static Wrapper logoutRequest(String senderId) {
|
||||
return new Wrapper(null, senderId, null, global.OPT_LOGOUT);
|
||||
}
|
||||
|
||||
// 初始化请求,请求服务器发送账户和当前的群聊数据
|
||||
public static Wrapper initRequest(String senderId, int opt) {
|
||||
return new Wrapper(null, senderId, null, opt);
|
||||
}
|
||||
|
||||
// 初始化回复,将群聊信息回复给客户端
|
||||
public static Wrapper initResponse(GroupData groupData) {
|
||||
return new Wrapper(groupData, global.SERVER_ACCOUNT, null, global.OPT_INIT_GROUP);
|
||||
}
|
||||
|
||||
// 初始化回复,将聊天记录回复给客户端
|
||||
public static Wrapper initResponse(List<String> chatRecords, String groupId) {
|
||||
return new Wrapper(chatRecords, global.SERVER_ACCOUNT, groupId, global.OPT_INIT_CHAT);
|
||||
}
|
||||
|
||||
// 将用户id/名字回复给客户端。
|
||||
public static Wrapper initResponse(Map<String, String> idNameMap) {
|
||||
return new Wrapper(idNameMap, global.SERVER_ACCOUNT, null, global.OPT_INIT_USER);
|
||||
}
|
||||
|
||||
// 更新用户昵称请求
|
||||
public static Wrapper updateUserNameRequest(String senderId, String nickName) {
|
||||
return new Wrapper(nickName, senderId, null, global.OPT_UPDATE_NICKNAME);
|
||||
}
|
||||
|
||||
// 申请加入群聊
|
||||
public static Wrapper joinGroupRequest(String senderId, String groupId) {
|
||||
return new Wrapper(null, senderId, groupId, global.OPT_GROUP_JOIN);
|
||||
}
|
||||
|
||||
// 申请添加好友
|
||||
public static Wrapper addFriendRequest(String senderId, String friendId) {
|
||||
// friendId 放在 data 中
|
||||
return new Wrapper(friendId, senderId, null, global.OPT_FRIEND_ADD);
|
||||
}
|
||||
|
||||
// 更新用户密码请求
|
||||
public static Wrapper updateUserPwdRequest2(String senderId, String newPwd) {
|
||||
return new Wrapper(newPwd, senderId, null, global.OPT_UPDATE_PASSWORD);
|
||||
}
|
||||
|
||||
// 创建群聊请求
|
||||
public static Wrapper createGroupRequest(String senderId, String groupName, String groupId) {
|
||||
return new Wrapper(groupName, senderId, groupId, global.OPT_GROUP_CREATE);
|
||||
}
|
||||
|
||||
// 邀请加入群聊请求
|
||||
public static Wrapper groupInviteRequest(String invitedIdOrGroupName, String senderId, String groupId) {
|
||||
return new Wrapper(invitedIdOrGroupName, senderId, groupId, global.OPT_GROUP_INVITE);
|
||||
}
|
||||
|
||||
// 退出群聊请求
|
||||
public static Wrapper groupQuitRequest(String senderId, String groupId) {
|
||||
return new Wrapper(null, senderId, groupId, global.OPT_GROUP_QUIT);
|
||||
}
|
||||
|
||||
// 退出群聊响应
|
||||
public static Wrapper groupQuitResponse(String quitMemberId, String groupId) {
|
||||
return new Wrapper(quitMemberId, global.SERVER_ACCOUNT, groupId, global.OPT_GROUP_QUIT);
|
||||
}
|
||||
|
||||
// 解散群聊请求
|
||||
public static Wrapper groupDisbandRequest(String senderId, String groupId) {
|
||||
return new Wrapper(null, senderId, groupId, global.OPT_GROUP_DISBAND);
|
||||
}
|
||||
|
||||
// 更新群聊名字请求
|
||||
public static Wrapper groupUpdateNameRequest(String senderId, String groupId, String groupName) {
|
||||
return new Wrapper(groupName, senderId, groupId, global.OPT_GROUP_UPDATE_NAME);
|
||||
}
|
||||
|
||||
// 更新群聊群主请求
|
||||
public static Wrapper groupUpdateOwnerRequest(String senderId, String groupId, String ownerId) {
|
||||
return new Wrapper(ownerId, senderId, groupId, global.OPT_GROUP_UPDATE_OWNER);
|
||||
}
|
||||
|
||||
// 聊天信息
|
||||
public static Wrapper groupChat(String text, String senderId, String groupId) {
|
||||
return new Wrapper(text, senderId, groupId, global.OPT_CHAT);
|
||||
}
|
||||
|
||||
// 私聊信息
|
||||
public static Wrapper privateChat(String text, String senderId, String receiverId) {
|
||||
// 私聊数据存储在 data/friends/chat_data 中
|
||||
// 为了复用字段,我们将 receiverId 放在 groupId 字段中作为目标ID
|
||||
return new Wrapper(text, senderId, receiverId, global.OPT_PRIVATE_CHAT);
|
||||
}
|
||||
|
||||
// 同意添加好友
|
||||
public static Wrapper friendAddAgree(String senderId, String friendId) {
|
||||
return new Wrapper(friendId, senderId, null, global.OPT_FRIEND_ADD_AGREE);
|
||||
}
|
||||
|
||||
// 拒绝添加好友
|
||||
public static Wrapper friendAddRefuse(String senderId, String friendId) {
|
||||
return new Wrapper(friendId, senderId, null, global.OPT_FRIEND_ADD_REFUSE);
|
||||
}
|
||||
|
||||
// 创建简单指令回复
|
||||
public static Wrapper simpleRequest(String senderId, String groupId, int opt) {
|
||||
return new Wrapper(null, senderId, groupId, opt);
|
||||
}
|
||||
|
||||
// 初始化用户详细信息回复(Map<String, UserData>)
|
||||
public static Wrapper initUserDetailResponse(Map<String, UserData> userDetails) {
|
||||
return new Wrapper(userDetails, global.SERVER_ACCOUNT, null, global.OPT_INIT_USER_DETAIL);
|
||||
}
|
||||
|
||||
// 更新用户详细信息请求
|
||||
public static Wrapper updateUserDetailRequest(String senderId, UserData userData) {
|
||||
return new Wrapper(userData, senderId, null, global.OPT_UPDATE_USER_DETAIL);
|
||||
}
|
||||
|
||||
// 更新用户详细信息响应(服务端广播)
|
||||
public static Wrapper updateUserDetailResponse(UserData userData) {
|
||||
return new Wrapper(userData, global.SERVER_ACCOUNT, null, global.OPT_UPDATE_USER_DETAIL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package test;
|
||||
|
||||
import server.data.ServerData;
|
||||
import util.FileUtil;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class MainTest {
|
||||
|
||||
@Test
|
||||
public void testLoadEmptyFile() throws IOException {
|
||||
// 测试加载空文件的情况
|
||||
Path dataPath = Paths.get(FileUtil.DATA_FILE, FileUtil.GROUPS_DIR, FileUtil.SERVER_DATA_FILENAME);
|
||||
File folder = dataPath.getParent().toFile();
|
||||
if (!folder.exists()) {
|
||||
folder.mkdirs();
|
||||
}
|
||||
|
||||
// 创建空文件
|
||||
Files.write(dataPath, new byte[0]);
|
||||
|
||||
System.out.println("Created empty data file: " + dataPath.toAbsolutePath());
|
||||
|
||||
// 重置实例,强制重新加载
|
||||
ServerData.resetInstance();
|
||||
|
||||
// 测试获取实例是否返回非空对象
|
||||
ServerData data = ServerData.getInstance();
|
||||
|
||||
assertNotNull(data);
|
||||
assertNotNull(data.getServerGroups()); // 确保群组映射不为空
|
||||
// assertTrue(data.getServerGroups().isEmpty()); // 可能会初始化空映射
|
||||
|
||||
System.out.println("Test passed: Empty file handled correctly.");
|
||||
|
||||
// 删除测试文件
|
||||
Files.deleteIfExists(dataPath);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadMissingFile() {
|
||||
// 测试加载缺失文件的情况
|
||||
Path dataPath = Paths.get(FileUtil.DATA_FILE, FileUtil.GROUPS_DIR, FileUtil.SERVER_DATA_FILENAME);
|
||||
try {
|
||||
Files.deleteIfExists(dataPath);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
System.out.println("Ensured data file is missing.");
|
||||
|
||||
// 重置实例,强制重新加载
|
||||
ServerData.resetInstance();
|
||||
|
||||
// 测试获取实例是否返回非空对象
|
||||
ServerData data = ServerData.getInstance();
|
||||
|
||||
assertNotNull(data);
|
||||
assertNotNull(data.getServerGroups()); // 确保群组映射不为空
|
||||
assertTrue(data.getServerGroups().isEmpty()); // 可能会初始化空映射
|
||||
|
||||
System.out.println("Test passed: Missing file handled correctly.");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadValidFile() throws IOException {
|
||||
// 测试加载有效文件的情况
|
||||
System.out.println("Starting testLoadValidFile...");
|
||||
// 测试加载有效文件的情况
|
||||
Path dataPath = Paths.get(FileUtil.DATA_FILE, FileUtil.GROUPS_DIR, FileUtil.SERVER_DATA_FILENAME);
|
||||
try {
|
||||
Files.deleteIfExists(dataPath);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// 初始化 ServerData 实例,确保其为空
|
||||
ServerData.resetInstance();
|
||||
ServerData data = ServerData.getInstance();
|
||||
System.out.println("Initialized ServerData for saving.");
|
||||
|
||||
// 保存 ServerData 实例,确保文件存在
|
||||
data.saveServerData();
|
||||
System.out.println("Saved ServerData.");
|
||||
|
||||
// 重置实例,强制重新加载
|
||||
ServerData.resetInstance();
|
||||
System.out.println("Reset instance. Now calling getInstance() to trigger load...");
|
||||
|
||||
// 测试获取实例是否返回非空对象
|
||||
// 确保加载的 ServerData 实例不为空
|
||||
ServerData loaded = ServerData.getInstance();
|
||||
|
||||
assertNotNull(loaded);
|
||||
System.out.println("Test passed: Valid file loaded correctly.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package util;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class DataObj implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String id;
|
||||
private String name;
|
||||
private String message;
|
||||
|
||||
public DataObj(){}
|
||||
|
||||
public DataObj(String id, String name, String message) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package util;
|
||||
|
||||
import server.data.ServerData;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
|
||||
/*
|
||||
*通用文件IO工具类
|
||||
*将聊天记录等信息写入本地文件
|
||||
*/
|
||||
public class FileUtil {
|
||||
// 数据文件夹
|
||||
public static final String DATA_FILE = "data";
|
||||
|
||||
// 子文件夹
|
||||
public static final String GROUPS_DIR = "groups";
|
||||
public static final String FRIENDS_DIR = "friends";
|
||||
|
||||
// 服务器数据文件名
|
||||
public static final String SERVER_DATA_FILENAME = "server_data.data";
|
||||
|
||||
// 聊天历史消息存储文件夹名
|
||||
public static final String CHAT_DATA_DIRNAME = "chat_data";
|
||||
|
||||
// 辅助方法:获取Group ServerData路径
|
||||
private static Path getGroupServerDataPath() {
|
||||
return Paths.get(DATA_FILE, GROUPS_DIR, SERVER_DATA_FILENAME);
|
||||
}
|
||||
|
||||
// 辅助方法:获取Friend ServerData路径
|
||||
private static Path getFriendServerDataPath() {
|
||||
return Paths.get(DATA_FILE, FRIENDS_DIR, SERVER_DATA_FILENAME);
|
||||
}
|
||||
|
||||
// 辅助方法:获取Chat Data路径
|
||||
private static Path getChatDataPath(String id, boolean isGroup) {
|
||||
String subDir = isGroup ? GROUPS_DIR : FRIENDS_DIR;
|
||||
return Paths.get(DATA_FILE, subDir, CHAT_DATA_DIRNAME, id + ".txt");
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文件中的serverData信息,返回ServerData对象
|
||||
* 从groups和friends文件夹分别读取并合并
|
||||
*/
|
||||
public static ServerData loadServerData() {
|
||||
ServerData finalData = new ServerData();
|
||||
boolean loadedAny = false;
|
||||
|
||||
// 1. Load Group Data
|
||||
ServerData groupData = loadDataFromFile(getGroupServerDataPath());
|
||||
if (groupData != null) {
|
||||
if (groupData.getServerGroups() != null) {
|
||||
finalData.setServerGroups(groupData.getServerGroups());
|
||||
}
|
||||
loadedAny = true;
|
||||
}
|
||||
|
||||
// 2. 加载好友数据
|
||||
ServerData friendData = loadDataFromFile(getFriendServerDataPath());
|
||||
if (friendData != null) {
|
||||
if (friendData.getServerUsers() != null) {
|
||||
finalData.setServerUsers(friendData.getServerUsers());
|
||||
}
|
||||
loadedAny = true;
|
||||
}
|
||||
|
||||
return loadedAny ? finalData : null;
|
||||
}
|
||||
|
||||
private static ServerData loadDataFromFile(Path path) {
|
||||
if (!Files.exists(path) || path.toFile().length() == 0)
|
||||
return null;
|
||||
try (ObjectInputStream ois = new ObjectInputStream(
|
||||
new BufferedInputStream(new FileInputStream(path.toFile())))) {
|
||||
return (ServerData) ois.readObject();
|
||||
} catch (Exception e) {
|
||||
System.err.println("加载数据失败 (" + path + "): " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用静态方法获取serverData对象,将serverData中的信息写入文件中
|
||||
*/
|
||||
public static void saveServerData() {
|
||||
ServerData data = ServerData.getInstance();
|
||||
|
||||
// Save Groups
|
||||
ServerData groupData = new ServerData();
|
||||
groupData.setServerGroups(data.getServerGroups());
|
||||
// 清空 Users,避免重复存储到groups文件
|
||||
groupData.setServerUsers(null);
|
||||
saveDataToFile(getGroupServerDataPath(), groupData);
|
||||
|
||||
// Save Friends
|
||||
ServerData friendData = new ServerData();
|
||||
friendData.setServerUsers(data.getServerUsers());
|
||||
// 清空 Groups,避免重复存储到friends文件
|
||||
friendData.setServerGroups(null);
|
||||
saveDataToFile(getFriendServerDataPath(), friendData);
|
||||
|
||||
System.out.println("服务器数据保存成功");
|
||||
}
|
||||
|
||||
private static void saveDataToFile(Path path, ServerData data) {
|
||||
File file = path.toFile();
|
||||
if (!file.getParentFile().exists()) {
|
||||
file.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
synchronized (ServerData.class) {
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(
|
||||
new BufferedOutputStream(new FileOutputStream(file)))) {
|
||||
oos.writeObject(data);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据文件是否存在 (任意一个存在即认为存在)
|
||||
*/
|
||||
public static boolean isDataFileExists() {
|
||||
return Files.exists(getGroupServerDataPath()) || Files.exists(getFriendServerDataPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据文件大小(字节)
|
||||
*/
|
||||
public static long getDataFileSize() {
|
||||
long size = 0;
|
||||
File gFile = getGroupServerDataPath().toFile();
|
||||
if (gFile.exists())
|
||||
size += gFile.length();
|
||||
File fFile = getFriendServerDataPath().toFile();
|
||||
if (fFile.exists())
|
||||
size += fFile.length();
|
||||
return size;
|
||||
}
|
||||
|
||||
// ===================================聊天信息=======================
|
||||
|
||||
/**
|
||||
* 将聊天信息追加写入文件
|
||||
*
|
||||
* @param groupId 群聊id 或 用户id
|
||||
* @param senderId 发送者id
|
||||
* @param senderName 发送者名字
|
||||
* @param content 发送内容
|
||||
*/
|
||||
public static void addChatMessage(String groupId, String senderId, String senderName, String content) {
|
||||
String msg = MsgUtil.combineMsg(senderId, senderName, content);
|
||||
writeChatMsg(groupId, msg);
|
||||
}
|
||||
|
||||
public static void addChatMessage(String groupId, String contentFromWrapper) {
|
||||
writeChatMsg(groupId, contentFromWrapper);
|
||||
}
|
||||
|
||||
private static void writeChatMsg(String id, String content) {
|
||||
// 判断是群聊还是私聊
|
||||
// 通过 ServerGroups 来判断,如果在群组Map中,则是群聊,否则认为是私聊
|
||||
boolean isGroup = false;
|
||||
if (ServerData.getInstance().getServerGroups() != null) {
|
||||
isGroup = ServerData.getInstance().getServerGroups().containsKey(id);
|
||||
}
|
||||
|
||||
Path path = getChatDataPath(id, isGroup);
|
||||
File file = path.toFile();
|
||||
|
||||
if (!file.getParentFile().exists()) {
|
||||
file.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
try (FileWriter fw = new FileWriter(file, true);
|
||||
BufferedWriter bw = new BufferedWriter(fw)) {
|
||||
// 写入文本并换行
|
||||
bw.write(content);
|
||||
bw.newLine(); // 跨平台换行
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取某个群的所有聊天记录
|
||||
*
|
||||
* @return 历史消息列表,无内容/文件不存在返回空List,绝不返回null
|
||||
*/
|
||||
public static List<String> loadGroupChatMsg(String groupId) {
|
||||
// 判断是群聊还是私聊
|
||||
boolean isGroup = false;
|
||||
if (ServerData.getInstance().getServerGroups() != null) {
|
||||
isGroup = ServerData.getInstance().getServerGroups().containsKey(groupId);
|
||||
}
|
||||
|
||||
Path path = getChatDataPath(groupId, isGroup);
|
||||
|
||||
List<String> lines;
|
||||
try {
|
||||
lines = Files.readAllLines(path);
|
||||
} catch (NoSuchFileException e) {
|
||||
lines = new ArrayList<>();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (lines == null) {
|
||||
lines = new ArrayList<>();
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package util;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static global.global.DATA_SPLIT;
|
||||
|
||||
/**
|
||||
* 工具类,用于将聊过天记录拆分/整合
|
||||
*/
|
||||
public class MsgUtil implements Serializable {
|
||||
public static final int SENDERID_POS = 0;
|
||||
public static final int SENDER_NAME_POS = 1;
|
||||
public static final int TIME_POS = 2;
|
||||
public static final int CONTENT_POS = 3;
|
||||
|
||||
private static final Pattern SPLIT_PATTERN = Pattern.compile(Pattern.quote(DATA_SPLIT));
|
||||
|
||||
public static String combineMsg(String senderId, String senderName, String content) {
|
||||
return senderId + DATA_SPLIT + senderName + DATA_SPLIT + content;
|
||||
}
|
||||
|
||||
public static String[] splitMsg(String msg) {
|
||||
if (msg == null) {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
// 使用 -1 作为 limit 参数,保留空字符串
|
||||
return SPLIT_PATTERN.split(msg, -1);
|
||||
}
|
||||
|
||||
public static String getSenderName(String msg) {
|
||||
return splitMsg(msg)[SENDER_NAME_POS];
|
||||
}
|
||||
|
||||
public static String getSenderId(String msg) {
|
||||
return splitMsg(msg)[SENDERID_POS];
|
||||
}
|
||||
|
||||
public static String getTime(String msg) {
|
||||
return splitMsg(msg)[TIME_POS];
|
||||
}
|
||||
|
||||
public static String getContent(String msg) {
|
||||
return splitMsg(msg)[CONTENT_POS];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# LocalChatApp 启动指南
|
||||
|
||||
**注意**:以下命令均假设您的命令行当前所在目录为项目根目录:
|
||||
`d:\Code\doing_exercises\programs\LocalChatApp`
|
||||
|
||||
## 1. 编译项目 (如果代码有更新)
|
||||
如果这是第一次运行或代码有过修改,请先执行编译:
|
||||
```cmd
|
||||
mvn clean compile
|
||||
```
|
||||
|
||||
## 2. 启动服务端
|
||||
在终端中执行以下命令:
|
||||
```cmd
|
||||
java -cp "target\classes;lib\*" server.Server
|
||||
```
|
||||
|
||||
**关闭服务端的方法**:
|
||||
在服务端运行的终端窗口中输入 `exit` 然后按回车键,服务器将会保存数据并安全退出。
|
||||
> 注意:直接关闭窗口可能会导致部分数据丢失。
|
||||
|
||||
## 3. 启动客户端
|
||||
请打开一个新的终端窗口(可打开多个以模拟多用户),执行以下命令:
|
||||
```cmd
|
||||
java -cp "target\classes;lib\*" client.Client
|
||||
```
|
||||
|
||||
## 4. 运行打包后的程序
|
||||
|
||||
本项目支持通过 `jpackage` 和 Inno Setup 打包生成的独立可执行文件。
|
||||
|
||||
### 客户端 (独立可执行文件)
|
||||
位于 `dist\LocalChatClient\LocalChatClient.exe`。
|
||||
双击即可运行,无需预先安装 JRE,因为已内置运行时环境。
|
||||
|
||||
### 安装包
|
||||
位于 `dist\Output\LocalChatApp_Setup.exe`。
|
||||
运行该安装程序可以将客户端安装到系统中,并创建桌面快捷方式。
|
||||
Reference in New Issue
Block a user