first commit
156
.gitignore
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# idea
|
||||
.idea/
|
||||
migrations/
|
||||
|
||||
# node
|
||||
web/.DS_Store
|
||||
web/node_modules
|
||||
web/dist
|
||||
|
||||
# local env files
|
||||
web/.env.local
|
||||
web/.env.*.local
|
||||
|
||||
# Log files
|
||||
web/npm-debug.log*
|
||||
web/yarn-debug.log*
|
||||
web/yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
web/.idea
|
||||
web/.vscode
|
||||
web/*.suo
|
||||
web/*.ntvs*
|
||||
web/*.njsproj
|
||||
web/*.sln
|
||||
web/*.sw?
|
||||
|
||||
webAdmin/.DS_Store
|
||||
webAdmin/node_modules
|
||||
webAdmin/dist
|
||||
|
||||
# local env files
|
||||
webAdmin/.env.local
|
||||
webAdmin/.env.*.local
|
||||
|
||||
# Log files
|
||||
webAdmin/npm-debug.log*
|
||||
webAdmin/yarn-debug.log*
|
||||
webAdmin/yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
webAdmin/.idea
|
||||
webAdmin/.vscode
|
||||
webAdmin/*.suo
|
||||
webAdmin/*.ntvs*
|
||||
webAdmin/*.njsproj
|
||||
webAdmin/*.sln
|
||||
webAdmin/*.sw?
|
||||
|
||||
|
||||
|
||||
674
LICENSE
Normal file
@@ -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>.
|
||||
51
README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# super-bbs
|
||||
一个基于Flask的bbs论坛类项目
|
||||
前端有用户和管理员两套界面
|
||||
|
||||
生产环境启动命令:
|
||||
|
||||
`gunicorn -w 8 -k gevent --log-level warning -b 0.0.0.0:8000 prod_run:app`
|
||||
|
||||
|
||||
### 声明: 严重高仿(照抄)V2EX
|
||||
### 开发原因: 前后端分离,替换原来的FakeV2EX项目
|
||||
### 用户界面图片展示:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
## 管理界面图片展示:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
73
celery_app.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from datetime import datetime
|
||||
from celery import Celery, Task
|
||||
from celery.schedules import crontab
|
||||
from super_bbs.app import create_app
|
||||
from super_bbs.model.users import CeleryTaskLogs
|
||||
|
||||
flask_app = create_app()
|
||||
flask_app.app_context().push()
|
||||
|
||||
schedule_config = {
|
||||
'CELERYBEAT_SCHEDULE': {
|
||||
'clean_celery_log': {
|
||||
'task': 'super_bbs.controller.account.tasks.clean_celery_log',
|
||||
'schedule': 10 if flask_app.config['DEBUG'] else crontab(minute=10, hour=3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class BaseTask(Task):
|
||||
"""
|
||||
celery 基类, 继承Task
|
||||
"""
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
log_obj = CeleryTaskLogs()
|
||||
log_obj.task_id = self.request.id
|
||||
log_obj.task_name = self.name
|
||||
log_obj.save()
|
||||
return super(BaseTask, self).__call__(*args, **kwargs)
|
||||
|
||||
def on_success(self, retval, task_id, args, kwargs):
|
||||
log_obj = CeleryTaskLogs.get_by_query(task_id=task_id)
|
||||
log_obj.done = True
|
||||
log_obj.time_done = datetime.now()
|
||||
log_obj.task_status = True
|
||||
if retval:
|
||||
log_obj.retval = str(retval)
|
||||
if args:
|
||||
log_obj.args = str(args)
|
||||
if kwargs:
|
||||
log_obj.kwargs = str(kwargs)
|
||||
log_obj.save()
|
||||
|
||||
return super(BaseTask, self).on_success(retval, task_id, args, kwargs)
|
||||
|
||||
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
||||
print('fail task: {0}'.format(task_id))
|
||||
log_obj = CeleryTaskLogs.get_by_query(task_id=task_id)
|
||||
log_obj.done = True
|
||||
log_obj.time_done = datetime.now()
|
||||
log_obj.task_status = False
|
||||
if args:
|
||||
log_obj.args = str(args)
|
||||
if kwargs:
|
||||
log_obj.kwargs = str(kwargs)
|
||||
if exc:
|
||||
log_obj.exc = str(exc)
|
||||
if einfo:
|
||||
log_obj.einfo = str(einfo)
|
||||
log_obj.save()
|
||||
|
||||
return super(BaseTask, self).on_failure(exc, task_id, args, kwargs, einfo)
|
||||
|
||||
|
||||
celery = Celery(flask_app.import_name, task_cls=BaseTask)
|
||||
celery.conf.update(flask_app.config)
|
||||
celery.conf.update(schedule_config)
|
||||
|
||||
# 导入task注册
|
||||
celery.autodiscover_tasks([
|
||||
'super_bbs.controller.account.task'
|
||||
])
|
||||
BIN
docs/pic/admin1.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
docs/pic/admin2.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
docs/pic/admin3.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/pic/admin4.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
docs/pic/admin5.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
docs/pic/admin6.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/pic/bbs1.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/pic/bbs10.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
docs/pic/bbs11.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
docs/pic/bbs12.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/pic/bbs13.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/pic/bbs14.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/pic/bbs2.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/pic/bbs3.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
docs/pic/bbs6.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/pic/bbs7.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/pic/bbs8.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/pic/bbs9.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
86
manage.py
Executable file
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
import json
|
||||
from flask_script import Manager
|
||||
from flask_migrate import MigrateCommand
|
||||
from super_bbs.app import create_app
|
||||
from super_bbs.model.users import Users
|
||||
from super_bbs.model.tabs import Tabs, SubTabs
|
||||
from super_bbs.constants import BASE_DIR
|
||||
|
||||
app = create_app()
|
||||
|
||||
manager = Manager(app)
|
||||
|
||||
manager.add_command('db', MigrateCommand)
|
||||
|
||||
|
||||
@manager.option('--username', dest='username', help='admin username', default='admin')
|
||||
@manager.option('--password', dest='password', help='admin password', default='admin')
|
||||
def init_app(username, password):
|
||||
# check
|
||||
if Users.filter_by_query(role_id=1).first():
|
||||
print('重复初始化!退出')
|
||||
exit(1)
|
||||
# create admin
|
||||
user_obj = Users.create_by_uid()
|
||||
user_obj.username = username
|
||||
user_obj.email = 'admin@admin.com'
|
||||
user_obj.role_id = 1
|
||||
user_obj.set_password(password)
|
||||
user_obj.save()
|
||||
# create base tab
|
||||
with open(os.path.join(BASE_DIR, 'tabs.json'), 'r') as f:
|
||||
tab_data = json.loads(f.read())
|
||||
for t in tab_data:
|
||||
t_obj = Tabs.create_by_uid()
|
||||
t_obj.name = t['name']
|
||||
t_obj.zh = t['zh']
|
||||
if t.get('sort_num'):
|
||||
t_obj.sort_num = t['sort_num']
|
||||
t_obj.save()
|
||||
for st in t.get('sub_tabs'):
|
||||
st_obj = SubTabs.create_by_uid()
|
||||
st_obj.tab_id = t_obj.id
|
||||
st_obj.name = st['name']
|
||||
st_obj.zh = st['zh']
|
||||
if st.get('sort_num'):
|
||||
st_obj.sort_num = st['sort_num']
|
||||
st_obj.desc = st['desc']
|
||||
st_obj.save()
|
||||
|
||||
print(f'init success! admin username: {username}, password: {password}')
|
||||
|
||||
|
||||
@manager.command
|
||||
def update_tabs():
|
||||
# update base tab
|
||||
with open(os.path.join(BASE_DIR, 'tabs.json'), 'r') as f:
|
||||
tab_data = json.loads(f.read())
|
||||
for t in tab_data:
|
||||
if not Tabs.filter_by_query(name=t['name']).first():
|
||||
t_obj = Tabs.create_by_uid()
|
||||
else:
|
||||
t_obj = Tabs.get_by_query(name=t['name'])
|
||||
t_obj.name = t['name']
|
||||
t_obj.zh = t['zh']
|
||||
if t.get('sort_num'):
|
||||
t_obj.sort_num = t['sort_num']
|
||||
t_obj.save()
|
||||
for st in t.get('sub_tabs'):
|
||||
if not SubTabs.filter_by_query(name=st['name']).first():
|
||||
st_obj = SubTabs.create_by_uid()
|
||||
else:
|
||||
st_obj = SubTabs.get_by_query(name=st['name'])
|
||||
st_obj.tab_id = t_obj.id
|
||||
st_obj.name = st['name']
|
||||
st_obj.zh = st['zh']
|
||||
if st.get('sort_num'):
|
||||
st_obj.sort_num = st['sort_num']
|
||||
st_obj.desc = st['desc']
|
||||
st_obj.save()
|
||||
|
||||
print('update tabs success !')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
manager.run()
|
||||
25
prod_run.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import sys
|
||||
from gevent.pywsgi import WSGIServer
|
||||
from gevent import monkey
|
||||
|
||||
monkey.patch_all()
|
||||
|
||||
from super_bbs.app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
# run host port
|
||||
host = '0.0.0.0'
|
||||
port = 5000
|
||||
|
||||
if len(sys.argv) >= 2:
|
||||
try:
|
||||
port = int(sys.argv[1])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(f" * Start prod app on: {host}:{port}")
|
||||
print(f" * Debug Mode: {app.config.get('DEBUG')}")
|
||||
http_server = WSGIServer((host, port), app)
|
||||
http_server.serve_forever()
|
||||
39
requirements.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
alembic==1.0.11
|
||||
amqp==2.5.1
|
||||
billiard==3.6.1.0
|
||||
blinker==1.4
|
||||
celery==4.3.0
|
||||
certifi==2019.6.16
|
||||
chardet==3.0.4
|
||||
Click==7.0
|
||||
Flask==1.1.1
|
||||
Flask-Mail==0.9.1
|
||||
Flask-Migrate==2.5.2
|
||||
flask-redis==0.4.0
|
||||
Flask-Script==2.0.6
|
||||
Flask-Session==0.3.1
|
||||
Flask-SQLAlchemy==2.4.0
|
||||
gevent==1.4.0
|
||||
greenlet==0.4.15
|
||||
idna==2.8
|
||||
importlib-metadata==0.19
|
||||
itsdangerous==1.1.0
|
||||
Jinja2==2.10.1
|
||||
kombu==4.6.4
|
||||
Mako==1.1.0
|
||||
MarkupSafe==1.1.1
|
||||
more-itertools==7.2.0
|
||||
Pillow==6.2.0
|
||||
PyMySQL==0.9.3
|
||||
python-dateutil==2.8.0
|
||||
python-editor==1.0.4
|
||||
pytz==2019.2
|
||||
redis==3.3.8
|
||||
requests==2.22.0
|
||||
six==1.12.0
|
||||
SQLAlchemy==1.3.7
|
||||
ujson==1.35
|
||||
urllib3==1.25.3
|
||||
vine==1.3.0
|
||||
Werkzeug==0.15.5
|
||||
zipp==0.6.0
|
||||
BIN
super_bbs/Rocko.ttf
Normal file
1
super_bbs/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
""" core """
|
||||
61
super_bbs/app.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import os
|
||||
from flask import Flask
|
||||
from super_bbs.config import DevConfig, TestConfig, ProdConfig
|
||||
from super_bbs.core.extensions import db, migrate, mail, session, celery, redis_store
|
||||
|
||||
|
||||
def create_app():
|
||||
current_env = os.environ.get('YOUNG_ENV', 'development')
|
||||
print(current_env)
|
||||
if current_env == 'test':
|
||||
config_object = TestConfig
|
||||
elif current_env == 'prod':
|
||||
config_object = ProdConfig
|
||||
else:
|
||||
config_object = DevConfig
|
||||
|
||||
app = Flask(__name__.split('.')[0])
|
||||
app.config.from_object(config_object)
|
||||
register_extensions(app)
|
||||
register_router(app)
|
||||
register_logging(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def register_extensions(app):
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
mail.init_app(app)
|
||||
session.init_app(app)
|
||||
redis_store.init_app(app)
|
||||
celery.config_from_object(app.config)
|
||||
return app
|
||||
|
||||
|
||||
def register_router(app):
|
||||
from super_bbs.router.v1 import routers as v1_routers
|
||||
from super_bbs.router.admin import routers as admin_routers
|
||||
for _r in v1_routers:
|
||||
app.add_url_rule(rule='/api/v1' + _r[0], view_func=_r[1].as_view(name=_r[2]))
|
||||
for _r in admin_routers:
|
||||
app.add_url_rule(rule='/api/admin' + _r[0], view_func=_r[1].as_view(name=_r[2]))
|
||||
return app
|
||||
|
||||
|
||||
def register_logging(app):
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
# Formatter
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s %(levelname)s %(pathname)s %(lineno)s %(module)s.%(funcName)s %(message)s')
|
||||
|
||||
# log dir
|
||||
if not os.path.exists(app.config['LOG_PATH']):
|
||||
os.makedirs(app.config['LOG_PATH'])
|
||||
|
||||
# FileHandler Info
|
||||
file_handler_info = RotatingFileHandler(filename=app.config['LOG_PATH_FILE'])
|
||||
file_handler_info.setFormatter(formatter)
|
||||
file_handler_info.setLevel(logging.INFO)
|
||||
app.logger.addHandler(file_handler_info)
|
||||
73
super_bbs/config.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import os
|
||||
import redis
|
||||
from datetime import timedelta
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
class BaseConfig(object):
|
||||
DEBUG = True
|
||||
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or 's8tar0ku120dm3id0s7sd93lr4'
|
||||
SESSION_COOKIE_NAME = 'token'
|
||||
SESSION_KEY_PREFIX = 'session:'
|
||||
REMEMBER_COOKIE_DURATION = timedelta(days=3)
|
||||
PERMANENT_SESSION_LIFETIME = timedelta(days=3)
|
||||
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||
SQLALCHEMY_COMMIT_TEARDOWN = True
|
||||
SQLALCHEMY_POOL_SIZE = 10
|
||||
# SQLALCHEMY_ECHO = True
|
||||
|
||||
LOG_PATH = os.path.join(BASE_DIR, 'logs')
|
||||
LOG_PATH_FILE = os.path.join(LOG_PATH, 'run.log')
|
||||
LOG_FILE_MAX_BYTES = 100 * 1024 * 1024
|
||||
|
||||
CELERY_TIMEZONE = 'Asia/Shanghai'
|
||||
CELERY_ENABLE_UTC = 'False'
|
||||
|
||||
SESSION_TYPE = 'redis'
|
||||
SESSION_REDIS = redis.Redis(host='127.0.0.1', port=6379, db=0)
|
||||
REDIS_URL = 'redis://127.0.0.1:6379/6'
|
||||
|
||||
MAIL_SERVER = 'smtpdm.aliyun.com'
|
||||
MAIL_PORT = 465
|
||||
MAIL_USE_SSL = True
|
||||
MAIL_USERNAME = 'noreply@mail.izhihu.me'
|
||||
MAIL_DEFAULT_SENDER = 'noreply@mail.izhihu.me'
|
||||
# TODO: change password
|
||||
MAIL_PASSWORD = ''
|
||||
|
||||
@staticmethod
|
||||
def init_app(app):
|
||||
pass
|
||||
|
||||
|
||||
class DevConfig(BaseConfig):
|
||||
BROKER_URL = 'redis://127.0.0.1:6379/1'
|
||||
PRIVATE_KEY_PATH = '/home/yang/.ssh/id_rsa'
|
||||
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://flask:123456@localhost:3306/super_bbs?charset=utf8mb4'
|
||||
|
||||
|
||||
class TestConfig(BaseConfig):
|
||||
BROKER_URL = 'redis://127.0.0.1:6379/1'
|
||||
PRIVATE_KEY_PATH = '/home/yang/.ssh/id_rsa'
|
||||
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://flask:123456@localhost:3306/super_bbs?charset=utf8mb4'
|
||||
|
||||
|
||||
class ProdConfig(BaseConfig):
|
||||
DEBUG = False
|
||||
SESSION_TYPE = 'redis'
|
||||
SESSION_REDIS = redis.Redis(host='127.0.0.1', port=6379, db=0)
|
||||
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://flask:123456@localhost:3306/super_bbs?charset=utf8mb4'
|
||||
BROKER_URL = 'redis://127.0.0.1:6379/1'
|
||||
REDIS_URL = 'redis://127.0.0.1:6379/6'
|
||||
PRIVATE_KEY_PATH = '/home/yang/.ssh/id_rsa'
|
||||
|
||||
MAIL_SERVER = 'smtpdm.aliyun.com'
|
||||
MAIL_PORT = 465
|
||||
MAIL_USE_SSL = True
|
||||
MAIL_USERNAME = 'noreply@mail.izhihu.me'
|
||||
MAIL_DEFAULT_SENDER = 'noreply@mail.izhihu.me'
|
||||
# TODO: change password
|
||||
MAIL_PASSWORD = ''
|
||||
27
super_bbs/constants.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
web_title = 'super_bbs'
|
||||
web_desc = "这是另一个FakeV2EX"
|
||||
|
||||
# 是否打开操作记录
|
||||
open_record_log = True
|
||||
|
||||
# 预定义角色
|
||||
"""
|
||||
admin 超级管理员 1
|
||||
"""
|
||||
|
||||
|
||||
# 释放锁lua脚本
|
||||
release_lock_script = """
|
||||
if redis.call('get', KEYS[1]) == ARGV[1]
|
||||
then
|
||||
return redis.call('del', KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
"""
|
||||
|
||||
default_password = '7758521'
|
||||
82
super_bbs/controller/admin/account/api.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from flask import session, current_app
|
||||
from super_bbs.core.viewhandler import ApiViewHandler
|
||||
from super_bbs.model.users import Users, Passport, UserFavUser
|
||||
from super_bbs.model.topics import TopicFav
|
||||
from super_bbs.model.tabs import SubTabFav
|
||||
from super_bbs.utils import need_params, login_required
|
||||
from .wrapper import check_login
|
||||
|
||||
|
||||
class LoginAPI(ApiViewHandler):
|
||||
@need_params(*['username', 'password'])
|
||||
def post(self):
|
||||
user_obj = check_login(username=self.input.username, password=self.input.password)
|
||||
user_dict = user_obj.to_dict(remove_fields_list=['password'])
|
||||
session['is_login'] = True
|
||||
session['role_id'] = user_dict['role_id']
|
||||
session['user_info'] = user_dict
|
||||
# add token to passport
|
||||
passport = Passport.create_or_update(
|
||||
query_dict={'user_id': user_obj.id},
|
||||
update_dict={'token': session.sid}
|
||||
)
|
||||
passport.expire = self.get_datetime_now() + current_app.config['REMEMBER_COOKIE_DURATION']
|
||||
passport.save()
|
||||
|
||||
return user_dict
|
||||
|
||||
|
||||
class LoginOutAPI(ApiViewHandler):
|
||||
@login_required(admin=True)
|
||||
def get(self):
|
||||
session.clear()
|
||||
|
||||
def post(self):
|
||||
return self.get()
|
||||
|
||||
|
||||
class LoginStatusCheck(ApiViewHandler):
|
||||
def get(self):
|
||||
user_info = session.get('user_info')
|
||||
if user_info:
|
||||
user_obj = Users.get_by_id(user_info['id'])
|
||||
user_dict = user_obj.to_dict(remove_fields_list=['password'])
|
||||
user_dict['fav_sub_tab_count'] = SubTabFav.filter_by_query(user_id=user_dict['id']).count()
|
||||
user_dict['fav_topic_count'] = TopicFav.filter_by_query(user_id=user_dict['id']).count()
|
||||
user_dict['fav_user_count'] = UserFavUser.filter_by_query(user_id=user_dict['id']).count()
|
||||
session['user_info'] = user_dict
|
||||
return user_dict
|
||||
|
||||
|
||||
class ProfileAPI(ApiViewHandler):
|
||||
@login_required(admin=True)
|
||||
def get(self):
|
||||
user_obj = Users.get_by_id(session['user_info']['id'])
|
||||
user_dict = user_obj.to_dict(remove_fields_list=['password'])
|
||||
user_dict['fav_sub_tab_count'] = SubTabFav.filter_by_query(user_id=user_dict['id']).count()
|
||||
user_dict['fav_topic_count'] = TopicFav.filter_by_query(user_id=user_dict['id']).count()
|
||||
user_dict['fav_user_count'] = UserFavUser.filter_by_query(user_id=user_dict['id']).count()
|
||||
session['user_info'] = user_dict
|
||||
return user_dict
|
||||
|
||||
@login_required(admin=True)
|
||||
def post(self):
|
||||
user_info = session.get('user_info')
|
||||
user_obj = Users.get_by_uid(user_info['uid'])
|
||||
|
||||
update_dict = dict()
|
||||
|
||||
for k in ['sex', 'avatar_url', 'site', 'location', 'company', 'github', 'twitter', 'weibo', 'bio']:
|
||||
if getattr(self.input, k) is not None:
|
||||
update_dict[k] = getattr(self.input, k)
|
||||
if self.input.sex in [0, 1, 2]:
|
||||
update_dict['sex'] = self.input.sex
|
||||
|
||||
if self.input.privacy_level in [0, 1, 2]:
|
||||
update_dict['privacy_level'] = self.input.privacy_level
|
||||
|
||||
user_obj.update(**update_dict)
|
||||
user_info.update(update_dict)
|
||||
session['user_info'] = user_info
|
||||
|
||||
return user_info
|
||||
15
super_bbs/controller/admin/account/wrapper.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from super_bbs.model.users import Users
|
||||
from super_bbs.core.basehandler import AuthError
|
||||
|
||||
|
||||
def check_login(username, password):
|
||||
user_obj = Users.filter_by_query(username=username).first()
|
||||
if not user_obj:
|
||||
raise AuthError("用户不存在")
|
||||
if not user_obj.check_password(password):
|
||||
raise AuthError("密码错误")
|
||||
if not user_obj.status:
|
||||
raise AuthError("账户已经停用")
|
||||
if user_obj.role_id != 1:
|
||||
raise AuthError("认证失败,账户不是超管")
|
||||
return user_obj
|
||||
78
super_bbs/controller/admin/comments/api.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from flask import session
|
||||
from super_bbs.core.viewhandler import ApiViewHandler
|
||||
from super_bbs.model.topics import Topics
|
||||
from super_bbs.model.users import Users
|
||||
from super_bbs.model.comments import Comments
|
||||
from super_bbs.utils import need_params, login_required
|
||||
|
||||
|
||||
class CommentAPI(ApiViewHandler):
|
||||
@login_required(admin=True)
|
||||
def get(self):
|
||||
if self.input.uid:
|
||||
obj = Comments.get_by_uid(self.input.uid)
|
||||
data = obj.to_dict()
|
||||
data['user'] = Users.get_by_id(data['user_id']).to_dict(remove_fields_list=['password'])
|
||||
data['topic'] = Topics.get_by_id(data['topic_id']).to_dict()
|
||||
else:
|
||||
data = Comments.filter_by_query()
|
||||
if self.input.tab_id:
|
||||
data.filter_by_query(tab_id=self.input.tab_id)
|
||||
elif self.input.sub_tab_id:
|
||||
data.filter_by_query(sub_tab_id=self.input.sub_tab_id)
|
||||
if self.input.sk_ and self.input.sv_:
|
||||
data = data.filter(getattr(Comments, self.input.sk_).like(f'%{self.input.sv_}%'))
|
||||
|
||||
if self.input.odb_ and self.input.odt_ in ['asc', 'desc']:
|
||||
data = data.order_by(getattr(getattr(Comments, self.input.odb_), self.input.odt_)())
|
||||
else:
|
||||
data = data.order_by(Comments.time_create.desc())
|
||||
|
||||
total = None
|
||||
if self.input.page and self.input.page_size:
|
||||
total = data.count()
|
||||
data = data.offset(
|
||||
(int(self.input.page) - 1) * int(self.input.page_size)
|
||||
).limit(int(self.input.page_size))
|
||||
|
||||
data = [_t.to_dict() for _t in data]
|
||||
|
||||
user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['user_id'] for i in data)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
topic_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Topics.query.filter(
|
||||
Topics.id.in_(set(i['topic_id'] for i in data)),
|
||||
Topics.available == 1
|
||||
)
|
||||
}
|
||||
|
||||
for _d in data:
|
||||
_d['user'] = user_dict[_d['user_id']]
|
||||
_d['topic'] = topic_dict[_d['topic_id']]
|
||||
|
||||
if total is not None:
|
||||
data = {'total': total, 'list': data}
|
||||
|
||||
return data
|
||||
|
||||
@login_required(admin=True)
|
||||
@need_params(*['uid'])
|
||||
def put(self):
|
||||
obj = Comments.get_by_uid(self.input.uid)
|
||||
if self.input.content:
|
||||
obj.content = self.input.content
|
||||
if self.input.like_count:
|
||||
obj.like_count = self.input.like_count
|
||||
obj.save()
|
||||
|
||||
@login_required(admin=True)
|
||||
@need_params(*['uid'])
|
||||
def delete(self):
|
||||
obj = Comments.get_by_uid(self.input.uid)
|
||||
obj.update(available=0)
|
||||
158
super_bbs/controller/admin/tabs/api.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from collections import defaultdict
|
||||
from super_bbs.core.viewhandler import ApiViewHandler, LogicError
|
||||
from super_bbs.model.tabs import Tabs, SubTabs
|
||||
from super_bbs.utils import need_params, login_required
|
||||
|
||||
|
||||
class TabAPI(ApiViewHandler):
|
||||
@login_required(admin=True)
|
||||
def get(self):
|
||||
if self.input.uid:
|
||||
obj = Tabs.get_by_uid(self.input.uid)
|
||||
data = obj.to_dict()
|
||||
data['sub_tabs'] = [i.to_dict() for i in SubTabs.filter_by_query(tab_id=data['id'])]
|
||||
else:
|
||||
data = Tabs.filter_by_query()
|
||||
if self.input.sk_ and self.input.sv_:
|
||||
data = data.filter(getattr(Tabs, self.input.sk_).like(f'%{self.input.sv_}%'))
|
||||
|
||||
if self.input.odb_ and self.input.odt_ in ['asc', 'desc']:
|
||||
data = data.order_by(getattr(getattr(Tabs, self.input.odb_), self.input.odt_)())
|
||||
else:
|
||||
data = data.order_by(Tabs.time_create.asc())
|
||||
|
||||
total = None
|
||||
if self.input.page and self.input.page_size:
|
||||
total = data.count()
|
||||
data = data.offset(
|
||||
(int(self.input.page) - 1) * int(self.input.page_size)
|
||||
).limit(int(self.input.page_size))
|
||||
|
||||
data = [_t.to_dict() for _t in data]
|
||||
|
||||
sub_tab_dict_list = defaultdict(list)
|
||||
for sub_tab in SubTabs.filter_by_query().order_by(SubTabs.sort_num.asc()):
|
||||
sub_tab_dict_list[sub_tab.tab_id].append(sub_tab.to_dict())
|
||||
|
||||
for _d in data:
|
||||
_d['sub_tabs'] = sub_tab_dict_list.get(_d['id']) or []
|
||||
|
||||
if total is not None:
|
||||
data = {'total': total, 'list': data}
|
||||
|
||||
return data
|
||||
|
||||
@login_required(admin=True)
|
||||
@need_params(*['name', 'zh'])
|
||||
def post(self):
|
||||
obj = Tabs.create_by_uid()
|
||||
if not self.is_valid_name(self.input.name):
|
||||
raise LogicError('name 参数格式只能为英文')
|
||||
obj.name = self.input.name
|
||||
obj.zh = self.input.zh
|
||||
if self.input.sort_num:
|
||||
obj.sort_num = self.input.sort_num
|
||||
|
||||
obj.save()
|
||||
|
||||
@login_required(admin=True)
|
||||
@need_params(*['uid'])
|
||||
def put(self):
|
||||
obj = Tabs.get_by_uid(self.input.uid)
|
||||
if self.input.name:
|
||||
if not self.is_valid_name(self.input.name):
|
||||
raise LogicError('name 参数格式只能为英文')
|
||||
obj.name = self.input.name
|
||||
if self.input.zh:
|
||||
obj.zh = self.input.zh
|
||||
if self.input.sort_num:
|
||||
obj.sort_num = self.input.sort_num
|
||||
obj.save()
|
||||
|
||||
@login_required(admin=True)
|
||||
@need_params(*['uid'])
|
||||
def delete(self):
|
||||
obj = Tabs.get_by_uid(self.input.uid)
|
||||
if SubTabs.filter_by_query(tab_id=obj.id).all():
|
||||
raise LogicError('存在子类别无法删除')
|
||||
obj.update(available=0)
|
||||
|
||||
|
||||
class SubTabAPI(ApiViewHandler):
|
||||
@login_required(admin=True)
|
||||
def get(self):
|
||||
if self.input.uid:
|
||||
obj = SubTabs.get_by_uid(self.input.uid)
|
||||
data = obj.to_dict()
|
||||
data['tab'] = Tabs.get_by_id(obj.tab_id).to_dict()
|
||||
else:
|
||||
if self.input.tab_id:
|
||||
data = SubTabs.filter_by_query(tab_id=self.input.tab_id)
|
||||
else:
|
||||
data = SubTabs.filter_by_query()
|
||||
if self.input.sk_ and self.input.sv_:
|
||||
data = data.filter(getattr(SubTabs, self.input.sk_).like(f'%{self.input.sv_}%'))
|
||||
|
||||
if self.input.odb_ and self.input.odt_ in ['asc', 'desc']:
|
||||
data = data.order_by(getattr(getattr(SubTabs, self.input.odb_), self.input.odt_)())
|
||||
else:
|
||||
data = data.order_by(SubTabs.time_create.desc())
|
||||
|
||||
total = None
|
||||
if self.input.page and self.input.page_size:
|
||||
total = data.count()
|
||||
data = data.offset(
|
||||
(int(self.input.page) - 1) * int(self.input.page_size)
|
||||
).limit(int(self.input.page_size))
|
||||
|
||||
data = [_t.to_dict() for _t in data]
|
||||
tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Tabs.filter_by_query()
|
||||
}
|
||||
|
||||
for d in data:
|
||||
d['tab'] = tab_dict[d['tab_id']]
|
||||
|
||||
if total is not None:
|
||||
data = {'total': total, 'list': data}
|
||||
|
||||
return data
|
||||
|
||||
@login_required(admin=True)
|
||||
@need_params(*['tab_id', 'name', 'zh'])
|
||||
def post(self):
|
||||
obj = SubTabs.create_by_uid()
|
||||
_tab_obj = Tabs.get_by_id(self.input.tab_id)
|
||||
obj.tab_id = self.input.tab_id
|
||||
if not self.is_valid_name(self.input.name):
|
||||
raise LogicError('name 参数格式只能为英文')
|
||||
obj.name = self.input.name
|
||||
obj.zh = self.input.zh
|
||||
if self.input.desc:
|
||||
obj.desc = self.input.desc
|
||||
if self.input.sort_num:
|
||||
obj.sort_num = self.input.sort_num
|
||||
obj.save()
|
||||
|
||||
@login_required(admin=True)
|
||||
@need_params(*['uid'])
|
||||
def put(self):
|
||||
obj = SubTabs.get_by_uid(self.input.uid)
|
||||
if self.input.name:
|
||||
if not self.is_valid_name(self.input.name):
|
||||
raise LogicError('name 参数格式只能为英文')
|
||||
obj.name = self.input.name
|
||||
if self.input.zh:
|
||||
obj.zh = self.input.zh
|
||||
if self.input.desc:
|
||||
obj.name = self.input.desc
|
||||
if self.input.sort_num:
|
||||
obj.sort_num = self.input.sort_num
|
||||
obj.save()
|
||||
|
||||
@login_required(admin=True)
|
||||
@need_params(*['uid'])
|
||||
def delete(self):
|
||||
obj = SubTabs.get_by_uid(self.input.uid)
|
||||
obj.update(available=0)
|
||||
149
super_bbs/controller/admin/topics/api.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from collections import defaultdict
|
||||
from flask import session
|
||||
from super_bbs.core.viewhandler import ApiViewHandler
|
||||
from super_bbs.model.topics import Topics, TopicAppends, Tags, TopicToTag, TopicUpDown
|
||||
from super_bbs.model.users import Users
|
||||
from super_bbs.model.tabs import Tabs, SubTabs
|
||||
from super_bbs.model.comments import Comments
|
||||
from super_bbs.utils import need_params, login_required
|
||||
|
||||
|
||||
class TopicAPI(ApiViewHandler):
|
||||
@login_required(admin=True)
|
||||
def get(self):
|
||||
if self.input.uid:
|
||||
obj = Topics.get_by_uid(self.input.uid)
|
||||
data = obj.to_dict()
|
||||
data['user'] = Users.get_by_id(data['user_id']).to_dict(remove_fields_list=['password'])
|
||||
if data['last_reply_user_id']:
|
||||
data['last_reply_user'] = Users.get_by_id(data['last_reply_user_id']).to_dict(remove_fields_list=['password'])
|
||||
data['tab'] = Tabs.get_by_id(data['tab_id']).to_dict()
|
||||
data['sub_tab'] = SubTabs.get_by_id(data['sub_tab_id']).to_dict()
|
||||
data['appends'] = [i.to_dict() for i in TopicAppends.filter_by_query(topic_id=data['id'])]
|
||||
comment_list = [i.to_dict() for i in Comments.filter_by_query(topic_id=data['id'])]
|
||||
comment_user_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['user_id'] for i in comment_list)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
for comment in comment_list:
|
||||
comment['user'] = comment_user_dict[comment['user_id']]
|
||||
data['comments'] = comment_list
|
||||
data['comment_count'] = len(comment_list)
|
||||
data['up_count'] = TopicUpDown.filter_by_query(topic_id=data['id'], action=True).count()
|
||||
data['down_count'] = TopicUpDown.filter_by_query(topic_id=data['id'], action=False).count()
|
||||
else:
|
||||
data = Topics.filter_by_query()
|
||||
if self.input.tab_id:
|
||||
data.filter_by_query(tab_id=self.input.tab_id)
|
||||
elif self.input.sub_tab_id:
|
||||
data.filter_by_query(sub_tab_id=self.input.sub_tab_id)
|
||||
if self.input.sk_ and self.input.sv_:
|
||||
data = data.filter(getattr(Topics, self.input.sk_).like(f'%{self.input.sv_}%'))
|
||||
|
||||
if self.input.odb_ and self.input.odt_ in ['asc', 'desc']:
|
||||
data = data.order_by(getattr(getattr(Topics, self.input.odb_), self.input.odt_)())
|
||||
else:
|
||||
data = data.order_by(Topics.time_create.desc())
|
||||
|
||||
total = None
|
||||
if self.input.page and self.input.page_size:
|
||||
total = data.count()
|
||||
data = data.offset(
|
||||
(int(self.input.page) - 1) * int(self.input.page_size)
|
||||
).limit(int(self.input.page_size))
|
||||
|
||||
data = [_t.to_dict() for _t in data]
|
||||
|
||||
user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['user_id'] for i in data)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
|
||||
last_reply_user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['last_reply_user_id'] for i in data)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
|
||||
tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Tabs.query.filter(
|
||||
Tabs.id.in_(set(i['tab_id'] for i in data)),
|
||||
Tabs.available == 1
|
||||
)
|
||||
}
|
||||
|
||||
sub_tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in SubTabs.query.filter(
|
||||
SubTabs.id.in_(set(i['sub_tab_id'] for i in data)),
|
||||
SubTabs.available == 1
|
||||
)
|
||||
}
|
||||
|
||||
comment_count_dict = dict()
|
||||
for c in Comments.query.filter(Comments.topic_id.in_(set(i['id'] for i in data)), Comments.available == 1):
|
||||
if comment_count_dict.get(c.topic_id):
|
||||
comment_count_dict[c.topic_id] += 1
|
||||
else:
|
||||
comment_count_dict[c.topic_id] = 1
|
||||
|
||||
up_down_dict_list = defaultdict(list)
|
||||
for up_down_obj in TopicUpDown.query.filter(
|
||||
TopicUpDown.topic_id.in_(set(i['id'] for i in data)),
|
||||
TopicUpDown.available == 1
|
||||
):
|
||||
up_down_dict_list[up_down_obj.topic_id].append(up_down_obj.action)
|
||||
|
||||
for _d in data:
|
||||
_d['user'] = user_dict[_d['user_id']]
|
||||
if _d['last_reply_user_id']:
|
||||
_d['last_reply_user'] = last_reply_user_dict[_d['last_reply_user_id']]
|
||||
_d['tab'] = tab_dict[_d['tab_id']]
|
||||
_d['sub_tab'] = sub_tab_dict[_d['sub_tab_id']]
|
||||
_d['comment_count'] = comment_count_dict.get(_d['id'], 0)
|
||||
# 获取up和down数量
|
||||
if up_down_dict_list.get(_d['id']):
|
||||
_d['up_count'] = len([i for i in up_down_dict_list[_d['id']] if i is True])
|
||||
_d['down_count'] = len([i for i in up_down_dict_list[_d['id']] if i is False])
|
||||
else:
|
||||
_d['up_count'] = 0
|
||||
_d['down_count'] = 0
|
||||
|
||||
if total is not None:
|
||||
data = {'total': total, 'list': data}
|
||||
|
||||
return data
|
||||
|
||||
@login_required(admin=True)
|
||||
@need_params(*['uid'])
|
||||
def put(self):
|
||||
obj = Topics.get_by_uid(self.input.uid)
|
||||
if self.input.title:
|
||||
obj.title = self.input.title
|
||||
if self.input.content:
|
||||
obj.content = self.input.content
|
||||
obj.content_length = len(self.input.content)
|
||||
if self.input.sub_tab_id:
|
||||
sub_tab_obj = SubTabs.get_by_id(self.input.sub_tab_id)
|
||||
obj.sub_tab_id = sub_tab_obj.id
|
||||
obj.tab_id = sub_tab_obj.tab_id
|
||||
obj.save()
|
||||
|
||||
@login_required(admin=True)
|
||||
@need_params(*['uid'])
|
||||
def delete(self):
|
||||
obj = Topics.get_by_uid(self.input.uid)
|
||||
for tmp_obj in TopicAppends.filter_by_query(topic_id=obj.id):
|
||||
tmp_obj.update(available=0)
|
||||
for tmp_obj in Comments.filter_by_query(topic_id=obj.id):
|
||||
tmp_obj.update(available=0)
|
||||
obj.update(available=0)
|
||||
44
super_bbs/controller/admin/users/api.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from super_bbs.core.viewhandler import ApiViewHandler
|
||||
from super_bbs.model.users import Users
|
||||
from super_bbs.utils import need_params, login_required
|
||||
|
||||
|
||||
class UserAPI(ApiViewHandler):
|
||||
@login_required(admin=True)
|
||||
def get(self):
|
||||
if self.input.uid:
|
||||
obj = Users.get_by_uid(self.input.uid)
|
||||
data = obj.to_dict(remove_fields_list=['password'])
|
||||
else:
|
||||
data = Users.filter_by_query()
|
||||
if self.input.sk_ and self.input.sv_:
|
||||
data = data.filter(getattr(Users, self.input.sk_).like(f'%{self.input.sv_}%'))
|
||||
|
||||
if self.input.odb_ and self.input.odt_ in ['asc', 'desc']:
|
||||
data = data.order_by(getattr(getattr(Users, self.input.odb_), self.input.odt_)())
|
||||
else:
|
||||
data = data.order_by(Users.time_create.desc())
|
||||
|
||||
total = None
|
||||
if self.input.page and self.input.page_size:
|
||||
total = data.count()
|
||||
data = data.offset(
|
||||
(int(self.input.page) - 1) * int(self.input.page_size)
|
||||
).limit(int(self.input.page_size))
|
||||
|
||||
data = [_t.to_dict(remove_fields_list=['password']) for _t in data]
|
||||
|
||||
if total is not None:
|
||||
data = {'total': total, 'list': data}
|
||||
|
||||
return data
|
||||
|
||||
@login_required(admin=True)
|
||||
@need_params(*['uid'])
|
||||
def put(self):
|
||||
obj = Users.get_by_uid(self.input.uid)
|
||||
if self.input.status:
|
||||
obj.status = self.input.status
|
||||
if self.input.role_id:
|
||||
obj.role_id = self.input.role_id
|
||||
obj.save()
|
||||
201
super_bbs/controller/v1/account/api.py
Normal file
@@ -0,0 +1,201 @@
|
||||
from io import BytesIO
|
||||
from flask import session, current_app, Response
|
||||
from super_bbs.core.viewhandler import BaseViewHandler, ApiViewHandler
|
||||
from super_bbs.core.basehandler import LogicError, VerifyError, ParamsError
|
||||
from super_bbs.core.extensions import redis_store
|
||||
from super_bbs.model.users import Users, Passport, UserFavUser
|
||||
from super_bbs.model.topics import TopicFav
|
||||
from super_bbs.model.tabs import SubTabFav
|
||||
from super_bbs.utils import need_params, login_required, generate_check_code, release_redis_lock
|
||||
from .wrapper import check_login
|
||||
from .tasks import send_register_mail
|
||||
|
||||
|
||||
class LoginAPI(ApiViewHandler):
|
||||
@need_params(*['username', 'password', 'code'])
|
||||
def post(self):
|
||||
code_key = f'{session.sid}:code'
|
||||
code = redis_store.get(code_key)
|
||||
if code is None or self.input.code.upper() != code.decode('utf-8'):
|
||||
raise VerifyError('图形验证码不正确,请重试')
|
||||
user_obj = check_login(username=self.input.username, password=self.input.password)
|
||||
user_dict = user_obj.to_dict(remove_fields_list=['password'])
|
||||
user_dict['fav_sub_tab_count'] = SubTabFav.filter_by_query(user_id=user_dict['id']).count()
|
||||
user_dict['fav_topic_count'] = TopicFav.filter_by_query(user_id=user_dict['id']).count()
|
||||
user_dict['fav_user_count'] = UserFavUser.filter_by_query(user_id=user_dict['id']).count()
|
||||
session['is_login'] = True
|
||||
session['role_id'] = user_dict['role_id']
|
||||
session['user_info'] = user_dict
|
||||
# add token to passport
|
||||
passport = Passport.create_or_update(
|
||||
query_dict={'user_id': user_obj.id},
|
||||
update_dict={'token': session.sid}
|
||||
)
|
||||
passport.expire = self.get_datetime_now() + current_app.config['REMEMBER_COOKIE_DURATION']
|
||||
passport.save()
|
||||
|
||||
return user_dict
|
||||
|
||||
|
||||
class LoginOutAPI(ApiViewHandler):
|
||||
@login_required()
|
||||
def get(self):
|
||||
session.clear()
|
||||
|
||||
def post(self):
|
||||
return self.get()
|
||||
|
||||
|
||||
class RegisterAPI(ApiViewHandler):
|
||||
@need_params(*['username', 'email', 'password', 'email_code'])
|
||||
def post(self):
|
||||
if Users.filter_by_query(username=self.input.username).all():
|
||||
raise LogicError("username重复,请使用其他username")
|
||||
|
||||
if Users.filter_by_query(email=self.input.email).all():
|
||||
raise LogicError("email重复,请更换email地址或使用原地址找回密码")
|
||||
|
||||
email_code_key = f'{session.sid}:email:code'
|
||||
email_code_raw = redis_store.get(email_code_key)
|
||||
if email_code_raw is None:
|
||||
raise LogicError('邮箱验证码不正确,请重新发送')
|
||||
code, email = email_code_raw.decode('utf-8').split('|')
|
||||
if self.input.email_code.upper() != code or self.input.email != email:
|
||||
raise LogicError('邮箱验证码不正确,请重新发送')
|
||||
|
||||
user_obj = Users.create_by_uid()
|
||||
user_obj.email = email
|
||||
user_obj.username = self.input.username
|
||||
user_obj.set_password(password=self.input.password)
|
||||
|
||||
update_dict = dict()
|
||||
for k in ['sex', 'avatar_url']:
|
||||
if getattr(self.input, k) is not None:
|
||||
update_dict[k] = getattr(self.input, k)
|
||||
|
||||
user_obj.update(**update_dict)
|
||||
|
||||
|
||||
class LoginStatusCheck(ApiViewHandler):
|
||||
def get(self):
|
||||
user_info = session.get('user_info')
|
||||
if user_info:
|
||||
user_obj = Users.get_by_id(user_info['id'])
|
||||
user_dict = user_obj.to_dict(remove_fields_list=['password'])
|
||||
user_dict['fav_sub_tab_count'] = SubTabFav.filter_by_query(user_id=user_dict['id']).count()
|
||||
user_dict['fav_topic_count'] = TopicFav.filter_by_query(user_id=user_dict['id']).count()
|
||||
user_dict['fav_user_count'] = UserFavUser.filter_by_query(user_id=user_dict['id']).count()
|
||||
session['user_info'] = user_dict
|
||||
return user_dict
|
||||
|
||||
|
||||
class ProfileAPI(ApiViewHandler):
|
||||
@login_required()
|
||||
def get(self):
|
||||
user_obj = Users.get_by_id(session['user_info']['id'])
|
||||
user_dict = user_obj.to_dict(remove_fields_list=['password'])
|
||||
user_dict['fav_sub_tab_count'] = SubTabFav.filter_by_query(user_id=user_dict['id']).count()
|
||||
user_dict['fav_topic_count'] = TopicFav.filter_by_query(user_id=user_dict['id']).count()
|
||||
user_dict['fav_user_count'] = UserFavUser.filter_by_query(user_id=user_dict['id']).count()
|
||||
session['user_info'] = user_dict
|
||||
return user_dict
|
||||
|
||||
@login_required()
|
||||
def post(self):
|
||||
user_info = session.get('user_info')
|
||||
user_obj = Users.get_by_uid(user_info['uid'])
|
||||
|
||||
update_dict = dict()
|
||||
|
||||
for k in ['sex', 'avatar_url', 'site', 'location', 'company', 'github', 'twitter', 'weibo', 'bio']:
|
||||
if getattr(self.input, k) is not None:
|
||||
update_dict[k] = getattr(self.input, k)
|
||||
if self.input.sex in [0, 1, 2]:
|
||||
update_dict['sex'] = self.input.sex
|
||||
|
||||
if self.input.privacy_level in [0, 1, 2]:
|
||||
update_dict['privacy_level'] = self.input.privacy_level
|
||||
|
||||
user_obj.update(**update_dict)
|
||||
user_info.update(update_dict)
|
||||
session['user_info'] = user_info
|
||||
|
||||
return user_info
|
||||
|
||||
|
||||
class UpdatePasswordAPI(ApiViewHandler):
|
||||
@login_required()
|
||||
@need_params(*['password', 'new_password'])
|
||||
def post(self):
|
||||
user_obj = Users.get_by_uid(session['user_info']['uid'])
|
||||
if not user_obj.check_password(self.input.password):
|
||||
raise LogicError('原密码错误!')
|
||||
user_obj.set_password(self.input.new_password)
|
||||
user_obj.save()
|
||||
# 强制下线
|
||||
token_id_list = Passport.query.with_entities(Passport.token).filter_by(user_id=user_obj.id).all()
|
||||
for _id in token_id_list:
|
||||
current_app.config['SESSION_REDIS'].delete(current_app.config['SESSION_KEY_PREFIX'] + _id[0])
|
||||
session.clear()
|
||||
|
||||
|
||||
class GetCaptchaHandler(BaseViewHandler):
|
||||
def get(self):
|
||||
stream = BytesIO()
|
||||
img, code = generate_check_code()
|
||||
img.save(stream, 'png')
|
||||
# 验证码 60秒有效
|
||||
code_key = f'{session.sid}:code'
|
||||
redis_store.set(code_key, code, ex=60)
|
||||
return Response(stream.getvalue(), mimetype='image/png')
|
||||
|
||||
|
||||
class CaptchaCheckAPI(ApiViewHandler):
|
||||
@need_params('code')
|
||||
def post(self):
|
||||
code_key = f'{session.sid}:code'
|
||||
code = redis_store.get(code_key)
|
||||
if code is None or self.input.code.upper() != code.decode('utf-8'):
|
||||
raise VerifyError('图形验证码不正确,请重试')
|
||||
|
||||
|
||||
class SendEmailAPI(ApiViewHandler):
|
||||
@need_params(*['email', 'code'])
|
||||
def post(self):
|
||||
code_key = f'{session.sid}:code'
|
||||
code = redis_store.get(code_key)
|
||||
if code is None or self.input.code.upper() != code.decode('utf-8'):
|
||||
raise VerifyError('图形验证码不正确,请重试')
|
||||
|
||||
if not self.is_valid_email(self.input.email):
|
||||
raise ParamsError('email 不合法')
|
||||
|
||||
if Users.filter_by_query(email=self.input.email).all():
|
||||
raise LogicError("该email地址已经注册过,请更换email地址")
|
||||
# 加计数锁
|
||||
lock_key_name = f'lock:send_mail_count:{session.sid}'
|
||||
lock_value = self.generate_hash_uuid(12)
|
||||
set_status = redis_store.set(lock_key_name, lock_value, ex=600, nx=True)
|
||||
if not set_status:
|
||||
return LogicError('正在发送,请稍后重试')
|
||||
# 获取发送次数 + 1
|
||||
send_count_key = f'{session.sid}:email:send'
|
||||
send_count = redis_store.get(send_count_key)
|
||||
if send_count:
|
||||
send_count = int(send_count.decode('utf-8'))
|
||||
if send_count >= 5:
|
||||
raise LogicError('发送次数太频繁,请稍后再发送')
|
||||
else:
|
||||
send_count = 0
|
||||
send_count += 1
|
||||
code = self.generate_hash_uuid(5).upper()
|
||||
send_register_mail.delay(self.input.email, code)
|
||||
# 设置发送的验证码
|
||||
email_code_key = f'{session.sid}:email:code'
|
||||
redis_store.set(email_code_key, f'{code}|{self.input.email}', ex=180)
|
||||
# 设置新的发送次数
|
||||
redis_store.set(send_count_key, send_count, ex=180)
|
||||
# 释放锁
|
||||
ret = release_redis_lock(lock_key_name, lock_value)
|
||||
if not ret:
|
||||
raise LogicError('发送解锁失败')
|
||||
34
super_bbs/controller/v1/account/tasks.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import datetime
|
||||
from flask import current_app
|
||||
from flask_mail import Message
|
||||
from super_bbs.model.users import CeleryTaskLogs
|
||||
from super_bbs.core.extensions import mail, celery
|
||||
from super_bbs.constants import web_title
|
||||
|
||||
|
||||
@celery.task()
|
||||
def send_register_mail(mail_addr, code):
|
||||
msg = Message(
|
||||
f'{web_title}注册码请查收',
|
||||
recipients=[mail_addr]
|
||||
)
|
||||
current_date = str(datetime.datetime.now())
|
||||
msg.html = f'''<h3>欢迎注册 {web_title}!</h3>
|
||||
<br>
|
||||
<div>您的注册码是: <span style="color: black;font-size:200%;font-weight:bold;">{code}</span></div>
|
||||
<br>
|
||||
<hr>
|
||||
<div>from: {web_title} at: {current_date}</div>
|
||||
'''
|
||||
mail.send(msg)
|
||||
current_app.logger.info(f'mail send to {mail_addr}, code {code}')
|
||||
|
||||
|
||||
@celery.task()
|
||||
def clean_celery_log():
|
||||
CeleryTaskLogs.query.filter(
|
||||
CeleryTaskLogs.done == 1,
|
||||
CeleryTaskLogs.task_status == 1,
|
||||
CeleryTaskLogs.time_modify < datetime.datetime.now() - datetime.timedelta(days=10)
|
||||
).delete()
|
||||
return '清理成功'
|
||||
13
super_bbs/controller/v1/account/wrapper.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from super_bbs.model.users import Users
|
||||
from super_bbs.core.basehandler import AuthError
|
||||
|
||||
|
||||
def check_login(username, password):
|
||||
user_obj = Users.filter_by_query(username=username).first()
|
||||
if not user_obj:
|
||||
raise AuthError("用户不存在")
|
||||
if not user_obj.check_password(password):
|
||||
raise AuthError("密码错误")
|
||||
if not user_obj.status:
|
||||
raise AuthError("账户已经停用")
|
||||
return user_obj
|
||||
123
super_bbs/controller/v1/index/api.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from collections import defaultdict
|
||||
from flask import session
|
||||
from super_bbs.core.viewhandler import ApiViewHandler
|
||||
from super_bbs.model.topics import Topics, TopicUpDown
|
||||
from super_bbs.model.users import Users
|
||||
from super_bbs.model.tabs import Tabs, SubTabs, SubTabFav
|
||||
from super_bbs.model.comments import Comments
|
||||
|
||||
|
||||
class IndexMixedAPI(ApiViewHandler):
|
||||
def get(self):
|
||||
data = dict()
|
||||
sub_tab_dict_list = defaultdict(list)
|
||||
for sub_tab in SubTabs.filter_by_query().order_by(SubTabs.sort_num.asc()):
|
||||
sub_tab_dict_list[sub_tab.tab_id].append(sub_tab.to_dict())
|
||||
tab_list = [t.to_dict() for t in Tabs.filter_by_query().order_by(Tabs.sort_num.asc())]
|
||||
for _d in tab_list:
|
||||
_d['sub_tabs'] = sub_tab_dict_list.get(_d['id']) or []
|
||||
|
||||
data['tabs'] = tab_list
|
||||
|
||||
if self.input.tab:
|
||||
tab_obj = Tabs.filter_by_query(name=self.input.tab).first()
|
||||
if not tab_obj:
|
||||
tab_obj = Tabs.get_by_query(name='tech')
|
||||
else:
|
||||
tab_obj = Tabs.get_by_query(name='tech')
|
||||
topic_objs = Topics.filter_by_query(tab_id=tab_obj.id).order_by(Topics.time_create.desc())
|
||||
data['tab_topic_count'] = topic_objs.count()
|
||||
topic_list = [_t.to_dict() for _t in topic_objs[:80]]
|
||||
user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(Users.id.in_(set(i['user_id'] for i in topic_list)), Users.available == 1)
|
||||
}
|
||||
last_reply_user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['last_reply_user_id'] for i in topic_list)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Tabs.query.filter(Tabs.id.in_(set(i['tab_id'] for i in topic_list)), Tabs.available == 1)
|
||||
}
|
||||
sub_tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in SubTabs.query.filter(
|
||||
SubTabs.id.in_(set(i['sub_tab_id'] for i in topic_list)),
|
||||
SubTabs.available == 1
|
||||
)
|
||||
}
|
||||
comment_count_dict = dict()
|
||||
for c in Comments.query.filter(
|
||||
Comments.topic_id.in_(set(i['id'] for i in topic_list)),
|
||||
Comments.available == 1
|
||||
):
|
||||
if comment_count_dict.get(c.topic_id):
|
||||
comment_count_dict[c.topic_id] += 1
|
||||
else:
|
||||
comment_count_dict[c.topic_id] = 1
|
||||
|
||||
up_down_dict_list = defaultdict(list)
|
||||
for up_down_obj in TopicUpDown.query.filter(
|
||||
TopicUpDown.topic_id.in_(set(i['id'] for i in topic_list)),
|
||||
TopicUpDown.available == 1
|
||||
):
|
||||
up_down_dict_list[up_down_obj.topic_id].append(up_down_obj.action)
|
||||
|
||||
for _t in topic_list:
|
||||
_t['user'] = user_dict[_t['user_id']]
|
||||
if _t['last_reply_user_id']:
|
||||
_t['last_reply_user'] = last_reply_user_dict[_t['last_reply_user_id']]
|
||||
_t['tab'] = tab_dict[_t['tab_id']]
|
||||
_t['sub_tab'] = sub_tab_dict[_t['sub_tab_id']]
|
||||
_t['comment_count'] = comment_count_dict.get(_t['id'], 0)
|
||||
# 获取up和down数量
|
||||
if up_down_dict_list.get(_t['id']):
|
||||
_t['up_count'] = len([i for i in up_down_dict_list[_t['id']] if i is True])
|
||||
_t['down_count'] = len([i for i in up_down_dict_list[_t['id']] if i is False])
|
||||
else:
|
||||
_t['up_count'] = 0
|
||||
_t['down_count'] = 0
|
||||
|
||||
data['topics'] = topic_list
|
||||
data['user_count'] = Users.filter_by_query().count()
|
||||
data['topic_count'] = Topics.filter_by_query().count()
|
||||
data['comment_count'] = Comments.filter_by_query().count()
|
||||
|
||||
today_time_start = self.strptime(self.get_datetime_now().strftime('%Y-%m-%d'), '%Y-%m-%d')
|
||||
today_topic_list = [
|
||||
i.to_dict() for i in Topics.query.filter(Topics.time_create >= today_time_start, Topics.available == 1)
|
||||
]
|
||||
if today_topic_list:
|
||||
today_comment_dict = dict()
|
||||
for _c in Comments.query.filter(
|
||||
Comments.topic_id.in_(set([i['id'] for i in today_topic_list])),
|
||||
Comments.available == 1
|
||||
):
|
||||
if today_comment_dict.get(_c.topic_id):
|
||||
today_comment_dict[_c.topic_id] += 1
|
||||
else:
|
||||
today_comment_dict[_c.topic_id] = 1
|
||||
|
||||
for today_topic in today_topic_list:
|
||||
today_topic['comment_count'] = today_comment_dict.get(today_topic['id'], 0)
|
||||
|
||||
today_hot_topic_list = sorted(today_topic_list, key=lambda x: x['comment_count'], reverse=True)
|
||||
if len(today_hot_topic_list) >= 10:
|
||||
today_hot_topic_list = today_hot_topic_list[:10]
|
||||
else:
|
||||
today_hot_topic_list = []
|
||||
|
||||
data['hot_topics'] = today_hot_topic_list
|
||||
if session.get('is_login'):
|
||||
fav_sub_tab_id_list = [i.sub_tab_id for i in SubTabFav.filter_by_query(user_id=session['user_info']['id'])]
|
||||
sub_tab_list = [
|
||||
i.to_dict()
|
||||
for i in SubTabs.query.filter(SubTabs.id.in_(set(fav_sub_tab_id_list)), SubTabs.available == 1)
|
||||
]
|
||||
data['fav_tabs'] = sub_tab_list
|
||||
|
||||
return data
|
||||
290
super_bbs/controller/v1/member/api.py
Normal file
@@ -0,0 +1,290 @@
|
||||
from collections import defaultdict
|
||||
from flask import session
|
||||
from super_bbs.core.viewhandler import ApiViewHandler, LogicError
|
||||
from super_bbs.model.tabs import Tabs, SubTabs
|
||||
from super_bbs.model.topics import Topics, TopicUpDown
|
||||
from super_bbs.model.comments import Comments, CommentThank
|
||||
from super_bbs.model.users import Users, UserFavUser
|
||||
from super_bbs.utils import need_params, login_required
|
||||
|
||||
|
||||
class MemberIndexAPI(ApiViewHandler):
|
||||
@need_params('username')
|
||||
def get(self):
|
||||
data = dict()
|
||||
user_info = Users.get_by_query(username=self.input.username).to_dict(remove_fields_list=['password'])
|
||||
user_info['fav_user_count'] = UserFavUser.filter_by_query(user_id=user_info['id']).count()
|
||||
user_info['be_fav_user_count'] = UserFavUser.filter_by_query(fav_user_id=user_info['id']).count()
|
||||
data['user'] = user_info
|
||||
data['is_open'] = True
|
||||
data['is_fav'] = False
|
||||
if session.get('is_login'):
|
||||
if UserFavUser.filter_by_query(user_id=session['user_info']['id'], fav_user_id=user_info['id']).first():
|
||||
data['is_fav'] = True
|
||||
|
||||
comment_list = [
|
||||
i.to_dict()
|
||||
for i in Comments.filter_by_query(user_id=user_info['id']).order_by(Comments.time_create.desc())[:10]
|
||||
]
|
||||
comment_topic_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Topics.query.filter(
|
||||
Topics.id.in_(set([i['topic_id'] for i in comment_list])),
|
||||
Topics.available == 1
|
||||
)
|
||||
}
|
||||
comment_topic_list = [i for i in comment_topic_dict.values()]
|
||||
user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['user_id'] for i in comment_topic_list)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Tabs.query.filter(
|
||||
Tabs.id.in_(set(i['tab_id'] for i in comment_topic_list)),
|
||||
Tabs.available == 1
|
||||
)
|
||||
}
|
||||
sub_tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in SubTabs.query.filter(
|
||||
SubTabs.id.in_(set(i['sub_tab_id'] for i in comment_topic_list)),
|
||||
SubTabs.available == 1
|
||||
)
|
||||
}
|
||||
for topic in comment_topic_list:
|
||||
comment_topic_dict[topic['id']]['user'] = user_dict[topic['user_id']]
|
||||
comment_topic_dict[topic['id']]['tab'] = tab_dict[topic['tab_id']]
|
||||
comment_topic_dict[topic['id']]['sub_tab'] = sub_tab_dict[topic['sub_tab_id']]
|
||||
|
||||
for comment in comment_list:
|
||||
comment['is_thank'] = False
|
||||
comment['topic'] = comment_topic_dict[comment['topic_id']]
|
||||
|
||||
if session.get('user_info'):
|
||||
comment_thank_id_list = [i.comment_id for i in
|
||||
CommentThank.filter_by_query(user_id=session['user_info']['id'])]
|
||||
for comment in comment_list:
|
||||
if comment['id'] in comment_thank_id_list:
|
||||
comment['is_thank'] = True
|
||||
data['comments'] = comment_list
|
||||
|
||||
if user_info['privacy_level'] == 1 and not session.get('is_login'):
|
||||
data['is_open'] = False
|
||||
data['topics'] = []
|
||||
return data
|
||||
|
||||
if user_info['privacy_level'] == 2:
|
||||
if not session.get('user_info') or session.get('user_info')['id'] != user_info['id']:
|
||||
data['is_open'] = False
|
||||
data['topics'] = []
|
||||
return data
|
||||
|
||||
topic_list = [
|
||||
i.to_dict()
|
||||
for i in Topics.filter_by_query(user_id=user_info['id']).order_by(Topics.time_create.desc())[:10]
|
||||
]
|
||||
last_reply_user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['last_reply_user_id'] for i in topic_list)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Tabs.query.filter(Tabs.id.in_(set(i['tab_id'] for i in topic_list)), Tabs.available == 1)
|
||||
}
|
||||
sub_tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in SubTabs.query.filter(
|
||||
SubTabs.id.in_(set(i['sub_tab_id'] for i in topic_list)),
|
||||
SubTabs.available == 1
|
||||
)
|
||||
}
|
||||
comment_count_dict = dict()
|
||||
for c in Comments.query.filter(
|
||||
Comments.topic_id.in_(set(i['id'] for i in topic_list)),
|
||||
Comments.available == 1
|
||||
):
|
||||
if comment_count_dict.get(c.topic_id):
|
||||
comment_count_dict[c.topic_id] += 1
|
||||
else:
|
||||
comment_count_dict[c.topic_id] = 1
|
||||
|
||||
up_down_dict_list = defaultdict(list)
|
||||
for up_down_obj in TopicUpDown.query.filter(
|
||||
TopicUpDown.topic_id.in_(set(i['id'] for i in topic_list)),
|
||||
TopicUpDown.available == 1
|
||||
):
|
||||
up_down_dict_list[up_down_obj.topic_id].append(up_down_obj.action)
|
||||
|
||||
for _t in topic_list:
|
||||
if _t['last_reply_user_id']:
|
||||
_t['last_reply_user'] = last_reply_user_dict[_t['last_reply_user_id']]
|
||||
_t['tab'] = tab_dict[_t['tab_id']]
|
||||
_t['sub_tab'] = sub_tab_dict[_t['sub_tab_id']]
|
||||
_t['comment_count'] = comment_count_dict.get(_t['id'], 0)
|
||||
# 获取up和down数量
|
||||
if up_down_dict_list.get(_t['id']):
|
||||
_t['up_count'] = len([i for i in up_down_dict_list[_t['id']] if i is True])
|
||||
_t['down_count'] = len([i for i in up_down_dict_list[_t['id']] if i is False])
|
||||
else:
|
||||
_t['up_count'] = 0
|
||||
_t['down_count'] = 0
|
||||
data['topics'] = topic_list
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class MemberTopicAPI(ApiViewHandler):
|
||||
@need_params('username')
|
||||
def get(self):
|
||||
data = dict()
|
||||
user_info = Users.get_by_query(username=self.input.username).to_dict(remove_fields_list=['password'])
|
||||
data['user'] = user_info
|
||||
|
||||
if user_info['privacy_level'] == 1 and not session.get('is_login'):
|
||||
raise LogicError('没有权限访问此用户的信息哦')
|
||||
|
||||
if user_info['privacy_level'] == 2:
|
||||
if not session.get('user_info') or session.get('user_info')['id'] != user_info['id']:
|
||||
raise LogicError('没有权限访问此用户的信息哦')
|
||||
|
||||
topic_objs = Topics.filter_by_query(user_id=user_info['id'])
|
||||
topic_count = topic_objs.count()
|
||||
data['total'] = topic_count
|
||||
if topic_count > 100:
|
||||
page = int(self.input.page) if self.input.page else 1
|
||||
page_size = 50
|
||||
topic_objs = topic_objs.order_by(
|
||||
Topics.time_create.desc()
|
||||
).order_by(Topics.id.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||
else:
|
||||
topic_objs = topic_objs.order_by(Topics.time_modify.desc()).order_by(Topics.id.desc())
|
||||
topic_list = [_t.to_dict() for _t in topic_objs]
|
||||
|
||||
last_reply_user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['last_reply_user_id'] for i in topic_list)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Tabs.query.filter(Tabs.id.in_(set(i['tab_id'] for i in topic_list)), Tabs.available == 1)
|
||||
}
|
||||
sub_tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in SubTabs.query.filter(
|
||||
SubTabs.id.in_(set(i['sub_tab_id'] for i in topic_list)),
|
||||
SubTabs.available == 1
|
||||
)
|
||||
}
|
||||
comment_count_dict = dict()
|
||||
for c in Comments.query.filter(
|
||||
Comments.topic_id.in_(set(i['id'] for i in topic_list)),
|
||||
Comments.available == 1
|
||||
):
|
||||
if comment_count_dict.get(c.topic_id):
|
||||
comment_count_dict[c.topic_id] += 1
|
||||
else:
|
||||
comment_count_dict[c.topic_id] = 1
|
||||
|
||||
up_down_dict_list = defaultdict(list)
|
||||
for up_down_obj in TopicUpDown.query.filter(
|
||||
TopicUpDown.topic_id.in_(set(i['id'] for i in topic_list)),
|
||||
TopicUpDown.available == 1
|
||||
):
|
||||
up_down_dict_list[up_down_obj.topic_id].append(up_down_obj.action)
|
||||
|
||||
for _t in topic_list:
|
||||
if _t['last_reply_user_id']:
|
||||
_t['last_reply_user'] = last_reply_user_dict[_t['last_reply_user_id']]
|
||||
_t['tab'] = tab_dict[_t['tab_id']]
|
||||
_t['sub_tab'] = sub_tab_dict[_t['sub_tab_id']]
|
||||
_t['comment_count'] = comment_count_dict.get(_t['id'], 0)
|
||||
# 获取up和down数量
|
||||
if up_down_dict_list.get(_t['id']):
|
||||
_t['up_count'] = len([i for i in up_down_dict_list[_t['id']] if i is True])
|
||||
_t['down_count'] = len([i for i in up_down_dict_list[_t['id']] if i is False])
|
||||
else:
|
||||
_t['up_count'] = 0
|
||||
_t['down_count'] = 0
|
||||
data['list'] = topic_list
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class MemberCommentAPI(ApiViewHandler):
|
||||
@need_params('username')
|
||||
def get(self):
|
||||
data = dict()
|
||||
user_info = Users.get_by_query(username=self.input.username).to_dict(remove_fields_list=['password'])
|
||||
data['user'] = user_info
|
||||
comment_objs = Comments.filter_by_query(user_id=user_info['id'])
|
||||
comment_count = comment_objs.count()
|
||||
data['total'] = comment_count
|
||||
|
||||
if comment_count > 100:
|
||||
page = int(self.input.page) if self.input.page else 1
|
||||
page_size = 50
|
||||
topic_objs = comment_objs.order_by(
|
||||
Comments.time_create.desc()
|
||||
).order_by(Comments.id.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||
else:
|
||||
topic_objs = comment_objs.order_by(Comments.time_modify.desc()).order_by(Comments.id.desc())
|
||||
comment_list = [_t.to_dict() for _t in topic_objs]
|
||||
|
||||
comment_topic_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Topics.query.filter(
|
||||
Topics.id.in_(set([i['topic_id'] for i in comment_list])),
|
||||
Topics.available == 1
|
||||
)
|
||||
}
|
||||
comment_topic_list = [i for i in comment_topic_dict.values()]
|
||||
user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['user_id'] for i in comment_topic_list)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Tabs.query.filter(
|
||||
Tabs.id.in_(set(i['tab_id'] for i in comment_topic_list)),
|
||||
Tabs.available == 1
|
||||
)
|
||||
}
|
||||
sub_tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in SubTabs.query.filter(
|
||||
SubTabs.id.in_(set(i['sub_tab_id'] for i in comment_topic_list)),
|
||||
SubTabs.available == 1
|
||||
)
|
||||
}
|
||||
for topic in comment_topic_list:
|
||||
comment_topic_dict[topic['id']]['user'] = user_dict[topic['user_id']]
|
||||
comment_topic_dict[topic['id']]['tab'] = tab_dict[topic['tab_id']]
|
||||
comment_topic_dict[topic['id']]['sub_tab'] = sub_tab_dict[topic['sub_tab_id']]
|
||||
|
||||
for comment in comment_list:
|
||||
comment['is_thank'] = False
|
||||
comment['topic'] = comment_topic_dict[comment['topic_id']]
|
||||
|
||||
if session.get('user_info'):
|
||||
comment_thank_id_list = [i.comment_id for i in
|
||||
CommentThank.filter_by_query(user_id=session['user_info']['id'])]
|
||||
for comment in comment_list:
|
||||
if comment['id'] in comment_thank_id_list:
|
||||
comment['is_thank'] = True
|
||||
|
||||
data['list'] = comment_list
|
||||
|
||||
return data
|
||||
160
super_bbs/controller/v1/tabs/api.py
Normal file
@@ -0,0 +1,160 @@
|
||||
from collections import defaultdict
|
||||
from flask import session
|
||||
from super_bbs.core.viewhandler import ApiViewHandler
|
||||
from super_bbs.model.tabs import Tabs, SubTabs, SubTabFav
|
||||
from super_bbs.model.topics import Topics, TopicUpDown
|
||||
from super_bbs.model.comments import Comments
|
||||
from super_bbs.model.users import Users
|
||||
from super_bbs.utils import need_params, login_required
|
||||
|
||||
|
||||
class TabAPI(ApiViewHandler):
|
||||
def get(self):
|
||||
sub_tab_dict_list = defaultdict(list)
|
||||
for sub_tab in SubTabs.filter_by_query().order_by(SubTabs.sort_num.asc()):
|
||||
sub_tab_dict_list[sub_tab.tab_id].append(sub_tab.to_dict())
|
||||
tab_list = [t.to_dict() for t in Tabs.filter_by_query().order_by(Tabs.sort_num.asc())]
|
||||
for _d in tab_list:
|
||||
_d['sub_tabs'] = sub_tab_dict_list.get(_d['id']) or []
|
||||
|
||||
return tab_list
|
||||
|
||||
|
||||
class SubTabAPI(ApiViewHandler):
|
||||
@login_required()
|
||||
def get(self):
|
||||
data = [i.to_dict() for i in SubTabs.filter_by_query()]
|
||||
tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Tabs.query.filter(Tabs.id.in_(set([i['tab_id'] for i in data])), Tabs.available == 1)
|
||||
}
|
||||
for d in data:
|
||||
d['tab'] = tab_dict[d['tab_id']]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class SubTabFavAPI(ApiViewHandler):
|
||||
@login_required()
|
||||
def get(self):
|
||||
fav_sub_tab_id_list = [i.sub_tab_id for i in SubTabFav.filter_by_query(user_id=session['user_info']['id'])]
|
||||
sub_tab_list = [
|
||||
i.to_dict()
|
||||
for i in SubTabs.query.filter(SubTabs.id.in_(set(fav_sub_tab_id_list)), SubTabs.available == 1)
|
||||
]
|
||||
topic_sub_tab_dict = dict()
|
||||
for topic_obj in Topics.query.filter(
|
||||
Topics.sub_tab_id.in_(set([i['id'] for i in sub_tab_list])),
|
||||
Topics.available == 1
|
||||
):
|
||||
if topic_sub_tab_dict.get(topic_obj.sub_tab_id):
|
||||
topic_sub_tab_dict[topic_obj.sub_tab_id] += 1
|
||||
else:
|
||||
topic_sub_tab_dict[topic_obj.sub_tab_id] = 1
|
||||
for sub_tab in sub_tab_list:
|
||||
sub_tab['topic_count'] = topic_sub_tab_dict.get(sub_tab['id'], 0)
|
||||
|
||||
return sub_tab_list
|
||||
|
||||
@login_required()
|
||||
@need_params(*['tab', 'action'])
|
||||
def post(self):
|
||||
obj = SubTabs.get_by_query(name=self.input.tab)
|
||||
if self.input.action == 'add':
|
||||
sub_tab_fav_obj = SubTabFav.filter_by_query(
|
||||
sub_tab_id=obj.id,
|
||||
user_id=session['user_info']['id'],
|
||||
show_deleted=True
|
||||
).first()
|
||||
if sub_tab_fav_obj:
|
||||
sub_tab_fav_obj.available = 1
|
||||
else:
|
||||
sub_tab_fav_obj = SubTabFav()
|
||||
sub_tab_fav_obj.sub_tab_id = obj.id
|
||||
sub_tab_fav_obj.user_id = session['user_info']['id']
|
||||
sub_tab_fav_obj.save()
|
||||
elif self.input.action == 'cal':
|
||||
sub_tab_fav_obj = SubTabFav.filter_by_query(
|
||||
sub_tab_id=obj.id,
|
||||
user_id=session['user_info']['id']
|
||||
).first()
|
||||
if sub_tab_fav_obj:
|
||||
sub_tab_fav_obj.available = 0
|
||||
sub_tab_fav_obj.save()
|
||||
|
||||
|
||||
class TabMixedAPI(ApiViewHandler):
|
||||
@need_params('tab')
|
||||
def get(self):
|
||||
data = dict()
|
||||
sub_tab_info = SubTabs.get_by_query(name=self.input.tab).to_dict()
|
||||
sub_tab_info['tab'] = Tabs.get_by_id(sub_tab_info['tab_id']).to_dict()
|
||||
sub_tab_info['other_tabs'] = [i.to_dict() for i in SubTabs.filter_by_query(tab_id=sub_tab_info['tab_id'])]
|
||||
data['sub_tab_info'] = sub_tab_info
|
||||
|
||||
topic_objs = Topics.filter_by_query(sub_tab_id=sub_tab_info['id']).order_by(Topics.time_create.desc())
|
||||
topic_count = topic_objs.count()
|
||||
if topic_count > 100:
|
||||
page = int(self.input.page) if self.input.page else 1
|
||||
page_size = 50
|
||||
topic_objs = topic_objs.offset((page - 1) * page_size).limit(page_size)
|
||||
topic_list = [_t.to_dict() for _t in topic_objs]
|
||||
|
||||
user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['user_id'] for i in topic_list)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
|
||||
last_reply_user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['last_reply_user_id'] for i in topic_list)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
|
||||
comment_count_dict = dict()
|
||||
comment_objs = Comments.query.filter(
|
||||
Comments.topic_id.in_(set(i['id'] for i in topic_list)),
|
||||
Comments.available == 1
|
||||
)
|
||||
for c in comment_objs:
|
||||
if comment_count_dict.get(c.topic_id):
|
||||
comment_count_dict[c.topic_id] += 1
|
||||
else:
|
||||
comment_count_dict[c.topic_id] = 1
|
||||
|
||||
up_down_dict_list = defaultdict(list)
|
||||
for up_down_obj in TopicUpDown.query.filter(
|
||||
TopicUpDown.topic_id.in_(set(i['id'] for i in topic_list)),
|
||||
TopicUpDown.available == 1
|
||||
):
|
||||
up_down_dict_list[up_down_obj.topic_id].append(up_down_obj.action)
|
||||
|
||||
for _d in topic_list:
|
||||
_d['user'] = user_dict[_d['user_id']]
|
||||
if _d['last_reply_user_id']:
|
||||
_d['last_reply_user'] = last_reply_user_dict[_d['last_reply_user_id']]
|
||||
_d['comment_count'] = comment_count_dict.get(_d['id'], 0)
|
||||
# 获取up和down数量
|
||||
if up_down_dict_list.get(_d['id']):
|
||||
_d['up_count'] = len([i for i in up_down_dict_list[_d['id']] if i is True])
|
||||
_d['down_count'] = len([i for i in up_down_dict_list[_d['id']] if i is False])
|
||||
else:
|
||||
_d['up_count'] = 0
|
||||
_d['down_count'] = 0
|
||||
|
||||
data['total'] = topic_count
|
||||
data['list'] = topic_list
|
||||
if session.get('is_login'):
|
||||
if SubTabFav.filter_by_query(
|
||||
user_id=session['user_info']['id'],
|
||||
sub_tab_id=sub_tab_info['id']
|
||||
).first():
|
||||
data['is_fav'] = True
|
||||
else:
|
||||
data['is_fav'] = False
|
||||
return data
|
||||
393
super_bbs/controller/v1/topic/api.py
Normal file
@@ -0,0 +1,393 @@
|
||||
from collections import defaultdict
|
||||
from flask import session
|
||||
from super_bbs.core.viewhandler import ApiViewHandler, LogicError
|
||||
from super_bbs.model.topics import Topics, TopicAppends, TopicFav, TopicUpDown, TopicThank
|
||||
from super_bbs.model.users import Users, UserFavUser
|
||||
from super_bbs.model.tabs import Tabs, SubTabs
|
||||
from super_bbs.model.comments import Comments, CommentThank
|
||||
from super_bbs.utils import need_params, login_required
|
||||
|
||||
|
||||
class TopicAPI(ApiViewHandler):
|
||||
@need_params('uid')
|
||||
def get(self):
|
||||
data = Topics.get_by_uid(self.input.uid).to_dict()
|
||||
data['user'] = Users.get_by_id(data['user_id']).to_dict()
|
||||
if data['last_reply_user_id']:
|
||||
data['last_reply_user'] = Users.get_by_id(data['last_reply_user_id']).to_dict()
|
||||
data['tab'] = Tabs.get_by_id(data['tab_id']).to_dict()
|
||||
data['sub_tab'] = SubTabs.get_by_id(data['sub_tab_id']).to_dict()
|
||||
data['appends'] = [i.to_dict() for i in TopicAppends.filter_by_query(topic_id=data['id'])]
|
||||
|
||||
comment_objs = Comments.filter_by_query(topic_id=data['id'])
|
||||
data['comment_count'] = comment_objs.count()
|
||||
if data['comment_count'] < 100:
|
||||
comment_list = [i.to_dict() for i in comment_objs]
|
||||
for index, comment in enumerate(comment_list):
|
||||
comment['index'] = index + 1
|
||||
else:
|
||||
page_size = 50
|
||||
page = int(self.input.page) if self.input.page else 1
|
||||
comment_objs = comment_objs.offset((page - 1) * page_size).limit(page_size)
|
||||
comment_list = [i.to_dict() for i in comment_objs]
|
||||
for index, comment in enumerate(comment_list):
|
||||
comment['index'] = (page - 1) * page_size + index + 1
|
||||
|
||||
comment_user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['user_id'] for i in comment_list)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
for comment in comment_list:
|
||||
comment['user'] = comment_user_dict[comment['user_id']]
|
||||
comment['is_thank'] = False
|
||||
data['up_count'] = TopicUpDown.filter_by_query(topic_id=data['id'], action=True).count()
|
||||
data['down_count'] = TopicUpDown.filter_by_query(topic_id=data['id'], action=False).count()
|
||||
data['is_fav'] = False
|
||||
data['is_thank'] = False
|
||||
if session.get('user_info'):
|
||||
if TopicFav.filter_by_query(topic_id=data['id'], user_id=session['user_info']['id']).first():
|
||||
data['is_fav'] = True
|
||||
if TopicThank.filter_by_query(topic_id=data['id'], user_id=session['user_info']['id']).first():
|
||||
data['is_thank'] = True
|
||||
comment_thank_objs = CommentThank.query.filter(
|
||||
CommentThank.comment_id.in_(set([i['id'] for i in comment_list])),
|
||||
CommentThank.available == 1
|
||||
)
|
||||
comment_thank_id_list = [i.comment_id for i in comment_thank_objs]
|
||||
for comment in comment_list:
|
||||
if comment['id'] in comment_thank_id_list:
|
||||
comment['is_thank'] = True
|
||||
data['comments'] = comment_list
|
||||
return data
|
||||
|
||||
@login_required()
|
||||
@need_params(*['title', 'sub_tab_id'])
|
||||
def post(self):
|
||||
obj = Topics.create_by_uid()
|
||||
obj.title = self.input.title
|
||||
sub_tab_obj = SubTabs.get_by_id(self.input.sub_tab_id)
|
||||
obj.sub_tab_id = self.input.sub_tab_id
|
||||
tab_obj = Tabs.get_by_id(sub_tab_obj.tab_id)
|
||||
obj.tab_id = tab_obj.id
|
||||
|
||||
if self.input.content:
|
||||
obj.content = self.input.content
|
||||
obj.content_length = len(self.input.content)
|
||||
obj.user_id = session['user_info']['id']
|
||||
obj.save()
|
||||
|
||||
|
||||
class TopicUpDownCountAPI(ApiViewHandler):
|
||||
@login_required()
|
||||
@need_params(*['uid', 'action'])
|
||||
def post(self):
|
||||
obj = Topics.get_by_uid(self.input.uid)
|
||||
if self.input.action in ['up', 'down']:
|
||||
topic_up_down_obj = TopicUpDown.filter_by_query(
|
||||
topic_id=obj.id,
|
||||
user_id=session['user_info']['id'],
|
||||
show_deleted=True
|
||||
).first()
|
||||
if topic_up_down_obj:
|
||||
topic_up_down_obj.available = 1
|
||||
else:
|
||||
topic_up_down_obj = TopicUpDown()
|
||||
topic_up_down_obj.topic_id = obj.id
|
||||
topic_up_down_obj.user_id = session['user_info']['id']
|
||||
if self.input.action == 'up':
|
||||
topic_up_down_obj.action = True
|
||||
else:
|
||||
topic_up_down_obj.action = False
|
||||
topic_up_down_obj.save()
|
||||
|
||||
return {
|
||||
'up_count': TopicUpDown.filter_by_query(topic_id=obj.id, action=True).count(),
|
||||
'down_count': TopicUpDown.filter_by_query(topic_id=obj.id, action=False).count()
|
||||
}
|
||||
|
||||
|
||||
class TopicAppendAPI(ApiViewHandler):
|
||||
@login_required()
|
||||
@need_params(*['uid', 'content'])
|
||||
def post(self):
|
||||
obj = Topics.get_by_uid(self.input.uid)
|
||||
if obj.user_id != session['user_info']['id']:
|
||||
raise LogicError('你不可以追加别人的帖子哦')
|
||||
append_obj = TopicAppends.create_by_uid()
|
||||
append_obj.topic_id = obj.id
|
||||
append_obj.content = self.input.content
|
||||
append_obj.save()
|
||||
|
||||
|
||||
class TopicAddView(ApiViewHandler):
|
||||
@need_params(*['uid'])
|
||||
def post(self):
|
||||
obj = Topics.get_by_uid(self.input.uid)
|
||||
obj.view_count += 1
|
||||
obj.save()
|
||||
|
||||
|
||||
class TopicCommentAPI(ApiViewHandler):
|
||||
@login_required()
|
||||
@need_params(*['uid', 'comment'])
|
||||
def post(self):
|
||||
obj = Topics.get_by_uid(self.input.uid)
|
||||
commend_obj = Comments.create_by_uid()
|
||||
commend_obj.topic_id = obj.id
|
||||
commend_obj.content = self.input.comment
|
||||
commend_obj.user_id = session['user_info']['id']
|
||||
obj.last_reply_user_id = session['user_info']['id']
|
||||
obj.last_reply_time = self.get_datetime_now()
|
||||
obj.save()
|
||||
commend_obj.save()
|
||||
|
||||
|
||||
class TopicFavAPI(ApiViewHandler):
|
||||
@login_required()
|
||||
def get(self):
|
||||
topic_fav_objs = TopicFav.filter_by_query(user_id=session['user_info']['id'])
|
||||
topic_fav_count = topic_fav_objs.count()
|
||||
if topic_fav_count > 100:
|
||||
page = int(self.input.page) if self.input.page else 1
|
||||
page_size = 50
|
||||
topic_fav_objs = topic_fav_objs.order_by(
|
||||
TopicFav.time_modify.desc()
|
||||
).order_by(TopicFav.id.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||
else:
|
||||
topic_fav_objs = topic_fav_objs.order_by(TopicFav.time_modify.desc()).order_by(TopicFav.id.desc())
|
||||
|
||||
topic_fav_dict = {i.topic_id: i.time_modify.strftime('%Y-%m-%d %H:%M:%S') for i in topic_fav_objs}
|
||||
topic_id_list = topic_fav_dict.keys()
|
||||
topic_objs = Topics.query.filter(Topics.id.in_(set(topic_id_list)), Topics.available == 1)
|
||||
topic_list = [_t.to_dict() for _t in topic_objs]
|
||||
for topic in topic_list:
|
||||
topic['fav_time'] = topic_fav_dict[topic['id']]
|
||||
# 排序
|
||||
filtered_topic_id_list = [i['id'] for i in topic_list]
|
||||
topic_id_sort_dict = {
|
||||
value: index
|
||||
for index, value in enumerate(topic_id_list) if value in filtered_topic_id_list
|
||||
}
|
||||
topic_list = sorted(topic_list, key=lambda x: topic_id_sort_dict[x['id']])
|
||||
|
||||
user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(Users.id.in_(set(i['user_id'] for i in topic_list)), Users.available == 1)
|
||||
}
|
||||
last_reply_user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['last_reply_user_id'] for i in topic_list)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Tabs.query.filter(Tabs.id.in_(set(i['tab_id'] for i in topic_list)), Tabs.available == 1)
|
||||
}
|
||||
sub_tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in SubTabs.query.filter(
|
||||
SubTabs.id.in_(set(i['sub_tab_id'] for i in topic_list)),
|
||||
SubTabs.available == 1
|
||||
)
|
||||
}
|
||||
comment_count_dict = dict()
|
||||
for c in Comments.query.filter(
|
||||
Comments.topic_id.in_(set(i['id'] for i in topic_list)),
|
||||
Comments.available == 1
|
||||
):
|
||||
if comment_count_dict.get(c.topic_id):
|
||||
comment_count_dict[c.topic_id] += 1
|
||||
else:
|
||||
comment_count_dict[c.topic_id] = 1
|
||||
|
||||
up_down_dict_list = defaultdict(list)
|
||||
for up_down_obj in TopicUpDown.query.filter(
|
||||
TopicUpDown.topic_id.in_(set(i['id'] for i in topic_list)),
|
||||
TopicUpDown.available == 1
|
||||
):
|
||||
up_down_dict_list[up_down_obj.topic_id].append(up_down_obj.action)
|
||||
|
||||
for _t in topic_list:
|
||||
_t['user'] = user_dict[_t['user_id']]
|
||||
if _t['last_reply_user_id']:
|
||||
_t['last_reply_user'] = last_reply_user_dict[_t['last_reply_user_id']]
|
||||
_t['tab'] = tab_dict[_t['tab_id']]
|
||||
_t['sub_tab'] = sub_tab_dict[_t['sub_tab_id']]
|
||||
_t['comment_count'] = comment_count_dict.get(_t['id'], 0)
|
||||
# 获取up和down数量
|
||||
if up_down_dict_list.get(_t['id']):
|
||||
_t['up_count'] = len([i for i in up_down_dict_list[_t['id']] if i is True])
|
||||
_t['down_count'] = len([i for i in up_down_dict_list[_t['id']] if i is False])
|
||||
else:
|
||||
_t['up_count'] = 0
|
||||
_t['down_count'] = 0
|
||||
|
||||
return {'total': topic_fav_count, 'list': topic_list}
|
||||
|
||||
@login_required()
|
||||
@need_params(*['uid', 'action'])
|
||||
def post(self):
|
||||
obj = Topics.get_by_uid(self.input.uid)
|
||||
if session['user_info']['id'] == obj.user_id:
|
||||
raise LogicError('自己不能收藏自己的主题哦')
|
||||
topic_fav_obj = TopicFav.filter_by_query(
|
||||
topic_id=obj.id,
|
||||
user_id=session['user_info']['id'],
|
||||
show_deleted=True
|
||||
).first()
|
||||
if self.input.action == 'add':
|
||||
if topic_fav_obj:
|
||||
topic_fav_obj.available = 1
|
||||
else:
|
||||
topic_fav_obj = TopicFav()
|
||||
topic_fav_obj.topic_id = obj.id
|
||||
topic_fav_obj.user_id = session['user_info']['id']
|
||||
topic_fav_obj.save()
|
||||
elif self.input.action == 'cal':
|
||||
if topic_fav_obj:
|
||||
topic_fav_obj.available = 0
|
||||
topic_fav_obj.save()
|
||||
|
||||
|
||||
class TopicThankAPI(ApiViewHandler):
|
||||
@login_required()
|
||||
@need_params(*['uid'])
|
||||
def post(self):
|
||||
obj = Topics.get_by_uid(self.input.uid)
|
||||
# 自己不能给自己感谢
|
||||
if session['user_info']['id'] == obj.user_id:
|
||||
raise LogicError('自己不能感谢自己哦')
|
||||
if not TopicThank.filter_by_query(topic_id=obj.id, user_id=session['user_info']['id']).first():
|
||||
thank_obj = TopicThank()
|
||||
thank_obj.topic_id = obj.id
|
||||
thank_obj.user_id = session['user_info']['id']
|
||||
thank_obj.save()
|
||||
|
||||
|
||||
class CommentThankAPI(ApiViewHandler):
|
||||
@login_required()
|
||||
@need_params(*['uid'])
|
||||
def post(self):
|
||||
obj = Comments.get_by_uid(self.input.uid)
|
||||
# 自己不能给自己点赞
|
||||
if session['user_info']['id'] == obj.user_id:
|
||||
raise LogicError('自己不能给自己点赞哦')
|
||||
if not CommentThank.filter_by_query(comment_id=obj.id, user_id=session['user_info']['id']).first():
|
||||
thank_obj = CommentThank()
|
||||
thank_obj.comment_id = obj.id
|
||||
thank_obj.user_id = session['user_info']['id']
|
||||
thank_obj.save()
|
||||
|
||||
|
||||
class UserFavAPI(ApiViewHandler):
|
||||
@login_required()
|
||||
def get(self):
|
||||
fav_user_id_list = [
|
||||
i.fav_user_id
|
||||
for i in UserFavUser.filter_by_query(user_id=session['user_info']['id'])
|
||||
]
|
||||
fav_user_list = [
|
||||
i.to_dict()
|
||||
for i in Users.query.filter(Users.id.in_(set(fav_user_id_list)), Users.available == 1)
|
||||
]
|
||||
topic_objs = Topics.query.filter(Topics.user_id.in_(set(fav_user_id_list)), Topics.available == 1)
|
||||
topic_count = topic_objs.count()
|
||||
if topic_count > 100:
|
||||
page = int(self.input.page) if self.input.page else 1
|
||||
page_size = 50
|
||||
topic_objs = topic_objs.order_by(
|
||||
Topics.time_create.desc()
|
||||
).order_by(Topics.id.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||
else:
|
||||
topic_objs = topic_objs.order_by(Topics.time_modify.desc()).order_by(Topics.id.desc())
|
||||
topic_list = [_t.to_dict() for _t in topic_objs]
|
||||
|
||||
user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(Users.id.in_(set(i['user_id'] for i in topic_list)), Users.available == 1)
|
||||
}
|
||||
last_reply_user_dict = {
|
||||
i.id: i.to_dict(remove_fields_list=['password'])
|
||||
for i in Users.query.filter(
|
||||
Users.id.in_(set(i['last_reply_user_id'] for i in topic_list)),
|
||||
Users.available == 1
|
||||
)
|
||||
}
|
||||
tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in Tabs.query.filter(Tabs.id.in_(set(i['tab_id'] for i in topic_list)), Tabs.available == 1)
|
||||
}
|
||||
sub_tab_dict = {
|
||||
i.id: i.to_dict()
|
||||
for i in SubTabs.query.filter(
|
||||
SubTabs.id.in_(set(i['sub_tab_id'] for i in topic_list)),
|
||||
SubTabs.available == 1
|
||||
)
|
||||
}
|
||||
comment_count_dict = dict()
|
||||
for c in Comments.query.filter(
|
||||
Comments.topic_id.in_(set(i['id'] for i in topic_list)),
|
||||
Comments.available == 1
|
||||
):
|
||||
if comment_count_dict.get(c.topic_id):
|
||||
comment_count_dict[c.topic_id] += 1
|
||||
else:
|
||||
comment_count_dict[c.topic_id] = 1
|
||||
|
||||
up_down_dict_list = defaultdict(list)
|
||||
for up_down_obj in TopicUpDown.query.filter(
|
||||
TopicUpDown.topic_id.in_(set(i['id'] for i in topic_list)),
|
||||
TopicUpDown.available == 1
|
||||
):
|
||||
up_down_dict_list[up_down_obj.topic_id].append(up_down_obj.action)
|
||||
|
||||
for _t in topic_list:
|
||||
_t['user'] = user_dict[_t['user_id']]
|
||||
if _t['last_reply_user_id']:
|
||||
_t['last_reply_user'] = last_reply_user_dict[_t['last_reply_user_id']]
|
||||
_t['tab'] = tab_dict[_t['tab_id']]
|
||||
_t['sub_tab'] = sub_tab_dict[_t['sub_tab_id']]
|
||||
_t['comment_count'] = comment_count_dict.get(_t['id'], 0)
|
||||
# 获取up和down数量
|
||||
if up_down_dict_list.get(_t['id']):
|
||||
_t['up_count'] = len([i for i in up_down_dict_list[_t['id']] if i is True])
|
||||
_t['down_count'] = len([i for i in up_down_dict_list[_t['id']] if i is False])
|
||||
else:
|
||||
_t['up_count'] = 0
|
||||
_t['down_count'] = 0
|
||||
|
||||
return {'total': topic_count, 'list': topic_list, 'fav_users': fav_user_list}
|
||||
|
||||
@login_required()
|
||||
@need_params(*['uid', 'action'])
|
||||
def post(self):
|
||||
obj = Users.get_by_uid(self.input.uid)
|
||||
# 自己不能关注自己
|
||||
if session['user_info']['uid'] == obj.uid:
|
||||
raise LogicError('自己不能关注自己哦')
|
||||
if self.input.action == 'add':
|
||||
user_fav_obj = UserFavUser.filter_by_query(
|
||||
fav_user_id=obj.id,
|
||||
user_id=session['user_info']['id'],
|
||||
show_deleted=True
|
||||
).first()
|
||||
if user_fav_obj:
|
||||
user_fav_obj.available = 1
|
||||
else:
|
||||
user_fav_obj = UserFavUser()
|
||||
user_fav_obj.fav_user_id = obj.id
|
||||
user_fav_obj.user_id = session['user_info']['id']
|
||||
user_fav_obj.save()
|
||||
elif self.input.action == 'cal':
|
||||
user_fav_obj = UserFavUser.filter_by_query(
|
||||
fav_user_id=obj.id,
|
||||
user_id=session['user_info']['id']
|
||||
).first()
|
||||
if user_fav_obj:
|
||||
user_fav_obj.available = 0
|
||||
user_fav_obj.save()
|
||||
123
super_bbs/core/basehandler.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import datetime
|
||||
import time
|
||||
import uuid
|
||||
import ujson
|
||||
import hashlib
|
||||
import base64
|
||||
import re
|
||||
|
||||
|
||||
class BaseHandler(object):
|
||||
|
||||
@classmethod
|
||||
def generate_hash_uuid(cls, limit=None):
|
||||
u = uuid.uuid4().hex
|
||||
if limit:
|
||||
u = u[:limit]
|
||||
return u
|
||||
|
||||
@classmethod
|
||||
def get_datetime_now(cls):
|
||||
return datetime.datetime.now()
|
||||
|
||||
@classmethod
|
||||
def get_timestamp(cls):
|
||||
return time.time()
|
||||
|
||||
@classmethod
|
||||
def time_create(cls, t):
|
||||
return t.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
@classmethod
|
||||
def strptime(cls, t, f='%Y-%m-%d %H:%M:%S'):
|
||||
return datetime.datetime.strptime(t, f)
|
||||
|
||||
@classmethod
|
||||
def get_datetime_utcnow(cls):
|
||||
return datetime.datetime.utcnow()
|
||||
|
||||
@classmethod
|
||||
def from_timestamp(cls, timestamp):
|
||||
return datetime.datetime.fromtimestamp(timestamp)
|
||||
|
||||
@classmethod
|
||||
def to_timestamp(cls, t):
|
||||
return time.mktime(t.timetuple())
|
||||
|
||||
@classmethod
|
||||
def sha1(cls, string):
|
||||
return hashlib.sha1(string.encode('UTF-8') if isinstance(string, str) else string).hexdigest()
|
||||
|
||||
@classmethod
|
||||
def b64encode(cls, bytestring):
|
||||
return base64.b64encode(bytestring.encode('UTF-8') if isinstance(bytestring, str) else bytestring)
|
||||
|
||||
@classmethod
|
||||
def b64decode(cls, bytestring):
|
||||
return base64.b64decode(bytestring.encode('UTF-8') if isinstance(bytestring, str) else bytestring)
|
||||
|
||||
@classmethod
|
||||
def dumps(cls, obj, ensure_ascii=False):
|
||||
return ujson.dumps(obj, ensure_ascii=ensure_ascii)
|
||||
|
||||
@classmethod
|
||||
def loads(cls, string, default=None):
|
||||
return ujson.loads(string) if string else default
|
||||
|
||||
@classmethod
|
||||
def timedelta(cls, **kwargs):
|
||||
return datetime.timedelta(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def is_valid_name(cls, name_str):
|
||||
pattern = re.compile(r'^[a-zA-Z][a-zA-Z0-9_-]*$')
|
||||
return pattern.match(name_str)
|
||||
|
||||
@classmethod
|
||||
def is_valid_email(cls, email_str):
|
||||
pattern = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)")
|
||||
return pattern.match(email_str)
|
||||
|
||||
|
||||
class BaseError(Exception):
|
||||
def __init__(self, msg):
|
||||
self.code = 1001
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
|
||||
class AuthError(BaseError):
|
||||
def __init__(self, msg):
|
||||
self.code = 1002
|
||||
self.msg = msg
|
||||
|
||||
|
||||
class VerifyError(BaseError):
|
||||
def __init__(self, msg):
|
||||
super(VerifyError, self).__init__(msg)
|
||||
self.code = 1003
|
||||
|
||||
|
||||
class LogicError(BaseError):
|
||||
def __init__(self, msg):
|
||||
self.code = 1004
|
||||
self.msg = msg
|
||||
|
||||
|
||||
class ParamsError(BaseError):
|
||||
def __init__(self, msg):
|
||||
self.code = 1005
|
||||
self.msg = msg
|
||||
|
||||
|
||||
class Dict(dict):
|
||||
def __getattr__(self, item):
|
||||
resp = self.get(item, None)
|
||||
if isinstance(resp, dict):
|
||||
resp = Dict(resp)
|
||||
return resp
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
self[key] = value
|
||||
130
super_bbs/core/dbwrapper.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from super_bbs.core.extensions import db
|
||||
from super_bbs.core.basehandler import BaseHandler, BaseError
|
||||
from flask import current_app
|
||||
import copy
|
||||
import datetime
|
||||
|
||||
|
||||
class DBMixin(BaseHandler):
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
instance = cls(**kwargs)
|
||||
return instance.save()
|
||||
|
||||
@classmethod
|
||||
def create_by_uid(cls, **kwargs):
|
||||
instance = cls(**kwargs)
|
||||
instance.uid = cls.generate_hash_uuid(12)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def get_by_id(cls, _id):
|
||||
instance = db.session.query(cls).filter_by(id=_id, available=1).first()
|
||||
if not instance:
|
||||
raise BaseError(cls.__name__ + ' Not Find')
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def get_by_uid(cls, uid):
|
||||
instance = db.session.query(cls).filter_by(uid=uid, available=1).first()
|
||||
if not instance:
|
||||
raise BaseError(cls.__name__ + ' Not Find')
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def get_by_query(cls, show_deleted=False, **query_dict):
|
||||
if show_deleted is False:
|
||||
query_dict['available'] = 1
|
||||
instance = db.session.query(cls).filter_by(**query_dict).first()
|
||||
if not instance:
|
||||
raise BaseError(cls.__name__ + ' Not Find')
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def filter_by_query(cls, show_deleted=False, **query_dict):
|
||||
if query_dict.get('available'):
|
||||
query_dict.pop('available')
|
||||
|
||||
if show_deleted is False:
|
||||
query_dict['available'] = 1
|
||||
|
||||
tmp = copy.deepcopy(query_dict)
|
||||
for k in tmp:
|
||||
if k not in cls.__dict__:
|
||||
query_dict.pop(k)
|
||||
return db.session.query(cls).filter_by(**query_dict)
|
||||
|
||||
@classmethod
|
||||
def create_from_dict(cls, d):
|
||||
assert isinstance(d, dict)
|
||||
instance = cls(**d)
|
||||
return instance.save()
|
||||
|
||||
def update(self, commit=True, **kwargs):
|
||||
for attr, value in list(kwargs.items()):
|
||||
setattr(self, attr, value)
|
||||
return commit and self.save() or self
|
||||
|
||||
def save(self, commit=True):
|
||||
db.session.add(self)
|
||||
if commit:
|
||||
try:
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
current_app.logger.error(e)
|
||||
db.session.rollback()
|
||||
raise e
|
||||
return self
|
||||
|
||||
def delete(self, commit=True):
|
||||
db.session.delete(self)
|
||||
if commit:
|
||||
try:
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
raise e
|
||||
|
||||
return self
|
||||
|
||||
def to_dict(self, fields_list=None, remove_fields_list=None, remove_available=True):
|
||||
if remove_fields_list:
|
||||
column_list = [column for column in self.__dict__ if column not in remove_fields_list]
|
||||
elif fields_list:
|
||||
column_list = [column for column in self.__dict__ if column in fields_list]
|
||||
else:
|
||||
column_list = self.__dict__
|
||||
|
||||
data = copy.deepcopy({column_name: getattr(self, column_name)
|
||||
for column_name in column_list})
|
||||
|
||||
if data.get('_sa_instance_state'):
|
||||
data.pop('_sa_instance_state')
|
||||
|
||||
if remove_available and data.get('available'):
|
||||
data.pop('available')
|
||||
|
||||
for k, v in data.items():
|
||||
if isinstance(v, datetime.datetime):
|
||||
data[k] = v.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def create_or_update(cls, query_dict, update_dict=None):
|
||||
instance = db.session.query(cls).filter_by(**query_dict).first()
|
||||
if instance:
|
||||
if update_dict is not None:
|
||||
return instance.update(**update_dict)
|
||||
else:
|
||||
return instance
|
||||
else:
|
||||
query_dict.update(update_dict or {})
|
||||
return cls.create(**query_dict)
|
||||
|
||||
|
||||
class BaseModal(DBMixin, db.Model):
|
||||
"""
|
||||
BaseModal
|
||||
"""
|
||||
__abstract__ = True
|
||||
13
super_bbs/core/extensions.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_mail import Mail
|
||||
from flask_session import Session
|
||||
from celery import Celery
|
||||
from flask_redis import FlaskRedis
|
||||
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
mail = Mail()
|
||||
session = Session()
|
||||
celery = Celery()
|
||||
redis_store = FlaskRedis()
|
||||
85
super_bbs/core/viewhandler.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import traceback
|
||||
from flask import views, request, jsonify, make_response, current_app, abort
|
||||
from .basehandler import BaseHandler, AuthError, LogicError, ParamsError, Dict, VerifyError, BaseError
|
||||
|
||||
|
||||
class BaseViewHandler(views.MethodView, BaseHandler):
|
||||
|
||||
@property
|
||||
def input(self):
|
||||
d = Dict(request.values.items())
|
||||
if request.is_json:
|
||||
d.update(request.json)
|
||||
return d
|
||||
|
||||
|
||||
class ApiViewHandler(BaseViewHandler):
|
||||
|
||||
def dispatch_request(self, *args, **kwargs):
|
||||
self.set_header = {}
|
||||
self.set_cookie = {}
|
||||
self.delete_cookie = []
|
||||
self.http_origin = request.environ.get('HTTP_ORIGIN', '')
|
||||
|
||||
handler = getattr(self, request.method.lower(), None)
|
||||
|
||||
if not callable(handler):
|
||||
if request.method == 'HEAD':
|
||||
return make_response('', 200)
|
||||
else:
|
||||
abort(405)
|
||||
|
||||
return self.api_wrapper(handler, *args, **kwargs)
|
||||
|
||||
def api_wrapper(self, handler, *args, **kwargs):
|
||||
code = 1000
|
||||
data = msg = None
|
||||
start_time = self.get_timestamp()
|
||||
try:
|
||||
data = handler(*args, **kwargs)
|
||||
except AuthError as e:
|
||||
code = e.code
|
||||
msg = str(e)
|
||||
except LogicError as e:
|
||||
code = e.code
|
||||
msg = str(e)
|
||||
err = traceback.format_exc()
|
||||
current_app.logger.error(err)
|
||||
except ParamsError as e:
|
||||
code = e.code
|
||||
msg = str(e)
|
||||
except VerifyError as e:
|
||||
code = e.code
|
||||
msg = str(e)
|
||||
except BaseError as e:
|
||||
code = e.code
|
||||
msg = str(e)
|
||||
err = traceback.format_exc()
|
||||
current_app.logger.error(err)
|
||||
except Exception:
|
||||
code = 5000
|
||||
msg = '系统内部错误'
|
||||
err = traceback.format_exc()
|
||||
current_app.logger.error(err)
|
||||
|
||||
cost_time = self.get_timestamp() - start_time
|
||||
|
||||
res = {'code': code, 'data': data, 'cost': "{0}ms".format(round(cost_time * 1000, 2))}
|
||||
if msg:
|
||||
res.update({'msg': msg})
|
||||
|
||||
response = make_response(jsonify(res))
|
||||
|
||||
if self.set_header:
|
||||
for k, v in self.set_header.items():
|
||||
response.headers[k] = v
|
||||
|
||||
if self.set_cookie:
|
||||
for k, v in self.set_cookie.items():
|
||||
response.set_cookie(k, v)
|
||||
|
||||
if self.delete_cookie:
|
||||
for k in self.delete_cookie:
|
||||
response.delete_cookie(k)
|
||||
|
||||
return response
|
||||
34
super_bbs/model/comments.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from datetime import datetime
|
||||
from super_bbs.core.dbwrapper import BaseModal, db
|
||||
|
||||
|
||||
class Comments(BaseModal):
|
||||
"""
|
||||
comments 表
|
||||
"""
|
||||
__tablename__ = 'super_bbs_comments'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
uid = db.Column(db.String(64), unique=True, index=True)
|
||||
topic_id = db.Column(db.Integer, nullable=False)
|
||||
user_id = db.Column(db.Integer, nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
like_count = db.Column(db.Integer, default=0)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class CommentThank(BaseModal):
|
||||
"""
|
||||
comment 感谢关联表
|
||||
"""
|
||||
__tablename__ = 'super_bbs_comment_like'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
comment_id = db.Column(db.Integer, nullable=False)
|
||||
user_id = db.Column(db.Integer, nullable=False)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
50
super_bbs/model/tabs.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from datetime import datetime
|
||||
from super_bbs.core.dbwrapper import BaseModal, db
|
||||
|
||||
|
||||
class Tabs(BaseModal):
|
||||
"""
|
||||
tab 表
|
||||
"""
|
||||
__tablename__ = 'super_bbs_tabs'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
uid = db.Column(db.String(64), unique=True, index=True)
|
||||
name = db.Column(db.String(128), nullable=False, unique=True)
|
||||
zh = db.Column(db.String(128), nullable=False, unique=True)
|
||||
sort_num = db.Column(db.Integer, default=100)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class SubTabs(BaseModal):
|
||||
"""
|
||||
tab 子表
|
||||
"""
|
||||
__tablename__ = 'super_bbs_sub_tabs'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
uid = db.Column(db.String(64), unique=True, index=True)
|
||||
tab_id = db.Column(db.Integer, nullable=False)
|
||||
name = db.Column(db.String(128), nullable=False, unique=True)
|
||||
zh = db.Column(db.String(128), nullable=False, unique=True)
|
||||
desc = db.Column(db.String(256))
|
||||
sort_num = db.Column(db.Integer, default=100)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class SubTabFav(BaseModal):
|
||||
"""
|
||||
tab user 收藏关联表
|
||||
"""
|
||||
__tablename__ = 'super_bbs_sub_tab_fav'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
sub_tab_id = db.Column(db.Integer, nullable=False)
|
||||
user_id = db.Column(db.Integer, nullable=False)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
110
super_bbs/model/topics.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from datetime import datetime
|
||||
from super_bbs.core.dbwrapper import BaseModal, db
|
||||
|
||||
|
||||
class Topics(BaseModal):
|
||||
"""
|
||||
topics 表
|
||||
"""
|
||||
__tablename__ = 'super_bbs_topics'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
uid = db.Column(db.String(64), unique=True, index=True)
|
||||
user_id = db.Column(db.Integer, nullable=False)
|
||||
title = db.Column(db.String(256), nullable=False)
|
||||
content = db.Column(db.Text(16000))
|
||||
content_length = db.Column(db.Integer)
|
||||
tab_id = db.Column(db.Integer, nullable=False)
|
||||
sub_tab_id = db.Column(db.Integer, nullable=False)
|
||||
last_reply_user_id = db.Column(db.Integer)
|
||||
last_reply_time = db.Column(db.DateTime)
|
||||
view_count = db.Column(db.Integer, default=0)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class TopicAppends(BaseModal):
|
||||
"""
|
||||
topic 的append表
|
||||
"""
|
||||
__tablename__ = 'super_bbs_topic_appends'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
uid = db.Column(db.String(64), unique=True, index=True)
|
||||
topic_id = db.Column(db.Integer, nullable=False)
|
||||
content = db.Column(db.Text(16000), nullable=False)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class TopicFav(BaseModal):
|
||||
"""
|
||||
topic 收藏表
|
||||
"""
|
||||
__tablename__ = 'super_bbs_topic_fav'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
topic_id = db.Column(db.Integer, nullable=False)
|
||||
user_id = db.Column(db.Integer, nullable=False)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class TopicThank(BaseModal):
|
||||
"""
|
||||
topic 感谢关联表
|
||||
"""
|
||||
__tablename__ = 'super_bbs_topic_like'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
topic_id = db.Column(db.Integer, nullable=False)
|
||||
user_id = db.Column(db.Integer, nullable=False)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class TopicUpDown(BaseModal):
|
||||
"""
|
||||
topic 点up和点down统计
|
||||
"""
|
||||
__tablename__ = 'super_bbs_topic_up_down'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
topic_id = db.Column(db.Integer, nullable=False)
|
||||
user_id = db.Column(db.Integer, nullable=False)
|
||||
action = db.Column(db.Boolean, nullable=False)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class Tags(BaseModal):
|
||||
"""
|
||||
tag 表
|
||||
"""
|
||||
__tablename__ = 'super_bbs_tags'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
uid = db.Column(db.String(64), unique=True, index=True)
|
||||
name = db.Column(db.String(256), nullable=False)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class TopicToTag(BaseModal):
|
||||
"""
|
||||
topic 关联 tag 表
|
||||
"""
|
||||
__tablename__ = 'super_bbs_topic_to_tag'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
topic_id = db.Column(db.Integer, nullable=False)
|
||||
tag_id = db.Column(db.Integer, nullable=False)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
95
super_bbs/model/users.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from datetime import datetime
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from super_bbs.core.dbwrapper import BaseModal, db
|
||||
from super_bbs.constants import default_password
|
||||
|
||||
|
||||
class Users(BaseModal):
|
||||
"""
|
||||
用户表
|
||||
"""
|
||||
__tablename__ = 'super_bbs_users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
uid = db.Column(db.String(64), unique=True, index=True)
|
||||
username = db.Column(db.String(64), nullable=False, unique=True)
|
||||
email = db.Column(db.String(64), nullable=False, unique=True)
|
||||
password = db.Column(db.String(256), nullable=False)
|
||||
sex = db.Column(db.Integer, default=0)
|
||||
avatar_url = db.Column(db.String(32))
|
||||
role_id = db.Column(db.Integer, default=0) # role_id 是否是管理员或者其他权限 0是一般用户 1 管理员用户
|
||||
site = db.Column(db.String(256))
|
||||
location = db.Column(db.String(256))
|
||||
company = db.Column(db.String(256))
|
||||
github = db.Column(db.String(256))
|
||||
twitter = db.Column(db.String(256))
|
||||
weibo = db.Column(db.String(256))
|
||||
bio = db.Column(db.String(512))
|
||||
privacy_level = db.Column(db.Integer, default=0) # 0 所有人可以查看我的回复 我的主题 1 只有登录的人可以查看 2 只有自己可以查看
|
||||
status = db.Column(db.Boolean, default=1)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
def set_password(self, password):
|
||||
self.password = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password, password)
|
||||
|
||||
def reset_password(self):
|
||||
return self.set_password(default_password)
|
||||
|
||||
|
||||
class Passport(BaseModal):
|
||||
"""
|
||||
用户session id
|
||||
"""
|
||||
__tablename__ = 'super_bbs_passports'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, nullable=False)
|
||||
data = db.Column(db.String(256))
|
||||
token = db.Column(db.String(64), nullable=False)
|
||||
expire = db.Column(db.DateTime)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class UserFavUser(BaseModal):
|
||||
"""
|
||||
user user 收藏关联表
|
||||
"""
|
||||
__tablename__ = 'super_bbs_user_fav_user'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
fav_user_id = db.Column(db.Integer, nullable=False)
|
||||
user_id = db.Column(db.Integer, nullable=False)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
|
||||
class CeleryTaskLogs(BaseModal):
|
||||
"""
|
||||
Celery 任务记录
|
||||
"""
|
||||
__tablename__ = 'super_bbs_celery_task_logs'
|
||||
|
||||
success_status = 1
|
||||
fail_status = 0
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
task_name = db.Column(db.String(128))
|
||||
task_id = db.Column(db.String(64), index=True, unique=True)
|
||||
retval = db.Column(db.Text)
|
||||
done = db.Column(db.Boolean)
|
||||
task_status = db.Column(db.Boolean)
|
||||
exc = db.Column(db.Text)
|
||||
einfo = db.Column(db.Text)
|
||||
args = db.Column(db.Text)
|
||||
kwargs = db.Column(db.Text)
|
||||
available = db.Column(db.Boolean, default=1)
|
||||
time_done = db.Column(db.DateTime)
|
||||
time_create = db.Column(db.DateTime, default=datetime.now)
|
||||
time_modify = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
23
super_bbs/router/admin.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from super_bbs.controller.admin.account.api import LoginAPI, LoginOutAPI, LoginStatusCheck
|
||||
from super_bbs.controller.admin.users.api import UserAPI
|
||||
from super_bbs.controller.admin.tabs.api import TabAPI, SubTabAPI
|
||||
from super_bbs.controller.admin.topics.api import TopicAPI
|
||||
from super_bbs.controller.admin.comments.api import CommentAPI
|
||||
|
||||
|
||||
routers = [
|
||||
# account
|
||||
('/account/login', LoginAPI, 'admin_account_login'),
|
||||
('/account/logout', LoginOutAPI, 'admin_account_logout'),
|
||||
('/account/check', LoginStatusCheck, 'admin_account_login_status_check'),
|
||||
# user
|
||||
('/user', UserAPI, 'admin_user'),
|
||||
# tab
|
||||
('/tab', TabAPI, 'admin_tab'),
|
||||
# sub_tab
|
||||
('/sub_tab', SubTabAPI, 'admin_sub_tab'),
|
||||
# topic
|
||||
('/topic', TopicAPI, 'admin_topic'),
|
||||
# comment
|
||||
('/comment', CommentAPI, 'admin_comment')
|
||||
]
|
||||
39
super_bbs/router/v1.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from super_bbs.controller.v1.account.api import LoginAPI, LoginOutAPI, RegisterAPI, ProfileAPI, \
|
||||
GetCaptchaHandler, CaptchaCheckAPI, SendEmailAPI, LoginStatusCheck, UpdatePasswordAPI
|
||||
from super_bbs.controller.v1.index.api import IndexMixedAPI
|
||||
from super_bbs.controller.v1.topic.api import TopicAPI, TopicUpDownCountAPI, TopicAppendAPI, TopicCommentAPI, \
|
||||
TopicFavAPI, TopicThankAPI, CommentThankAPI, TopicAddView, UserFavAPI
|
||||
from super_bbs.controller.v1.tabs.api import TabMixedAPI, SubTabAPI, TabAPI, SubTabFavAPI
|
||||
from super_bbs.controller.v1.member.api import MemberIndexAPI, MemberTopicAPI, MemberCommentAPI
|
||||
|
||||
routers = [
|
||||
# account
|
||||
('/account/login', LoginAPI, 'account_login'),
|
||||
('/account/register', RegisterAPI, 'account_register'),
|
||||
('/account/logout', LoginOutAPI, 'account_logout'),
|
||||
('/account/check', LoginStatusCheck, 'account_login_status_check'),
|
||||
('/account/profile', ProfileAPI, 'account_profile'),
|
||||
('/account/password', UpdatePasswordAPI, 'account_password'),
|
||||
# captcha
|
||||
('/captcha', GetCaptchaHandler, 'captcha_get'),
|
||||
('/captcha/check', CaptchaCheckAPI, 'captcha_check'),
|
||||
# send email
|
||||
('/email/send', SendEmailAPI, 'email_send'),
|
||||
('/index', IndexMixedAPI, 'index'),
|
||||
('/go', TabMixedAPI, 'go'),
|
||||
('/tab', TabAPI, 'tab'),
|
||||
('/sub_tab', SubTabAPI, 'sub_tab'),
|
||||
('/sub_tab/fav', SubTabFavAPI, 'sub_tab_fav'),
|
||||
('/topic', TopicAPI, 'topic'),
|
||||
('/topic/append', TopicAppendAPI, 'topic_append'),
|
||||
('/topic/fav', TopicFavAPI, 'topic_fav'),
|
||||
('/topic/view', TopicAddView, 'topic_view'),
|
||||
('/topic/thank', TopicThankAPI, 'topic_thank'),
|
||||
('/topic/comment', TopicCommentAPI, 'topic_comment'),
|
||||
('/topic/comment/thank', CommentThankAPI, 'topic_comment_thank'),
|
||||
('/topic/up_down', TopicUpDownCountAPI, 'topic_up_down'),
|
||||
('/user/fav', UserFavAPI, 'user_fav'),
|
||||
('/member', MemberIndexAPI, 'member'),
|
||||
('/member/topic', MemberTopicAPI, 'member_topic'),
|
||||
('/member/comment', MemberCommentAPI, 'member_comment'),
|
||||
]
|
||||
189
super_bbs/tabs.json
Normal file
@@ -0,0 +1,189 @@
|
||||
[
|
||||
{
|
||||
"zh": "技术",
|
||||
"name": "tech",
|
||||
"sort_num": 1,
|
||||
"sub_tabs": [
|
||||
{
|
||||
"zh": "程序员",
|
||||
"name": "programmer",
|
||||
"sort_num": 1,
|
||||
"desc": "程序员频道,大家可以讨论程序员相关的事情。"
|
||||
},
|
||||
{
|
||||
"zh": "Python",
|
||||
"name": "python",
|
||||
"sort_num": 2,
|
||||
"desc": "这里讨论各种 Python 语言编程话题,也包括 Django,Tornado 等框架的讨论。这里是一个能够帮助你解决实际问题的地方。"
|
||||
},
|
||||
{
|
||||
"zh": "iDev",
|
||||
"name": "idev",
|
||||
"sort_num": 3,
|
||||
"desc": "iOS 及 OS X 开发技术讨论区,iOS 是 iPhone 及 iPad 上运行的操作系统。"
|
||||
},
|
||||
{
|
||||
"zh": "Android",
|
||||
"name": "android",
|
||||
"sort_num": 4,
|
||||
"desc": "来自 Google 的开放源代码智能手机平台。"
|
||||
},
|
||||
{
|
||||
"zh": "Linux",
|
||||
"name": "linux",
|
||||
"sort_num": 5,
|
||||
"desc": "Ubuntu CnametOS RedHat ..."
|
||||
},
|
||||
{
|
||||
"zh": "云计算",
|
||||
"name": "cloud",
|
||||
"sort_num": 6,
|
||||
"desc": "关于云计算技术和平台的综合讨论区。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"zh": "创意",
|
||||
"name": "creative",
|
||||
"sort_num": 2,
|
||||
"sub_tabs": [
|
||||
{
|
||||
"zh": "分享创造",
|
||||
"name": "create",
|
||||
"sort_num": 1,
|
||||
"desc": "欢迎你在这里发布自己的最新作品!"
|
||||
},
|
||||
{
|
||||
"zh": "设计",
|
||||
"name": "design",
|
||||
"sort_num": 2,
|
||||
"desc": "Beautiful adj. Pleasing the snameses or mind aesthetically."
|
||||
},
|
||||
{
|
||||
"zh": "奇思妙想",
|
||||
"name": "ideas",
|
||||
"sort_num": 3,
|
||||
"desc": "让你的创意在这里自由流动吧。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"zh": "好玩",
|
||||
"name": "play",
|
||||
"sort_num": 3,
|
||||
"sub_tabs": [
|
||||
{
|
||||
"zh": "分享发现",
|
||||
"name": "share",
|
||||
"sort_num": 1,
|
||||
"desc": "分享你看到的好玩的,有信息量的,欢迎从这里获取灵感。"
|
||||
},
|
||||
{
|
||||
"zh": "电子游戏",
|
||||
"name": "games",
|
||||
"sort_num": 2,
|
||||
"desc": "Life is short, have more fun."
|
||||
},
|
||||
{
|
||||
"zh": "电影",
|
||||
"name": "movie",
|
||||
"sort_num": 3,
|
||||
"desc": "用 90 分钟去体验另外一个世界。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"zh": "酷工作",
|
||||
"name": "jobs",
|
||||
"sort_num": 4,
|
||||
"sub_tabs": [
|
||||
{
|
||||
"zh": "酷工作",
|
||||
"name": "jobs",
|
||||
"sort_num": 1,
|
||||
"desc": "做有趣的有意义的事情。"
|
||||
},
|
||||
{
|
||||
"zh": "求职",
|
||||
"name": "cv",
|
||||
"sort_num": 2,
|
||||
"desc": "欢迎在这里发布自己的求职简历。"
|
||||
},
|
||||
{
|
||||
"zh": "职场话题",
|
||||
"name": "career",
|
||||
"sort_num": 3,
|
||||
"desc": "这里,我们聊聊那些工作中遇到的开心和不开心的事。"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"zh": "交易",
|
||||
"name": "deals",
|
||||
"sort_num": 5,
|
||||
"sub_tabs": [
|
||||
{
|
||||
"zh": "二手交易",
|
||||
"name": "all4all",
|
||||
"sort_num": 1,
|
||||
"desc": "为自己的闲置物品找到更好的主人。"
|
||||
},
|
||||
{
|
||||
"zh": "物物交换",
|
||||
"name": "exchange",
|
||||
"sort_num": 2,
|
||||
"desc": "将自己不需要的闲置物品拿出来和大家交换吧。"
|
||||
},
|
||||
{
|
||||
"zh": "免费赠送",
|
||||
"name": "free",
|
||||
"sort_num": 3,
|
||||
"desc": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"zh": "城市",
|
||||
"name": "city",
|
||||
"sort_num": 6,
|
||||
"sub_tabs": [
|
||||
{
|
||||
"zh": "北京",
|
||||
"name": "beijing",
|
||||
"sort_num": 1,
|
||||
"desc": null
|
||||
},
|
||||
{
|
||||
"zh": "上海",
|
||||
"name": "shanghai",
|
||||
"sort_num": 2,
|
||||
"desc": null
|
||||
},
|
||||
{
|
||||
"zh": "深圳",
|
||||
"name": "shenzhen",
|
||||
"sort_num": 3,
|
||||
"desc": null
|
||||
},
|
||||
{
|
||||
"zh": "广州",
|
||||
"name": "guangzhou",
|
||||
"sort_num": 4,
|
||||
"desc": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"zh": "问与答",
|
||||
"name": "qna",
|
||||
"sort_num": 7,
|
||||
"sub_tabs": [
|
||||
{
|
||||
"zh": "问与答",
|
||||
"name": "qna",
|
||||
"sort_num": 1,
|
||||
"desc": "一个更好的世界需要你持续地提出好问题。"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
156
super_bbs/utils.py
Normal file
@@ -0,0 +1,156 @@
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
import traceback
|
||||
import random
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
||||
from flask import current_app
|
||||
from flask import session
|
||||
from flask_mail import Message
|
||||
from super_bbs.core.extensions import mail, redis_store
|
||||
from super_bbs.core.basehandler import LogicError, AuthError, BaseError, VerifyError
|
||||
from super_bbs.config import BASE_DIR
|
||||
from super_bbs.constants import release_lock_script
|
||||
|
||||
|
||||
class HttpClient(object):
|
||||
|
||||
def __init__(self, cookies=None):
|
||||
self.client = requests
|
||||
self.cookies = cookies
|
||||
|
||||
def get(self, url, params=None, cookies=None):
|
||||
if cookies:
|
||||
cookies.update(self.cookies)
|
||||
else:
|
||||
cookies = self.cookies
|
||||
try:
|
||||
ret = self.client.get(url=url, params=params, cookies=cookies)
|
||||
if ret.status_code not in [200, 201]:
|
||||
raise BaseError(f'request {url}, params {params} error, http code is {ret.status_code}')
|
||||
return ret
|
||||
except Exception:
|
||||
err = traceback.format_exc()
|
||||
raise BaseError(f'request error: {err}')
|
||||
|
||||
def post(self, url, data=None, cookies=None):
|
||||
if cookies:
|
||||
cookies.update(self.cookies)
|
||||
else:
|
||||
cookies = self.cookies
|
||||
try:
|
||||
ret = self.client.post(url=url, json=data, cookies=cookies)
|
||||
if ret.status_code not in [200, 201]:
|
||||
raise BaseError(f'request {url}, data {data} error, http code is {ret.status_code}')
|
||||
return ret
|
||||
except Exception:
|
||||
err = traceback.format_exc()
|
||||
raise BaseError(f'request error: {err}')
|
||||
|
||||
|
||||
def login_required(admin=False):
|
||||
def func_wrapper(func):
|
||||
@functools.wraps(func)
|
||||
def _func_wrapper(cls, *args, **kwargs):
|
||||
if not session.get('is_login'):
|
||||
raise AuthError('登录后才可以操作哦')
|
||||
if admin:
|
||||
if session.get('role_id') != 1:
|
||||
raise VerifyError('没有操作权限')
|
||||
|
||||
return func(cls, *args, **kwargs)
|
||||
|
||||
return _func_wrapper
|
||||
|
||||
return func_wrapper
|
||||
|
||||
|
||||
def need_params(*params, **type_params):
|
||||
def dec(func):
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
for arg in params:
|
||||
if getattr(self.input, arg) is None:
|
||||
raise LogicError('需要:%s 参数' % arg)
|
||||
if getattr(self.input, arg) == '':
|
||||
raise LogicError('参数:%s 不能为空' % arg)
|
||||
for k, _type in type_params.items():
|
||||
if getattr(self.input, k) is None:
|
||||
raise LogicError('需要:%s 参数' % k)
|
||||
if getattr(self.input, k) == '':
|
||||
raise LogicError('参数:%s 不能为空' % k)
|
||||
if not isinstance(getattr(self.input, k), _type):
|
||||
raise LogicError('参数 "%s" 类型应该是: %s' % (k, _type))
|
||||
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return dec
|
||||
|
||||
|
||||
def generate_check_code(width=120, height=30, char_length=5, font_name='Rocko.ttf', font_size=28):
|
||||
font_file = os.path.join(BASE_DIR, font_name)
|
||||
|
||||
code = []
|
||||
img = Image.new(mode='RGB', size=(width, height), color=(255, 255, 255))
|
||||
draw = ImageDraw.Draw(img, mode='RGB')
|
||||
|
||||
def rndChar():
|
||||
"""
|
||||
生成随机字母
|
||||
:return:
|
||||
"""
|
||||
return chr(random.randint(65, 90))
|
||||
|
||||
def rndColor():
|
||||
"""
|
||||
生成随机颜色
|
||||
:return:
|
||||
"""
|
||||
return (random.randint(0, 255), random.randint(10, 255), random.randint(64, 255))
|
||||
|
||||
font = ImageFont.truetype(font_file, font_size)
|
||||
for i in range(char_length):
|
||||
char = rndChar()
|
||||
code.append(char)
|
||||
h = random.randint(0, 4)
|
||||
draw.text([i * width / char_length, h], char, font=font, fill=rndColor())
|
||||
|
||||
for i in range(40):
|
||||
draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor())
|
||||
|
||||
for i in range(40):
|
||||
draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor())
|
||||
x = random.randint(0, width)
|
||||
y = random.randint(0, height)
|
||||
draw.arc((x, y, x + 4, y + 4), 0, 90, fill=rndColor())
|
||||
|
||||
for i in range(5):
|
||||
x1 = random.randint(0, width)
|
||||
y1 = random.randint(0, height)
|
||||
x2 = random.randint(0, width)
|
||||
y2 = random.randint(0, height)
|
||||
|
||||
draw.line((x1, y1, x2, y2), fill=rndColor())
|
||||
|
||||
img = img.filter(ImageFilter.EDGE_ENHANCE_MORE)
|
||||
return img, ''.join(code)
|
||||
|
||||
|
||||
def send_mail(body, title, recipients):
|
||||
msg = Message(
|
||||
title,
|
||||
recipients=recipients
|
||||
)
|
||||
msg.body = body
|
||||
msg.html = "<h1>big</h1>"
|
||||
mail.send(msg)
|
||||
current_app.logger.info('mail send to {0}'.format(','.join(recipients)))
|
||||
|
||||
|
||||
def release_redis_lock(lock_key_name, lock_value):
|
||||
script_client = redis_store.register_script(release_lock_script)
|
||||
return script_client(keys=[lock_key_name], args=[lock_value])
|
||||
29
web/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# vue-startup
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn run build
|
||||
```
|
||||
|
||||
### Run your tests
|
||||
```
|
||||
yarn run test
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
5
web/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
||||
87
web/package.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "vue-startup",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"core-js": "^2.6.5",
|
||||
"echarts": "^4.2.1",
|
||||
"lodash": "^4.17.11",
|
||||
"normalize.css": "^8.0.1",
|
||||
"qs": "^6.7.0",
|
||||
"uuid": "^3.3.2",
|
||||
"view-design": "^4.0.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-meditor": "^2.0.2",
|
||||
"vue-router": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.6.0",
|
||||
"@vue/cli-plugin-eslint": "^3.6.0",
|
||||
"@vue/cli-service": "^3.6.0",
|
||||
"@vue/eslint-config-standard": "^4.0.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-config-vue": "^2.0.2",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-loader": "^2.1.2",
|
||||
"eslint-plugin-flow-vars": "^0.5.0",
|
||||
"eslint-plugin-html": "^5.0.5",
|
||||
"eslint-plugin-import": "^2.17.2",
|
||||
"eslint-plugin-node": "^9.0.1",
|
||||
"eslint-plugin-promise": "^4.1.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^5.2.2",
|
||||
"lint-staged": "^8.1.5",
|
||||
"node-sass": "^4.9.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"vue-template-compiler": "^2.5.21"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"@vue/standard"
|
||||
],
|
||||
"rules": {
|
||||
"vue/no-parsing-error": [
|
||||
2,
|
||||
{
|
||||
"x-invalid-end-tag": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
],
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue}": [
|
||||
"vue-cli-service lint",
|
||||
"git add"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
web/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
20
web/public/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="height: 100%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
||||
<title>super-bbs admin</title>
|
||||
</head>
|
||||
<body style="height: 100%;margin: 0;">
|
||||
<noscript>
|
||||
<strong
|
||||
>We're sorry but vue-startup doesn't work properly without JavaScript
|
||||
enabled. Please enable it to continue.</strong
|
||||
>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
3
web/src/App.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
32
web/src/api/account.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import api from '@/common/api'
|
||||
|
||||
const check = () => api.get('account/check', { t: new Date().getTime() })
|
||||
|
||||
const profile = () => {
|
||||
return api.get(`account/profile`, { t: new Date().getTime() })
|
||||
}
|
||||
|
||||
const login = params => api.post(`account/login`, params)
|
||||
const register = params => api.post(`account/register`, params)
|
||||
const updateProfile = params => api.post(`account/profile`, params)
|
||||
const updatePassword = params => api.post('account/password', params)
|
||||
|
||||
const logout = () => {
|
||||
return api.post(`account/logout`)
|
||||
}
|
||||
|
||||
const checkCode = params => api.post('captcha/check', params)
|
||||
|
||||
const sendEmailCode = params => api.post('email/send', params)
|
||||
|
||||
export default {
|
||||
check,
|
||||
profile,
|
||||
login,
|
||||
updateProfile,
|
||||
updatePassword,
|
||||
logout,
|
||||
register,
|
||||
checkCode,
|
||||
sendEmailCode
|
||||
}
|
||||
9
web/src/api/main.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import api from '@/common/api'
|
||||
|
||||
const index = params => api.get(`index`, params)
|
||||
const go = params => api.get(`go`, params)
|
||||
|
||||
export default {
|
||||
index,
|
||||
go
|
||||
}
|
||||
11
web/src/api/member.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import api from '@/common/api'
|
||||
|
||||
const detail = params => api.get(`member`, params)
|
||||
const listTopic = params => api.get('member/topic', params)
|
||||
const listComment = params => api.get('member/comment', params)
|
||||
|
||||
export default {
|
||||
detail,
|
||||
listTopic,
|
||||
listComment
|
||||
}
|
||||
12
web/src/api/tab.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import api from '@/common/api'
|
||||
|
||||
const list = params => api.get(`tab`, params)
|
||||
const listSubTab = params => api.get(`sub_tab`, params)
|
||||
const favSubTab = params => api.post(`sub_tab/fav`, params)
|
||||
const listFavSubTab = params => api.get('sub_tab/fav', params)
|
||||
export default {
|
||||
list,
|
||||
listSubTab,
|
||||
favSubTab,
|
||||
listFavSubTab
|
||||
}
|
||||
25
web/src/api/topic.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import api from '@/common/api'
|
||||
|
||||
const list = params => api.get(`topic`, params)
|
||||
const create = params => api.post(`topic`, params)
|
||||
const append = params => api.post(`topic/append`, params)
|
||||
const comment = params => api.post(`topic/comment`, params)
|
||||
const fav = params => api.post(`topic/fav`, params)
|
||||
const listFav = params => api.get(`topic/fav`, params)
|
||||
const addView = params => api.post(`topic/view`, params)
|
||||
const upDown = params => api.post(`topic/up_down`, params)
|
||||
const thank = params => api.post(`topic/thank`, params)
|
||||
const commentThank = params => api.post(`topic/comment/thank`, params)
|
||||
|
||||
export default {
|
||||
list,
|
||||
create,
|
||||
append,
|
||||
comment,
|
||||
fav,
|
||||
listFav,
|
||||
upDown,
|
||||
thank,
|
||||
commentThank,
|
||||
addView
|
||||
}
|
||||
9
web/src/api/user.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import api from '@/common/api'
|
||||
|
||||
const fav = params => api.post(`user/fav`, params)
|
||||
const listFav = params => api.get(`user/fav`, params)
|
||||
|
||||
export default {
|
||||
fav,
|
||||
listFav
|
||||
}
|
||||
BIN
web/src/assets/bg-img.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
53
web/src/common/api.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as http from './http'
|
||||
|
||||
const handleResponse = res => {
|
||||
if (!res.data || res.data.code !== 1000) {
|
||||
console.error(
|
||||
res.config.method,
|
||||
res.config.url,
|
||||
res.config.params,
|
||||
res.config.data
|
||||
)
|
||||
throw res.data || res
|
||||
}
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
class Api {
|
||||
constructor (baseUrl) {
|
||||
this.baseUrl = baseUrl
|
||||
}
|
||||
|
||||
_getUrl (url) {
|
||||
return `/${this.baseUrl}/${url}`
|
||||
}
|
||||
|
||||
async get (url, data) {
|
||||
let res = await http.get(this._getUrl(url), data)
|
||||
return handleResponse(res)
|
||||
}
|
||||
|
||||
async post (url, data) {
|
||||
let res = await http.post(this._getUrl(url), data)
|
||||
return handleResponse(res)
|
||||
}
|
||||
|
||||
async put (url, data) {
|
||||
let res = await http.put(this._getUrl(url), data)
|
||||
return handleResponse(res)
|
||||
}
|
||||
|
||||
async _delete (url, data) {
|
||||
let res = await http._delete(this._getUrl(url), data)
|
||||
return handleResponse(res)
|
||||
}
|
||||
|
||||
async postForm (url, data) {
|
||||
let res = await http.postForm(this._getUrl(url), data)
|
||||
return handleResponse(res)
|
||||
}
|
||||
}
|
||||
|
||||
const api = new Api('api/v1')
|
||||
|
||||
export default api
|
||||
43
web/src/common/date.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import dateFormatter from './dateFormatter'
|
||||
|
||||
export const getThisWeek = () => {
|
||||
let date = new Date()
|
||||
let day = date.getDay() || 7
|
||||
date.setDate(date.getDate() - day)
|
||||
return date
|
||||
}
|
||||
|
||||
export const getThisMonth = () => {
|
||||
let date = new Date()
|
||||
date.setDate(1)
|
||||
return date
|
||||
}
|
||||
|
||||
export const getThisYear = () => {
|
||||
let date = new Date()
|
||||
return date.getFullYear()
|
||||
}
|
||||
|
||||
export const getToday = () => {
|
||||
let date = new Date()
|
||||
date.setHours(0)
|
||||
date.setMinutes(0)
|
||||
date.setSeconds(0)
|
||||
date.setMilliseconds(0)
|
||||
return date
|
||||
}
|
||||
|
||||
export const getMonthStart = date => {
|
||||
let startTime = new Date(date)
|
||||
startTime.setDate(1)
|
||||
startTime.setHours(0, 0, 0, 0)
|
||||
return dateFormatter(startTime, 'yyyy-MM-dd hh:mm:ss')
|
||||
}
|
||||
|
||||
export const getMonthEnd = date => {
|
||||
let endTime = new Date(date)
|
||||
endTime.setMonth(endTime.getMonth() + 1)
|
||||
endTime.setDate(1)
|
||||
endTime.setHours(0, 0, 0, 0)
|
||||
return dateFormatter(endTime, 'yyyy-MM-dd hh:mm:ss')
|
||||
}
|
||||
22
web/src/common/dateFormatter.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default (date, format) => {
|
||||
const o = {
|
||||
'M+': date.getMonth() + 1,
|
||||
'd+': date.getDate(),
|
||||
'h+': date.getHours(),
|
||||
'm+': date.getMinutes(),
|
||||
's+': date.getSeconds(),
|
||||
'q+': Math.floor((date.getMonth() + 3) / 3),
|
||||
'S+': date.getMilliseconds()
|
||||
}
|
||||
format = format.replace(/y+/, match =>
|
||||
(date.getFullYear() + '').substr(4 - match.length)
|
||||
)
|
||||
for (let k in o) {
|
||||
format = format.replace(new RegExp(k), match => {
|
||||
return match.length === 1
|
||||
? o[k]
|
||||
: ('00' + o[k]).substr(('' + o[k]).length)
|
||||
})
|
||||
}
|
||||
return format
|
||||
}
|
||||
36
web/src/common/http.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import axios from 'axios'
|
||||
import qs from 'qs'
|
||||
|
||||
export const get = (url, params) => {
|
||||
return axios.get(url, {
|
||||
params,
|
||||
paramsSerializer (params) {
|
||||
return qs.stringify(params, { arrayFormat: 'repeat' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const post = (url, params) => {
|
||||
return axios.post(url, params)
|
||||
}
|
||||
|
||||
export const put = (url, params) => {
|
||||
return axios.put(url, params)
|
||||
}
|
||||
|
||||
export const _delete = (url, params) => {
|
||||
return axios.delete(url, {
|
||||
params,
|
||||
paramsSerializer (params) {
|
||||
return qs.stringify(params, { arrayFormat: 'repeat' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const postForm = (url, params) => {
|
||||
let formData = new FormData()
|
||||
Object.keys(params).forEach(key => {
|
||||
formData.append(key, params[key])
|
||||
})
|
||||
return axios.post(url, formData)
|
||||
}
|
||||
8
web/src/common/sleep.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 延迟 timeout
|
||||
* 延迟10ms:sleep(10);延迟 20ms 后返回 1:sleep(20, 1)
|
||||
* @param timeout
|
||||
* @param data
|
||||
*/
|
||||
export default (timeout, data) =>
|
||||
new Promise(resolve => setTimeout(() => resolve(data), timeout))
|
||||
37
web/src/components/loading-spin/index.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="spin-col">
|
||||
<Spin fix>
|
||||
<Icon type="ios-loading"
|
||||
size=18
|
||||
class="spin-icon-load"></Icon>
|
||||
<div>Loading</div>
|
||||
</Spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'loading'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@keyframes ani-demo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.spin-col {
|
||||
height: 200px;
|
||||
position: relative;
|
||||
}
|
||||
.spin-icon-load {
|
||||
animation: ani-demo-spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
65
web/src/components/user-box/index.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="box"
|
||||
v-if="!$user">
|
||||
<div class="cell">
|
||||
<div>
|
||||
<h4>v22x</h4>
|
||||
</div>
|
||||
<div style="color: #ccc;margin-top:5px;">这是另一个FakeV2EX</div>
|
||||
</div>
|
||||
<div class="inner">
|
||||
<div class="center"><Button to="/account/register">现在注册</Button></div>
|
||||
<div class="center"
|
||||
style="margin-top: 10px;">已注册用户请 <a class="a-link"
|
||||
href="/#/account/login">登录</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box"
|
||||
v-else>
|
||||
<div class="cell">
|
||||
<div style="display: flex;">
|
||||
<div class="avatar"><a></a></div>
|
||||
<div style="flex-grow: 1;display: flex;margin-left: 10px;">
|
||||
<div style="display: flex;align-items:center"><a :href="`/#/member?username=${$user.username}`"
|
||||
class="a-link">{{$user.username}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;justify-content:center;margin-top:10px;">
|
||||
<div style="text-align: center;">
|
||||
<a href="/#/my/nodes">
|
||||
<div>{{$user.fav_sub_tab_count}}</div>
|
||||
<div style="color: #ccc;">节点收藏</div>
|
||||
</a>
|
||||
</div>
|
||||
<div style="width: 1px;height: 35px; background: rgba(100, 100, 100, 0.4);margin: 0 10px;"></div>
|
||||
<div style="text-align: center;">
|
||||
<a href="/#/my/topics">
|
||||
<div>{{$user.fav_topic_count}}</div>
|
||||
<div style="color: #ccc;">主题收藏</div>
|
||||
</a>
|
||||
</div>
|
||||
<div style="width: 1px;height: 35px; background: rgba(100, 100, 100, 0.4);margin: 0 10px;"></div>
|
||||
<div style="text-align: center;">
|
||||
<a href="/#/my/following">
|
||||
<div>{{$user.fav_user_count}}</div>
|
||||
<div style="color: #ccc;">特别关注</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div><a class="a-link"
|
||||
href="/#/new">
|
||||
<Icon type="ios-create-outline"
|
||||
size="18" />创作新主题</a></div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div><a class="a-link">0 条未读提醒</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'user-box'
|
||||
}
|
||||
</script>
|
||||
45
web/src/main.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import Vue from 'vue'
|
||||
import 'babel-polyfill'
|
||||
import App from '@/App.vue'
|
||||
import router from '@/routers'
|
||||
import ViewUI from 'view-design'
|
||||
import 'view-design/dist/styles/iview.css'
|
||||
import 'normalize.css'
|
||||
import _ from 'lodash'
|
||||
import * as qs from 'qs'
|
||||
import sleep from '@/common/sleep'
|
||||
import * as date from '@/common/date'
|
||||
import api from '@/common/api'
|
||||
import session from '@/api/account'
|
||||
import {
|
||||
goBack,
|
||||
setItem,
|
||||
getItem,
|
||||
getUuid,
|
||||
removeItem,
|
||||
removeAll,
|
||||
diffObj,
|
||||
sortArray
|
||||
} from '@/utils'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
Vue.use(ViewUI)
|
||||
Vue.prototype.$_ = _
|
||||
Vue.prototype.$api = api
|
||||
Vue.prototype.$qs = qs
|
||||
Vue.prototype.$date = date
|
||||
Vue.prototype.$sleep = sleep
|
||||
Vue.prototype.$goBack = goBack
|
||||
Vue.prototype.$setItem = setItem
|
||||
Vue.prototype.$getItem = getItem
|
||||
Vue.prototype.$removeItem = removeItem
|
||||
Vue.prototype.$removeAll = removeAll
|
||||
Vue.prototype.$diffObj = diffObj
|
||||
Vue.prototype.$getUuid = getUuid
|
||||
Vue.prototype.$session = session
|
||||
Vue.prototype.$sortArray = sortArray
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
186
web/src/routers/index.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import ViewUI from 'view-design'
|
||||
import 'view-design/dist/styles/iview.css'
|
||||
import accountAPI from '@/api/account'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
const _import = file => () => import('@/views/' + file + '.vue')
|
||||
|
||||
let router = new Router({
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: '/',
|
||||
meta: {},
|
||||
component: _import('Layer'),
|
||||
redirect: '/index',
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
name: 'index',
|
||||
component: _import('Index'),
|
||||
meta: {
|
||||
auth: false,
|
||||
title: '首页'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'go',
|
||||
name: 'go',
|
||||
component: _import('Go'),
|
||||
meta: {
|
||||
auth: false,
|
||||
title: 'Go页面'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
name: 'new',
|
||||
component: _import('New'),
|
||||
meta: {
|
||||
auth: true,
|
||||
title: '创作新主题'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 't',
|
||||
name: 't',
|
||||
component: _import('Topic'),
|
||||
meta: {
|
||||
auth: false,
|
||||
title: '详情'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 't/append',
|
||||
name: 't_append',
|
||||
component: _import('Append'),
|
||||
meta: {
|
||||
auth: true,
|
||||
title: '追加'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'my/topics',
|
||||
name: 'my_topics',
|
||||
component: _import('MyFavTopic'),
|
||||
meta: {
|
||||
auth: true,
|
||||
title: '我收藏的主题'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'my/nodes',
|
||||
name: 'my_nodes',
|
||||
component: _import('MyFavNode'),
|
||||
meta: {
|
||||
auth: true,
|
||||
title: '我收藏的节点'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'my/following',
|
||||
name: 'my_following',
|
||||
component: _import('MyFavUserTopic'),
|
||||
meta: {
|
||||
auth: true,
|
||||
title: '我关注的用户以及他们的主题'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'member',
|
||||
name: 'member',
|
||||
component: _import('Member'),
|
||||
meta: {
|
||||
auth: false,
|
||||
title: '用户详情页'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'member/topic',
|
||||
name: 'member_topic',
|
||||
component: _import('MemberTopic'),
|
||||
meta: {
|
||||
auth: false,
|
||||
title: '用户Topic页'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'member/comment',
|
||||
name: 'member_comment',
|
||||
component: _import('MemberComment'),
|
||||
meta: {
|
||||
auth: false,
|
||||
title: '用户Comment页'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'setting',
|
||||
name: 'setting',
|
||||
component: _import('Setting'),
|
||||
meta: {
|
||||
auth: true,
|
||||
title: '用户Setting页'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/account/register',
|
||||
name: 'register',
|
||||
meta: {
|
||||
auth: false
|
||||
},
|
||||
component: _import('Register')
|
||||
}, {
|
||||
path: '/account/login',
|
||||
name: 'login',
|
||||
meta: {
|
||||
auth: false
|
||||
},
|
||||
component: _import('Login')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
name: 'default',
|
||||
redirect: '/index'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 路由拦截器
|
||||
router.beforeEach(async function (to, from, next) {
|
||||
ViewUI.LoadingBar.start()
|
||||
window.scrollTo(0, 0)
|
||||
try {
|
||||
// 组件实例添加用户对象 $user
|
||||
if (to.fullPath !== from.fullPath) {
|
||||
Vue.prototype.$user = await accountAPI.check()
|
||||
if (to.meta.auth && !Vue.prototype.$user) {
|
||||
ViewUI.Message.error('登录后才可以操作哦')
|
||||
next({
|
||||
path: '/account/login'
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
if (e.msg) {
|
||||
ViewUI.Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
ViewUI.Message.error('服务器出了点小差')
|
||||
}
|
||||
ViewUI.LoadingBar.error()
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
ViewUI.LoadingBar.finish()
|
||||
})
|
||||
|
||||
export default router
|
||||
83
web/src/utils/index.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import uuidv4 from 'uuid/v4'
|
||||
import router from '@/routers'
|
||||
import _ from 'lodash'
|
||||
|
||||
export function goBack () {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
export function setItem (name, value) {
|
||||
if (value) {
|
||||
localStorage.setItem(name, JSON.stringify(value))
|
||||
}
|
||||
}
|
||||
|
||||
export function getItem (name) {
|
||||
let _tmp = localStorage.getItem(name)
|
||||
if (_tmp) {
|
||||
return JSON.parse(_tmp)
|
||||
}
|
||||
}
|
||||
|
||||
export function removeItem (name) {
|
||||
localStorage.removeItem(name)
|
||||
}
|
||||
|
||||
export function clearItem () {
|
||||
localStorage.clear()
|
||||
}
|
||||
|
||||
export function diffObj (oldObj, newObj, ignoreKeys) {
|
||||
let dif = {}
|
||||
for (let k in newObj) {
|
||||
if (Array.isArray(ignoreKeys)) {
|
||||
if (!ignoreKeys.includes(k)) {
|
||||
if (!_.isEqual(newObj[k], oldObj[k])) {
|
||||
dif[k] = newObj[k]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!_.isEqual(newObj[k], oldObj[k])) {
|
||||
dif[k] = newObj[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
return dif
|
||||
}
|
||||
|
||||
export function removeAll () {
|
||||
sessionStorage.clear()
|
||||
}
|
||||
|
||||
export function getUuid () {
|
||||
return uuidv4()
|
||||
}
|
||||
|
||||
export function sortArray (prop) {
|
||||
return function (obj1, obj2) {
|
||||
let val1 = obj1[prop]
|
||||
let val2 = obj2[prop]
|
||||
if (!isNaN(Number(val1)) && !isNaN(Number(val2))) {
|
||||
val1 = Number(val1)
|
||||
val2 = Number(val2)
|
||||
}
|
||||
if (val1 < val2) {
|
||||
return -1
|
||||
} else if (val1 > val2) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
goBack,
|
||||
setItem,
|
||||
getItem,
|
||||
getUuid,
|
||||
removeItem,
|
||||
removeAll,
|
||||
diffObj,
|
||||
sortArray
|
||||
}
|
||||
128
web/src/views/Append.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content">
|
||||
<div class="box">
|
||||
<div class="cell">
|
||||
<a href="/#/index"
|
||||
class="a-link">V22X</a><span> › </span>
|
||||
<a :href="`/#/go?sub_tab=${data.sub_tab.name}`"
|
||||
class="a-link">{{data.sub_tab.zh}}</a><span> › </span>
|
||||
<a :href="`/#/t/?uid=${data.uid}`"
|
||||
class="a-link">{{data.title}}</a><span> › </span>
|
||||
<span>增加附言</span>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<Input v-model="content"
|
||||
type="textarea"
|
||||
:rows="16"
|
||||
placeholder="追加内容" />
|
||||
<div style="display: flex;margin-top: 10px;">
|
||||
<div style="margin-right: 20px;"><Button @click="handlePreview">预览主题</Button></div>
|
||||
<div><Button @click="handleAppend">提交</Button></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div>请在确有必要的情况下再使用此功能为原主题补充信息</div>
|
||||
</div>
|
||||
<div class="cell"
|
||||
v-if="preview">
|
||||
<MarkdownPreview theme="oneDark"
|
||||
:bordered="false"
|
||||
:initialValue="content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<userBox></userBox>
|
||||
<br>
|
||||
<div class="box">
|
||||
<div class="cell">
|
||||
<div>社区指导原则</div>
|
||||
</div>
|
||||
<div class="inner">
|
||||
<div>
|
||||
<Icon type="md-water" />尊重原创</div>
|
||||
<div>
|
||||
<Icon type="md-water" />友好互助</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { MarkdownPreview } from 'vue-meditor'
|
||||
import userBox from '@/components/user-box'
|
||||
import topicAPI from '@/api/topic'
|
||||
export default {
|
||||
components: {
|
||||
userBox,
|
||||
MarkdownPreview
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
preview: false,
|
||||
data: {
|
||||
sub_tab: {
|
||||
name: null,
|
||||
zh: null
|
||||
}
|
||||
},
|
||||
content: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlePreview () {
|
||||
if (this.content) {
|
||||
this.preview = !this.preview
|
||||
}
|
||||
if(!this.content) {
|
||||
this.preview = false
|
||||
}
|
||||
},
|
||||
async handleAppend () {
|
||||
if (!this.content) {
|
||||
this.$Message.error('内容不能为空哦')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await topicAPI.append({
|
||||
'uid': this.$route.query.uid,
|
||||
'content': this.content
|
||||
})
|
||||
this.$router.push({
|
||||
path: '/t',
|
||||
query: { 'uid': this.$route.query.uid }
|
||||
})
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
if (!this.$route.query.uid) {
|
||||
this.$router.push({ 'path': '/index' })
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await topicAPI.list({ 'uid': this.$route.query.uid })
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
this.$router.push({ 'path': '/index' })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
242
web/src/views/Go.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content">
|
||||
<div class="box">
|
||||
<div class="inner"
|
||||
style="display: flex; background-color: #001d25;border-radius: 4px 4px 0 0;">
|
||||
<div class="avatar-big"><a href="javascript:"><img></a></div>
|
||||
<div style="flex-grow: 1;margin-left: 10px;">
|
||||
<div style="display: flex;">
|
||||
<div style="flex-grow: 1;"><a href="/#/index"
|
||||
class="a-link-info"
|
||||
style="">V22X</a><span style="color: #fff;"> › </span><span style="color: #fff;">{{data.sub_tab_info.zh}}</span></div>
|
||||
<div style="width: 200px;color: #fff;text-align:right;"><strong>主题总数 {{data.total}}</strong>
|
||||
<span v-if="$user"> •
|
||||
<a class="a-link-info"
|
||||
v-if="data.is_fav"
|
||||
href="javascript:"
|
||||
@click="handleFavTab('cal')">取消收藏</a>
|
||||
<a class="a-link-info"
|
||||
v-else
|
||||
href="javascript:"
|
||||
@click="handleFavTab('add')">加入收藏</a>
|
||||
</span></div>
|
||||
</div>
|
||||
<div style="color: #fff;font-size: 10px;">{{data.sub_tab_info.desc}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell"
|
||||
v-if="data.total > 100">
|
||||
<Page :total="data.total"
|
||||
size="small"
|
||||
:current="query.page"
|
||||
:page-size="query.page_size"
|
||||
@on-change="changePage"
|
||||
show-elevator />
|
||||
</div>
|
||||
<loadingSpin v-if="loading"></loadingSpin>
|
||||
<div class="topic"
|
||||
v-if="!loading">
|
||||
<div class="cell topic-item"
|
||||
v-for="(item,k) in data.list"
|
||||
:key="k">
|
||||
<div class="avatar"><a :href="`/#/member?username=${item.user.username}`"><img :src="item.user.avatar_url"></a></div>
|
||||
<div class="topic-content">
|
||||
<div class="topic-title"><a class="a-link"
|
||||
:href="`/#/t?uid=${item.uid}`">{{item.title}}</a></div>
|
||||
<div class="topic-info">
|
||||
<div v-if="item.up_count">
|
||||
<Icon type="ios-arrow-up"
|
||||
size="18" /><span style="margin-left: 3px;">{{item.up_count}}</span></div>
|
||||
<div v-if="item.down_count">
|
||||
<Icon type="ios-arrow-down"
|
||||
size="18" /><span style="margin-left: 3px;">{{item.down_count}}</span></div>
|
||||
<div><a class="a-link"
|
||||
:href="`/#/member?username=${item.user.username}`">{{item.user.username}}</a></div>
|
||||
<div>创建于 {{item.time_create}}</div>
|
||||
<div v-if="item.last_reply_user">最后回复来自 <a class="a-link"
|
||||
:href="`/#/member?username=${item.last_reply_user.username}`">{{item.last_reply_user.username}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic-comment">
|
||||
<Badge :text="String(item.comment_count)"
|
||||
type="normal"></Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inner"
|
||||
v-if="data.total > 100">
|
||||
<div>
|
||||
<Page :total="data.total"
|
||||
size="small"
|
||||
:current="query.page"
|
||||
:page-size="query.page_size"
|
||||
@on-change="changePage"
|
||||
show-elevator />
|
||||
</div>
|
||||
<div style="margin: 5px 0 0 10px;">共{{data.total}}个主题,当前是第 {{query.page}} 页</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<userBox></userBox>
|
||||
<br>
|
||||
<div class="box">
|
||||
<div class="cell">
|
||||
<div style="margin-bottom: 10px;"><strong>父节点</strong></div>
|
||||
<div><a class="a-link"
|
||||
:href="`/#/index?tab=${data.sub_tab_info.tab.name}`">{{data.sub_tab_info.tab.zh}}</a></div>
|
||||
</div>
|
||||
<div class="cell"
|
||||
v-if="loading">
|
||||
<loadingSpin></loadingSpin>
|
||||
</div>
|
||||
<div class="cell"
|
||||
v-if="!loading">
|
||||
<div style="margin-bottom: 10px;"><strong>相关节点</strong></div>
|
||||
<div v-for="(item,k) in data.sub_tab_info.other_tabs"
|
||||
:key="k"
|
||||
style="margin-bottom: 5px;">
|
||||
<a class="a-link"
|
||||
:href="`/#/go?tab=${item.name}`">{{item.zh}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BackTop></BackTop>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import loadingSpin from '@/components/loading-spin'
|
||||
import userBox from '@/components/user-box'
|
||||
import mainAPI from '@/api/main'
|
||||
import tabAPI from '@/api/tab'
|
||||
export default {
|
||||
components: {
|
||||
userBox,
|
||||
loadingSpin
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
selectTab: null,
|
||||
data: {
|
||||
total: 0,
|
||||
list: [],
|
||||
is_fav: false,
|
||||
sub_tab_info: {
|
||||
other_tabs: [],
|
||||
tab: {
|
||||
name: null
|
||||
},
|
||||
name: null,
|
||||
zh: null,
|
||||
desc: null
|
||||
}
|
||||
},
|
||||
query: {
|
||||
page: 1,
|
||||
page_size: 50
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changePage (page) {
|
||||
this.query.page = page
|
||||
this.$router.push({ 'path': this.$route.path, 'query': this.query })
|
||||
},
|
||||
async handleFavTab (action) {
|
||||
try {
|
||||
await tabAPI.favSubTab({ 'tab': this.query.tab, 'action': action })
|
||||
this.data.is_fav = !this.data.is_fav
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
if (!this.$route.query.tab) {
|
||||
this.$router.push({ 'path': '/index' })
|
||||
return
|
||||
}
|
||||
this.query.tab = this.$route.query.tab
|
||||
if (parseInt(this.$route.query.page)) {
|
||||
this.query.page = parseInt(this.$route.query.page)
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await mainAPI.go(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async beforeRouteUpdate (to, from, next) {
|
||||
if (parseInt(to.query.page)) {
|
||||
this.query.page = parseInt(to.query.page)
|
||||
} else {
|
||||
this.query.page = 1
|
||||
}
|
||||
this.query.tab = to.query.tab
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await mainAPI.go(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
next()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.topic {
|
||||
.topic-item {
|
||||
display: flex;
|
||||
}
|
||||
.topic-content {
|
||||
flex: 1;
|
||||
margin-left: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
.topic-title {
|
||||
font-size: 16px;
|
||||
line-height: 130%;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
}
|
||||
.topic-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
div {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.topic-comment {
|
||||
width: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
253
web/src/views/Index.vue
Normal file
@@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content">
|
||||
<div class="box">
|
||||
<div class="inner">
|
||||
<div class="tab">
|
||||
<span v-for="(item,k) in indexData.tabs"
|
||||
:key="k">
|
||||
<a class="tab-item-current"
|
||||
:href="`/#/index?tab=${item.name}`"
|
||||
v-if="item.name === selectTab">{{item.zh}}</a>
|
||||
<a class="tab-item"
|
||||
:href="`/#/index?tab=${item.name}`"
|
||||
v-else>{{item.zh}}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-tab">
|
||||
<span v-for="(item,k) in subTabs"
|
||||
:key="k">
|
||||
<a class="sub-tab-item a-link"
|
||||
:href="`/#/go?tab=${item.name}`">{{item.zh}}</a>
|
||||
</span>
|
||||
</div>
|
||||
<loadingSpin v-if="loading"></loadingSpin>
|
||||
<div class="topic"
|
||||
v-if="!loading">
|
||||
<div class="cell topic-item"
|
||||
v-for="(item,k) in indexData.topics"
|
||||
:key="k">
|
||||
<div class="avatar"><a :href="`/#/member?username=${item.user.username}`"><img :src="item.user.avatar_url"></a></div>
|
||||
<div class="topic-content">
|
||||
<div class="topic-title"><a class="a-link"
|
||||
:href="`/#/t?uid=${item.uid}`">{{item.title}}</a></div>
|
||||
<div class="topic-info">
|
||||
<div v-if="item.up_count">
|
||||
<Icon type="ios-arrow-up"
|
||||
size="18" /><span style="margin-left: 3px;">{{item.up_count}}</span></div>
|
||||
<div v-if="item.down_count">
|
||||
<Icon type="ios-arrow-down"
|
||||
size="18" /><span style="margin-left: 3px;">{{item.down_count}}</span></div>
|
||||
<div>
|
||||
<Tag><a :href="`/#/go?tab=${item.sub_tab.name}`">{{item.sub_tab.zh}}</a></Tag>
|
||||
</div>
|
||||
<div><a class="a-link"
|
||||
:href="`/#/member?username=${item.user.username}`">{{item.user.username}}</a></div>
|
||||
<div>创建于 {{item.time_create}}</div>
|
||||
<div v-if="item.last_reply_user">最后回复来自 <a class="a-link"
|
||||
:href="`/#/member?username=${item.last_reply_user.username}`">{{item.last_reply_user.username}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic-comment">
|
||||
<Badge :text="String(item.comment_count)"
|
||||
type="normal"></Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inner"
|
||||
v-if="indexData.tab_topic_count > 80"><a class="a-link"
|
||||
href="/#/recent"><span>→</span>更多新主题</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<userBox></userBox>
|
||||
<br>
|
||||
<div class="box">
|
||||
<div class="inner">
|
||||
<a class="a-link">
|
||||
<Icon type="ios-ice-cream" /><span style="margin-left: 5px;">领取今日的登录奖励</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div v-if="indexData.fav_tabs">
|
||||
<div class="box">
|
||||
<div class="cell">我收藏的节点</div>
|
||||
<div class="cell"
|
||||
v-for="(item,k) in indexData.fav_tabs"
|
||||
:key="k"><a class="a-link"
|
||||
:href="`/#/go?tab=${item.name}`">{{item.zh}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="box">
|
||||
<div class="cell">今日热议主题</div>
|
||||
<div class="cell"
|
||||
v-for="(item,k) in indexData.hot_topics"
|
||||
:key="k"><a class="a-link"
|
||||
:href="`/#/t?uid=${item.uid}`">{{item.title}}</a></div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="box">
|
||||
<div class="cell">社区运行状况</div>
|
||||
<div class="inner">
|
||||
<div style="display: flex; text-align: right;">
|
||||
<div style="width: 70px;margin-right: 10px;">注册会员</div>
|
||||
<div><strong>{{indexData.user_count}}</strong></div>
|
||||
</div>
|
||||
<div style="display: flex; text-align: right;">
|
||||
<div style="width: 70px;margin-right: 10px;">主题</div>
|
||||
<div><strong>{{indexData.topic_count}}</strong></div>
|
||||
</div>
|
||||
<div style="display: flex; text-align: right;">
|
||||
<div style="width: 70px;margin-right: 10px;">回复</div>
|
||||
<div><strong>{{indexData.comment_count}}</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BackTop></BackTop>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import loadingSpin from '@/components/loading-spin'
|
||||
import userBox from '@/components/user-box'
|
||||
import indexMixedAPI from '@/api/main'
|
||||
export default {
|
||||
components: {
|
||||
userBox,
|
||||
loadingSpin
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
selectTab: null,
|
||||
indexData: {
|
||||
tabs: [],
|
||||
topics: []
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
subTabs: function () {
|
||||
if (this.indexData.tabs.length > 0) {
|
||||
let tab = this.indexData.tabs.find(item => item.name === this.selectTab)
|
||||
if (tab && tab.sub_tabs) {
|
||||
return tab.sub_tabs
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
this.selectTab = this.$route.query.tab ? this.$route.query.tab : 'tech'
|
||||
this.loading = true
|
||||
try {
|
||||
this.indexData = await indexMixedAPI.index({ 'tab': this.selectTab })
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async beforeRouteUpdate (to, from, next) {
|
||||
this.selectTab = to.query.tab ? to.query.tab : 'tech'
|
||||
this.loading = true
|
||||
try {
|
||||
this.indexData = await indexMixedAPI.index({ 'tab': this.selectTab })
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
next()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.tab {
|
||||
.tab-item {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
padding: 5px 8px;
|
||||
margin-right: 5px;
|
||||
color: #555;
|
||||
}
|
||||
.tab-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
.tab-item-current {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
padding: 5px 8px;
|
||||
margin-right: 5px;
|
||||
border-radius: 3px;
|
||||
background-color: #334;
|
||||
color: #fff;
|
||||
}
|
||||
.tab-item-current:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.sub-tab {
|
||||
background-color: #f9f9f9;
|
||||
padding: 10px 10px 10px 20px;
|
||||
font-size: 12px;
|
||||
line-height: 100%;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
.sub-tab-item {
|
||||
padding: 5px 8px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
.topic {
|
||||
.topic-item {
|
||||
display: flex;
|
||||
}
|
||||
.topic-content {
|
||||
flex: 1;
|
||||
margin-left: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
.topic-title {
|
||||
font-size: 16px;
|
||||
line-height: 130%;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
}
|
||||
.topic-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
div {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.topic-comment {
|
||||
width: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
217
web/src/views/Layer.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div class="main-layout">
|
||||
<div class="main-header">
|
||||
<div class="content">
|
||||
<div class="logo"><a href="/#/">V22X</a></div>
|
||||
<div class="search">
|
||||
<Input v-model="searchKey"
|
||||
placeholder="search ..."
|
||||
size="small"
|
||||
style="width: 160px" />
|
||||
<Button style="margin-left:5px"
|
||||
size="small"
|
||||
icon="ios-search">搜</Button>
|
||||
</div>
|
||||
<div class="menu">
|
||||
<span class="menu-item"><a href="/#/">首页</a></span>
|
||||
<span v-if="$user">
|
||||
<span class="menu-item"><a :href="`/#/member?username=${$user.username}`">{{$user.username}}</a></span>
|
||||
<span class="menu-item"><a href="/#/setting">设置</a></span>
|
||||
<span class="menu-item"><a @click="handlerLogOut">登出</a></span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span class="menu-item"><a href="/#/account/register">注册</a></span>
|
||||
<span class="menu-item"><a href="/#/account/login">登录</a></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-content">
|
||||
<div class="content">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-footer">
|
||||
<center>Copyright © {{year}} V22X </center>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import accountAPI from '@/api/account'
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
searchKey: null,
|
||||
userInfo: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handlerLogOut () {
|
||||
try {
|
||||
this.loading = true
|
||||
await accountAPI.logout()
|
||||
this.$Message.success('退出成功')
|
||||
window.location.href = '/index'
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
year: function () {
|
||||
return this.$date.getThisYear()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
a {
|
||||
color: #555;
|
||||
}
|
||||
a:hover {
|
||||
color: #000;
|
||||
}
|
||||
.box {
|
||||
border: 1px solid #dcdee2;
|
||||
border-color: #e8eaec;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.inner {
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 150%;
|
||||
text-align: left;
|
||||
}
|
||||
.cell {
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 120%;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
}
|
||||
.a-link {
|
||||
color: #778087;
|
||||
text-decoration: none;
|
||||
word-break: break-word;
|
||||
}
|
||||
.a-link:hover {
|
||||
color: #4d5256;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.a-link-info {
|
||||
text-decoration: none;
|
||||
word-break: break-word;
|
||||
color: #03c8ff;
|
||||
}
|
||||
.a-link-info:hover {
|
||||
color: #03c8ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background-color: cadetblue;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.avatar-big {
|
||||
width: 73px;
|
||||
height: 73px;
|
||||
background-color: cadetblue;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
.form-label {
|
||||
width: 120px;
|
||||
margin-right: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
.main-layout {
|
||||
background-color: white;
|
||||
.main-header {
|
||||
text-align: center;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.22);
|
||||
padding: 0 20px;
|
||||
.content {
|
||||
min-width: 600px;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
.logo {
|
||||
a {
|
||||
color: black;
|
||||
}
|
||||
width: 100px;
|
||||
font-size: 200%;
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
float: left;
|
||||
}
|
||||
.search {
|
||||
float: left;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.menu {
|
||||
float: right;
|
||||
margin-right: 10px;
|
||||
.menu-item {
|
||||
margin-left: 10px;
|
||||
a:hover {
|
||||
color: #778087;
|
||||
}
|
||||
}
|
||||
}
|
||||
.layout-nav {
|
||||
width: 420px;
|
||||
margin: 0 auto;
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.main-content {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
text-align: center;
|
||||
background-color: #e2e2e2;
|
||||
box-shadow: inset 0px 12px 8px -12px #848383;
|
||||
.content {
|
||||
min-width: 600px;
|
||||
max-width: 1100px;
|
||||
min-height: 800px;
|
||||
margin: 0 auto;
|
||||
.left-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.right-content {
|
||||
width: 260px;
|
||||
min-width: 260px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.main-footer {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.22);
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
132
web/src/views/Login.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content">
|
||||
<div class="box">
|
||||
<div class="cell"><a href="/#/index"
|
||||
class="a-link">V22X</a><span> › </span><span>登录</span>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>用户名</strong></div>
|
||||
<div class="form-content"><Input v-model="formData.username"
|
||||
style="width: 200px;" /></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>密码</strong></div>
|
||||
<div class="form-content"><Input v-model="formData.password"
|
||||
style="width: 200px;"
|
||||
type='password' /></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>图形验证码</strong></div>
|
||||
<div class="form-content"> <Input style="width: 80px;"
|
||||
@on-change="checkImgCode()"
|
||||
v-model="formData.code" />
|
||||
<img :src="codeImgSrc"
|
||||
@click="refreshCode()"
|
||||
style="margin-left: 10px;height: 20px;"
|
||||
align="center"></img></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"></div>
|
||||
<div class="form-content">
|
||||
<span><Button type='primary'
|
||||
:loading="loading"
|
||||
@click="handleLogin('formData')">登录</Button></span>
|
||||
<span style="margin-left: 10px;">
|
||||
<span style="margin-right:5px;">没有账号?</span>
|
||||
<Button to='/account/register'>注册</Button></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import sleep from '@/common/sleep'
|
||||
import accountAPI from '@/api/account'
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
formData: {},
|
||||
codeImgSrc: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleLogin (name) {
|
||||
if (this.loading) return
|
||||
if (!this.formData.username) {
|
||||
this.$Message.error('用户名不能为空')
|
||||
return
|
||||
}
|
||||
if (!this.formData.password) {
|
||||
this.$Message.error('密码不能为空')
|
||||
return
|
||||
}
|
||||
if (!this.formData.code) {
|
||||
this.$Message.error('验证码不能为空')
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
await accountAPI.login(this.formData)
|
||||
this.$Message.success('登入成功')
|
||||
this.$router.replace('/')
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
refreshCode () {
|
||||
this.codeImgSrc = '/api/v1/captcha' + '?t=' + new Date().getTime()
|
||||
},
|
||||
async checkImgCode () {
|
||||
if (this.formData.code.length !== 5) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await accountAPI.checkCode({ 'code': this.formData.code })
|
||||
this.$Message.success('验证码正确')
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('出了点小差')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.codeImgSrc = '/api/v1/captcha' + '?t=' + new Date().getTime()
|
||||
var that = this
|
||||
document.onkeydown = function (e) {
|
||||
var key = window.event.keyCode
|
||||
if (key === 13 || key === 100) {
|
||||
that.handleLogin('formData')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
.form-label {
|
||||
width: 120px;
|
||||
margin-right: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
244
web/src/views/Member.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content">
|
||||
<div class="box">
|
||||
<div class="cell"
|
||||
style="display: flex;border-radius: 4px 4px 0 0;">
|
||||
<div class="avatar-big"><a href="javascript:"><img></a></div>
|
||||
<div style="flex-grow: 1;margin-left: 10px;">
|
||||
<div style="display: flex;flex-direction: column;justify-content: space-between;">
|
||||
<div style="display: flex;justify-content: space-between;">
|
||||
<div style="display: flex;align-items: center;">
|
||||
<h1 style="margin: 5px 0;color: black;display: inline-block;">{{data.user.username}}</h1>
|
||||
<span style="margin-left: 10px;">
|
||||
<Icon :type="data.user.sex === 0 ? 'ios-man' : 'ios-woman'"
|
||||
size="25"></Icon>
|
||||
</span>
|
||||
</div>
|
||||
<div style="width: 150px;text-align:right;">
|
||||
<span v-if="!loading&&$user&&$user.username !== data.user.username">
|
||||
<Button type="default"
|
||||
@click="handleFavUser('cal')"
|
||||
v-if="data.is_fav">取消特别关注</Button>
|
||||
<Button type="warning"
|
||||
@click="handleFavUser('add')"
|
||||
v-else>加入特别关注</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="color: #999;margin-top: 5px;">V22X 第 {{data.user.id}} 号会员,加入于 {{data.user.time_create}}</div>
|
||||
<div style="color: #999;margin-top: 5px;">
|
||||
<span style="margin-right: 10px;">被 <strong>{{data.user.be_fav_user_count}}</strong> 人关注</span>
|
||||
<span>关注了 <strong>{{data.user.fav_user_count}}</strong> 人</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div>
|
||||
<Button type="info"
|
||||
icon="md-home"
|
||||
v-if="data.user.site"
|
||||
:to="data.user.site"
|
||||
target="_blank"
|
||||
class="widget-item">{{data.user.site}}</Button>
|
||||
<Button type="default"
|
||||
icon="md-locate"
|
||||
v-if="data.user.location"
|
||||
class="widget-item">{{data.user.location}}</Button>
|
||||
<Button type="primary"
|
||||
icon="md-briefcase"
|
||||
v-if="data.user.company"
|
||||
class="widget-item">{{data.user.company}}</Button>
|
||||
<Button type="success"
|
||||
icon="logo-github"
|
||||
v-if="data.user.github"
|
||||
:to="data.user.github"
|
||||
target="_blank"
|
||||
class="widget-item">{{data.user.github}}</Button>
|
||||
<Button type="primary"
|
||||
icon="logo-twitter"
|
||||
v-if="data.user.twitter"
|
||||
:to="data.user.twitter"
|
||||
target="_blank"
|
||||
class="widget-item">{{data.user.twitter}}</Button>
|
||||
<Button type="warning"
|
||||
v-if="data.user.weibo"
|
||||
:to="data.user.weibo"
|
||||
target="_blank"
|
||||
class="widget-item">微博 {{data.user.weibo}}</Button>
|
||||
</div>
|
||||
<div style="margin-top: 20px;color: rgb(0,0,0)">{{data.user.bio}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="box">
|
||||
<div class="cell">
|
||||
<div>{{data.user.username}} 的所有主题</div>
|
||||
</div>
|
||||
<div v-if="data.is_open"
|
||||
class="topic">
|
||||
<div class="cell topic-item"
|
||||
v-for="(item,k) in data.topics"
|
||||
:key="k">
|
||||
<div class="avatar"><a :href="`/#/member?username=${data.user.username}`"><img :src="data.user.avatar_url"></a></div>
|
||||
<div class="topic-content">
|
||||
<div class="topic-title">
|
||||
<a class="a-link"
|
||||
:href="`/#/t?uid=${item.uid}`">{{item.title}}</a>
|
||||
</div>
|
||||
<div class="topic-info">
|
||||
<div v-if="item.up_count">
|
||||
<Icon type="ios-arrow-up"
|
||||
size="18" /><span style="margin-left: 3px;">{{item.up_count}}</span></div>
|
||||
<div v-if="item.down_count">
|
||||
<Icon type="ios-arrow-down"
|
||||
size="18" /><span style="margin-left: 3px;">{{item.down_count}}</span></div>
|
||||
<div>
|
||||
<Tag><a :href="`/#/go?tab=${item.sub_tab.name}`">{{item.sub_tab.zh}}</a></Tag>
|
||||
</div>
|
||||
<div><a class="a-link"
|
||||
:href="`/#/member?username=${data.user.username}`">{{data.user.username}}</a></div>
|
||||
<div>创建于 {{item.time_create}}</div>
|
||||
<div v-if="item.last_reply_user">最后回复来自 <a class="a-link"
|
||||
:href="`/#/member?username=${item.last_reply_user.username}`">{{item.last_reply_user.username}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic-comment">
|
||||
<Badge :text="String(item.comment_count)"
|
||||
type="normal"></Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell"
|
||||
v-if="data.topics.length"><a class="a-link"
|
||||
:href="`/#/member/topic?username=${data.user.username}`">» {{data.user.username}} 创建的更多主题</a></div>
|
||||
</div>
|
||||
<div v-else
|
||||
class="inner"
|
||||
style="display: flex;align-items: center;">
|
||||
<div style="margin-left: 20px;margin-right: 20px;">
|
||||
<Avatar icon="ios-lock"
|
||||
size="120">
|
||||
</Avatar>
|
||||
</div>
|
||||
<div style="margin-left: 10px;">根据 {{data.user.username}} 的设置,主题列表被隐藏</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="box">
|
||||
<div class="cell">{{data.user.username}} 最近回复了</div>
|
||||
<div v-for="(item,k) in data.comments"
|
||||
:key="k">
|
||||
<div style="background-color: #edf3f5;padding: 0;text-align: left;padding: 10px;font-size: 14px;display: flex;justify-content: space-between;">
|
||||
<div>回复了 <a class="a-link"
|
||||
:href="`/#/member?username=${item.topic.user.username}`">{{item.topic.user.username}}</a> 创建的主题 › <a class="a-link"
|
||||
:href="`/#/go?tab=${item.topic.sub_tab.name}`">{{item.topic.sub_tab.zh}}</a> › <a class="a-link"
|
||||
:href="`/#/t?uid=${item.topic.uid}`">{{item.topic.title}}</a></div>
|
||||
<div>于 {{item.time_create}}</div>
|
||||
</div>
|
||||
<div class="cell">{{item.content}}</div>
|
||||
</div>
|
||||
<div class="cell"
|
||||
v-if="data.comments.length"><a class="a-link"
|
||||
:href="`/#/member/comment?username=${data.user.username}`">» {{data.user.username}} 创建的更多回复</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import memberAPI from '@/api/member'
|
||||
import userAPI from '@/api/user'
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
data: {
|
||||
is_open: true,
|
||||
user: {
|
||||
username: null
|
||||
},
|
||||
comments: [],
|
||||
topics: []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleFavUser (action) {
|
||||
if (!this.$user) {
|
||||
this.$Message.error('登录后才可以操作哦')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await userAPI.fav({ 'uid': this.data.user.uid, 'action': action })
|
||||
this.data.is_fav = !this.data.is_fav
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
if (!this.$route.query.username) {
|
||||
this.$router.push({ 'path': '/index' })
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await memberAPI.detail({ 'username': this.$route.query.username })
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.widget-item {
|
||||
margin-right: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.topic {
|
||||
.topic-item {
|
||||
display: flex;
|
||||
}
|
||||
.topic-content {
|
||||
flex: 1;
|
||||
margin-left: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
.topic-title {
|
||||
font-size: 16px;
|
||||
line-height: 130%;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
}
|
||||
.topic-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
div {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.topic-comment {
|
||||
width: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
131
web/src/views/MemberComment.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content">
|
||||
<div class="box">
|
||||
<div class="cell"
|
||||
style="display: flex;">
|
||||
<div style="flex-grow: 1;"><a href="/#/index"
|
||||
class="a-link">V22X</a><span> › </span><a :href="`/#/member?username=${data.user.username}`"
|
||||
class="a-link">{{data.user.username}}</a><span> › </span><span>全部回复</span></div>
|
||||
<div style="width: 200px;color: #778087;text-align:right;"
|
||||
v-if="!loading"><strong>回复总数 {{data.total}}</strong></div>
|
||||
</div>
|
||||
<div class="cell"
|
||||
v-if="data.total > 100">
|
||||
<Page :total="data.total"
|
||||
size="small"
|
||||
:current="query.page"
|
||||
:page-size="query.page_size"
|
||||
@on-change="changePage"
|
||||
show-elevator />
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div v-for="(item,k) in data.list"
|
||||
:key="k">
|
||||
<div style="background-color: #edf3f5;padding: 0;text-align: left;padding: 10px;font-size: 14px;display: flex;justify-content: space-between;">
|
||||
<div>回复了 <a class="a-link"
|
||||
:href="`/#/member?username=${item.topic.user.username}`">{{item.topic.user.username}}</a> 创建的主题 › <a class="a-link"
|
||||
:href="`/#/go?tab=${item.topic.sub_tab.name}`">{{item.topic.sub_tab.zh}}</a> › <a class="a-link"
|
||||
:href="`/#/t?uid=${item.topic.uid}`">{{item.topic.title}}</a></div>
|
||||
<div>于 {{item.time_create}}</div>
|
||||
</div>
|
||||
<div class="cell">{{item.content}}</div>
|
||||
</div>
|
||||
<div class="inner"
|
||||
v-if="data.total > 100">
|
||||
<div>
|
||||
<Page :total="data.total"
|
||||
size="small"
|
||||
:current="query.page"
|
||||
:page-size="query.page_size"
|
||||
@on-change="changePage"
|
||||
show-elevator />
|
||||
</div>
|
||||
<div style="margin: 5px 0 0 10px;">共{{data.total}}个主题,当前是第 {{query.page}} 页</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<userBox></userBox>
|
||||
</div>
|
||||
<BackTop></BackTop>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import userBox from '@/components/user-box'
|
||||
import memberAPI from '@/api/member'
|
||||
export default {
|
||||
components: {
|
||||
userBox
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
data: {
|
||||
user: {
|
||||
username: null
|
||||
},
|
||||
list: [],
|
||||
total: 0
|
||||
},
|
||||
query: {
|
||||
page: 1,
|
||||
page_size: 50
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changePage (page) {
|
||||
this.query.page = page
|
||||
this.$router.push({ 'path': this.$route.path, 'query': this.query })
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
if (!this.$route.query.username) {
|
||||
this.$router.push({ 'path': '/index' })
|
||||
return
|
||||
}
|
||||
this.query.username = this.$route.query.username
|
||||
if (parseInt(this.$route.query.page)) {
|
||||
this.query.page = parseInt(this.$route.query.page)
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await memberAPI.listComment(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async beforeRouteUpdate (to, from, next) {
|
||||
if (parseInt(to.query.page)) {
|
||||
this.query.page = parseInt(to.query.page)
|
||||
} else {
|
||||
this.query.page = 1
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await memberAPI.listComment(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
next()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
</style>
|
||||
183
web/src/views/MemberTopic.vue
Normal file
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content">
|
||||
<div class="box">
|
||||
<div class="cell"
|
||||
style="display: flex;">
|
||||
<div style="flex-grow: 1;"><a href="/#/index"
|
||||
class="a-link">V22X</a><span> › </span><a :href="`/#/member?username=${data.user.username}`"
|
||||
class="a-link">{{data.user.username}}</a><span> › </span><span>全部主题</span></div>
|
||||
<div style="width: 200px;color: #778087;text-align:right;"
|
||||
v-if="!loading"><strong>主题总数 {{data.total}}</strong></div>
|
||||
</div>
|
||||
<div class="cell"
|
||||
v-if="data.total > 100">
|
||||
<Page :total="data.total"
|
||||
size="small"
|
||||
:current="query.page"
|
||||
:page-size="query.page_size"
|
||||
@on-change="changePage"
|
||||
show-elevator />
|
||||
</div>
|
||||
<div class="topic">
|
||||
<div class="cell topic-item"
|
||||
v-for="(item,k) in data.list"
|
||||
:key="k">
|
||||
<div class="avatar"><a :href="`/#/member?username=${data.user.username}`"><img :src="data.user.avatar_url"></a></div>
|
||||
<div class="topic-content">
|
||||
<div class="topic-title">
|
||||
<a class="a-link"
|
||||
:href="`/#/t?uid=${item.uid}`">{{item.title}}</a>
|
||||
</div>
|
||||
<div class="topic-info">
|
||||
<div v-if="item.up_count">
|
||||
<Icon type="ios-arrow-up"
|
||||
size="18" /><span style="margin-left: 3px;">{{item.up_count}}</span></div>
|
||||
<div v-if="item.down_count">
|
||||
<Icon type="ios-arrow-down"
|
||||
size="18" /><span style="margin-left: 3px;">{{item.down_count}}</span></div>
|
||||
<div>
|
||||
<Tag><a :href="`/#/go?tab=${item.sub_tab.name}`">{{item.sub_tab.zh}}</a></Tag>
|
||||
</div>
|
||||
<div><a class="a-link"
|
||||
:href="`/#/member?username=${data.user.username}`">{{data.user.username}}</a></div>
|
||||
<div>创建于 {{item.time_create}}</div>
|
||||
<div v-if="item.last_reply_user">最后回复来自 <a class="a-link"
|
||||
:href="`/#/member?username=${item.last_reply_user.username}`">{{item.last_reply_user.username}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic-comment">
|
||||
<Badge :text="String(item.comment_count)"
|
||||
type="normal"></Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inner"
|
||||
v-if="data.total > 100">
|
||||
<div>
|
||||
<Page :total="data.total"
|
||||
size="small"
|
||||
:current="query.page"
|
||||
:page-size="query.page_size"
|
||||
@on-change="changePage"
|
||||
show-elevator />
|
||||
</div>
|
||||
<div style="margin: 5px 0 0 10px;">共{{data.total}}个主题,当前是第 {{query.page}} 页</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<userBox></userBox>
|
||||
</div>
|
||||
<BackTop></BackTop>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import userBox from '@/components/user-box'
|
||||
import memberAPI from '@/api/member'
|
||||
export default {
|
||||
components: {
|
||||
userBox
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
data: {
|
||||
user: {
|
||||
username: null
|
||||
},
|
||||
list: [],
|
||||
total: 0
|
||||
},
|
||||
query: {
|
||||
page: 1,
|
||||
page_size: 50
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changePage (page) {
|
||||
this.query.page = page
|
||||
this.$router.push({ 'path': this.$route.path, 'query': this.query })
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
if (!this.$route.query.username) {
|
||||
this.$router.push({ 'path': '/index' })
|
||||
return
|
||||
}
|
||||
this.query.username = this.$route.query.username
|
||||
if (parseInt(this.$route.query.page)) {
|
||||
this.query.page = parseInt(this.$route.query.page)
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await memberAPI.listTopic(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async beforeRouteUpdate (to, from, next) {
|
||||
if (parseInt(to.query.page)) {
|
||||
this.query.page = parseInt(to.query.page)
|
||||
} else {
|
||||
this.query.page = 1
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await memberAPI.listTopic(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
next()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.topic {
|
||||
.topic-item {
|
||||
display: flex;
|
||||
}
|
||||
.topic-content {
|
||||
flex: 1;
|
||||
margin-left: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
.topic-title {
|
||||
font-size: 16px;
|
||||
line-height: 130%;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
}
|
||||
.topic-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
div {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.topic-comment {
|
||||
width: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
69
web/src/views/MyFavNode.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content">
|
||||
<div class="box">
|
||||
<div class="cell"><a href="/#/index"
|
||||
class="a-link">V22X</a><span> › </span><span>我收藏的节点</span>
|
||||
</div>
|
||||
<div class="cell"
|
||||
v-if="loading">
|
||||
<loadingSpin></loadingSpin>
|
||||
</div>
|
||||
<div class="inner"
|
||||
v-if="!loading"
|
||||
style="display: flex;">
|
||||
<Card style="width:150px;margin-right: 20px;"
|
||||
v-for="(item,k) in data"
|
||||
:key="k">
|
||||
<a :href="`/#/go?tab=${item.name}`">
|
||||
<div style="text-align:center">
|
||||
<img>
|
||||
<h2>{{item.zh}}</h2>
|
||||
<div style="margin-top: 5px;">
|
||||
<Icon type="ios-text" /><strong style="margin-left: 5px;">{{item.topic_count}}</strong></div>
|
||||
</div>
|
||||
</a>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="inner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<userBox></userBox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import loadingSpin from '@/components/loading-spin'
|
||||
import userBox from '@/components/user-box'
|
||||
import tabAPI from '@/api/tab'
|
||||
export default {
|
||||
components: {
|
||||
userBox,
|
||||
loadingSpin
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
data: []
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await tabAPI.listFavSubTab()
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
</style>
|
||||
184
web/src/views/MyFavTopic.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content">
|
||||
<div class="box">
|
||||
<div class="cell"
|
||||
style="display: flex;">
|
||||
<div style="flex-grow: 1;"><a href="/#/index"
|
||||
class="a-link">V22X</a><span> › </span><span>我收藏的主题</span></div>
|
||||
<div style="width: 200px;color: #778087;text-align:right;"
|
||||
v-if="!loading"><strong>主题总数 {{data.total}}</strong></div>
|
||||
</div>
|
||||
<div class="cell"
|
||||
v-if="data.total > 100">
|
||||
<Page :total="data.total"
|
||||
size="small"
|
||||
:current="query.page"
|
||||
:page-size="query.page_size"
|
||||
@on-change="changePage"
|
||||
show-elevator />
|
||||
</div>
|
||||
<loadingSpin v-if="loading"></loadingSpin>
|
||||
<div class="topic"
|
||||
v-if="!loading">
|
||||
<div class="cell topic-item"
|
||||
v-for="(item,k) in data.list"
|
||||
:key="k">
|
||||
<div class="avatar"><a :href="`/#/member?username=${item.user.username}`"><img :src="item.user.avatar_url"></a></div>
|
||||
<div class="topic-content">
|
||||
<div class="topic-title">
|
||||
<div class="title"><a class="a-link"
|
||||
:href="`/#/t?uid=${item.uid}`">{{item.title}}</a></div>
|
||||
<div class="fav-time">收藏于 {{item.fav_time}}</div>
|
||||
</div>
|
||||
<div class="topic-info">
|
||||
<div v-if="item.up_count">
|
||||
<Icon type="ios-arrow-up"
|
||||
size="18" /><span style="margin-left: 3px;">{{item.up_count}}</span></div>
|
||||
<div v-if="item.down_count">
|
||||
<Icon type="ios-arrow-down"
|
||||
size="18" /><span style="margin-left: 3px;">{{item.down_count}}</span></div>
|
||||
<div>
|
||||
<Tag><a :href="`/#/go?tab=${item.sub_tab.name}`">{{item.sub_tab.zh}}</a></Tag>
|
||||
</div>
|
||||
<div><a class="a-link"
|
||||
:href="`/#/member?username=${item.user.username}`">{{item.user.username}}</a></div>
|
||||
<div>创建于 {{item.time_create}}</div>
|
||||
<div v-if="item.last_reply_user">最后回复来自 <a class="a-link"
|
||||
:href="`/#/member?username=${item.last_reply_user.username}`">{{item.last_reply_user.username}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic-comment">
|
||||
<Badge :text="String(item.comment_count)"
|
||||
type="normal"></Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inner"
|
||||
v-if="data.total > 100">
|
||||
<Page :total="data.total"
|
||||
size="small"
|
||||
:current="query.page"
|
||||
:page-size="query.page_size"
|
||||
@on-change="changePage"
|
||||
show-elevator />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<userBox></userBox>
|
||||
</div>
|
||||
<BackTop></BackTop>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import loadingSpin from '@/components/loading-spin'
|
||||
import userBox from '@/components/user-box'
|
||||
import topicAPI from '@/api/topic'
|
||||
export default {
|
||||
components: {
|
||||
userBox,
|
||||
loadingSpin
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
data: {
|
||||
total: 0,
|
||||
list: []
|
||||
},
|
||||
query: {
|
||||
page: 1,
|
||||
page_size: 50
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changePage (page) {
|
||||
this.query.page = page
|
||||
this.$router.push({ 'path': this.$route.path, 'query': this.query })
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
if (parseInt(this.$route.query.page)) {
|
||||
this.query.page = parseInt(this.$route.query.page)
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await topicAPI.listFav(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async beforeRouteUpdate (to, from, next) {
|
||||
if (parseInt(to.query.page)) {
|
||||
this.query.page = parseInt(to.query.page)
|
||||
} else {
|
||||
this.query.page = 1
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await topicAPI.listFav(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
next()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.topic {
|
||||
.topic-item {
|
||||
display: flex;
|
||||
}
|
||||
.topic-content {
|
||||
flex: 1;
|
||||
margin-left: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
.topic-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.title {
|
||||
font-size: 16px;
|
||||
line-height: 130%;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
}
|
||||
.fav-time {
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
.topic-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
div {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.topic-comment {
|
||||
width: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
186
web/src/views/MyFavUserTopic.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content">
|
||||
<div class="box">
|
||||
<div class="cell"
|
||||
style="display: flex;">
|
||||
<div style="flex-grow: 1;"><a href="/#/index"
|
||||
class="a-link">V22X</a><span> › </span><span>我关注的人的最新主题</span></div>
|
||||
<div style="width: 200px;color: #778087;text-align:right;"
|
||||
v-if="!loading"><strong>主题总数 {{data.total}}</strong></div>
|
||||
</div>
|
||||
<div class="cell"
|
||||
v-if="data.total > 100">
|
||||
<Page :total="data.total"
|
||||
size="small"
|
||||
:current="query.page"
|
||||
:page-size="query.page_size"
|
||||
@on-change="changePage"
|
||||
show-elevator />
|
||||
</div>
|
||||
<loadingSpin v-if="loading"></loadingSpin>
|
||||
<div class="topic"
|
||||
v-if="!loading">
|
||||
<div class="cell topic-item"
|
||||
v-for="(item,k) in data.list"
|
||||
:key="k">
|
||||
<div class="avatar"><a :href="`/#/member?username=${item.user.username}`"><img :src="item.user.avatar_url"></a></div>
|
||||
<div class="topic-content">
|
||||
<div class="topic-title">
|
||||
<a class="a-link"
|
||||
:href="`/#/t?uid=${item.uid}`">{{item.title}}</a>
|
||||
</div>
|
||||
<div class="topic-info">
|
||||
<div v-if="item.up_count">
|
||||
<Icon type="ios-arrow-up"
|
||||
size="18" /><span style="margin-left: 3px;">{{item.up_count}}</span></div>
|
||||
<div v-if="item.down_count">
|
||||
<Icon type="ios-arrow-down"
|
||||
size="18" /><span style="margin-left: 3px;">{{item.down_count}}</span></div>
|
||||
<div>
|
||||
<Tag><a :href="`/#/go?tab=${item.sub_tab.name}`">{{item.sub_tab.zh}}</a></Tag>
|
||||
</div>
|
||||
<div><a class="a-link"
|
||||
:href="`/#/member?username=${item.user.username}`">{{item.user.username}}</a></div>
|
||||
<div>创建于 {{item.time_create}}</div>
|
||||
<div v-if="item.last_reply_user">最后回复来自 <a class="a-link"
|
||||
:href="`/#/member?username=${item.last_reply_user.username}`">{{item.last_reply_user.username}}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topic-comment">
|
||||
<Badge :text="String(item.comment_count)"
|
||||
type="normal"></Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inner"
|
||||
v-if="data.total > 100">
|
||||
<Page :total="data.total"
|
||||
size="small"
|
||||
:current="query.page"
|
||||
:page-size="query.page_size"
|
||||
@on-change="changePage"
|
||||
show-elevator />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<userBox></userBox>
|
||||
<br>
|
||||
<div class="box">
|
||||
<div class="cell">我关注的人</div>
|
||||
<div class="cell"
|
||||
v-for="(item,k) in data.fav_users"
|
||||
:key="k">
|
||||
<a class="a-link"
|
||||
:href="`/#/member?username=${item.username}`">{{item.username}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BackTop></BackTop>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import loadingSpin from '@/components/loading-spin'
|
||||
import userBox from '@/components/user-box'
|
||||
import userAPI from '@/api/user'
|
||||
export default {
|
||||
components: {
|
||||
userBox,
|
||||
loadingSpin
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
data: {
|
||||
total: 0,
|
||||
list: [],
|
||||
fav_users: []
|
||||
},
|
||||
query: {
|
||||
page: 1,
|
||||
page_size: 50
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changePage (page) {
|
||||
this.query.page = page
|
||||
this.$router.push({ 'path': this.$route.path, 'query': this.query })
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
if (parseInt(this.$route.query.page)) {
|
||||
this.query.page = parseInt(this.$route.query.page)
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await userAPI.listFav(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async beforeRouteUpdate (to, from, next) {
|
||||
if (parseInt(to.query.page)) {
|
||||
this.query.page = parseInt(to.query.page)
|
||||
} else {
|
||||
this.query.page = 1
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await userAPI.listFav(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
next()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.topic {
|
||||
.topic-item {
|
||||
display: flex;
|
||||
}
|
||||
.topic-content {
|
||||
flex: 1;
|
||||
margin-left: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
.topic-title {
|
||||
font-size: 16px;
|
||||
line-height: 130%;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
}
|
||||
.topic-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
div {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.topic-comment {
|
||||
width: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
141
web/src/views/New.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content">
|
||||
<div class="box">
|
||||
<div class="cell">
|
||||
<a href="/#/index"
|
||||
class="a-link">V22X</a><span> › </span><span>创作新主题</span>
|
||||
</div>
|
||||
<div class="cell">主题标题</div>
|
||||
<div class="cell"
|
||||
style="padding: 0px;"><textarea class="custom-input"
|
||||
rows="1"
|
||||
maxlength="120"
|
||||
v-model="postForm.title"
|
||||
placeholder="请输入主题标题,如果标题能够表达完整内容,则正文可以为空"></textarea></div>
|
||||
<div class="cell">正文</div>
|
||||
<div class="cell"><textarea class="custom-input"
|
||||
rows="12"
|
||||
v-model="postForm.content"></textarea></div>
|
||||
<div class="cell">
|
||||
<Select v-model="postForm.sub_tab_id"
|
||||
filterable
|
||||
style="width:260px">
|
||||
<Option v-for="item in tabs"
|
||||
:value="item.id"
|
||||
:key="item.id">{{ item.zh }} / {{item.name}}</Option>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="cell"
|
||||
style="display: flex;justify-content: space-between;">
|
||||
<div><Button @click="handlePreview">预览主题</Button></div>
|
||||
<div><Button @click="handleNewPost">发布主题</Button></div>
|
||||
</div>
|
||||
<div class="cell"
|
||||
v-if="preview">
|
||||
<MarkdownPreview theme="oneDark"
|
||||
:bordered="false"
|
||||
:initialValue="postForm.content" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<div class="box">
|
||||
<div class="cell">
|
||||
<div>社区指导原则</div>
|
||||
</div>
|
||||
<div class="inner">
|
||||
<div>
|
||||
<Icon type="md-water" />尊重原创</div>
|
||||
<div>
|
||||
<Icon type="md-water" />友好互助</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { MarkdownPreview } from 'vue-meditor'
|
||||
import tabAPI from '@/api/tab'
|
||||
import topicAPI from '@/api/topic'
|
||||
export default {
|
||||
components: {
|
||||
MarkdownPreview
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
preview: false,
|
||||
tabs: [],
|
||||
postForm: {
|
||||
title: null,
|
||||
sub_tab_id: null,
|
||||
content: null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlePreview () {
|
||||
if (this.postForm.content) {
|
||||
this.preview = !this.preview
|
||||
}
|
||||
if(!this.postForm.content) {
|
||||
this.preview = false
|
||||
}
|
||||
},
|
||||
async handleNewPost () {
|
||||
if (!this.postForm.title) {
|
||||
this.$Message.error('请先输入标题哦')
|
||||
return
|
||||
}
|
||||
if (!this.postForm.sub_tab_id) {
|
||||
this.$Message.error('请选择好分类哦')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await topicAPI.create(this.postForm)
|
||||
this.$router.push({
|
||||
path: '/index'
|
||||
})
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
this.loading = true
|
||||
try {
|
||||
this.tabs = await tabAPI.listSubTab()
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.custom-input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
resize: none;
|
||||
background-color: #f9f9f9;
|
||||
outline: 0;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
.custom-input:focus {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
155
web/src/views/Register.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content">
|
||||
<div class="box">
|
||||
<div class="cell"><a href="/#/index"
|
||||
class="a-link">V22X</a><span> › </span><span>注册新用户</span>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>用户名</strong></div>
|
||||
<div class="form-content"><Input v-model="formData.username"
|
||||
style="width: 200px;" /></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>邮箱地址</strong></div>
|
||||
<div class="form-content"><Input v-model="formData.email"
|
||||
style="width: 200px;" /></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>密码</strong></div>
|
||||
<div class="form-content"><Input v-model="formData.password"
|
||||
style="width: 200px;"
|
||||
type='password' /></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>图形验证码</strong></div>
|
||||
<div class="form-content"> <Input style="width: 80px;"
|
||||
@on-change="checkImgCode()"
|
||||
v-model="formData.code" />
|
||||
<img :src="codeImgSrc"
|
||||
@click="refreshCode()"
|
||||
style="margin-left: 10px;height: 20px;"
|
||||
align="center"></img></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>邮箱验证码</strong></div>
|
||||
<div class="form-content"><Input v-model="formData.email_code"
|
||||
style="width: 80px;" />
|
||||
<span><Button style="margin-left: 10px;"
|
||||
:loading='sendCodeLoading'
|
||||
@click="handleSendCode()">发送验证码</Button></span></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"></div>
|
||||
<div class="form-content"><Button type='primary'
|
||||
style="width: 80px;"
|
||||
:loading='loading'
|
||||
@click="handleRegister('formData')">注册</Button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import sleep from '@/common/sleep'
|
||||
import accountAPI from '@/api/account'
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
sendCodeLoading: false,
|
||||
loading: false,
|
||||
formData: {},
|
||||
codeImgSrc: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleRegister (name) {
|
||||
if (this.loading) return
|
||||
if (!this.formData.username) {
|
||||
this.$Message.error('用户名不能为空')
|
||||
return
|
||||
}
|
||||
if (!this.formData.email) {
|
||||
this.$Message.error('邮箱不能为空')
|
||||
return
|
||||
}
|
||||
if (!this.formData.password) {
|
||||
this.$Message.error('密码不能为空')
|
||||
return
|
||||
}
|
||||
if (!this.formData.email_code) {
|
||||
this.$Message.error('邮箱验证码不能为空')
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.loading = true
|
||||
await accountAPI.register(this.formData)
|
||||
this.$Message.success('注册成功,自动跳转到登陆页面')
|
||||
await Promise.race([
|
||||
sleep(500)
|
||||
])
|
||||
this.$router.replace('/account/login')
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
refreshCode () {
|
||||
this.codeImgSrc = '/api/v1/captcha' + '?t=' + new Date().getTime()
|
||||
},
|
||||
async handleSendCode () {
|
||||
if (!this.formData.email) {
|
||||
this.$Message.error('邮箱不能为空')
|
||||
return
|
||||
}
|
||||
if (!this.formData.code) {
|
||||
this.$Message.error('图形验证码不能为空')
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.sendCodeLoading = true
|
||||
await accountAPI.sendEmailCode({ 'email': this.formData.email, 'code': this.formData.code })
|
||||
this.$Message.success('验证码发送成功,请检查收件箱')
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.sendCodeLoading = false
|
||||
}
|
||||
},
|
||||
async checkImgCode () {
|
||||
if (this.formData.code.length !== 5) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await accountAPI.checkCode({ 'code': this.formData.code })
|
||||
this.$Message.success('验证码正确')
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('出了点小差')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.codeImgSrc = '/api/v1/captcha' + '?t=' + new Date().getTime()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
222
web/src/views/Setting.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content">
|
||||
<div class="box">
|
||||
<div class="cell"><a href="/#/index"
|
||||
class="a-link">V22X</a><span> › </span><span>设置</span>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="form-item">
|
||||
<div class="form-label"></div>
|
||||
<div class="form-content"><strong>V22X 第 {{data.id}} 号会员</strong></div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>用户名</strong></div>
|
||||
<div class="form-content">{{data.username}}</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>邮箱地址</strong></div>
|
||||
<div class="form-content">{{data.email}}</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>个人网站</strong></div>
|
||||
<div class="form-content"><Input v-model="data.site"
|
||||
style="width: 200px;" /></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>所在地</strong></div>
|
||||
<div class="form-content"><Input v-model="data.location"
|
||||
style="width: 200px;" /></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>所在公司</strong></div>
|
||||
<div class="form-content"><Input v-model="data.company"
|
||||
style="width: 200px;" /></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label">
|
||||
<Icon type="logo-github"
|
||||
size="30" />
|
||||
</div>
|
||||
<div class="form-content"><Input v-model="data.github"
|
||||
style="width: 200px;" /></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label">
|
||||
<Icon type="logo-twitter"
|
||||
color="#4086cd"
|
||||
size="30" />
|
||||
</div>
|
||||
<div class="form-content"><Input v-model="data.twitter"
|
||||
style="width: 200px;" /></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>微博地址</strong></div>
|
||||
<div class="form-content"><Input v-model="data.weibo"
|
||||
style="width: 200px;" /></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>个人简介</strong></div>
|
||||
<div class="form-content"><Input type="textarea"
|
||||
v-model="data.bio"
|
||||
style="width: 200px;" /></div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>主题列表隐私设置</strong></div>
|
||||
<div class="form-content"><Select v-model="data.privacy_level"
|
||||
style="width: 150px">
|
||||
<Option v-for="item in privacyLevelList"
|
||||
:value="item.value"
|
||||
:key="item.value">{{ item.label }}</Option>
|
||||
</Select></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>性别</strong></div>
|
||||
<div class="form-content">
|
||||
<RadioGroup v-model="data.sex">
|
||||
<Radio :label="0">
|
||||
<Icon type="ios-man"></Icon>
|
||||
<span>男</span>
|
||||
</Radio>
|
||||
<Radio :label="1">
|
||||
<Icon type="ios-woman"></Icon>
|
||||
<span>女</span>
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"></div>
|
||||
<div class="form-content"><Button type='primary'
|
||||
:loading='loading'
|
||||
@click="handleUpdateProfile()">更新</Button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="box">
|
||||
<div class="cell">
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>原密码</strong></div>
|
||||
<div class="form-content"><Input v-model="passwordForm.password"
|
||||
style="width: 200px;" /></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"><strong>新密码</strong></div>
|
||||
<div class="form-content"><Input v-model="passwordForm.new_password"
|
||||
style="width: 200px;" /></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label"></div>
|
||||
<div class="form-content"><Button type='warning'
|
||||
:loading='loading'
|
||||
@click="handleUpdatePassword()">更改密码</Button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<userBox></userBox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import userBox from '@/components/user-box'
|
||||
import accountAPI from '@/api/account'
|
||||
export default {
|
||||
components: {
|
||||
userBox
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
privacyLevelList: [
|
||||
{
|
||||
label: '所有人可见',
|
||||
value: 0
|
||||
}, {
|
||||
label: '登录的人可见',
|
||||
value: 1
|
||||
}, {
|
||||
label: '仅自己可见',
|
||||
value: 2
|
||||
}
|
||||
],
|
||||
data: {
|
||||
id: 0,
|
||||
username: '',
|
||||
email: '',
|
||||
sex: 0,
|
||||
avatar_url: '',
|
||||
site: '',
|
||||
location: '',
|
||||
company: '',
|
||||
github: '',
|
||||
twitter: '',
|
||||
weibo: '',
|
||||
bio: '',
|
||||
privacy_level: 0
|
||||
},
|
||||
passwordForm: {
|
||||
password: '',
|
||||
new_password: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleUpdateProfile () {
|
||||
try {
|
||||
this.data = await accountAPI.updateProfile(this.data)
|
||||
this.$Message.success('更新成功')
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async handleUpdatePassword () {
|
||||
if (!this.passwordForm.password || !this.passwordForm.new_password) {
|
||||
this.$Message.error('请输入原密码和新密码')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await accountAPI.updatePassword(this.passwordForm)
|
||||
this.$Message.success('更改成功,请重新登录')
|
||||
this.$router.push({ 'path': '/account/login' })
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await accountAPI.profile()
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
382
web/src/views/Topic.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<div style="display: flex;">
|
||||
<div class="left-content"
|
||||
style="position: relative; flex-grow: 1">
|
||||
<div class="box">
|
||||
<div class="cell"
|
||||
style="display: flex;">
|
||||
<div style="flex-grow: 1">
|
||||
<div style="margin-bottom:20px;"><a href="/#/index"
|
||||
class="a-link">V22X</a><span> › </span><a :href="`/#/go?tab=${data.sub_tab.name}`"
|
||||
class="a-link">{{data.sub_tab.zh}}</a></div>
|
||||
<div style="margin-bottom:10px;">
|
||||
<h2>{{data.title}}</h2>
|
||||
</div>
|
||||
<div>
|
||||
<span style="margin-right: 5px;"><Button size="small"
|
||||
@click="handleUpDown('up')"
|
||||
style="height: 20px;"
|
||||
icon="ios-arrow-up">{{data.up_count}}</Button></span>
|
||||
<span style="margin-right: 15px;"><Button size="small"
|
||||
@click="handleUpDown('down')"
|
||||
style="height: 20px;"
|
||||
icon="ios-arrow-down">{{data.down_count}}</Button></span>
|
||||
<span style="margin-right: 5px;"><a class="a-link"
|
||||
:href="`/#/member?username=${data.user.username}`">{{data.user.username}}</a></span>·
|
||||
<span style="margin-right: 5px;color: #999;">创建于{{data.time_create}}</span>·
|
||||
<span style="margin-right: 5px;color: #999;">{{data.view_count}} 次点击</span>·
|
||||
<span v-if="$user&&$user.id === data.user.id"><Button size="small"
|
||||
:to="`/t/append?uid=${data.uid}`">追加</Button></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="avatar-big"></div>
|
||||
</div>
|
||||
<div v-if="data.content"
|
||||
class="cell">
|
||||
<MarkdownPreview theme="oneDark"
|
||||
:bordered="false"
|
||||
:initialValue="data.content" />
|
||||
</div>
|
||||
<div class="cell append"
|
||||
v-for="(item,k) in data.appends"
|
||||
:key="k">
|
||||
<div style="color: #ccc;margin-bottom: 6px;">第{{k + 1}}条追加 * {{ item.time_create }}</div>
|
||||
<div>
|
||||
<MarkdownPreview theme="oneDark"
|
||||
:bordered="false"
|
||||
:initialValue="item.content" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell"
|
||||
style="display: flex;justify-content: space-between;align-items: center;">
|
||||
<div style="display: flex;">
|
||||
<div style="margin-right: 10px;">
|
||||
<div v-if="$user&&$user.id === data.user.id"><Button size="small"
|
||||
disabled>加入收藏</Button></div>
|
||||
<div v-else>
|
||||
<Button size="small"
|
||||
v-if="data.is_fav"
|
||||
@click="handleFav('cal')">取消收藏</Button>
|
||||
<Button size="small"
|
||||
v-else
|
||||
@click="handleFav('add')">加入收藏</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="$user&&$user.id === data.user.id||data.is_thank"><Button size="small"
|
||||
shape="circle"
|
||||
disabled>感谢</Button></div>
|
||||
<div v-else>
|
||||
<Button size="small"
|
||||
shape="circle"
|
||||
@click="handleThank()">感谢</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>{{data.view_count}} 次点击</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="box"
|
||||
v-if="data.comments">
|
||||
<div class="cell">{{data.comment_count}}条回复 * 直到 {{currentData}}</div>
|
||||
<div class="cell"
|
||||
v-if="data.comment_count > 100">
|
||||
<Page :total="data.comment_count"
|
||||
size="small"
|
||||
:current="query.page"
|
||||
:page-size="query.page_size"
|
||||
@on-change="changePage"
|
||||
show-elevator />
|
||||
</div>
|
||||
<div class="cell comment-item"
|
||||
v-for="(item,k) in data.comments"
|
||||
:key="k">
|
||||
<div class="avatar"><a :href="`/#/member?username=${item.user.username}`"><img :src="item.user.avatar_url"></a></div>
|
||||
<div class="comment-content">
|
||||
<div class="info">
|
||||
<div class="comment-user">
|
||||
<div style="display: flex;align-items: center;"><a class="a-link"
|
||||
:href="`/#/member?username=${item.user.username}`">{{item.user.username}}</a>
|
||||
<span style="margin-left: 10px;">回复于 {{item.time_create}}</span></div>
|
||||
<div style="margin-left: 8px;"><Button size="small"
|
||||
v-if="$user&&$user.id === item.user.id||item.is_thank"
|
||||
shape="circle"
|
||||
disabled>感谢</Button>
|
||||
<Button size="small"
|
||||
v-else
|
||||
shape="circle"
|
||||
@click="handleCommentThank(item)">感谢</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment-count">
|
||||
<a href="javascript:"
|
||||
@click="handleReturnComment(item)">
|
||||
<Icon type="md-arrow-round-back"
|
||||
size="18"
|
||||
style="margin-right: 10px;"></Icon>
|
||||
</a>
|
||||
<Badge :text="String(item.index)"
|
||||
type="normal"></Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data">{{item.content}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell"
|
||||
v-if="data.comment_count > 100">
|
||||
<Page :total="data.comment_count"
|
||||
size="small"
|
||||
:current="query.page"
|
||||
:page-size="query.page_size"
|
||||
@on-change="changePage"
|
||||
show-elevator />
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="box">
|
||||
<div class="cell">添加一条新回复</div>
|
||||
<div class="cell">
|
||||
<Input v-model="comment"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="添加评论" />
|
||||
<div style="display: flex;margin-top: 10px;justify-content: space-between;align-items: center;">
|
||||
<div style="margin-right: 20px;"><Button @click="handleComment">回复</Button></div>
|
||||
<div>请尽量让自己的回复能够对别人有帮助</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-content">
|
||||
<userBox></userBox>
|
||||
<br>
|
||||
<div class="box">
|
||||
<div class="inner">
|
||||
这是一个专门讨论 idea 的地方。
|
||||
<br>
|
||||
每个人的时间,资源是有限的,有的时候你或许能够想到很多 idea,但是由于现实的限制,却并不是所有的 idea 都能够成为现实。
|
||||
<br>
|
||||
那这个时候,不妨可以把那些 idea 分享出来,启发别人。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BackTop></BackTop>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { MarkdownPreview } from 'vue-meditor'
|
||||
import userBox from '@/components/user-box'
|
||||
import topicAPI from '@/api/topic'
|
||||
export default {
|
||||
components: {
|
||||
userBox,
|
||||
MarkdownPreview
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
currentData: new Date().toLocaleString(),
|
||||
data: {
|
||||
user: {
|
||||
id: null,
|
||||
username: null
|
||||
},
|
||||
sub_tab: {
|
||||
name: null,
|
||||
zh: null
|
||||
}
|
||||
},
|
||||
comment: null,
|
||||
query: {
|
||||
page: 1,
|
||||
page_size: 50
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changePage (page) {
|
||||
this.query.page = page
|
||||
this.$router.push({ 'path': this.$route.path, 'query': this.query })
|
||||
},
|
||||
handleReturnComment (item) {
|
||||
if (this.comment) {
|
||||
if (this.comment.search(` @${item.user.username} `) === -1) {
|
||||
this.comment += ` @${item.user.username} `
|
||||
}
|
||||
} else {
|
||||
this.comment = ` @${item.user.username} `
|
||||
}
|
||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })
|
||||
},
|
||||
async handleFav (action) {
|
||||
if (!this.$user) {
|
||||
this.$Message.error('登录后才可以操作哦')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await topicAPI.fav({ 'uid': this.query.uid, 'action': action })
|
||||
this.data.is_fav = !this.data.is_fav
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
}
|
||||
},
|
||||
async handleThank () {
|
||||
if (!this.$user) {
|
||||
this.$Message.error('登录后才可以操作哦')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await topicAPI.thank({ 'uid': this.query.uid })
|
||||
this.data.is_thank = true
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
}
|
||||
},
|
||||
async handleCommentThank (item) {
|
||||
if (!this.$user) {
|
||||
this.$Message.error('登录后才可以操作哦')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await topicAPI.commentThank({ 'uid': item.uid })
|
||||
this.data = await topicAPI.list(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
}
|
||||
},
|
||||
async handleComment () {
|
||||
if (!this.$user) {
|
||||
this.$Message.error('登录后才可以操作哦')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await topicAPI.comment({'uid': this.query.uid, 'comment': this.comment})
|
||||
this.comment = null
|
||||
this.data = await topicAPI.list(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
}
|
||||
},
|
||||
async handleUpDown (action) {
|
||||
if (!this.$user) {
|
||||
this.$Message.error('登录后才可以操作哦')
|
||||
return
|
||||
}
|
||||
try {
|
||||
let tmp = await topicAPI.upDown({ 'uid': this.query.uid, 'action': action })
|
||||
this.data.up_count = tmp.up_count
|
||||
this.data.down_count = tmp.down_count
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
if (!this.$route.query.uid) {
|
||||
this.$router.push({ 'path': '/index' })
|
||||
return
|
||||
}
|
||||
this.query.uid = this.$route.query.uid
|
||||
if (parseInt(this.$route.query.page)) {
|
||||
this.query.page = parseInt(this.$route.query.page)
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
await topicAPI.addView({ 'uid': this.query.uid })
|
||||
this.data = await topicAPI.list(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
this.$router.push({ 'path': '/index' })
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async beforeRouteUpdate (to, from, next) {
|
||||
if (parseInt(to.query.page)) {
|
||||
this.query.page = parseInt(to.query.page)
|
||||
} else {
|
||||
this.query.page = 1
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
this.data = await topicAPI.list(this.query)
|
||||
} catch (e) {
|
||||
if (e.msg) {
|
||||
this.$Message.error(e.msg)
|
||||
} else {
|
||||
console.log(e)
|
||||
this.$Message.error('服务器出了点小差')
|
||||
}
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
next()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.append {
|
||||
background-color: #fffff9;
|
||||
border-left: 3px solid #fffbc1;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 120%;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e2e2e2;
|
||||
}
|
||||
.comment-item {
|
||||
display: flex;
|
||||
.comment-content {
|
||||
flex-grow: 1;
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.comment-user {
|
||||
display: flex;
|
||||
a {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
.data {
|
||||
font-size: 15px;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
35
web/vue.config.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const path = require('path')
|
||||
|
||||
const resolve = dir => path.join(__dirname, dir)
|
||||
|
||||
let assetsDir = (() => {
|
||||
return 'static/'
|
||||
})()
|
||||
|
||||
module.exports = {
|
||||
baseUrl: process.env.BASE_URL,
|
||||
devServer: {
|
||||
hot: true,
|
||||
host: '0.0.0.0',
|
||||
port: 80,
|
||||
open: false,
|
||||
disableHostCheck: true,
|
||||
https: false,
|
||||
hotOnly: false,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://172.17.76.64:5000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
configureWebpack: {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve('src')
|
||||
}
|
||||
}
|
||||
},
|
||||
assetsDir: assetsDir,
|
||||
productionSourceMap: false
|
||||
}
|
||||
9311
web/yarn.lock
Normal file
29
webAdmin/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# vue-startup
|
||||
|
||||
## Project setup
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn run build
|
||||
```
|
||||
|
||||
### Run your tests
|
||||
```
|
||||
yarn run test
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
5
webAdmin/babel.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
]
|
||||
}
|
||||
87
webAdmin/package.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "vue-startup",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"core-js": "^2.6.5",
|
||||
"echarts": "^4.2.1",
|
||||
"lodash": "^4.17.11",
|
||||
"normalize.css": "^8.0.1",
|
||||
"qs": "^6.7.0",
|
||||
"uuid": "^3.3.2",
|
||||
"view-design": "^4.0.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-meditor": "^2.0.2",
|
||||
"vue-router": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.6.0",
|
||||
"@vue/cli-plugin-eslint": "^3.6.0",
|
||||
"@vue/cli-service": "^3.6.0",
|
||||
"@vue/eslint-config-standard": "^4.0.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-config-vue": "^2.0.2",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-loader": "^2.1.2",
|
||||
"eslint-plugin-flow-vars": "^0.5.0",
|
||||
"eslint-plugin-html": "^5.0.5",
|
||||
"eslint-plugin-import": "^2.17.2",
|
||||
"eslint-plugin-node": "^9.0.1",
|
||||
"eslint-plugin-promise": "^4.1.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^5.2.2",
|
||||
"lint-staged": "^8.1.5",
|
||||
"node-sass": "^4.9.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"vue-template-compiler": "^2.5.21"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"@vue/standard"
|
||||
],
|
||||
"rules": {
|
||||
"vue/no-parsing-error": [
|
||||
2,
|
||||
{
|
||||
"x-invalid-end-tag": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
},
|
||||
"postcss": {
|
||||
"plugins": {
|
||||
"autoprefixer": {}
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
],
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,vue}": [
|
||||
"vue-cli-service lint",
|
||||
"git add"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
webAdmin/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
20
webAdmin/public/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="height: 100%">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
||||
<title>super-bbs</title>
|
||||
</head>
|
||||
<body style="height: 100%">
|
||||
<noscript>
|
||||
<strong
|
||||
>We're sorry but vue-startup doesn't work properly without JavaScript
|
||||
enabled. Please enable it to continue.</strong
|
||||
>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||