@ -0,0 +1,11 @@ |
||||
[*.{js,jsx,ts,tsx,vue,hbs}] |
||||
indent_style = space |
||||
indent_size = 2 |
||||
end_of_line = lf |
||||
trim_trailing_whitespace = true |
||||
insert_final_newline = true |
||||
max_line_length = 180 |
||||
|
||||
[*.md] |
||||
insert_final_newline = false |
||||
trim_trailing_whitespace = false |
@ -0,0 +1,7 @@ |
||||
VITE_APP_TITLE=金融产品设计及数字化营销沙盘 |
||||
VITE_PORT=9520 |
||||
VITE_PROXY=http://192.168.31.126:8080 |
||||
VITE_PUBLIC_PATH=./ |
||||
VITE_BASE_API=/api |
||||
VITE_I18N_LOCALE=zh-cn |
||||
VITE_I18N_FALLBACK_LOCALE=zh-cn |
@ -0,0 +1,4 @@ |
||||
build/*.js |
||||
src/assets |
||||
public |
||||
dist |
@ -0,0 +1,37 @@ |
||||
module.exports = { |
||||
root: true, |
||||
env: { |
||||
node: true, |
||||
}, |
||||
extends: ['plugin:vue/vue3-essential', 'airbnb-base', '@vue/typescript/recommended', '@vue/prettier', '@vue/prettier/@typescript-eslint'], |
||||
parserOptions: { |
||||
ecmaVersion: 2020, |
||||
}, |
||||
rules: { |
||||
// 'no-param-reassign': ['error', { props: true, ignorePropertyModificationsFor: ['state'] }],
|
||||
// 'linebreak-style': [0, 'error', 'windows'],
|
||||
// 'import/prefer-default-export': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off', |
||||
// '@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
// // 避免使用Q_作为查询参数时报错
|
||||
// '@typescript-eslint/camelcase': 'off',
|
||||
|
||||
'import/extensions': 'off', |
||||
}, |
||||
settings: { |
||||
'import/resolver': { |
||||
alias: { |
||||
map: [['@', './src']], |
||||
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json', '.vue'], |
||||
}, |
||||
}, |
||||
}, |
||||
overrides: [ |
||||
{ |
||||
files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/unit/**/*.spec.{j,t}s?(x)'], |
||||
env: { |
||||
jest: true, |
||||
}, |
||||
}, |
||||
], |
||||
}; |
@ -0,0 +1,6 @@ |
||||
node_modules |
||||
.DS_Store |
||||
.history |
||||
dist |
||||
dist-ssr |
||||
*.local |
@ -0,0 +1,661 @@ |
||||
GNU AFFERO GENERAL PUBLIC LICENSE |
||||
Version 3, 19 November 2007 |
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. |
||||
Everyone is permitted to copy and distribute verbatim copies |
||||
of this license document, but changing it is not allowed. |
||||
|
||||
Preamble |
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for |
||||
software and other kinds of works, specifically designed to ensure |
||||
cooperation with the community in the case of network server software. |
||||
|
||||
The licenses for most software and other practical works are designed |
||||
to take away your freedom to share and change the works. By contrast, |
||||
our General Public Licenses are intended to guarantee your freedom to |
||||
share and change all versions of a program--to make sure it remains free |
||||
software for all its users. |
||||
|
||||
When we speak of free software, we are referring to freedom, not |
||||
price. Our General Public Licenses are designed to make sure that you |
||||
have the freedom to distribute copies of free software (and charge for |
||||
them if you wish), that you receive source code or can get it if you |
||||
want it, that you can change the software or use pieces of it in new |
||||
free programs, and that you know you can do these things. |
||||
|
||||
Developers that use our General Public Licenses protect your rights |
||||
with two steps: (1) assert copyright on the software, and (2) offer |
||||
you this License which gives you legal permission to copy, distribute |
||||
and/or modify the software. |
||||
|
||||
A secondary benefit of defending all users' freedom is that |
||||
improvements made in alternate versions of the program, if they |
||||
receive widespread use, become available for other developers to |
||||
incorporate. Many developers of free software are heartened and |
||||
encouraged by the resulting cooperation. However, in the case of |
||||
software used on network servers, this result may fail to come about. |
||||
The GNU General Public License permits making a modified version and |
||||
letting the public access it on a server without ever releasing its |
||||
source code to the public. |
||||
|
||||
The GNU Affero General Public License is designed specifically to |
||||
ensure that, in such cases, the modified source code becomes available |
||||
to the community. It requires the operator of a network server to |
||||
provide the source code of the modified version running there to the |
||||
users of that server. Therefore, public use of a modified version, on |
||||
a publicly accessible server, gives the public access to the source |
||||
code of the modified version. |
||||
|
||||
An older license, called the Affero General Public License and |
||||
published by Affero, was designed to accomplish similar goals. This is |
||||
a different license, not a version of the Affero GPL, but Affero has |
||||
released a new version of the Affero GPL which permits relicensing under |
||||
this license. |
||||
|
||||
The precise terms and conditions for copying, distribution and |
||||
modification follow. |
||||
|
||||
TERMS AND CONDITIONS |
||||
|
||||
0. Definitions. |
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License. |
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of |
||||
works, such as semiconductor masks. |
||||
|
||||
"The Program" refers to any copyrightable work licensed under this |
||||
License. Each licensee is addressed as "you". "Licensees" and |
||||
"recipients" may be individuals or organizations. |
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work |
||||
in a fashion requiring copyright permission, other than the making of an |
||||
exact copy. The resulting work is called a "modified version" of the |
||||
earlier work or a work "based on" the earlier work. |
||||
|
||||
A "covered work" means either the unmodified Program or a work based |
||||
on the Program. |
||||
|
||||
To "propagate" a work means to do anything with it that, without |
||||
permission, would make you directly or secondarily liable for |
||||
infringement under applicable copyright law, except executing it on a |
||||
computer or modifying a private copy. Propagation includes copying, |
||||
distribution (with or without modification), making available to the |
||||
public, and in some countries other activities as well. |
||||
|
||||
To "convey" a work means any kind of propagation that enables other |
||||
parties to make or receive copies. Mere interaction with a user through |
||||
a computer network, with no transfer of a copy, is not conveying. |
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices" |
||||
to the extent that it includes a convenient and prominently visible |
||||
feature that (1) displays an appropriate copyright notice, and (2) |
||||
tells the user that there is no warranty for the work (except to the |
||||
extent that warranties are provided), that licensees may convey the |
||||
work under this License, and how to view a copy of this License. If |
||||
the interface presents a list of user commands or options, such as a |
||||
menu, a prominent item in the list meets this criterion. |
||||
|
||||
1. Source Code. |
||||
|
||||
The "source code" for a work means the preferred form of the work |
||||
for making modifications to it. "Object code" means any non-source |
||||
form of a work. |
||||
|
||||
A "Standard Interface" means an interface that either is an official |
||||
standard defined by a recognized standards body, or, in the case of |
||||
interfaces specified for a particular programming language, one that |
||||
is widely used among developers working in that language. |
||||
|
||||
The "System Libraries" of an executable work include anything, other |
||||
than the work as a whole, that (a) is included in the normal form of |
||||
packaging a Major Component, but which is not part of that Major |
||||
Component, and (b) serves only to enable use of the work with that |
||||
Major Component, or to implement a Standard Interface for which an |
||||
implementation is available to the public in source code form. A |
||||
"Major Component", in this context, means a major essential component |
||||
(kernel, window system, and so on) of the specific operating system |
||||
(if any) on which the executable work runs, or a compiler used to |
||||
produce the work, or an object code interpreter used to run it. |
||||
|
||||
The "Corresponding Source" for a work in object code form means all |
||||
the source code needed to generate, install, and (for an executable |
||||
work) run the object code and to modify the work, including scripts to |
||||
control those activities. However, it does not include the work's |
||||
System Libraries, or general-purpose tools or generally available free |
||||
programs which are used unmodified in performing those activities but |
||||
which are not part of the work. For example, Corresponding Source |
||||
includes interface definition files associated with source files for |
||||
the work, and the source code for shared libraries and dynamically |
||||
linked subprograms that the work is specifically designed to require, |
||||
such as by intimate data communication or control flow between those |
||||
subprograms and other parts of the work. |
||||
|
||||
The Corresponding Source need not include anything that users |
||||
can regenerate automatically from other parts of the Corresponding |
||||
Source. |
||||
|
||||
The Corresponding Source for a work in source code form is that |
||||
same work. |
||||
|
||||
2. Basic Permissions. |
||||
|
||||
All rights granted under this License are granted for the term of |
||||
copyright on the Program, and are irrevocable provided the stated |
||||
conditions are met. This License explicitly affirms your unlimited |
||||
permission to run the unmodified Program. The output from running a |
||||
covered work is covered by this License only if the output, given its |
||||
content, constitutes a covered work. This License acknowledges your |
||||
rights of fair use or other equivalent, as provided by copyright law. |
||||
|
||||
You may make, run and propagate covered works that you do not |
||||
convey, without conditions so long as your license otherwise remains |
||||
in force. You may convey covered works to others for the sole purpose |
||||
of having them make modifications exclusively for you, or provide you |
||||
with facilities for running those works, provided that you comply with |
||||
the terms of this License in conveying all material for which you do |
||||
not control copyright. Those thus making or running the covered works |
||||
for you must do so exclusively on your behalf, under your direction |
||||
and control, on terms that prohibit them from making any copies of |
||||
your copyrighted material outside their relationship with you. |
||||
|
||||
Conveying under any other circumstances is permitted solely under |
||||
the conditions stated below. Sublicensing is not allowed; section 10 |
||||
makes it unnecessary. |
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law. |
||||
|
||||
No covered work shall be deemed part of an effective technological |
||||
measure under any applicable law fulfilling obligations under article |
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or |
||||
similar laws prohibiting or restricting circumvention of such |
||||
measures. |
||||
|
||||
When you convey a covered work, you waive any legal power to forbid |
||||
circumvention of technological measures to the extent such circumvention |
||||
is effected by exercising rights under this License with respect to |
||||
the covered work, and you disclaim any intention to limit operation or |
||||
modification of the work as a means of enforcing, against the work's |
||||
users, your or third parties' legal rights to forbid circumvention of |
||||
technological measures. |
||||
|
||||
4. Conveying Verbatim Copies. |
||||
|
||||
You may convey verbatim copies of the Program's source code as you |
||||
receive it, in any medium, provided that you conspicuously and |
||||
appropriately publish on each copy an appropriate copyright notice; |
||||
keep intact all notices stating that this License and any |
||||
non-permissive terms added in accord with section 7 apply to the code; |
||||
keep intact all notices of the absence of any warranty; and give all |
||||
recipients a copy of this License along with the Program. |
||||
|
||||
You may charge any price or no price for each copy that you convey, |
||||
and you may offer support or warranty protection for a fee. |
||||
|
||||
5. Conveying Modified Source Versions. |
||||
|
||||
You may convey a work based on the Program, or the modifications to |
||||
produce it from the Program, in the form of source code under the |
||||
terms of section 4, provided that you also meet all of these conditions: |
||||
|
||||
a) The work must carry prominent notices stating that you modified |
||||
it, and giving a relevant date. |
||||
|
||||
b) The work must carry prominent notices stating that it is |
||||
released under this License and any conditions added under section |
||||
7. This requirement modifies the requirement in section 4 to |
||||
"keep intact all notices". |
||||
|
||||
c) You must license the entire work, as a whole, under this |
||||
License to anyone who comes into possession of a copy. This |
||||
License will therefore apply, along with any applicable section 7 |
||||
additional terms, to the whole of the work, and all its parts, |
||||
regardless of how they are packaged. This License gives no |
||||
permission to license the work in any other way, but it does not |
||||
invalidate such permission if you have separately received it. |
||||
|
||||
d) If the work has interactive user interfaces, each must display |
||||
Appropriate Legal Notices; however, if the Program has interactive |
||||
interfaces that do not display Appropriate Legal Notices, your |
||||
work need not make them do so. |
||||
|
||||
A compilation of a covered work with other separate and independent |
||||
works, which are not by their nature extensions of the covered work, |
||||
and which are not combined with it such as to form a larger program, |
||||
in or on a volume of a storage or distribution medium, is called an |
||||
"aggregate" if the compilation and its resulting copyright are not |
||||
used to limit the access or legal rights of the compilation's users |
||||
beyond what the individual works permit. Inclusion of a covered work |
||||
in an aggregate does not cause this License to apply to the other |
||||
parts of the aggregate. |
||||
|
||||
6. Conveying Non-Source Forms. |
||||
|
||||
You may convey a covered work in object code form under the terms |
||||
of sections 4 and 5, provided that you also convey the |
||||
machine-readable Corresponding Source under the terms of this License, |
||||
in one of these ways: |
||||
|
||||
a) Convey the object code in, or embodied in, a physical product |
||||
(including a physical distribution medium), accompanied by the |
||||
Corresponding Source fixed on a durable physical medium |
||||
customarily used for software interchange. |
||||
|
||||
b) Convey the object code in, or embodied in, a physical product |
||||
(including a physical distribution medium), accompanied by a |
||||
written offer, valid for at least three years and valid for as |
||||
long as you offer spare parts or customer support for that product |
||||
model, to give anyone who possesses the object code either (1) a |
||||
copy of the Corresponding Source for all the software in the |
||||
product that is covered by this License, on a durable physical |
||||
medium customarily used for software interchange, for a price no |
||||
more than your reasonable cost of physically performing this |
||||
conveying of source, or (2) access to copy the |
||||
Corresponding Source from a network server at no charge. |
||||
|
||||
c) Convey individual copies of the object code with a copy of the |
||||
written offer to provide the Corresponding Source. This |
||||
alternative is allowed only occasionally and noncommercially, and |
||||
only if you received the object code with such an offer, in accord |
||||
with subsection 6b. |
||||
|
||||
d) Convey the object code by offering access from a designated |
||||
place (gratis or for a charge), and offer equivalent access to the |
||||
Corresponding Source in the same way through the same place at no |
||||
further charge. You need not require recipients to copy the |
||||
Corresponding Source along with the object code. If the place to |
||||
copy the object code is a network server, the Corresponding Source |
||||
may be on a different server (operated by you or a third party) |
||||
that supports equivalent copying facilities, provided you maintain |
||||
clear directions next to the object code saying where to find the |
||||
Corresponding Source. Regardless of what server hosts the |
||||
Corresponding Source, you remain obligated to ensure that it is |
||||
available for as long as needed to satisfy these requirements. |
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided |
||||
you inform other peers where the object code and Corresponding |
||||
Source of the work are being offered to the general public at no |
||||
charge under subsection 6d. |
||||
|
||||
A separable portion of the object code, whose source code is excluded |
||||
from the Corresponding Source as a System Library, need not be |
||||
included in conveying the object code work. |
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any |
||||
tangible personal property which is normally used for personal, family, |
||||
or household purposes, or (2) anything designed or sold for incorporation |
||||
into a dwelling. In determining whether a product is a consumer product, |
||||
doubtful cases shall be resolved in favor of coverage. For a particular |
||||
product received by a particular user, "normally used" refers to a |
||||
typical or common use of that class of product, regardless of the status |
||||
of the particular user or of the way in which the particular user |
||||
actually uses, or expects or is expected to use, the product. A product |
||||
is a consumer product regardless of whether the product has substantial |
||||
commercial, industrial or non-consumer uses, unless such uses represent |
||||
the only significant mode of use of the product. |
||||
|
||||
"Installation Information" for a User Product means any methods, |
||||
procedures, authorization keys, or other information required to install |
||||
and execute modified versions of a covered work in that User Product from |
||||
a modified version of its Corresponding Source. The information must |
||||
suffice to ensure that the continued functioning of the modified object |
||||
code is in no case prevented or interfered with solely because |
||||
modification has been made. |
||||
|
||||
If you convey an object code work under this section in, or with, or |
||||
specifically for use in, a User Product, and the conveying occurs as |
||||
part of a transaction in which the right of possession and use of the |
||||
User Product is transferred to the recipient in perpetuity or for a |
||||
fixed term (regardless of how the transaction is characterized), the |
||||
Corresponding Source conveyed under this section must be accompanied |
||||
by the Installation Information. But this requirement does not apply |
||||
if neither you nor any third party retains the ability to install |
||||
modified object code on the User Product (for example, the work has |
||||
been installed in ROM). |
||||
|
||||
The requirement to provide Installation Information does not include a |
||||
requirement to continue to provide support service, warranty, or updates |
||||
for a work that has been modified or installed by the recipient, or for |
||||
the User Product in which it has been modified or installed. Access to a |
||||
network may be denied when the modification itself materially and |
||||
adversely affects the operation of the network or violates the rules and |
||||
protocols for communication across the network. |
||||
|
||||
Corresponding Source conveyed, and Installation Information provided, |
||||
in accord with this section must be in a format that is publicly |
||||
documented (and with an implementation available to the public in |
||||
source code form), and must require no special password or key for |
||||
unpacking, reading or copying. |
||||
|
||||
7. Additional Terms. |
||||
|
||||
"Additional permissions" are terms that supplement the terms of this |
||||
License by making exceptions from one or more of its conditions. |
||||
Additional permissions that are applicable to the entire Program shall |
||||
be treated as though they were included in this License, to the extent |
||||
that they are valid under applicable law. If additional permissions |
||||
apply only to part of the Program, that part may be used separately |
||||
under those permissions, but the entire Program remains governed by |
||||
this License without regard to the additional permissions. |
||||
|
||||
When you convey a copy of a covered work, you may at your option |
||||
remove any additional permissions from that copy, or from any part of |
||||
it. (Additional permissions may be written to require their own |
||||
removal in certain cases when you modify the work.) You may place |
||||
additional permissions on material, added by you to a covered work, |
||||
for which you have or can give appropriate copyright permission. |
||||
|
||||
Notwithstanding any other provision of this License, for material you |
||||
add to a covered work, you may (if authorized by the copyright holders of |
||||
that material) supplement the terms of this License with terms: |
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the |
||||
terms of sections 15 and 16 of this License; or |
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or |
||||
author attributions in that material or in the Appropriate Legal |
||||
Notices displayed by works containing it; or |
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or |
||||
requiring that modified versions of such material be marked in |
||||
reasonable ways as different from the original version; or |
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or |
||||
authors of the material; or |
||||
|
||||
e) Declining to grant rights under trademark law for use of some |
||||
trade names, trademarks, or service marks; or |
||||
|
||||
f) Requiring indemnification of licensors and authors of that |
||||
material by anyone who conveys the material (or modified versions of |
||||
it) with contractual assumptions of liability to the recipient, for |
||||
any liability that these contractual assumptions directly impose on |
||||
those licensors and authors. |
||||
|
||||
All other non-permissive additional terms are considered "further |
||||
restrictions" within the meaning of section 10. If the Program as you |
||||
received it, or any part of it, contains a notice stating that it is |
||||
governed by this License along with a term that is a further |
||||
restriction, you may remove that term. If a license document contains |
||||
a further restriction but permits relicensing or conveying under this |
||||
License, you may add to a covered work material governed by the terms |
||||
of that license document, provided that the further restriction does |
||||
not survive such relicensing or conveying. |
||||
|
||||
If you add terms to a covered work in accord with this section, you |
||||
must place, in the relevant source files, a statement of the |
||||
additional terms that apply to those files, or a notice indicating |
||||
where to find the applicable terms. |
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the |
||||
form of a separately written license, or stated as exceptions; |
||||
the above requirements apply either way. |
||||
|
||||
8. Termination. |
||||
|
||||
You may not propagate or modify a covered work except as expressly |
||||
provided under this License. Any attempt otherwise to propagate or |
||||
modify it is void, and will automatically terminate your rights under |
||||
this License (including any patent licenses granted under the third |
||||
paragraph of section 11). |
||||
|
||||
However, if you cease all violation of this License, then your |
||||
license from a particular copyright holder is reinstated (a) |
||||
provisionally, unless and until the copyright holder explicitly and |
||||
finally terminates your license, and (b) permanently, if the copyright |
||||
holder fails to notify you of the violation by some reasonable means |
||||
prior to 60 days after the cessation. |
||||
|
||||
Moreover, your license from a particular copyright holder is |
||||
reinstated permanently if the copyright holder notifies you of the |
||||
violation by some reasonable means, this is the first time you have |
||||
received notice of violation of this License (for any work) from that |
||||
copyright holder, and you cure the violation prior to 30 days after |
||||
your receipt of the notice. |
||||
|
||||
Termination of your rights under this section does not terminate the |
||||
licenses of parties who have received copies or rights from you under |
||||
this License. If your rights have been terminated and not permanently |
||||
reinstated, you do not qualify to receive new licenses for the same |
||||
material under section 10. |
||||
|
||||
9. Acceptance Not Required for Having Copies. |
||||
|
||||
You are not required to accept this License in order to receive or |
||||
run a copy of the Program. Ancillary propagation of a covered work |
||||
occurring solely as a consequence of using peer-to-peer transmission |
||||
to receive a copy likewise does not require acceptance. However, |
||||
nothing other than this License grants you permission to propagate or |
||||
modify any covered work. These actions infringe copyright if you do |
||||
not accept this License. Therefore, by modifying or propagating a |
||||
covered work, you indicate your acceptance of this License to do so. |
||||
|
||||
10. Automatic Licensing of Downstream Recipients. |
||||
|
||||
Each time you convey a covered work, the recipient automatically |
||||
receives a license from the original licensors, to run, modify and |
||||
propagate that work, subject to this License. You are not responsible |
||||
for enforcing compliance by third parties with this License. |
||||
|
||||
An "entity transaction" is a transaction transferring control of an |
||||
organization, or substantially all assets of one, or subdividing an |
||||
organization, or merging organizations. If propagation of a covered |
||||
work results from an entity transaction, each party to that |
||||
transaction who receives a copy of the work also receives whatever |
||||
licenses to the work the party's predecessor in interest had or could |
||||
give under the previous paragraph, plus a right to possession of the |
||||
Corresponding Source of the work from the predecessor in interest, if |
||||
the predecessor has it or can get it with reasonable efforts. |
||||
|
||||
You may not impose any further restrictions on the exercise of the |
||||
rights granted or affirmed under this License. For example, you may |
||||
not impose a license fee, royalty, or other charge for exercise of |
||||
rights granted under this License, and you may not initiate litigation |
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that |
||||
any patent claim is infringed by making, using, selling, offering for |
||||
sale, or importing the Program or any portion of it. |
||||
|
||||
11. Patents. |
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this |
||||
License of the Program or a work on which the Program is based. The |
||||
work thus licensed is called the contributor's "contributor version". |
||||
|
||||
A contributor's "essential patent claims" are all patent claims |
||||
owned or controlled by the contributor, whether already acquired or |
||||
hereafter acquired, that would be infringed by some manner, permitted |
||||
by this License, of making, using, or selling its contributor version, |
||||
but do not include claims that would be infringed only as a |
||||
consequence of further modification of the contributor version. For |
||||
purposes of this definition, "control" includes the right to grant |
||||
patent sublicenses in a manner consistent with the requirements of |
||||
this License. |
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free |
||||
patent license under the contributor's essential patent claims, to |
||||
make, use, sell, offer for sale, import and otherwise run, modify and |
||||
propagate the contents of its contributor version. |
||||
|
||||
In the following three paragraphs, a "patent license" is any express |
||||
agreement or commitment, however denominated, not to enforce a patent |
||||
(such as an express permission to practice a patent or covenant not to |
||||
sue for patent infringement). To "grant" such a patent license to a |
||||
party means to make such an agreement or commitment not to enforce a |
||||
patent against the party. |
||||
|
||||
If you convey a covered work, knowingly relying on a patent license, |
||||
and the Corresponding Source of the work is not available for anyone |
||||
to copy, free of charge and under the terms of this License, through a |
||||
publicly available network server or other readily accessible means, |
||||
then you must either (1) cause the Corresponding Source to be so |
||||
available, or (2) arrange to deprive yourself of the benefit of the |
||||
patent license for this particular work, or (3) arrange, in a manner |
||||
consistent with the requirements of this License, to extend the patent |
||||
license to downstream recipients. "Knowingly relying" means you have |
||||
actual knowledge that, but for the patent license, your conveying the |
||||
covered work in a country, or your recipient's use of the covered work |
||||
in a country, would infringe one or more identifiable patents in that |
||||
country that you have reason to believe are valid. |
||||
|
||||
If, pursuant to or in connection with a single transaction or |
||||
arrangement, you convey, or propagate by procuring conveyance of, a |
||||
covered work, and grant a patent license to some of the parties |
||||
receiving the covered work authorizing them to use, propagate, modify |
||||
or convey a specific copy of the covered work, then the patent license |
||||
you grant is automatically extended to all recipients of the covered |
||||
work and works based on it. |
||||
|
||||
A patent license is "discriminatory" if it does not include within |
||||
the scope of its coverage, prohibits the exercise of, or is |
||||
conditioned on the non-exercise of one or more of the rights that are |
||||
specifically granted under this License. You may not convey a covered |
||||
work if you are a party to an arrangement with a third party that is |
||||
in the business of distributing software, under which you make payment |
||||
to the third party based on the extent of your activity of conveying |
||||
the work, and under which the third party grants, to any of the |
||||
parties who would receive the covered work from you, a discriminatory |
||||
patent license (a) in connection with copies of the covered work |
||||
conveyed by you (or copies made from those copies), or (b) primarily |
||||
for and in connection with specific products or compilations that |
||||
contain the covered work, unless you entered into that arrangement, |
||||
or that patent license was granted, prior to 28 March 2007. |
||||
|
||||
Nothing in this License shall be construed as excluding or limiting |
||||
any implied license or other defenses to infringement that may |
||||
otherwise be available to you under applicable patent law. |
||||
|
||||
12. No Surrender of Others' Freedom. |
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or |
||||
otherwise) that contradict the conditions of this License, they do not |
||||
excuse you from the conditions of this License. If you cannot convey a |
||||
covered work so as to satisfy simultaneously your obligations under this |
||||
License and any other pertinent obligations, then as a consequence you may |
||||
not convey it at all. For example, if you agree to terms that obligate you |
||||
to collect a royalty for further conveying from those to whom you convey |
||||
the Program, the only way you could satisfy both those terms and this |
||||
License would be to refrain entirely from conveying the Program. |
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License. |
||||
|
||||
Notwithstanding any other provision of this License, if you modify the |
||||
Program, your modified version must prominently offer all users |
||||
interacting with it remotely through a computer network (if your version |
||||
supports such interaction) an opportunity to receive the Corresponding |
||||
Source of your version by providing access to the Corresponding Source |
||||
from a network server at no charge, through some standard or customary |
||||
means of facilitating copying of software. This Corresponding Source |
||||
shall include the Corresponding Source for any work covered by version 3 |
||||
of the GNU General Public License that is incorporated pursuant to the |
||||
following paragraph. |
||||
|
||||
Notwithstanding any other provision of this License, you have |
||||
permission to link or combine any covered work with a work licensed |
||||
under version 3 of the GNU General Public License into a single |
||||
combined work, and to convey the resulting work. The terms of this |
||||
License will continue to apply to the part which is the covered work, |
||||
but the work with which it is combined will remain governed by version |
||||
3 of the GNU General Public License. |
||||
|
||||
14. Revised Versions of this License. |
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of |
||||
the GNU Affero General Public License from time to time. Such new versions |
||||
will be similar in spirit to the present version, but may differ in detail to |
||||
address new problems or concerns. |
||||
|
||||
Each version is given a distinguishing version number. If the |
||||
Program specifies that a certain numbered version of the GNU Affero General |
||||
Public License "or any later version" applies to it, you have the |
||||
option of following the terms and conditions either of that numbered |
||||
version or of any later version published by the Free Software |
||||
Foundation. If the Program does not specify a version number of the |
||||
GNU Affero General Public License, you may choose any version ever published |
||||
by the Free Software Foundation. |
||||
|
||||
If the Program specifies that a proxy can decide which future |
||||
versions of the GNU Affero General Public License can be used, that proxy's |
||||
public statement of acceptance of a version permanently authorizes you |
||||
to choose that version for the Program. |
||||
|
||||
Later license versions may give you additional or different |
||||
permissions. However, no additional obligations are imposed on any |
||||
author or copyright holder as a result of your choosing to follow a |
||||
later version. |
||||
|
||||
15. Disclaimer of Warranty. |
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY |
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT |
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY |
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, |
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM |
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF |
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION. |
||||
|
||||
16. Limitation of Liability. |
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS |
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY |
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE |
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF |
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD |
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), |
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF |
||||
SUCH DAMAGES. |
||||
|
||||
17. Interpretation of Sections 15 and 16. |
||||
|
||||
If the disclaimer of warranty and limitation of liability provided |
||||
above cannot be given local legal effect according to their terms, |
||||
reviewing courts shall apply local law that most closely approximates |
||||
an absolute waiver of all civil liability in connection with the |
||||
Program, unless a warranty or assumption of liability accompanies a |
||||
copy of the Program in return for a fee. |
||||
|
||||
END OF TERMS AND CONDITIONS |
||||
|
||||
How to Apply These Terms to Your New Programs |
||||
|
||||
If you develop a new program, and you want it to be of the greatest |
||||
possible use to the public, the best way to achieve this is to make it |
||||
free software which everyone can redistribute and change under these terms. |
||||
|
||||
To do so, attach the following notices to the program. It is safest |
||||
to attach them to the start of each source file to most effectively |
||||
state the exclusion of warranty; and each file should have at least |
||||
the "copyright" line and a pointer to where the full notice is found. |
||||
|
||||
|
||||
Copyright (C) |
||||
|
||||
This program is free software: you can redistribute it and/or modify |
||||
it under the terms of the GNU Affero General Public License as published by |
||||
the Free Software Foundation, either version 3 of the License, or |
||||
(at your option) any later version. |
||||
|
||||
This program is distributed in the hope that it will be useful, |
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
GNU Affero General Public License for more details. |
||||
|
||||
You should have received a copy of the GNU Affero General Public License |
||||
along with this program. If not, see . |
||||
|
||||
Also add information on how to contact you by electronic and paper mail. |
||||
|
||||
If your software can interact with users remotely through a computer |
||||
network, you should also make sure that it provides a way for users to |
||||
get its source. For example, if your program is a web application, its |
||||
interface could display a "Source" link that leads users to an archive |
||||
of the code. There are many ways you could offer source, and different |
||||
solutions will be better for different programs; see section 13 for the |
||||
specific requirements. |
||||
|
||||
You should also get your employer (if you work as a programmer) or school, |
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. |
||||
For more information on this, and how to apply and follow the GNU AGPL, see |
||||
. |
@ -0,0 +1,6 @@ |
||||
// Generated by 'unplugin-auto-import'
|
||||
// We suggest you to commit this file into source control
|
||||
declare global { |
||||
|
||||
} |
||||
export {} |
@ -0,0 +1,89 @@ |
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/vue-next/pull/3399
|
||||
|
||||
declare module 'vue' { |
||||
export interface GlobalComponents { |
||||
403: typeof import('./src/views/403.vue')['default']; |
||||
404: typeof import('./src/views/404.vue')['default']; |
||||
AppHeader: typeof import('./src/layout/components/AppHeader.vue')['default']; |
||||
AppMain: typeof import('./src/layout/components/AppMain.vue')['default']; |
||||
AppSidebar: typeof import('./src/layout/components/AppSidebar/index.vue')['default']; |
||||
BaseUpload: typeof import('./src/components/Upload/BaseUpload.vue')['default']; |
||||
Breadcrumb: typeof import('./src/components/Breadcrumb/index.vue')['default']; |
||||
ColumnList: typeof import('./src/components/TableList/ColumnList.vue')['default']; |
||||
ColumnSetting: typeof import('./src/components/TableList/ColumnSetting.vue')['default']; |
||||
DialogForm: typeof import('./src/components/DialogForm.vue')['default']; |
||||
ElAside: typeof import('element-plus/es')['ElAside']; |
||||
ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']; |
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']; |
||||
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']; |
||||
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']; |
||||
ElButton: typeof import('element-plus/es')['ElButton']; |
||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']; |
||||
ElCascader: typeof import('element-plus/es')['ElCascader']; |
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']; |
||||
ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']; |
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']; |
||||
ElCol: typeof import('element-plus/es')['ElCol']; |
||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']; |
||||
ElContainer: typeof import('element-plus/es')['ElContainer']; |
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']; |
||||
ElDialog: typeof import('element-plus/es')['ElDialog']; |
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']; |
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']; |
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']; |
||||
ElForm: typeof import('element-plus/es')['ElForm']; |
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']; |
||||
ElHeader: typeof import('element-plus/es')['ElHeader']; |
||||
ElIcon: typeof import('element-plus/es')['ElIcon']; |
||||
ElImage: typeof import('element-plus/es')['ElImage']; |
||||
ElInput: typeof import('element-plus/es')['ElInput']; |
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']; |
||||
ElLink: typeof import('element-plus/es')['ElLink']; |
||||
ElMain: typeof import('element-plus/es')['ElMain']; |
||||
ElMenu: typeof import('element-plus/es')['ElMenu']; |
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']; |
||||
ElOption: typeof import('element-plus/es')['ElOption']; |
||||
ElPagination: typeof import('element-plus/es')['ElPagination']; |
||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']; |
||||
ElProgress: typeof import('element-plus/es')['ElProgress']; |
||||
ElRadio: typeof import('element-plus/es')['ElRadio']; |
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']; |
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']; |
||||
ElRow: typeof import('element-plus/es')['ElRow']; |
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']; |
||||
ElSelect: typeof import('element-plus/es')['ElSelect']; |
||||
ElSlider: typeof import('element-plus/es')['ElSlider']; |
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']; |
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']; |
||||
ElTable: typeof import('element-plus/es')['ElTable']; |
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']; |
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']; |
||||
ElTabs: typeof import('element-plus/es')['ElTabs']; |
||||
ElTag: typeof import('element-plus/es')['ElTag']; |
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']; |
||||
ElTree: typeof import('element-plus/es')['ElTree']; |
||||
ElUpload: typeof import('element-plus/es')['ElUpload']; |
||||
FileListUpload: typeof import('./src/components/Upload/FileListUpload.vue')['default']; |
||||
HelloI18n: typeof import('./src/components/HelloI18n.vue')['default']; |
||||
Home: typeof import('./src/views/Home.vue')['default']; |
||||
ImageCropper: typeof import('./src/components/Upload/ImageCropper.vue')['default']; |
||||
ImageListUpload: typeof import('./src/components/Upload/ImageListUpload.vue')['default']; |
||||
ImageUpload: typeof import('./src/components/Upload/ImageUpload.vue')['default']; |
||||
LabelTip: typeof import('./src/components/LabelTip.vue')['default']; |
||||
ListMove: typeof import('./src/components/ListMove.vue')['default']; |
||||
Loading: typeof import('element-plus/es')['ElLoadingDirective']; |
||||
Login: typeof import('./src/views/Login.vue')['default']; |
||||
Logo: typeof import('./src/layout/components/AppSidebar/Logo.vue')['default']; |
||||
MenuItem: typeof import('./src/layout/components/AppSidebar/MenuItem.vue')['default']; |
||||
NavLink: typeof import('./src/components/NavLink/index.vue')['default']; |
||||
QueryForm: typeof import('./src/components/QueryForm/QueryForm.vue')['default']; |
||||
QueryInput: typeof import('./src/components/QueryForm/QueryInput.vue')['default']; |
||||
QueryItem: typeof import('./src/components/QueryForm/QueryItem.vue')['default']; |
||||
Tinymce: typeof import('./src/components/Tinymce/index.vue')['default']; |
||||
Undefined: typeof import('./src/layout/index.vue')['default']; |
||||
} |
||||
} |
||||
|
||||
export {}; |
@ -0,0 +1,12 @@ |
||||
<!DOCTYPE html> |
||||
<html lang="zh-cn"> |
||||
<head> |
||||
<meta charset="UTF-8" /> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
<title>金融产品设计及数字化营销沙盘</title> |
||||
</head> |
||||
<body> |
||||
<div id="app"></div> |
||||
<script type="module" src="/src/main.ts"></script> |
||||
</body> |
||||
</html> |
@ -0,0 +1,67 @@ |
||||
{ |
||||
"name": "ujcms-cp", |
||||
"version": "2.0.0", |
||||
"private": true, |
||||
"scripts": { |
||||
"dev": "vite", |
||||
"build": "vite build", |
||||
"preview": "vite preview", |
||||
"plop": "plop" |
||||
}, |
||||
"dependencies": { |
||||
"@element-plus/icons-vue": "^0.2.7", |
||||
"axios": "^0.25.0", |
||||
"core-js": "^3.21.0", |
||||
"cropperjs": "^1.5.12", |
||||
"dayjs": "^1.10.7", |
||||
"element-plus": "^2.0.2", |
||||
"js-cookie": "^3.0.1", |
||||
"lodash": "^4.17.21", |
||||
"nprogress": "^0.2.0", |
||||
"path-to-regexp": "^6.2.0", |
||||
"tinymce": "^5.9.2", |
||||
"vue": "^3.2.25", |
||||
"vue-i18n": "^9.2.0-beta.30", |
||||
"vue-router": "^4.0.12", |
||||
"vuedraggable": "^4.1.0" |
||||
}, |
||||
"devDependencies": { |
||||
"@intlify/vite-plugin-vue-i18n": "^3.2.2", |
||||
"@types/js-cookie": "^3.0.1", |
||||
"@types/lodash": "^4.14.178", |
||||
"@types/node": "^17.0.16", |
||||
"@types/nprogress": "^0.2.0", |
||||
"@typescript-eslint/eslint-plugin": "^4.33.0", |
||||
"@typescript-eslint/parser": "^4.33.0", |
||||
"@vitejs/plugin-legacy": "^1.7.1", |
||||
"@vitejs/plugin-vue": "^2.0.0", |
||||
"@vue/eslint-config-prettier": "^6.0.0", |
||||
"@vue/eslint-config-typescript": "^7.0.0", |
||||
"autoprefixer": "^10.4.2", |
||||
"eslint": "^6.8.0", |
||||
"eslint-config-airbnb-base": "^14.2.1", |
||||
"eslint-import-resolver-alias": "^1.1.2", |
||||
"eslint-plugin-import": "^2.25.4", |
||||
"eslint-plugin-prettier": "^3.4.1", |
||||
"eslint-plugin-vue": "^7.20.0", |
||||
"plop": "^3.0.5", |
||||
"postcss": "^8.4.6", |
||||
"prettier": "^2.5.1", |
||||
"sass": "^1.49.7", |
||||
"tailwindcss": "^3.0.19", |
||||
"typescript": "^4.4.4", |
||||
"vite": "^2.8.2", |
||||
"vue-tsc": "^0.29.8" |
||||
}, |
||||
"prettier": { |
||||
"printWidth": 180, |
||||
"singleQuote": true, |
||||
"trailingComma": "all", |
||||
"arrowParens": "always" |
||||
}, |
||||
"browserslist": [ |
||||
"> 1%", |
||||
"last 2 versions", |
||||
"not dead" |
||||
] |
||||
} |
@ -0,0 +1,8 @@ |
||||
export const query{{pascalCase name}}{{pascalCase type}} = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/{{kebabCase name}}', { params })).data; |
||||
export const query{{pascalCase name}} = async (id: number): Promise<any> => (await axios.get(`/backend/core/{{kebabCase name}}/${id}`)).data; |
||||
export const create{{pascalCase name}} = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/{{kebabCase name}}', data)).data; |
||||
export const update{{pascalCase name}} = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/{{kebabCase name}}?_method=put', data)).data; |
||||
{{#if isList}} |
||||
export const update{{pascalCase name}}Order = async (data: number[]): Promise<any> => (await axios.post('/backend/core/{{kebabCase name}}/order?_method=put', data)).data; |
||||
{{/if}} |
||||
export const delete{{pascalCase name}} = async (data: number[]): Promise<any> => (await axios.post('/backend/core/{{kebabCase name}}?_method=delete', data)).data; |
@ -0,0 +1,34 @@ |
||||
<template> |
||||
<dialog-form |
||||
:name="$t('menu.{{camelCase path}}.{{camelCase name}}')" |
||||
:queryBean="query{{pascalCase name}}" |
||||
:createBean="create{{pascalCase name}}" |
||||
:updateBean="update{{pascalCase name}}" |
||||
:deleteBean="delete{{pascalCase name}}" |
||||
:beanId="beanId" |
||||
:beanIds="beanIds" |
||||
:focus="focus" |
||||
:initValues="() => ({})" |
||||
:toValues="(bean) => ({ ...bean })" |
||||
perms="{{camelCase name}}" |
||||
:model-value="modelValue" |
||||
@update:model-value="$emit('update:modelValue', $event)" |
||||
@finished="$emit('finished')" |
||||
> |
||||
<template #default="{ values }"> |
||||
<el-form-item prop="name" :label="$t('{{camelCase name}}.name')" :rules="{ required: true, message: () => $t('v.required') }"> |
||||
<el-input v-model="values.name" ref="focus" maxlength="50"></el-input> |
||||
</el-form-item> |
||||
</template> |
||||
</dialog-form> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { defineProps, defineEmits, ref } from 'vue'; |
||||
import { query{{pascalCase name}}, create{{pascalCase name}}, update{{pascalCase name}}, delete{{pascalCase name}} } from '@/api/{{kebabCase path}}'; |
||||
import DialogForm from '@/components/DialogForm.vue'; |
||||
|
||||
defineProps({ modelValue: { type: Boolean, required: true }, beanId: { required: true }, beanIds: { type: Array, required: true } }); |
||||
defineEmits({ 'update:modelValue': null, finished: null }); |
||||
const focus = ref<any>(); |
||||
</script> |
@ -0,0 +1,116 @@ |
||||
<template> |
||||
<div> |
||||
<div class="mb-3"> |
||||
<query-form :params="params" @search="handleSearch" @reset="handleReset"> |
||||
<query-item :label="$t('{{camelCase name}}.name')" name="Q_Contains_name"></query-item> |
||||
</query-form> |
||||
</div> |
||||
<div> |
||||
<el-button type="primary" :disabled="perm('{{camelCase name}}:create')" :icon="Plus" @click="handleAdd()">\{{ $t('add') }}</el-button> |
||||
<el-popconfirm :title="$t('confirmDelete')" @confirm="handleDelete(selection.map((row) => row.id))"> |
||||
<template #reference> |
||||
<el-button :disabled="selection.length <= 0 || perm('{{camelCase name}}:delete')" :icon="Delete">\{{ $t('delete') }}</el-button> |
||||
</template> |
||||
</el-popconfirm> |
||||
<list-move :disabled="selection.length <= 0 || filtered || perm('org:update')" @move="(type) => move(selection, type)" class="ml-2" /> |
||||
<column-setting name="{{camelCase name}}" class="ml-2" /> |
||||
</div> |
||||
<div class="app-block mt-3"> |
||||
<el-table |
||||
ref="table" |
||||
v-loading="loading" |
||||
:data="data" |
||||
@selection-change="(rows) => (selection = rows)" |
||||
@row-dblclick="(row) => handleEdit(row.id)" |
||||
@sort-change="handleSort" |
||||
> |
||||
<column-list name="{{camelCase name}}"> |
||||
<el-table-column type="selection" width="45"></el-table-column> |
||||
<el-table-column property="id" label="ID" width="64" sortable="custom"></el-table-column> |
||||
<el-table-column property="name" :label="$t('{{camelCase name}}.name')" sortable="custom" show-overflow-tooltip></el-table-column> |
||||
<el-table-column :label="$t('table.action')"> |
||||
<template #default="{row}"> |
||||
<el-button type="text" :disabled="perm('{{camelCase name}}:update')" @click="handleEdit(row.id)" size="small">\{{ $t('edit') }}</el-button> |
||||
<el-popconfirm :title="$t('confirmDelete')" @confirm="handleDelete([row.id])"> |
||||
<template #reference> |
||||
<el-button type="text" :disabled="perm('{{camelCase name}}:delete')" size="small">\{{ $t('delete') }}</el-button> |
||||
</template> |
||||
</el-popconfirm> |
||||
</template> |
||||
</el-table-column> |
||||
</column-list> |
||||
</el-table> |
||||
</div> |
||||
<{{kebabCase name}}-form v-model="formVisible" :beanId="beanId" :beanIds="beanIds" @finished="fetchData" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { computed, onMounted, ref } from 'vue'; |
||||
import { ElMessage } from 'element-plus'; |
||||
import { Plus, Delete } from '@element-plus/icons-vue'; |
||||
import { useI18n } from 'vue-i18n'; |
||||
import { perm } from '@/store/useCurrentUser'; |
||||
import { moveList, toParams, resetParams } from '@/utils/common'; |
||||
import { delete{{pascalCase name}}, query{{pascalCase name}}List, update{{pascalCase name}}Order } from '@/api/{{kebabCase path}}'; |
||||
import { ColumnList, ColumnSetting } from '@/components/TableList'; |
||||
import { QueryForm, QueryItem } from '@/components/QueryForm'; |
||||
import ListMove from '@/components/ListMove.vue'; |
||||
import {{pascalCase name}}Form from './{{pascalCase name}}Form.vue'; |
||||
|
||||
const { t } = useI18n(); |
||||
const params = ref<any>({}); |
||||
const sort = ref<any>(); |
||||
const table = ref<any>(); |
||||
const data = ref<any[]>([]); |
||||
const selection = ref<any[]>([]); |
||||
const loading = ref<boolean>(false); |
||||
const formVisible = ref<boolean>(false); |
||||
const beanId = ref<number>(); |
||||
const beanIds = computed(() => data.value.map((row) => row.id)); |
||||
const filtered = ref<boolean>(false); |
||||
const fetchData = async () => { |
||||
loading.value = true; |
||||
try { |
||||
data.value = await query{{pascalCase name}}List({ ...toParams(params.value), Q_OrderBy: sort.value }); |
||||
filtered.value = Object.values(params.value).filter((v) => v !== undefined && v !== '').length > 0 || sort.value !== undefined; |
||||
} finally { |
||||
loading.value = false; |
||||
} |
||||
}; |
||||
onMounted(fetchData); |
||||
|
||||
const handleSort = ({ column, prop, order }: { column: any; prop: string; order: string }) => { |
||||
if (prop) { |
||||
sort.value = (column.sortBy ?? prop) + (order === 'descending' ? '_desc' : ''); |
||||
} else { |
||||
sort.value = undefined; |
||||
} |
||||
fetchData(); |
||||
}; |
||||
const handleSearch = () => fetchData(); |
||||
const handleReset = () => { |
||||
table.value.clearSort(); |
||||
resetParams(params.value); |
||||
sort.value = undefined; |
||||
fetchData(); |
||||
}; |
||||
|
||||
const handleAdd = () => { |
||||
beanId.value = undefined; |
||||
formVisible.value = true; |
||||
}; |
||||
const handleEdit = (id: number) => { |
||||
beanId.value = id; |
||||
formVisible.value = true; |
||||
}; |
||||
const handleDelete = async (ids: number[]) => { |
||||
await delete{{pascalCase name}}(ids); |
||||
fetchData(); |
||||
ElMessage.success(t('success')); |
||||
}; |
||||
const move = async (selected: any[], type: 'top' | 'up' | 'down' | 'bottom') => { |
||||
const list = moveList(selected, data.value, type); |
||||
await update{{pascalCase name}}Order(list.map((item) => item.id)); |
||||
}; |
||||
</script> |
@ -0,0 +1,125 @@ |
||||
<template> |
||||
<div> |
||||
<div class="mb-3"> |
||||
<query-form :params="params" @search="handleSearch" @reset="handleReset"> |
||||
<query-item :label="$t('{{camelCase name}}.name')" name="Q_Contains_name"></query-item> |
||||
</query-form> |
||||
</div> |
||||
<div> |
||||
<el-button type="primary" :disabled="perm('{{camelCase name}}:create')" :icon="Plus" @click="handleAdd()">\{{ $t('add') }}</el-button> |
||||
<el-popconfirm :title="$t('confirmDelete')" @confirm="handleDelete(selection.map((row) => row.id))"> |
||||
<template #reference> |
||||
<el-button :disabled="selection.length <= 0 || perm('{{camelCase name}}:delete')" :icon="Delete">\{{ $t('delete') }}</el-button> |
||||
</template> |
||||
</el-popconfirm> |
||||
<column-setting name="{{camelCase name}}" class="ml-2" /> |
||||
</div> |
||||
<div class="app-block mt-3"> |
||||
<el-table |
||||
ref="table" |
||||
v-loading="loading" |
||||
:data="data" |
||||
@selection-change="(rows) => (selection = rows)" |
||||
@row-dblclick="(row) => handleEdit(row.id)" |
||||
@sort-change="handleSort" |
||||
> |
||||
<column-list name="{{camelCase name}}"> |
||||
<el-table-column type="selection" width="45"></el-table-column> |
||||
<el-table-column property="id" label="ID" width="64" sortable="custom"></el-table-column> |
||||
<el-table-column property="name" :label="$t('{{camelCase name}}.name')" sortable="custom" show-overflow-tooltip></el-table-column> |
||||
<el-table-column :label="$t('table.action')"> |
||||
<template #default="{row}"> |
||||
<el-button type="text" :disabled="perm('{{camelCase name}}:update')" @click="handleEdit(row.id)" size="small">\{{ $t('edit') }}</el-button> |
||||
<el-popconfirm :title="$t('confirmDelete')" @confirm="handleDelete([row.id])"> |
||||
<template #reference> |
||||
<el-button type="text" :disabled="perm('{{camelCase name}}:delete')" size="small">\{{ $t('delete') }}</el-button> |
||||
</template> |
||||
</el-popconfirm> |
||||
</template> |
||||
</el-table-column> |
||||
</column-list> |
||||
</el-table> |
||||
<el-pagination |
||||
v-model:currentPage="currentPage" |
||||
v-model:pageSize="pageSize" |
||||
:total="total" |
||||
:page-sizes="pageSizes" |
||||
:layout="pageLayout" |
||||
@size-change="fetchData()" |
||||
@current-change="fetchData()" |
||||
small |
||||
background |
||||
class="px-3 py-2 justify-end" |
||||
></el-pagination> |
||||
</div> |
||||
<{{kebabCase name}}-form v-model="formVisible" :beanId="beanId" :beanIds="beanIds" @finished="fetchData" /> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { computed, onMounted, ref } from 'vue'; |
||||
import { ElMessage } from 'element-plus'; |
||||
import { Plus, Delete } from '@element-plus/icons-vue'; |
||||
import { useI18n } from 'vue-i18n'; |
||||
import { perm } from '@/store/useCurrentUser'; |
||||
import { pageSizes, pageLayout, toParams, resetParams } from '@/utils/common'; |
||||
import { delete{{pascalCase name}}, query{{pascalCase name}}Page } from '@/api/{{kebabCase path}}'; |
||||
import { ColumnList, ColumnSetting } from '@/components/TableList'; |
||||
import { QueryForm, QueryItem } from '@/components/QueryForm'; |
||||
import {{pascalCase name}}Form from './{{pascalCase name}}Form.vue'; |
||||
|
||||
const { t } = useI18n(); |
||||
const params = ref<any>({}); |
||||
const sort = ref<any>(); |
||||
const currentPage = ref<number>(1); |
||||
const pageSize = ref<number>(10); |
||||
const total = ref<number>(0); |
||||
const table = ref<any>(); |
||||
const data = ref<any[]>([]); |
||||
const selection = ref<any[]>([]); |
||||
const loading = ref<boolean>(false); |
||||
const formVisible = ref<boolean>(false); |
||||
const beanId = ref<number>(); |
||||
const beanIds = computed(() => data.value.map((row) => row.id)); |
||||
const fetchData = async () => { |
||||
loading.value = true; |
||||
try { |
||||
const { content, totalElements } = await query{{pascalCase name}}Page({ ...toParams(params.value), Q_OrderBy: sort.value, page: currentPage.value, pageSize: pageSize.value }); |
||||
data.value = content; |
||||
total.value = totalElements; |
||||
} finally { |
||||
loading.value = false; |
||||
} |
||||
}; |
||||
onMounted(fetchData); |
||||
|
||||
const handleSort = ({ column, prop, order }: { column: any; prop: string; order: string }) => { |
||||
if (prop) { |
||||
sort.value = (column.sortBy ?? prop) + (order === 'descending' ? '_desc' : ''); |
||||
} else { |
||||
sort.value = undefined; |
||||
} |
||||
fetchData(); |
||||
}; |
||||
const handleSearch = () => fetchData(); |
||||
const handleReset = () => { |
||||
table.value.clearSort(); |
||||
resetParams(params.value); |
||||
sort.value = undefined; |
||||
fetchData(); |
||||
}; |
||||
|
||||
const handleAdd = () => { |
||||
beanId.value = undefined; |
||||
formVisible.value = true; |
||||
}; |
||||
const handleEdit = (id: number) => { |
||||
beanId.value = id; |
||||
formVisible.value = true; |
||||
}; |
||||
const handleDelete = async (ids: number[]) => { |
||||
await delete{{pascalCase name}}(ids); |
||||
fetchData(); |
||||
ElMessage.success(t('success')); |
||||
}; |
||||
</script> |
@ -0,0 +1,45 @@ |
||||
// npm run plop user user page
|
||||
/* eslint-disable func-names */ |
||||
module.exports = function (plop) { |
||||
// controller generator
|
||||
plop.setGenerator('view', { |
||||
description: 'application views', |
||||
prompts: [ |
||||
{ |
||||
type: 'input', |
||||
name: 'path', |
||||
message: 'path:', |
||||
}, |
||||
{ |
||||
type: 'input', |
||||
name: 'name', |
||||
message: 'name:', |
||||
}, |
||||
{ |
||||
type: 'input', |
||||
name: 'type', |
||||
message: 'type:', |
||||
}, |
||||
], |
||||
actions: (data) => { |
||||
const actions = []; |
||||
actions.push({ |
||||
type: 'add', |
||||
path: 'src/views/{{kebabCase path}}/{{pascalCase name}}Form.vue', |
||||
templateFile: 'plop-templates/view_form.hbs', |
||||
}); |
||||
actions.push({ |
||||
type: 'add', |
||||
path: 'src/views/{{kebabCase path}}/{{pascalCase name}}List.vue', |
||||
templateFile: `plop-templates/view_${data.type}.hbs`, |
||||
}); |
||||
actions.push({ |
||||
type: 'append', |
||||
path: 'src/api/{{kebabCase path}}.ts', |
||||
templateFile: 'plop-templates/api.hbs', |
||||
data: { isList: data.type === 'list' }, |
||||
}); |
||||
return actions; |
||||
}, |
||||
}); |
||||
}; |
@ -0,0 +1,6 @@ |
||||
module.exports = { |
||||
plugins: { |
||||
tailwindcss: {}, |
||||
autoprefixer: {}, |
||||
}, |
||||
} |
@ -0,0 +1,462 @@ |
||||
tinymce.addI18n('zh_CN',{ |
||||
"Redo": "\u91cd\u505a", |
||||
"Undo": "\u64a4\u9500", |
||||
"Cut": "\u526a\u5207", |
||||
"Copy": "\u590d\u5236", |
||||
"Paste": "\u7c98\u8d34", |
||||
"Select all": "\u5168\u9009", |
||||
"New document": "\u65b0\u6587\u4ef6", |
||||
"Ok": "\u786e\u5b9a", |
||||
"Cancel": "\u53d6\u6d88", |
||||
"Visual aids": "\u7f51\u683c\u7ebf", |
||||
"Bold": "\u7c97\u4f53", |
||||
"Italic": "\u659c\u4f53", |
||||
"Underline": "\u4e0b\u5212\u7ebf", |
||||
"Strikethrough": "\u5220\u9664\u7ebf", |
||||
"Superscript": "\u4e0a\u6807", |
||||
"Subscript": "\u4e0b\u6807", |
||||
"Clear formatting": "\u6e05\u9664\u683c\u5f0f", |
||||
"Align left": "\u5de6\u8fb9\u5bf9\u9f50", |
||||
"Align center": "\u4e2d\u95f4\u5bf9\u9f50", |
||||
"Align right": "\u53f3\u8fb9\u5bf9\u9f50", |
||||
"Justify": "\u4e24\u7aef\u5bf9\u9f50", |
||||
"Bullet list": "\u9879\u76ee\u7b26\u53f7", |
||||
"Numbered list": "\u7f16\u53f7\u5217\u8868", |
||||
"Decrease indent": "\u51cf\u5c11\u7f29\u8fdb", |
||||
"Increase indent": "\u589e\u52a0\u7f29\u8fdb", |
||||
"Close": "\u5173\u95ed", |
||||
"Formats": "\u683c\u5f0f", |
||||
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u4f60\u7684\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u6253\u5f00\u526a\u8d34\u677f\uff0c\u8bf7\u4f7f\u7528Ctrl+X\/C\/V\u7b49\u5feb\u6377\u952e\u3002", |
||||
"Headers": "\u6807\u9898", |
||||
"Header 1": "\u6807\u98981", |
||||
"Header 2": "\u6807\u98982", |
||||
"Header 3": "\u6807\u98983", |
||||
"Header 4": "\u6807\u98984", |
||||
"Header 5": "\u6807\u98985", |
||||
"Header 6": "\u6807\u98986", |
||||
"Headings": "\u6807\u9898", |
||||
"Heading 1": "\u6807\u98981", |
||||
"Heading 2": "\u6807\u98982", |
||||
"Heading 3": "\u6807\u98983", |
||||
"Heading 4": "\u6807\u98984", |
||||
"Heading 5": "\u6807\u98985", |
||||
"Heading 6": "\u6807\u98986", |
||||
"Preformatted": "\u9884\u5148\u683c\u5f0f\u5316\u7684", |
||||
"Div": "Div", |
||||
"Pre": "Pre", |
||||
"Code": "\u4ee3\u7801", |
||||
"Paragraph": "\u6bb5\u843d", |
||||
"Blockquote": "\u5f15\u6587\u533a\u5757", |
||||
"Inline": "\u6587\u672c", |
||||
"Blocks": "\u57fa\u5757", |
||||
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u5f53\u524d\u4e3a\u7eaf\u6587\u672c\u7c98\u8d34\u6a21\u5f0f\uff0c\u518d\u6b21\u70b9\u51fb\u53ef\u4ee5\u56de\u5230\u666e\u901a\u7c98\u8d34\u6a21\u5f0f\u3002", |
||||
"Fonts": "\u5b57\u4f53", |
||||
"Font Sizes": "\u5b57\u53f7", |
||||
"Class": "\u7c7b\u578b", |
||||
"Browse for an image": "\u6d4f\u89c8\u56fe\u50cf", |
||||
"OR": "\u6216", |
||||
"Drop an image here": "\u62d6\u653e\u4e00\u5f20\u56fe\u50cf\u81f3\u6b64", |
||||
"Upload": "\u4e0a\u4f20", |
||||
"Block": "\u5757", |
||||
"Align": "\u5bf9\u9f50", |
||||
"Default": "\u9ed8\u8ba4", |
||||
"Circle": "\u7a7a\u5fc3\u5706", |
||||
"Disc": "\u5b9e\u5fc3\u5706", |
||||
"Square": "\u65b9\u5757", |
||||
"Lower Alpha": "\u5c0f\u5199\u82f1\u6587\u5b57\u6bcd", |
||||
"Lower Greek": "\u5c0f\u5199\u5e0c\u814a\u5b57\u6bcd", |
||||
"Lower Roman": "\u5c0f\u5199\u7f57\u9a6c\u5b57\u6bcd", |
||||
"Upper Alpha": "\u5927\u5199\u82f1\u6587\u5b57\u6bcd", |
||||
"Upper Roman": "\u5927\u5199\u7f57\u9a6c\u5b57\u6bcd", |
||||
"Anchor...": "\u951a\u70b9...", |
||||
"Name": "\u540d\u79f0", |
||||
"Id": "\u6807\u8bc6\u7b26", |
||||
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\u6807\u8bc6\u7b26\u5e94\u8be5\u4ee5\u5b57\u6bcd\u5f00\u5934\uff0c\u540e\u8ddf\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u7834\u6298\u53f7\u3001\u70b9\u3001\u5192\u53f7\u6216\u4e0b\u5212\u7ebf\u3002", |
||||
"You have unsaved changes are you sure you want to navigate away?": "\u4f60\u8fd8\u6709\u6587\u6863\u5c1a\u672a\u4fdd\u5b58\uff0c\u786e\u5b9a\u8981\u79bb\u5f00\uff1f", |
||||
"Restore last draft": "\u6062\u590d\u4e0a\u6b21\u7684\u8349\u7a3f", |
||||
"Special character...": "\u7279\u6b8a\u5b57\u7b26...", |
||||
"Source code": "\u6e90\u4ee3\u7801", |
||||
"Insert\/Edit code sample": "\u63d2\u5165\/\u7f16\u8f91\u4ee3\u7801\u793a\u4f8b", |
||||
"Language": "\u8bed\u8a00", |
||||
"Code sample...": "\u793a\u4f8b\u4ee3\u7801...", |
||||
"Color Picker": "\u9009\u8272\u5668", |
||||
"R": "R", |
||||
"G": "G", |
||||
"B": "B", |
||||
"Left to right": "\u4ece\u5de6\u5230\u53f3", |
||||
"Right to left": "\u4ece\u53f3\u5230\u5de6", |
||||
"Emoticons": "\u8868\u60c5", |
||||
"Emoticons...": "\u8868\u60c5\u7b26\u53f7...", |
||||
"Metadata and Document Properties": "\u5143\u6570\u636e\u548c\u6587\u6863\u5c5e\u6027", |
||||
"Title": "\u6807\u9898", |
||||
"Keywords": "\u5173\u952e\u8bcd", |
||||
"Description": "\u63cf\u8ff0", |
||||
"Robots": "\u673a\u5668\u4eba", |
||||
"Author": "\u4f5c\u8005", |
||||
"Encoding": "\u7f16\u7801", |
||||
"Fullscreen": "\u5168\u5c4f", |
||||
"Action": "\u64cd\u4f5c", |
||||
"Shortcut": "\u5feb\u6377\u952e", |
||||
"Help": "\u5e2e\u52a9", |
||||
"Address": "\u5730\u5740", |
||||
"Focus to menubar": "\u79fb\u52a8\u7126\u70b9\u5230\u83dc\u5355\u680f", |
||||
"Focus to toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u5de5\u5177\u680f", |
||||
"Focus to element path": "\u79fb\u52a8\u7126\u70b9\u5230\u5143\u7d20\u8def\u5f84", |
||||
"Focus to contextual toolbar": "\u79fb\u52a8\u7126\u70b9\u5230\u4e0a\u4e0b\u6587\u83dc\u5355", |
||||
"Insert link (if link plugin activated)": "\u63d2\u5165\u94fe\u63a5 (\u5982\u679c\u94fe\u63a5\u63d2\u4ef6\u5df2\u6fc0\u6d3b)", |
||||
"Save (if save plugin activated)": "\u4fdd\u5b58(\u5982\u679c\u4fdd\u5b58\u63d2\u4ef6\u5df2\u6fc0\u6d3b)", |
||||
"Find (if searchreplace plugin activated)": "\u67e5\u627e(\u5982\u679c\u67e5\u627e\u66ff\u6362\u63d2\u4ef6\u5df2\u6fc0\u6d3b)", |
||||
"Plugins installed ({0}):": "\u5df2\u5b89\u88c5\u63d2\u4ef6 ({0}):", |
||||
"Premium plugins:": "\u4f18\u79c0\u63d2\u4ef6\uff1a", |
||||
"Learn more...": "\u4e86\u89e3\u66f4\u591a...", |
||||
"You are using {0}": "\u4f60\u6b63\u5728\u4f7f\u7528 {0}", |
||||
"Plugins": "\u63d2\u4ef6", |
||||
"Handy Shortcuts": "\u5feb\u6377\u952e", |
||||
"Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf", |
||||
"Insert\/edit image": "\u63d2\u5165\/\u7f16\u8f91\u56fe\u7247", |
||||
"Alternative description": "\u66ff\u4ee3\u63cf\u8ff0", |
||||
"Accessibility": "\u8f85\u52a9\u529f\u80fd", |
||||
"Image is decorative": "\u56fe\u50cf\u662f\u88c5\u9970\u6027\u7684", |
||||
"Source": "\u5730\u5740", |
||||
"Dimensions": "\u5927\u5c0f", |
||||
"Constrain proportions": "\u4fdd\u6301\u7eb5\u6a2a\u6bd4", |
||||
"General": "\u666e\u901a", |
||||
"Advanced": "\u9ad8\u7ea7", |
||||
"Style": "\u6837\u5f0f", |
||||
"Vertical space": "\u5782\u76f4\u8fb9\u8ddd", |
||||
"Horizontal space": "\u6c34\u5e73\u8fb9\u8ddd", |
||||
"Border": "\u8fb9\u6846", |
||||
"Insert image": "\u63d2\u5165\u56fe\u7247", |
||||
"Image...": "\u56fe\u7247...", |
||||
"Image list": "\u56fe\u7247\u5217\u8868", |
||||
"Rotate counterclockwise": "\u9006\u65f6\u9488\u65cb\u8f6c", |
||||
"Rotate clockwise": "\u987a\u65f6\u9488\u65cb\u8f6c", |
||||
"Flip vertically": "\u5782\u76f4\u7ffb\u8f6c", |
||||
"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f6c", |
||||
"Edit image": "\u7f16\u8f91\u56fe\u7247", |
||||
"Image options": "\u56fe\u7247\u9009\u9879", |
||||
"Zoom in": "\u653e\u5927", |
||||
"Zoom out": "\u7f29\u5c0f", |
||||
"Crop": "\u88c1\u526a", |
||||
"Resize": "\u8c03\u6574\u5927\u5c0f", |
||||
"Orientation": "\u65b9\u5411", |
||||
"Brightness": "\u4eae\u5ea6", |
||||
"Sharpen": "\u9510\u5316", |
||||
"Contrast": "\u5bf9\u6bd4\u5ea6", |
||||
"Color levels": "\u989c\u8272\u5c42\u6b21", |
||||
"Gamma": "\u4f3d\u9a6c\u503c", |
||||
"Invert": "\u53cd\u8f6c", |
||||
"Apply": "\u5e94\u7528", |
||||
"Back": "\u540e\u9000", |
||||
"Insert date\/time": "\u63d2\u5165\u65e5\u671f\/\u65f6\u95f4", |
||||
"Date\/time": "\u65e5\u671f\/\u65f6\u95f4", |
||||
"Insert\/edit link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5", |
||||
"Text to display": "\u663e\u793a\u6587\u5b57", |
||||
"Url": "\u5730\u5740", |
||||
"Open link in...": "\u94fe\u63a5\u6253\u5f00\u4f4d\u7f6e...", |
||||
"Current window": "\u5f53\u524d\u7a97\u53e3", |
||||
"None": "\u65e0", |
||||
"New window": "\u5728\u65b0\u7a97\u53e3\u6253\u5f00", |
||||
"Open link": "\u6253\u5f00\u94fe\u63a5", |
||||
"Remove link": "\u5220\u9664\u94fe\u63a5", |
||||
"Anchors": "\u951a\u70b9", |
||||
"Link...": "\u94fe\u63a5...", |
||||
"Paste or type a link": "\u7c98\u8d34\u6216\u8f93\u5165\u94fe\u63a5", |
||||
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u4e3a\u90ae\u4ef6\u5730\u5740\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7f00\u5417\uff1f", |
||||
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u5c5e\u4e8e\u5916\u90e8\u94fe\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp:\/\/:\u524d\u7f00\u5417\uff1f", |
||||
"The URL you entered seems to be an external link. Do you want to add the required https:\/\/ prefix?": "\u60a8\u8f93\u5165\u7684 URL \u4f3c\u4e4e\u662f\u4e00\u4e2a\u5916\u90e8\u94fe\u63a5\u3002\u60a8\u60f3\u6dfb\u52a0\u6240\u9700\u7684 https:\/\/ \u524d\u7f00\u5417\uff1f", |
||||
"Link list": "\u94fe\u63a5\u5217\u8868", |
||||
"Insert video": "\u63d2\u5165\u89c6\u9891", |
||||
"Insert\/edit video": "\u63d2\u5165\/\u7f16\u8f91\u89c6\u9891", |
||||
"Insert\/edit media": "\u63d2\u5165\/\u7f16\u8f91\u5a92\u4f53", |
||||
"Alternative source": "\u955c\u50cf", |
||||
"Alternative source URL": "\u66ff\u4ee3\u6765\u6e90\u7f51\u5740", |
||||
"Media poster (Image URL)": "\u5c01\u9762(\u56fe\u7247\u5730\u5740)", |
||||
"Paste your embed code below:": "\u5c06\u5185\u5d4c\u4ee3\u7801\u7c98\u8d34\u5728\u4e0b\u9762:", |
||||
"Embed": "\u5185\u5d4c", |
||||
"Media...": "\u591a\u5a92\u4f53...", |
||||
"Nonbreaking space": "\u4e0d\u95f4\u65ad\u7a7a\u683c", |
||||
"Page break": "\u5206\u9875\u7b26", |
||||
"Paste as text": "\u7c98\u8d34\u4e3a\u6587\u672c", |
||||
"Preview": "\u9884\u89c8", |
||||
"Print...": "\u6253\u5370...", |
||||
"Save": "\u4fdd\u5b58", |
||||
"Find": "\u67e5\u627e", |
||||
"Replace with": "\u66ff\u6362\u4e3a", |
||||
"Replace": "\u66ff\u6362", |
||||
"Replace all": "\u5168\u90e8\u66ff\u6362", |
||||
"Previous": "\u4e0a\u4e00\u4e2a", |
||||
"Next": "\u4e0b\u4e00\u4e2a", |
||||
"Find and Replace": "\u67e5\u627e\u548c\u66ff\u6362", |
||||
"Find and replace...": "\u67e5\u627e\u5e76\u66ff\u6362...", |
||||
"Could not find the specified string.": "\u672a\u627e\u5230\u641c\u7d22\u5185\u5bb9.", |
||||
"Match case": "\u533a\u5206\u5927\u5c0f\u5199", |
||||
"Find whole words only": "\u5168\u5b57\u5339\u914d", |
||||
"Find in selection": "\u5728\u9009\u533a\u4e2d\u67e5\u627e", |
||||
"Spellcheck": "\u62fc\u5199\u68c0\u67e5", |
||||
"Spellcheck Language": "\u62fc\u5199\u68c0\u67e5\u8bed\u8a00", |
||||
"No misspellings found.": "\u6ca1\u6709\u53d1\u73b0\u62fc\u5199\u9519\u8bef", |
||||
"Ignore": "\u5ffd\u7565", |
||||
"Ignore all": "\u5168\u90e8\u5ffd\u7565", |
||||
"Finish": "\u5b8c\u6210", |
||||
"Add to Dictionary": "\u6dfb\u52a0\u5230\u5b57\u5178", |
||||
"Insert table": "\u63d2\u5165\u8868\u683c", |
||||
"Table properties": "\u8868\u683c\u5c5e\u6027", |
||||
"Delete table": "\u5220\u9664\u8868\u683c", |
||||
"Cell": "\u5355\u5143\u683c", |
||||
"Row": "\u884c", |
||||
"Column": "\u5217", |
||||
"Cell properties": "\u5355\u5143\u683c\u5c5e\u6027", |
||||
"Merge cells": "\u5408\u5e76\u5355\u5143\u683c", |
||||
"Split cell": "\u62c6\u5206\u5355\u5143\u683c", |
||||
"Insert row before": "\u5728\u4e0a\u65b9\u63d2\u5165", |
||||
"Insert row after": "\u5728\u4e0b\u65b9\u63d2\u5165", |
||||
"Delete row": "\u5220\u9664\u884c", |
||||
"Row properties": "\u884c\u5c5e\u6027", |
||||
"Cut row": "\u526a\u5207\u884c", |
||||
"Copy row": "\u590d\u5236\u884c", |
||||
"Paste row before": "\u7c98\u8d34\u5230\u4e0a\u65b9", |
||||
"Paste row after": "\u7c98\u8d34\u5230\u4e0b\u65b9", |
||||
"Insert column before": "\u5728\u5de6\u4fa7\u63d2\u5165", |
||||
"Insert column after": "\u5728\u53f3\u4fa7\u63d2\u5165", |
||||
"Delete column": "\u5220\u9664\u5217", |
||||
"Cols": "\u5217", |
||||
"Rows": "\u884c", |
||||
"Width": "\u5bbd", |
||||
"Height": "\u9ad8", |
||||
"Cell spacing": "\u5355\u5143\u683c\u5916\u95f4\u8ddd", |
||||
"Cell padding": "\u5355\u5143\u683c\u5185\u8fb9\u8ddd", |
||||
"Caption": "\u6807\u9898", |
||||
"Show caption": "\u663e\u793a\u6807\u9898", |
||||
"Left": "\u5de6\u5bf9\u9f50", |
||||
"Center": "\u5c45\u4e2d", |
||||
"Right": "\u53f3\u5bf9\u9f50", |
||||
"Cell type": "\u5355\u5143\u683c\u7c7b\u578b", |
||||
"Scope": "\u8303\u56f4", |
||||
"Alignment": "\u5bf9\u9f50\u65b9\u5f0f", |
||||
"H Align": "\u6c34\u5e73\u5bf9\u9f50", |
||||
"V Align": "\u5782\u76f4\u5bf9\u9f50", |
||||
"Top": "\u9876\u90e8\u5bf9\u9f50", |
||||
"Middle": "\u5782\u76f4\u5c45\u4e2d", |
||||
"Bottom": "\u5e95\u90e8\u5bf9\u9f50", |
||||
"Header cell": "\u8868\u5934\u5355\u5143\u683c", |
||||
"Row group": "\u884c\u7ec4", |
||||
"Column group": "\u5217\u7ec4", |
||||
"Row type": "\u884c\u7c7b\u578b", |
||||
"Header": "\u8868\u5934", |
||||
"Body": "\u8868\u4f53", |
||||
"Footer": "\u8868\u5c3e", |
||||
"Border color": "\u8fb9\u6846\u989c\u8272", |
||||
"Insert template...": "\u63d2\u5165\u6a21\u677f...", |
||||
"Templates": "\u6a21\u677f", |
||||
"Template": "\u6a21\u677f", |
||||
"Text color": "\u6587\u5b57\u989c\u8272", |
||||
"Background color": "\u80cc\u666f\u8272", |
||||
"Custom...": "\u81ea\u5b9a\u4e49...", |
||||
"Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272", |
||||
"No color": "\u65e0", |
||||
"Remove color": "\u79fb\u9664\u989c\u8272", |
||||
"Table of Contents": "\u5185\u5bb9\u5217\u8868", |
||||
"Show blocks": "\u663e\u793a\u533a\u5757\u8fb9\u6846", |
||||
"Show invisible characters": "\u663e\u793a\u4e0d\u53ef\u89c1\u5b57\u7b26", |
||||
"Word count": "\u5b57\u6570", |
||||
"Count": "\u8ba1\u6570", |
||||
"Document": "\u6587\u6863", |
||||
"Selection": "\u9009\u62e9", |
||||
"Words": "\u5355\u8bcd", |
||||
"Words: {0}": "\u5b57\u6570\uff1a{0}", |
||||
"{0} words": "{0} \u5b57", |
||||
"File": "\u6587\u4ef6", |
||||
"Edit": "\u7f16\u8f91", |
||||
"Insert": "\u63d2\u5165", |
||||
"View": "\u89c6\u56fe", |
||||
"Format": "\u683c\u5f0f", |
||||
"Table": "\u8868\u683c", |
||||
"Tools": "\u5de5\u5177", |
||||
"Powered by {0}": "\u7531{0}\u9a71\u52a8", |
||||
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u5728\u7f16\u8f91\u533a\u6309ALT-F9\u6253\u5f00\u83dc\u5355\uff0c\u6309ALT-F10\u6253\u5f00\u5de5\u5177\u680f\uff0c\u6309ALT-0\u67e5\u770b\u5e2e\u52a9", |
||||
"Image title": "\u56fe\u7247\u6807\u9898", |
||||
"Border width": "\u8fb9\u6846\u5bbd\u5ea6", |
||||
"Border style": "\u8fb9\u6846\u6837\u5f0f", |
||||
"Error": "\u9519\u8bef", |
||||
"Warn": "\u8b66\u544a", |
||||
"Valid": "\u6709\u6548", |
||||
"To open the popup, press Shift+Enter": "\u6309Shitf+Enter\u952e\u6253\u5f00\u5bf9\u8bdd\u6846", |
||||
"Rich Text Area. Press ALT-0 for help.": "\u7f16\u8f91\u533a\u3002\u6309Alt+0\u952e\u6253\u5f00\u5e2e\u52a9\u3002", |
||||
"System Font": "\u7cfb\u7edf\u5b57\u4f53", |
||||
"Failed to upload image: {0}": "\u56fe\u7247\u4e0a\u4f20\u5931\u8d25: {0}", |
||||
"Failed to load plugin: {0} from url {1}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25: {0} \u6765\u81ea\u94fe\u63a5 {1}", |
||||
"Failed to load plugin url: {0}": "\u63d2\u4ef6\u52a0\u8f7d\u5931\u8d25 \u94fe\u63a5: {0}", |
||||
"Failed to initialize plugin: {0}": "\u63d2\u4ef6\u521d\u59cb\u5316\u5931\u8d25: {0}", |
||||
"example": "\u793a\u4f8b", |
||||
"Search": "\u641c\u7d22", |
||||
"All": "\u5168\u90e8", |
||||
"Currency": "\u8d27\u5e01", |
||||
"Text": "\u6587\u5b57", |
||||
"Quotations": "\u5f15\u7528", |
||||
"Mathematical": "\u6570\u5b66", |
||||
"Extended Latin": "\u62c9\u4e01\u8bed\u6269\u5145", |
||||
"Symbols": "\u7b26\u53f7", |
||||
"Arrows": "\u7bad\u5934", |
||||
"User Defined": "\u81ea\u5b9a\u4e49", |
||||
"dollar sign": "\u7f8e\u5143\u7b26\u53f7", |
||||
"currency sign": "\u8d27\u5e01\u7b26\u53f7", |
||||
"euro-currency sign": "\u6b27\u5143\u7b26\u53f7", |
||||
"colon sign": "\u5192\u53f7", |
||||
"cruzeiro sign": "\u514b\u9c81\u8d5b\u7f57\u5e01\u7b26\u53f7", |
||||
"french franc sign": "\u6cd5\u90ce\u7b26\u53f7", |
||||
"lira sign": "\u91cc\u62c9\u7b26\u53f7", |
||||
"mill sign": "\u5bc6\u5c14\u7b26\u53f7", |
||||
"naira sign": "\u5948\u62c9\u7b26\u53f7", |
||||
"peseta sign": "\u6bd4\u585e\u5854\u7b26\u53f7", |
||||
"rupee sign": "\u5362\u6bd4\u7b26\u53f7", |
||||
"won sign": "\u97e9\u5143\u7b26\u53f7", |
||||
"new sheqel sign": "\u65b0\u8c22\u514b\u5c14\u7b26\u53f7", |
||||
"dong sign": "\u8d8a\u5357\u76fe\u7b26\u53f7", |
||||
"kip sign": "\u8001\u631d\u57fa\u666e\u7b26\u53f7", |
||||
"tugrik sign": "\u56fe\u683c\u91cc\u514b\u7b26\u53f7", |
||||
"drachma sign": "\u5fb7\u62c9\u514b\u9a6c\u7b26\u53f7", |
||||
"german penny symbol": "\u5fb7\u56fd\u4fbf\u58eb\u7b26\u53f7", |
||||
"peso sign": "\u6bd4\u7d22\u7b26\u53f7", |
||||
"guarani sign": "\u74dc\u62c9\u5c3c\u7b26\u53f7", |
||||
"austral sign": "\u6fb3\u5143\u7b26\u53f7", |
||||
"hryvnia sign": "\u683c\u91cc\u592b\u5c3c\u4e9a\u7b26\u53f7", |
||||
"cedi sign": "\u585e\u5730\u7b26\u53f7", |
||||
"livre tournois sign": "\u91cc\u5f17\u5f17\u5c14\u7b26\u53f7", |
||||
"spesmilo sign": "spesmilo\u7b26\u53f7", |
||||
"tenge sign": "\u575a\u6208\u7b26\u53f7", |
||||
"indian rupee sign": "\u5370\u5ea6\u5362\u6bd4", |
||||
"turkish lira sign": "\u571f\u8033\u5176\u91cc\u62c9", |
||||
"nordic mark sign": "\u5317\u6b27\u9a6c\u514b", |
||||
"manat sign": "\u9a6c\u7eb3\u7279\u7b26\u53f7", |
||||
"ruble sign": "\u5362\u5e03\u7b26\u53f7", |
||||
"yen character": "\u65e5\u5143\u5b57\u6837", |
||||
"yuan character": "\u4eba\u6c11\u5e01\u5143\u5b57\u6837", |
||||
"yuan character, in hong kong and taiwan": "\u5143\u5b57\u6837\uff08\u6e2f\u53f0\u5730\u533a\uff09", |
||||
"yen\/yuan character variant one": "\u5143\u5b57\u6837\uff08\u5927\u5199\uff09", |
||||
"Loading emoticons...": "\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7...", |
||||
"Could not load emoticons": "\u4e0d\u80fd\u52a0\u8f7d\u8868\u60c5\u7b26\u53f7", |
||||
"People": "\u4eba\u7c7b", |
||||
"Animals and Nature": "\u52a8\u7269\u548c\u81ea\u7136", |
||||
"Food and Drink": "\u98df\u7269\u548c\u996e\u54c1", |
||||
"Activity": "\u6d3b\u52a8", |
||||
"Travel and Places": "\u65c5\u6e38\u548c\u5730\u70b9", |
||||
"Objects": "\u7269\u4ef6", |
||||
"Flags": "\u65d7\u5e1c", |
||||
"Characters": "\u5b57\u7b26", |
||||
"Characters (no spaces)": "\u5b57\u7b26(\u65e0\u7a7a\u683c)", |
||||
"{0} characters": "{0} \u4e2a\u5b57\u7b26", |
||||
"Error: Form submit field collision.": "\u9519\u8bef: \u8868\u5355\u63d0\u4ea4\u5b57\u6bb5\u51b2\u7a81\u3002", |
||||
"Error: No form element found.": "\u9519\u8bef: \u6ca1\u6709\u8868\u5355\u63a7\u4ef6\u3002", |
||||
"Update": "\u66f4\u65b0", |
||||
"Color swatch": "\u989c\u8272\u6837\u672c", |
||||
"Turquoise": "\u9752\u7eff\u8272", |
||||
"Green": "\u7eff\u8272", |
||||
"Blue": "\u84dd\u8272", |
||||
"Purple": "\u7d2b\u8272", |
||||
"Navy Blue": "\u6d77\u519b\u84dd", |
||||
"Dark Turquoise": "\u6df1\u84dd\u7eff\u8272", |
||||
"Dark Green": "\u6df1\u7eff\u8272", |
||||
"Medium Blue": "\u4e2d\u84dd\u8272", |
||||
"Medium Purple": "\u4e2d\u7d2b\u8272", |
||||
"Midnight Blue": "\u6df1\u84dd\u8272", |
||||
"Yellow": "\u9ec4\u8272", |
||||
"Orange": "\u6a59\u8272", |
||||
"Red": "\u7ea2\u8272", |
||||
"Light Gray": "\u6d45\u7070\u8272", |
||||
"Gray": "\u7070\u8272", |
||||
"Dark Yellow": "\u6697\u9ec4\u8272", |
||||
"Dark Orange": "\u6df1\u6a59\u8272", |
||||
"Dark Red": "\u6df1\u7ea2\u8272", |
||||
"Medium Gray": "\u4e2d\u7070\u8272", |
||||
"Dark Gray": "\u6df1\u7070\u8272", |
||||
"Light Green": "\u6d45\u7eff\u8272", |
||||
"Light Yellow": "\u6d45\u9ec4\u8272", |
||||
"Light Red": "\u6d45\u7ea2\u8272", |
||||
"Light Purple": "\u6d45\u7d2b\u8272", |
||||
"Light Blue": "\u6d45\u84dd\u8272", |
||||
"Dark Purple": "\u6df1\u7d2b\u8272", |
||||
"Dark Blue": "\u6df1\u84dd\u8272", |
||||
"Black": "\u9ed1\u8272", |
||||
"White": "\u767d\u8272", |
||||
"Switch to or from fullscreen mode": "\u5207\u6362\u5168\u5c4f\u6a21\u5f0f", |
||||
"Open help dialog": "\u6253\u5f00\u5e2e\u52a9\u5bf9\u8bdd\u6846", |
||||
"history": "\u5386\u53f2", |
||||
"styles": "\u6837\u5f0f", |
||||
"formatting": "\u683c\u5f0f\u5316", |
||||
"alignment": "\u5bf9\u9f50", |
||||
"indentation": "\u7f29\u8fdb", |
||||
"Font": "\u5b57\u4f53", |
||||
"Size": "\u5b57\u53f7", |
||||
"More...": "\u66f4\u591a...", |
||||
"Select...": "\u9009\u62e9...", |
||||
"Preferences": "\u9996\u9009\u9879", |
||||
"Yes": "\u662f", |
||||
"No": "\u5426", |
||||
"Keyboard Navigation": "\u952e\u76d8\u6307\u5f15", |
||||
"Version": "\u7248\u672c", |
||||
"Code view": "\u4ee3\u7801\u89c6\u56fe", |
||||
"Open popup menu for split buttons": "\u6253\u5f00\u5f39\u51fa\u5f0f\u83dc\u5355\uff0c\u7528\u4e8e\u62c6\u5206\u6309\u94ae", |
||||
"List Properties": "\u5217\u8868\u5c5e\u6027", |
||||
"List properties...": "\u6807\u9898\u5b57\u4f53\u5c5e\u6027", |
||||
"Start list at number": "\u4ee5\u6570\u5b57\u5f00\u59cb\u5217\u8868", |
||||
"Line height": "\u884c\u9ad8", |
||||
"comments": "\u5907\u6ce8", |
||||
"Format Painter": "\u683c\u5f0f\u5237", |
||||
"Insert\/edit iframe": "\u63d2\u5165\/\u7f16\u8f91\u6846\u67b6", |
||||
"Capitalization": "\u5927\u5199", |
||||
"lowercase": "\u5c0f\u5199", |
||||
"UPPERCASE": "\u5927\u5199", |
||||
"Title Case": "\u9996\u5b57\u6bcd\u5927\u5199", |
||||
"permanent pen": "\u8bb0\u53f7\u7b14", |
||||
"Permanent Pen Properties": "\u6c38\u4e45\u7b14\u5c5e\u6027", |
||||
"Permanent pen properties...": "\u6c38\u4e45\u7b14\u5c5e\u6027...", |
||||
"case change": "\u6848\u4f8b\u66f4\u6539", |
||||
"page embed": "\u9875\u9762\u5d4c\u5165", |
||||
"Advanced sort...": "\u9ad8\u7ea7\u6392\u5e8f...", |
||||
"Advanced Sort": "\u9ad8\u7ea7\u6392\u5e8f", |
||||
"Sort table by column ascending": "\u6309\u5217\u5347\u5e8f\u8868", |
||||
"Sort table by column descending": "\u6309\u5217\u964d\u5e8f\u8868", |
||||
"Sort": "\u6392\u5e8f", |
||||
"Order": "\u6392\u5e8f", |
||||
"Sort by": "\u6392\u5e8f\u65b9\u5f0f", |
||||
"Ascending": "\u5347\u5e8f", |
||||
"Descending": "\u964d\u5e8f", |
||||
"Column {0}": "\u5217{0}", |
||||
"Row {0}": "\u884c{0}", |
||||
"Spellcheck...": "\u62fc\u5199\u68c0\u67e5...", |
||||
"Misspelled word": "\u62fc\u5199\u9519\u8bef\u7684\u5355\u8bcd", |
||||
"Suggestions": "\u5efa\u8bae", |
||||
"Change": "\u66f4\u6539", |
||||
"Finding word suggestions": "\u67e5\u627e\u5355\u8bcd\u5efa\u8bae", |
||||
"Success": "\u6210\u529f", |
||||
"Repair": "\u4fee\u590d", |
||||
"Issue {0} of {1}": "\u5171\u8ba1{1}\u95ee\u9898{0}", |
||||
"Images must be marked as decorative or have an alternative text description": "\u56fe\u50cf\u5fc5\u987b\u6807\u8bb0\u4e3a\u88c5\u9970\u6027\u6216\u5177\u6709\u66ff\u4ee3\u6587\u672c\u63cf\u8ff0", |
||||
"Images must have an alternative text description. Decorative images are not allowed.": "\u56fe\u50cf\u5fc5\u987b\u5177\u6709\u66ff\u4ee3\u6587\u672c\u63cf\u8ff0\u3002\u4e0d\u5141\u8bb8\u4f7f\u7528\u88c5\u9970\u56fe\u50cf\u3002", |
||||
"Or provide alternative text:": "\u6216\u63d0\u4f9b\u5907\u9009\u6587\u672c\uff1a", |
||||
"Make image decorative:": "\u4f7f\u56fe\u50cf\u88c5\u9970\uff1a", |
||||
"ID attribute must be unique": "ID \u5c5e\u6027\u5fc5\u987b\u662f\u552f\u4e00\u7684", |
||||
"Make ID unique": "\u4f7f ID \u72ec\u4e00\u65e0\u4e8c", |
||||
"Keep this ID and remove all others": "\u4fdd\u7559\u6b64 ID \u5e76\u5220\u9664\u6240\u6709\u5176\u4ed6", |
||||
"Remove this ID": "\u5220\u9664\u6b64 ID", |
||||
"Remove all IDs": "\u6e05\u9664\u5168\u90e8IDs", |
||||
"Checklist": "\u6e05\u5355", |
||||
"Anchor": "\u951a\u70b9", |
||||
"Special character": "\u7279\u6b8a\u7b26\u53f7", |
||||
"Code sample": "\u4ee3\u7801\u793a\u4f8b", |
||||
"Color": "\u989c\u8272", |
||||
"Document properties": "\u6587\u6863\u5c5e\u6027", |
||||
"Image description": "\u56fe\u7247\u63cf\u8ff0", |
||||
"Image": "\u56fe\u7247", |
||||
"Insert link": "\u63d2\u5165\u94fe\u63a5", |
||||
"Target": "\u6253\u5f00\u65b9\u5f0f", |
||||
"Link": "\u94fe\u63a5", |
||||
"Poster": "\u5c01\u9762", |
||||
"Media": "\u5a92\u4f53", |
||||
"Print": "\u6253\u5370", |
||||
"Prev": "\u4e0a\u4e00\u4e2a", |
||||
"Find and replace": "\u67e5\u627e\u548c\u66ff\u6362", |
||||
"Whole words": "\u5168\u5b57\u5339\u914d", |
||||
"Insert template": "\u63d2\u5165\u6a21\u677f" |
||||
}); |
@ -0,0 +1,419 @@ |
||||
tinymce.addI18n('zh_TW',{ |
||||
"Redo": "\u91cd\u505a", |
||||
"Undo": "\u64a4\u92b7", |
||||
"Cut": "\u526a\u4e0b", |
||||
"Copy": "\u8907\u88fd", |
||||
"Paste": "\u8cbc\u4e0a", |
||||
"Select all": "\u5168\u9078", |
||||
"New document": "\u65b0\u6587\u4ef6", |
||||
"Ok": "\u78ba\u5b9a", |
||||
"Cancel": "\u53d6\u6d88", |
||||
"Visual aids": "\u5c0f\u5e6b\u624b", |
||||
"Bold": "\u7c97\u9ad4", |
||||
"Italic": "\u659c\u9ad4", |
||||
"Underline": "\u4e0b\u5283\u7dda", |
||||
"Strikethrough": "\u522a\u9664\u7dda", |
||||
"Superscript": "\u4e0a\u6a19", |
||||
"Subscript": "\u4e0b\u6a19", |
||||
"Clear formatting": "\u6e05\u9664\u683c\u5f0f", |
||||
"Align left": "\u5de6\u908a\u5c0d\u9f4a", |
||||
"Align center": "\u4e2d\u9593\u5c0d\u9f4a", |
||||
"Align right": "\u53f3\u908a\u5c0d\u9f4a", |
||||
"Justify": "\u5de6\u53f3\u5c0d\u9f4a", |
||||
"Bullet list": "\u9805\u76ee\u6e05\u55ae", |
||||
"Numbered list": "\u6578\u5b57\u6e05\u55ae", |
||||
"Decrease indent": "\u6e1b\u5c11\u7e2e\u6392", |
||||
"Increase indent": "\u589e\u52a0\u7e2e\u6392", |
||||
"Close": "\u95dc\u9589", |
||||
"Formats": "\u683c\u5f0f", |
||||
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u60a8\u7684\u700f\u89bd\u5668\u4e0d\u652f\u63f4\u5b58\u53d6\u526a\u8cbc\u7c3f\uff0c\u53ef\u4ee5\u4f7f\u7528\u5feb\u901f\u9375 Ctrl + X\/C\/V \u4ee3\u66ff\u526a\u4e0b\u3001\u8907\u88fd\u8207\u8cbc\u4e0a\u3002", |
||||
"Headers": "\u6a19\u984c", |
||||
"Header 1": "\u6a19\u984c 1", |
||||
"Header 2": "\u6a19\u984c 2", |
||||
"Header 3": "\u6a19\u984c 3", |
||||
"Header 4": "\u6a19\u984c 4", |
||||
"Header 5": "\u6a19\u984c 5", |
||||
"Header 6": "\u6a19\u984c 6", |
||||
"Headings": "\u6a19\u984c", |
||||
"Heading 1": "\u6a19\u984c1", |
||||
"Heading 2": "\u6a19\u984c2", |
||||
"Heading 3": "\u6a19\u984c3", |
||||
"Heading 4": "\u6a19\u984c4", |
||||
"Heading 5": "\u6a19\u984c5", |
||||
"Heading 6": "\u6a19\u984c6", |
||||
"Preformatted": "\u9810\u5148\u683c\u5f0f\u5316\u7684", |
||||
"Div": "Div", |
||||
"Pre": "Pre", |
||||
"Code": "\u4ee3\u78bc", |
||||
"Paragraph": "\u6bb5\u843d", |
||||
"Blockquote": "\u5f15\u6587\u5340\u584a", |
||||
"Inline": "\u5167\u806f", |
||||
"Blocks": "\u57fa\u584a", |
||||
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u76ee\u524d\u5c07\u4ee5\u7d14\u6587\u5b57\u7684\u6a21\u5f0f\u8cbc\u4e0a\uff0c\u60a8\u53ef\u4ee5\u518d\u9ede\u9078\u4e00\u6b21\u53d6\u6d88\u3002", |
||||
"Fonts": "\u5b57\u578b", |
||||
"Font Sizes": "\u5b57\u578b\u5927\u5c0f", |
||||
"Class": "\u985e\u578b", |
||||
"Browse for an image": "\u5f9e\u5716\u7247\u4e2d\u700f\u89bd", |
||||
"OR": "\u6216", |
||||
"Drop an image here": "\u62d6\u66f3\u5716\u7247\u81f3\u6b64", |
||||
"Upload": "\u4e0a\u50b3", |
||||
"Block": "\u5340\u584a", |
||||
"Align": "\u5c0d\u9f4a", |
||||
"Default": "\u9810\u8a2d", |
||||
"Circle": "\u7a7a\u5fc3\u5713", |
||||
"Disc": "\u5be6\u5fc3\u5713", |
||||
"Square": "\u6b63\u65b9\u5f62", |
||||
"Lower Alpha": "\u5c0f\u5beb\u82f1\u6587\u5b57\u6bcd", |
||||
"Lower Greek": "\u5e0c\u81d8\u5b57\u6bcd", |
||||
"Lower Roman": "\u5c0f\u5beb\u7f85\u99ac\u6578\u5b57", |
||||
"Upper Alpha": "\u5927\u5beb\u82f1\u6587\u5b57\u6bcd", |
||||
"Upper Roman": "\u5927\u5beb\u7f85\u99ac\u6578\u5b57", |
||||
"Anchor...": "\u9328\u9ede...", |
||||
"Name": "\u540d\u7a31", |
||||
"Id": "Id", |
||||
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "Id\u61c9\u4ee5\u5b57\u6bcd\u958b\u982d\uff0c\u5f8c\u9762\u63a5\u8457\u5b57\u6bcd\uff0c\u6578\u5b57\uff0c\u7834\u6298\u865f\uff0c\u9ede\u6578\uff0c\u5192\u865f\u6216\u4e0b\u5283\u7dda\u3002", |
||||
"You have unsaved changes are you sure you want to navigate away?": "\u7de8\u8f2f\u5c1a\u672a\u88ab\u5132\u5b58\uff0c\u4f60\u78ba\u5b9a\u8981\u96e2\u958b\uff1f", |
||||
"Restore last draft": "\u8f09\u5165\u4e0a\u4e00\u6b21\u7de8\u8f2f\u7684\u8349\u7a3f", |
||||
"Special character...": "\u7279\u6b8a\u5b57\u5143......", |
||||
"Source code": "\u539f\u59cb\u78bc", |
||||
"Insert\/Edit code sample": "\u63d2\u5165\/\u7de8\u8f2f \u7a0b\u5f0f\u78bc\u7bc4\u4f8b", |
||||
"Language": "\u8a9e\u8a00", |
||||
"Code sample...": "\u7a0b\u5f0f\u78bc\u7bc4\u4f8b...", |
||||
"Color Picker": "\u9078\u8272\u5668", |
||||
"R": "\u7d05", |
||||
"G": "\u7da0", |
||||
"B": "\u85cd", |
||||
"Left to right": "\u5f9e\u5de6\u5230\u53f3", |
||||
"Right to left": "\u5f9e\u53f3\u5230\u5de6", |
||||
"Emoticons...": "\u8868\u60c5\u7b26\u865f\u2026", |
||||
"Metadata and Document Properties": "\u5f8c\u8a2d\u8cc7\u6599\u8207\u6587\u4ef6\u5c6c\u6027", |
||||
"Title": "\u6a19\u984c", |
||||
"Keywords": "\u95dc\u9375\u5b57", |
||||
"Description": "\u63cf\u8ff0", |
||||
"Robots": "\u6a5f\u5668\u4eba", |
||||
"Author": "\u4f5c\u8005", |
||||
"Encoding": "\u7de8\u78bc", |
||||
"Fullscreen": "\u5168\u87a2\u5e55", |
||||
"Action": "\u52d5\u4f5c", |
||||
"Shortcut": "\u5feb\u901f\u9375", |
||||
"Help": "\u5e6b\u52a9", |
||||
"Address": "\u5730\u5740", |
||||
"Focus to menubar": "\u8df3\u81f3\u9078\u55ae\u5217", |
||||
"Focus to toolbar": "\u8df3\u81f3\u5de5\u5177\u5217", |
||||
"Focus to element path": "\u8df3\u81f3HTML\u5143\u7d20\u5217", |
||||
"Focus to contextual toolbar": "\u8df3\u81f3\u5feb\u6377\u9078\u55ae", |
||||
"Insert link (if link plugin activated)": "\u65b0\u589e\u6377\u5f91 (\u6377\u5f91\u5916\u639b\u555f\u7528\u6642)", |
||||
"Save (if save plugin activated)": "\u5132\u5b58 (\u5132\u5b58\u5916\u639b\u555f\u7528\u6642)", |
||||
"Find (if searchreplace plugin activated)": "\u5c0b\u627e (\u5c0b\u627e\u53d6\u4ee3\u5916\u639b\u555f\u7528\u6642)", |
||||
"Plugins installed ({0}):": "({0}) \u500b\u5916\u639b\u5df2\u5b89\u88dd\uff1a", |
||||
"Premium plugins:": "\u52a0\u503c\u5916\u639b\uff1a", |
||||
"Learn more...": "\u4e86\u89e3\u66f4\u591a...", |
||||
"You are using {0}": "\u60a8\u6b63\u5728\u4f7f\u7528 {0}", |
||||
"Plugins": "\u5916\u639b", |
||||
"Handy Shortcuts": "\u5feb\u901f\u9375", |
||||
"Horizontal line": "\u6c34\u5e73\u7dda", |
||||
"Insert\/edit image": "\u63d2\u5165\/\u7de8\u8f2f \u5716\u7247", |
||||
"Image description": "\u5716\u7247\u63cf\u8ff0", |
||||
"Source": "\u5716\u7247\u7db2\u5740", |
||||
"Dimensions": "\u5c3a\u5bf8", |
||||
"Constrain proportions": "\u7b49\u6bd4\u4f8b\u7e2e\u653e", |
||||
"General": "\u4e00\u822c", |
||||
"Advanced": "\u9032\u968e", |
||||
"Style": "\u6a23\u5f0f", |
||||
"Vertical space": "\u9ad8\u5ea6", |
||||
"Horizontal space": "\u5bec\u5ea6", |
||||
"Border": "\u908a\u6846", |
||||
"Insert image": "\u63d2\u5165\u5716\u7247", |
||||
"Image...": "\u5716\u7247......", |
||||
"Image list": "\u5716\u7247\u6e05\u55ae", |
||||
"Rotate counterclockwise": "\u9006\u6642\u91dd\u65cb\u8f49", |
||||
"Rotate clockwise": "\u9806\u6642\u91dd\u65cb\u8f49", |
||||
"Flip vertically": "\u5782\u76f4\u7ffb\u8f49", |
||||
"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f49", |
||||
"Edit image": "\u7de8\u8f2f\u5716\u7247", |
||||
"Image options": "\u5716\u7247\u9078\u9805", |
||||
"Zoom in": "\u653e\u5927", |
||||
"Zoom out": "\u7e2e\u5c0f", |
||||
"Crop": "\u88c1\u526a", |
||||
"Resize": "\u8abf\u6574\u5927\u5c0f", |
||||
"Orientation": "\u65b9\u5411", |
||||
"Brightness": "\u4eae\u5ea6", |
||||
"Sharpen": "\u92b3\u5316", |
||||
"Contrast": "\u5c0d\u6bd4", |
||||
"Color levels": "\u984f\u8272\u5c64\u6b21", |
||||
"Gamma": "\u4f3d\u99ac\u503c", |
||||
"Invert": "\u53cd\u8f49", |
||||
"Apply": "\u61c9\u7528", |
||||
"Back": "\u5f8c\u9000", |
||||
"Insert date\/time": "\u63d2\u5165 \u65e5\u671f\/\u6642\u9593", |
||||
"Date\/time": "\u65e5\u671f\/\u6642\u9593", |
||||
"Insert\/Edit Link": "\u63d2\u5165\/\u7de8\u8f2f\u9023\u7d50", |
||||
"Insert\/edit link": "\u63d2\u5165\/\u7de8\u8f2f\u9023\u7d50", |
||||
"Text to display": "\u986f\u793a\u6587\u5b57", |
||||
"Url": "\u7db2\u5740", |
||||
"Open link in...": "\u958b\u555f\u9023\u7d50\u65bc...", |
||||
"Current window": "\u76ee\u524d\u8996\u7a97", |
||||
"None": "\u7121", |
||||
"New window": "\u53e6\u958b\u8996\u7a97", |
||||
"Remove link": "\u79fb\u9664\u9023\u7d50", |
||||
"Anchors": "\u52a0\u5165\u9328\u9ede", |
||||
"Link...": "\u9023\u7d50...", |
||||
"Paste or type a link": "\u8cbc\u4e0a\u6216\u8f38\u5165\u9023\u7d50", |
||||
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5beb\u7684URL\u70ba\u96fb\u5b50\u90f5\u4ef6\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7db4\u55ce\uff1f", |
||||
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u4f60\u6240\u586b\u5beb\u7684URL\u5c6c\u65bc\u5916\u90e8\u93c8\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp:\/\/:\u524d\u7db4\u55ce\uff1f", |
||||
"Link list": "\u9023\u7d50\u6e05\u55ae", |
||||
"Insert video": "\u63d2\u5165\u5f71\u97f3", |
||||
"Insert\/edit video": "\u63d2\u4ef6\/\u7de8\u8f2f \u5f71\u97f3", |
||||
"Insert\/edit media": "\u63d2\u5165\/\u7de8\u8f2f \u5a92\u9ad4", |
||||
"Alternative source": "\u66ff\u4ee3\u5f71\u97f3", |
||||
"Alternative source URL": "\u66ff\u4ee3\u4f86\u6e90URL", |
||||
"Media poster (Image URL)": "\u5a92\u9ad4\u6d77\u5831\uff08\u5f71\u50cfImage URL\uff09", |
||||
"Paste your embed code below:": "\u8acb\u5c07\u60a8\u7684\u5d4c\u5165\u5f0f\u7a0b\u5f0f\u78bc\u8cbc\u5728\u4e0b\u9762:", |
||||
"Embed": "\u5d4c\u5165\u78bc", |
||||
"Media...": "\u5a92\u9ad4...", |
||||
"Nonbreaking space": "\u4e0d\u5206\u884c\u7684\u7a7a\u683c", |
||||
"Page break": "\u5206\u9801", |
||||
"Paste as text": "\u4ee5\u7d14\u6587\u5b57\u8cbc\u4e0a", |
||||
"Preview": "\u9810\u89bd", |
||||
"Print...": "\u5217\u5370...", |
||||
"Save": "\u5132\u5b58", |
||||
"Find": "\u641c\u5c0b", |
||||
"Replace with": "\u66f4\u63db", |
||||
"Replace": "\u66ff\u63db", |
||||
"Replace all": "\u66ff\u63db\u5168\u90e8", |
||||
"Previous": "\u4e0a\u4e00\u500b", |
||||
"Next": "\u4e0b\u4e00\u500b", |
||||
"Find and replace...": "\u5c0b\u627e\u53ca\u53d6\u4ee3...", |
||||
"Could not find the specified string.": "\u7121\u6cd5\u67e5\u8a62\u5230\u6b64\u7279\u5b9a\u5b57\u4e32", |
||||
"Match case": "\u76f8\u5339\u914d\u6848\u4ef6", |
||||
"Find whole words only": "\u50c5\u627e\u51fa\u5b8c\u6574\u5b57\u532f", |
||||
"Spell check": "\u62fc\u5beb\u6aa2\u67e5", |
||||
"Ignore": "\u5ffd\u7565", |
||||
"Ignore all": "\u5ffd\u7565\u6240\u6709", |
||||
"Finish": "\u5b8c\u6210", |
||||
"Add to Dictionary": "\u52a0\u5165\u5b57\u5178\u4e2d", |
||||
"Insert table": "\u63d2\u5165\u8868\u683c", |
||||
"Table properties": "\u8868\u683c\u5c6c\u6027", |
||||
"Delete table": "\u522a\u9664\u8868\u683c", |
||||
"Cell": "\u5132\u5b58\u683c", |
||||
"Row": "\u5217", |
||||
"Column": "\u884c", |
||||
"Cell properties": "\u5132\u5b58\u683c\u5c6c\u6027", |
||||
"Merge cells": "\u5408\u4f75\u5132\u5b58\u683c", |
||||
"Split cell": "\u5206\u5272\u5132\u5b58\u683c", |
||||
"Insert row before": "\u63d2\u5165\u5217\u5728...\u4e4b\u524d", |
||||
"Insert row after": "\u63d2\u5165\u5217\u5728...\u4e4b\u5f8c", |
||||
"Delete row": "\u522a\u9664\u5217", |
||||
"Row properties": "\u5217\u5c6c\u6027", |
||||
"Cut row": "\u526a\u4e0b\u5217", |
||||
"Copy row": "\u8907\u88fd\u5217", |
||||
"Paste row before": "\u8cbc\u4e0a\u5217\u5728...\u4e4b\u524d", |
||||
"Paste row after": "\u8cbc\u4e0a\u5217\u5728...\u4e4b\u5f8c", |
||||
"Insert column before": "\u63d2\u5165\u6b04\u4f4d\u5728...\u4e4b\u524d", |
||||
"Insert column after": "\u63d2\u5165\u6b04\u4f4d\u5728...\u4e4b\u5f8c", |
||||
"Delete column": "\u522a\u9664\u884c", |
||||
"Cols": "\u6b04\u4f4d\u6bb5", |
||||
"Rows": "\u5217", |
||||
"Width": "\u5bec\u5ea6", |
||||
"Height": "\u9ad8\u5ea6", |
||||
"Cell spacing": "\u5132\u5b58\u683c\u5f97\u9593\u8ddd", |
||||
"Cell padding": "\u5132\u5b58\u683c\u7684\u908a\u8ddd", |
||||
"Show caption": "\u986f\u793a\u6a19\u984c", |
||||
"Left": "\u5de6\u908a", |
||||
"Center": "\u4e2d\u9593", |
||||
"Right": "\u53f3\u908a", |
||||
"Cell type": "\u5132\u5b58\u683c\u7684\u985e\u578b", |
||||
"Scope": "\u7bc4\u570d", |
||||
"Alignment": "\u5c0d\u9f4a", |
||||
"H Align": "\u6c34\u5e73\u4f4d\u7f6e", |
||||
"V Align": "\u5782\u76f4\u4f4d\u7f6e", |
||||
"Top": "\u7f6e\u9802", |
||||
"Middle": "\u7f6e\u4e2d", |
||||
"Bottom": "\u7f6e\u5e95", |
||||
"Header cell": "\u6a19\u982d\u5132\u5b58\u683c", |
||||
"Row group": "\u5217\u7fa4\u7d44", |
||||
"Column group": "\u6b04\u4f4d\u7fa4\u7d44", |
||||
"Row type": "\u884c\u7684\u985e\u578b", |
||||
"Header": "\u6a19\u982d", |
||||
"Body": "\u4e3b\u9ad4", |
||||
"Footer": "\u9801\u5c3e", |
||||
"Border color": "\u908a\u6846\u984f\u8272", |
||||
"Insert template...": "\u63d2\u5165\u6a23\u7248...", |
||||
"Templates": "\u6a23\u7248", |
||||
"Template": "\u6a23\u677f", |
||||
"Text color": "\u6587\u5b57\u984f\u8272", |
||||
"Background color": "\u80cc\u666f\u984f\u8272", |
||||
"Custom...": "\u81ea\u8a02", |
||||
"Custom color": "\u81ea\u8a02\u984f\u8272", |
||||
"No color": "No color", |
||||
"Remove color": "\u79fb\u9664\u984f\u8272", |
||||
"Table of Contents": "\u76ee\u9304", |
||||
"Show blocks": "\u986f\u793a\u5340\u584a\u8cc7\u8a0a", |
||||
"Show invisible characters": "\u986f\u793a\u96b1\u85cf\u5b57\u5143", |
||||
"Word count": "\u8a08\u7b97\u5b57\u6578", |
||||
"Count": "\u8a08\u7b97", |
||||
"Document": "\u6587\u4ef6", |
||||
"Selection": "\u9078\u9805", |
||||
"Words": "\u5b57\u6578", |
||||
"Words: {0}": "\u5b57\u6578\uff1a{0}", |
||||
"{0} words": "{0} \u5b57\u5143", |
||||
"File": "\u6a94\u6848", |
||||
"Edit": "\u7de8\u8f2f", |
||||
"Insert": "\u63d2\u5165", |
||||
"View": "\u6aa2\u8996", |
||||
"Format": "\u683c\u5f0f", |
||||
"Table": "\u8868\u683c", |
||||
"Tools": "\u5de5\u5177", |
||||
"Powered by {0}": "\u7531 {0} \u63d0\u4f9b", |
||||
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u8c50\u5bcc\u7684\u6587\u672c\u5340\u57df\u3002\u6309ALT-F9\u524d\u5f80\u4e3b\u9078\u55ae\u3002\u6309ALT-F10\u547c\u53eb\u5de5\u5177\u6b04\u3002\u6309ALT-0\u5c0b\u6c42\u5e6b\u52a9", |
||||
"Image title": "\u5716\u7247\u6a19\u984c", |
||||
"Border width": "\u6846\u7dda\u5bec\u5ea6", |
||||
"Border style": "\u6846\u7dda\u6a23\u5f0f", |
||||
"Error": "\u932f\u8aa4", |
||||
"Warn": "\u8b66\u544a", |
||||
"Valid": "\u6709\u6548", |
||||
"To open the popup, press Shift+Enter": "\u8981\u958b\u555f\u5f48\u51fa\u8996\u7a97\uff0c\u8acb\u6309Shift+Enter", |
||||
"Rich Text Area. Press ALT-0 for help.": "\u5bcc\u6587\u672c\u5340\u57df\u3002\u8acb\u6309ALT-0\u5c0b\u6c42\u5354\u52a9\u3002", |
||||
"System Font": "\u7cfb\u7d71\u5b57\u578b", |
||||
"Failed to upload image: {0}": "\u7121\u6cd5\u4e0a\u50b3\u5f71\u50cf\uff1a{0}", |
||||
"Failed to load plugin: {0} from url {1}": "\u7121\u6cd5\u4e0a\u50b3\u63d2\u4ef6\uff1a{0}\u81eaurl{1}", |
||||
"Failed to load plugin url: {0}": "\u7121\u6cd5\u4e0a\u50b3\u63d2\u4ef6\uff1a{0}", |
||||
"Failed to initialize plugin: {0}": "\u7121\u6cd5\u555f\u52d5\u63d2\u4ef6\uff1a{0}", |
||||
"example": "\u7bc4\u4f8b", |
||||
"Search": "\u641c\u7d22", |
||||
"All": "\u5168\u90e8", |
||||
"Currency": "\u8ca8\u5e63", |
||||
"Text": "\u6587\u672c", |
||||
"Quotations": "\u5f15\u7528", |
||||
"Mathematical": "\u6578\u5b78", |
||||
"Extended Latin": "\u62c9\u4e01\u5b57\u6bcd\u64f4\u5145", |
||||
"Symbols": "\u7b26\u865f", |
||||
"Arrows": "\u7bad\u982d", |
||||
"User Defined": "\u4f7f\u7528\u8005\u5df2\u5b9a\u7fa9", |
||||
"dollar sign": "\u7f8e\u5143\u7b26\u865f", |
||||
"currency sign": "\u8ca8\u5e63\u7b26\u865f", |
||||
"euro-currency sign": "\u6b50\u5143\u7b26\u865f", |
||||
"colon sign": "\u79d1\u6717\u7b26\u865f", |
||||
"cruzeiro sign": "\u514b\u9b6f\u8cfd\u7f85\u7b26\u865f", |
||||
"french franc sign": "\u6cd5\u6717\u7b26\u865f", |
||||
"lira sign": "\u91cc\u62c9\u7b26\u865f", |
||||
"mill sign": "\u6587\u7b26\u865f", |
||||
"naira sign": "\u5948\u62c9\u7b26\u865f", |
||||
"peseta sign": "\u6bd4\u585e\u5854\u7b26\u865f", |
||||
"rupee sign": "\u76e7\u6bd4\u7b26\u865f", |
||||
"won sign": "\u97d3\u571c\u7b26\u865f", |
||||
"new sheqel sign": "\u65b0\u8b1d\u514b\u723e\u7b26\u865f", |
||||
"dong sign": "\u8d8a\u5357\u76fe\u7b26\u865f", |
||||
"kip sign": "\u8001\u64be\u5e63\u7b26\u865f", |
||||
"tugrik sign": "\u8499\u53e4\u5e63\u7b26\u865f", |
||||
"drachma sign": "\u5fb7\u514b\u62c9\u99ac\u7b26\u865f", |
||||
"german penny symbol": "\u5fb7\u570b\u5206\u7b26\u865f", |
||||
"peso sign": "\u62ab\u7d22\u7b26\u865f", |
||||
"guarani sign": "\u5df4\u62c9\u572d\u5e63\u7b26\u865f", |
||||
"austral sign": "\u963f\u6839\u5ef7\u5e63\u7b26\u865f", |
||||
"hryvnia sign": "\u70cf\u514b\u862d\u5e63\u7b26\u865f", |
||||
"cedi sign": "\u8fe6\u7d0d\u5e63\u7b26\u865f", |
||||
"livre tournois sign": "\u91cc\u5f17\u723e\u7b26\u865f", |
||||
"spesmilo sign": "\u570b\u969b\u5e63\u7b26\u865f", |
||||
"tenge sign": "\u54c8\u85a9\u514b\u5e63\u7b26\u865f", |
||||
"indian rupee sign": "\u5370\u5ea6\u76e7\u6bd4\u7b26\u865f", |
||||
"turkish lira sign": "\u571f\u8033\u5176\u91cc\u62c9\u7b26\u865f", |
||||
"nordic mark sign": "\u5317\u6b50\u99ac\u514b\u7b26\u865f", |
||||
"manat sign": "\u4e9e\u585e\u62dc\u7136\u5e63\u7b26\u865f", |
||||
"ruble sign": "\u76e7\u5e03\u7b26\u865f", |
||||
"yen character": "\u65e5\u5713\u7b26\u865f", |
||||
"yuan character": "\u4eba\u6c11\u5e63\u7b26\u865f", |
||||
"yuan character, in hong kong and taiwan": "\u6e2f\u5143\u8207\u53f0\u5e63\u7b26\u865f", |
||||
"yen\/yuan character variant one": "\u65e5\u5713\/\u4eba\u6c11\u5e63\u7b26\u865f\u8b8a\u5316\u578b", |
||||
"Loading emoticons...": "\u8f09\u5165\u8868\u60c5\u7b26\u865f\u2026", |
||||
"Could not load emoticons": "\u7121\u6cd5\u8f09\u5165\u8868\u60c5\u7b26\u865f", |
||||
"People": "\u4eba", |
||||
"Animals and Nature": "\u52d5\u7269\u8207\u81ea\u7136", |
||||
"Food and Drink": "\u98f2\u98df", |
||||
"Activity": "\u6d3b\u52d5", |
||||
"Travel and Places": "\u65c5\u884c\u8207\u5730\u9ede", |
||||
"Objects": "\u7269\u4ef6", |
||||
"Flags": "\u65d7\u6a19", |
||||
"Characters": "\u5b57\u5143", |
||||
"Characters (no spaces)": "\u5b57\u5143\uff08\u7121\u7a7a\u683c\uff09", |
||||
"{0} characters": "{0}\u5b57\u5143", |
||||
"Error: Form submit field collision.": "\u932f\u8aa4\uff1a\u8868\u683c\u905e\u4ea4\u6b04\u4f4d\u885d\u7a81\u3002", |
||||
"Error: No form element found.": "\u932f\u8aa4\uff1a\u627e\u4e0d\u5230\u8868\u683c\u5143\u7d20\u3002", |
||||
"Update": "\u66f4\u65b0", |
||||
"Color swatch": "\u8272\u5f69\u6a23\u672c", |
||||
"Turquoise": "\u571f\u8033\u5176\u85cd", |
||||
"Green": "\u7da0\u8272", |
||||
"Blue": "\u85cd\u8272", |
||||
"Purple": "\u7d2b\u8272", |
||||
"Navy Blue": "\u6df1\u85cd\u8272", |
||||
"Dark Turquoise": "\u6df1\u571f\u8033\u5176\u85cd", |
||||
"Dark Green": "\u6df1\u7da0\u8272", |
||||
"Medium Blue": "\u4e2d\u85cd\u8272", |
||||
"Medium Purple": "\u4e2d\u7d2b\u8272", |
||||
"Midnight Blue": "\u9ed1\u85cd\u8272", |
||||
"Yellow": "\u9ec3\u8272", |
||||
"Orange": "\u6a59\u8272", |
||||
"Red": "\u7d05\u8272", |
||||
"Light Gray": "\u6dfa\u7070\u8272", |
||||
"Gray": "\u7070\u8272", |
||||
"Dark Yellow": "\u6df1\u9ec3\u8272", |
||||
"Dark Orange": "\u6df1\u6a59\u8272", |
||||
"Dark Red": "\u6697\u7d05\u8272", |
||||
"Medium Gray": "\u4e2d\u7070\u8272", |
||||
"Dark Gray": "\u6df1\u7070\u8272", |
||||
"Light Green": "\u6de1\u7da0\u8272", |
||||
"Light Yellow": "\u6dfa\u9ec3\u8272", |
||||
"Light Red": "\u6dfa\u7d05\u8272", |
||||
"Light Purple": "\u6dfa\u7d2b\u8272", |
||||
"Light Blue": "\u6dfa\u85cd\u8272", |
||||
"Dark Purple": "\u6df1\u7d2b\u8272", |
||||
"Dark Blue": "\u6df1\u85cd\u8272", |
||||
"Black": "\u9ed1\u8272", |
||||
"White": "\u767d\u8272", |
||||
"Switch to or from fullscreen mode": "\u8f49\u63db\u81ea\/\u81f3\u5168\u87a2\u5e55\u6a21\u5f0f", |
||||
"Open help dialog": "\u958b\u555f\u5354\u52a9\u5c0d\u8a71", |
||||
"history": "\u6b77\u53f2", |
||||
"styles": "\u6a23\u5f0f", |
||||
"formatting": "\u683c\u5f0f", |
||||
"alignment": "\u5c0d\u9f4a", |
||||
"indentation": "\u7e2e\u6392", |
||||
"permanent pen": "\u6c38\u4e45\u6027\u7b46", |
||||
"comments": "\u8a3b\u89e3", |
||||
"Format Painter": "\u8907\u88fd\u683c\u5f0f", |
||||
"Insert\/edit iframe": "\u63d2\u5165\/\u7de8\u8f2fiframe", |
||||
"Capitalization": "\u5927\u5beb", |
||||
"lowercase": "\u5c0f\u5beb", |
||||
"UPPERCASE": "\u5927\u5beb", |
||||
"Title Case": "\u5b57\u9996\u5927\u5beb", |
||||
"Permanent Pen Properties": "\u6c38\u4e45\u6a19\u8a18\u5c6c\u6027", |
||||
"Permanent pen properties...": "\u6c38\u4e45\u6a19\u8a18\u5c6c\u6027......", |
||||
"Font": "\u5b57\u578b", |
||||
"Size": "\u5b57\u5f62\u5927\u5c0f", |
||||
"More...": "\u66f4\u591a\u8cc7\u8a0a......", |
||||
"Spellcheck Language": "\u62fc\u5beb\u8a9e\u8a00", |
||||
"Select...": "\u9078\u64c7......", |
||||
"Preferences": "\u9996\u9078\u9805", |
||||
"Yes": "\u662f", |
||||
"No": "\u5426", |
||||
"Keyboard Navigation": "\u9375\u76e4\u5c0e\u822a", |
||||
"Version": "\u7248\u672c", |
||||
"Anchor": "\u52a0\u5165\u9328\u9ede", |
||||
"Special character": "\u7279\u6b8a\u5b57\u5143", |
||||
"Code sample": "\u7a0b\u5f0f\u78bc\u7bc4\u4f8b", |
||||
"Color": "\u984f\u8272", |
||||
"Emoticons": "\u8868\u60c5", |
||||
"Document properties": "\u6587\u4ef6\u7684\u5c6c\u6027", |
||||
"Image": "\u5716\u7247", |
||||
"Insert link": "\u63d2\u5165\u9023\u7d50", |
||||
"Target": "\u958b\u555f\u65b9\u5f0f", |
||||
"Link": "\u9023\u7d50", |
||||
"Poster": "\u9810\u89bd\u5716\u7247", |
||||
"Media": "\u5a92\u9ad4", |
||||
"Print": "\u5217\u5370", |
||||
"Prev": "\u4e0a\u4e00\u500b", |
||||
"Find and replace": "\u5c0b\u627e\u53ca\u53d6\u4ee3", |
||||
"Whole words": "\u6574\u500b\u55ae\u5b57", |
||||
"Spellcheck": "\u62fc\u5b57\u6aa2\u67e5", |
||||
"Caption": "\u8868\u683c\u6a19\u984c", |
||||
"Insert template": "\u63d2\u5165\u6a23\u7248" |
||||
}); |
@ -0,0 +1,7 @@ |
||||
/** |
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved. |
||||
* Licensed under the LGPL or a commercial license. |
||||
* For LGPL see License.txt in the project root for license information. |
||||
* For commercial licenses see https://www.tiny.cloud/ |
||||
*/ |
||||
body{background-color:#2f3742;color:#dfe0e4;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}a{color:#4099ff}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#6d737b}figure{display:table;margin:1rem auto}figure figcaption{color:#8a8f97;display:block;margin-top:.25rem;text-align:center}hr{border-color:#6d737b;border-style:solid;border-width:1px 0 0 0}code{background-color:#6d737b;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #6d737b;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #6d737b;margin-right:1.5rem;padding-right:1rem} |
@ -0,0 +1,7 @@ |
||||
/** |
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved. |
||||
* Licensed under the LGPL or a commercial license. |
||||
* For LGPL see License.txt in the project root for license information. |
||||
* For commercial licenses see https://www.tiny.cloud/ |
||||
*/ |
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem} |
@ -0,0 +1,7 @@ |
||||
/** |
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved. |
||||
* Licensed under the LGPL or a commercial license. |
||||
* For LGPL see License.txt in the project root for license information. |
||||
* For commercial licenses see https://www.tiny.cloud/ |
||||
*/ |
||||
@media screen{html{background:#f4f4f4;min-height:100%}}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif}@media screen{body{background-color:#fff;box-shadow:0 0 4px rgba(0,0,0,.15);box-sizing:border-box;margin:1rem auto 0;max-width:820px;min-height:calc(100vh - 1rem);padding:4rem 6rem 6rem 6rem}}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure figcaption{color:#999;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem} |
@ -0,0 +1,7 @@ |
||||
/** |
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved. |
||||
* Licensed under the LGPL or a commercial license. |
||||
* For LGPL see License.txt in the project root for license information. |
||||
* For commercial licenses see https://www.tiny.cloud/ |
||||
*/ |
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem auto;max-width:900px}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem} |
@ -0,0 +1,7 @@ |
||||
/** |
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved. |
||||
* Licensed under the LGPL or a commercial license. |
||||
* For LGPL see License.txt in the project root for license information. |
||||
* For commercial licenses see https://www.tiny.cloud/ |
||||
*/ |
||||
.tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection{background-color:green;display:inline-block;opacity:.5;position:absolute}body{-webkit-text-size-adjust:none}body img{max-width:96vw}body table img{max-width:95%}body{font-family:sans-serif}table{border-collapse:collapse} |
@ -0,0 +1,7 @@ |
||||
/** |
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved. |
||||
* Licensed under the LGPL or a commercial license. |
||||
* For LGPL see License.txt in the project root for license information. |
||||
* For commercial licenses see https://www.tiny.cloud/ |
||||
*/ |
||||
body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;left:0;margin:0;overflow:hidden;-ms-scroll-chaining:none;overscroll-behavior:none;padding:0;position:fixed;top:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox.tox-tinymce.tox-fullscreen{background-color:transparent;z-index:1200}.tox-shadowhost.tox-fullscreen{z-index:1200}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201} |
@ -0,0 +1,7 @@ |
||||
/** |
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved. |
||||
* Licensed under the LGPL or a commercial license. |
||||
* For LGPL see License.txt in the project root for license information. |
||||
* For commercial licenses see https://www.tiny.cloud/ |
||||
*/ |
||||
.tinymce-mobile-unfocused-selections .tinymce-mobile-unfocused-selection{background-color:green;display:inline-block;opacity:.5;position:absolute}body{-webkit-text-size-adjust:none}body img{max-width:96vw}body table img{max-width:95%}body{font-family:sans-serif}table{border-collapse:collapse} |
@ -0,0 +1,7 @@ |
||||
/** |
||||
* Copyright (c) Tiny Technologies, Inc. All rights reserved. |
||||
* Licensed under the LGPL or a commercial license. |
||||
* For LGPL see License.txt in the project root for license information. |
||||
* For commercial licenses see https://www.tiny.cloud/ |
||||
*/ |
||||
body.tox-dialog__disable-scroll{overflow:hidden}.tox-fullscreen{border:0;height:100%;left:0;margin:0;overflow:hidden;-ms-scroll-chaining:none;overscroll-behavior:none;padding:0;position:fixed;top:0;touch-action:pinch-zoom;width:100%}.tox.tox-tinymce.tox-fullscreen .tox-statusbar__resize-handle{display:none}.tox.tox-tinymce.tox-fullscreen{background-color:transparent;z-index:1200}.tox-shadowhost.tox-fullscreen{z-index:1200}.tox-fullscreen .tox.tox-tinymce-aux,.tox-fullscreen~.tox.tox-tinymce-aux{z-index:1201} |
@ -0,0 +1,15 @@ |
||||
<template> |
||||
<el-config-provider :locale="lang"> |
||||
<router-view /> |
||||
</el-config-provider> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { computed } from 'vue'; |
||||
import { ElConfigProvider } from 'element-plus'; |
||||
import { useI18n } from 'vue-i18n'; |
||||
import { getElementPlusLocale } from '@/i18n'; |
||||
|
||||
const { locale } = useI18n({ useScope: 'global' }); |
||||
const lang = computed(() => getElementPlusLocale(locale.value as string)); |
||||
</script> |
@ -0,0 +1,50 @@ |
||||
import axios from '@/utils/request'; |
||||
|
||||
export const imageUploadUrl = `${import.meta.env.VITE_BASE_API}/backend/image-upload`; |
||||
export const videoUploadUrl = `${import.meta.env.VITE_BASE_API}/backend/video-upload`; |
||||
export const docUploadUrl = `${import.meta.env.VITE_BASE_API}/backend/doc-upload`; |
||||
export const fileUploadUrl = `${import.meta.env.VITE_BASE_API}/backend/file-upload`; |
||||
|
||||
export const cropImage = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/image-crop', data)).data; |
||||
|
||||
export const queryGlobalModel = async (): Promise<any> => (await axios.get('/backend/core/global-settings/model')).data; |
||||
export const queryGlobalSettings = async (): Promise<any> => (await axios.get('/backend/core/global-settings')).data; |
||||
export const updateGlobalBaseSettings = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/global-settings/base?_method=put', data)).data; |
||||
export const updateGlobalCustomsSettings = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/global-settings/customs?_method=put', data)).data; |
||||
export const updateGlobalUploadSettings = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/global-settings/upload?_method=put', data)).data; |
||||
|
||||
export const querySiteSettings = async (): Promise<any> => (await axios.get('/backend/core/site-settings')).data; |
||||
export const updateSiteBaseSettings = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/site-settings/base?_method=put', data)).data; |
||||
export const updateSiteCustomsSettings = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/site-settings/customs?_method=put', data)).data; |
||||
export const updateSiteWatermarkSettings = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/site-settings/watermark?_method=put', data)).data; |
||||
export const querySiteHtmlSettings = async (): Promise<any> => (await axios.get('/backend/core/site-settings/html')).data; |
||||
export const updateSiteHtmlSettings = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/site-settings/html?_method=put', data)).data; |
||||
export const queryCurrentSiteThemeList = async (): Promise<any> => (await axios.get('/backend/core/site/theme')).data; |
||||
|
||||
export const queryModelList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/model', { params })).data; |
||||
export const queryModel = async (id: number): Promise<any> => (await axios.get(`/backend/core/model/${id}`)).data; |
||||
export const createModel = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/model', data)).data; |
||||
export const updateModel = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/model?_method=put', data)).data; |
||||
export const updateModelOrder = async (data: number[]): Promise<any> => (await axios.post('/backend/core/model/order?_method=put', data)).data; |
||||
export const deleteModel = async (data: number[]): Promise<any> => (await axios.post('/backend/core/model?_method=delete', data)).data; |
||||
|
||||
export const queryDictTypeList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/dict-type', { params })).data; |
||||
export const queryDictType = async (id: number): Promise<any> => (await axios.get(`/backend/core/dict-type/${id}`)).data; |
||||
export const createDictType = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/dict-type', data)).data; |
||||
export const updateDictType = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/dict-type?_method=put', data)).data; |
||||
export const updateDictTypeOrder = async (data: number[]): Promise<any> => (await axios.post('/backend/core/dict-type/order?_method=put', data)).data; |
||||
export const deleteDictType = async (data: number[]): Promise<any> => (await axios.post('/backend/core/dict-type?_method=delete', data)).data; |
||||
|
||||
export const queryDictList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/dict', { params })).data; |
||||
export const queryDict = async (id: number): Promise<any> => (await axios.get(`/backend/core/dict/${id}`)).data; |
||||
export const createDict = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/dict', data)).data; |
||||
export const updateDict = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/dict?_method=put', data)).data; |
||||
export const updateDictOrder = async (data: number[]): Promise<any> => (await axios.post('/backend/core/dict/order?_method=put', data)).data; |
||||
export const deleteDict = async (data: number[]): Promise<any> => (await axios.post('/backend/core/dict?_method=delete', data)).data; |
||||
|
||||
export const queryBlockList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/block', { params })).data; |
||||
export const queryBlock = async (id: number): Promise<any> => (await axios.get(`/backend/core/block/${id}`)).data; |
||||
export const createBlock = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/block', data)).data; |
||||
export const updateBlock = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/block?_method=put', data)).data; |
||||
export const updateBlockOrder = async (data: number[]): Promise<any> => (await axios.post('/backend/core/block/order?_method=put', data)).data; |
||||
export const deleteBlock = async (data: number[]): Promise<any> => (await axios.post('/backend/core/block?_method=delete', data)).data; |
@ -0,0 +1,30 @@ |
||||
import axios from '@/utils/request'; |
||||
|
||||
export const queryChannelList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/channel', { params })).data; |
||||
export const queryChannel = async (id: number): Promise<any> => (await axios.get(`/backend/core/channel/${id}`)).data; |
||||
export const createChannel = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/channel', data)).data; |
||||
export const updateChannel = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/channel?_method=put', data)).data; |
||||
export const updateChannelOrder = async (data: number[]): Promise<any> => (await axios.post('/backend/core/channel/order?_method=put', data)).data; |
||||
export const deleteChannel = async (data: number[]): Promise<any> => (await axios.post('/backend/core/channel?_method=delete', data)).data; |
||||
export const queryChannelTemplates = async (): Promise<any> => (await axios.get('/backend/core/channel/channel-templates')).data; |
||||
export const queryArticleTemplates = async (): Promise<any> => (await axios.get('/backend/core/channel/article-templates')).data; |
||||
|
||||
export const queryArticlePage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/article', { params })).data; |
||||
export const queryArticle = async (id: number): Promise<any> => (await axios.get(`/backend/core/article/${id}`)).data; |
||||
export const createArticle = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/article', data)).data; |
||||
export const updateArticle = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/article?_method=put', data)).data; |
||||
export const deleteArticle = async (data: number[]): Promise<any> => (await axios.post('/backend/core/article?_method=delete', data)).data; |
||||
|
||||
export const queryBlockItemList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/block-item', { params })).data; |
||||
export const queryBlockItem = async (id: number): Promise<any> => (await axios.get(`/backend/core/block-item/${id}`)).data; |
||||
export const createBlockItem = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/block-item', data)).data; |
||||
export const updateBlockItem = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/block-item?_method=put', data)).data; |
||||
export const updateBlockItemOrder = async (data: number[]): Promise<any> => (await axios.post('/backend/core/block-item/order?_method=put', data)).data; |
||||
export const deleteBlockItem = async (data: number[]): Promise<any> => (await axios.post('/backend/core/block-item?_method=delete', data)).data; |
||||
|
||||
export const fulltextReindexAll = async (): Promise<any> => (await axios.post('/backend/core/generator/fulltext-reindex-all')).data; |
||||
export const fulltextReindexSite = async (): Promise<any> => (await axios.post('/backend/core/generator/fulltext-reindex-site')).data; |
||||
export const htmlAll = async (): Promise<any> => (await axios.post('/backend/core/generator/html-all')).data; |
||||
export const htmlHome = async (): Promise<any> => (await axios.post('/backend/core/generator/html-home')).data; |
||||
export const htmlChannel = async (): Promise<any> => (await axios.post('/backend/core/generator/html-channel')).data; |
||||
export const htmlArticle = async (): Promise<any> => (await axios.post('/backend/core/generator/html-article')).data; |
@ -0,0 +1,16 @@ |
||||
import axios from '@/utils/request'; |
||||
|
||||
export interface LoginParam { |
||||
username: string; |
||||
password: string; |
||||
browser?: boolean; |
||||
} |
||||
|
||||
export interface RefreshTokenParam { |
||||
refreshToken: string; |
||||
browser?: boolean; |
||||
} |
||||
|
||||
export const accountLogin = async (data: LoginParam): Promise<any> => (await axios.post('/auth/jwt/login', data)).data; |
||||
export const accountRefreshToken = async (data: RefreshTokenParam): Promise<any> => (await axios.post('/auth/jwt/refresh-token', data)).data; |
||||
export const queryCurrentUser = async (): Promise<any> => (await axios.get('/user/current')).data; |
@ -0,0 +1,4 @@ |
||||
import axios from '@/utils/request'; |
||||
|
||||
export const updatePassword = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/personal/password?_method=put', data)).data; |
||||
export const passwordValidation = async (password?: string): Promise<any> => (await axios.get('/backend/core/personal/password-validation', { params: { password } })).data; |
@ -0,0 +1,28 @@ |
||||
import axios from '@/utils/request'; |
||||
|
||||
export const queryStorageList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/storage', { params })).data; |
||||
export const queryStorage = async (id: number): Promise<any> => (await axios.get(`/backend/core/storage/${id}`)).data; |
||||
export const createStorage = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/storage', data)).data; |
||||
export const updateStorage = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/storage?_method=put', data)).data; |
||||
export const updateStorageOrder = async (data: number[]): Promise<any> => (await axios.post('/backend/core/storage/order?_method=put', data)).data; |
||||
export const deleteStorage = async (data: number[]): Promise<any> => (await axios.post('/backend/core/storage?_method=delete', data)).data; |
||||
|
||||
export const querySiteList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/site', { params })).data; |
||||
export const querySite = async (id: number): Promise<any> => (await axios.get(`/backend/core/site/${id}`)).data; |
||||
export const createSite = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/site', data)).data; |
||||
export const updateSite = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/site?_method=put', data)).data; |
||||
export const updateSiteOrder = async (data: number[]): Promise<any> => (await axios.post('/backend/core/site/order?_method=put', data)).data; |
||||
export const deleteSite = async (data: number[]): Promise<any> => (await axios.post('/backend/core/site?_method=delete', data)).data; |
||||
export const querySiteThemeList = async (id: number): Promise<any> => (await axios.get(`/backend/core/site/${id}/theme`)).data; |
||||
|
||||
export const queryAttachmentPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/attachment', { params })).data; |
||||
export const queryAttachment = async (id: number): Promise<any> => (await axios.get(`/backend/core/attachment/${id}`)).data; |
||||
export const createAttachment = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/attachment', data)).data; |
||||
export const updateAttachment = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/attachment?_method=put', data)).data; |
||||
export const deleteAttachment = async (data: number[]): Promise<any> => (await axios.post('/backend/core/attachment?_method=delete', data)).data; |
||||
|
||||
export const queryTaskPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/task', { params })).data; |
||||
export const queryTask = async (id: number): Promise<any> => (await axios.get(`/backend/core/task/${id}`)).data; |
||||
export const createTask = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/task', data)).data; |
||||
export const updateTask = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/task?_method=put', data)).data; |
||||
export const deleteTask = async (data: number[]): Promise<any> => (await axios.post('/backend/core/task?_method=delete', data)).data; |
@ -0,0 +1,31 @@ |
||||
import axios from '@/utils/request'; |
||||
|
||||
export const queryOrgList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/org', { params })).data; |
||||
export const queryOrg = async (id: number): Promise<any> => (await axios.get(`/backend/core/org/${id}`)).data; |
||||
export const createOrg = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/org', data)).data; |
||||
export const updateOrg = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/org?_method=put', data)).data; |
||||
export const updateOrgOrder = async (data: number[]): Promise<any> => (await axios.post('/backend/core/org/order?_method=put', data)).data; |
||||
export const deleteOrg = async (data: number[]): Promise<any> => (await axios.post('/backend/core/org?_method=delete', data)).data; |
||||
|
||||
export const queryRoleList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/role', { params })).data; |
||||
export const queryRole = async (id: number): Promise<any> => (await axios.get(`/backend/core/role/${id}`)).data; |
||||
export const createRole = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/role', data)).data; |
||||
export const updateRole = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/role?_method=put', data)).data; |
||||
export const updateRoleOrder = async (data: number[]): Promise<any> => (await axios.post('/backend/core/role/order?_method=put', data)).data; |
||||
export const deleteRole = async (data: number[]): Promise<any> => (await axios.post('/backend/core/role?_method=delete', data)).data; |
||||
|
||||
export const queryGroupList = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/group', { params })).data; |
||||
export const queryGroup = async (id: number): Promise<any> => (await axios.get(`/backend/core/group/${id}`)).data; |
||||
export const createGroup = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/group', data)).data; |
||||
export const updateGroup = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/group?_method=put', data)).data; |
||||
export const updateGroupOrder = async (data: number[]): Promise<any> => (await axios.post('/backend/core/group/order?_method=put', data)).data; |
||||
export const deleteGroup = async (data: number[]): Promise<any> => (await axios.post('/backend/core/group?_method=delete', data)).data; |
||||
|
||||
export const queryUserPage = async (params?: Record<string, any>): Promise<any> => (await axios.get('/backend/core/user', { params })).data; |
||||
export const queryUser = async (id: number): Promise<any> => (await axios.get(`/backend/core/user/${id}`)).data; |
||||
export const createUser = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/user', data)).data; |
||||
export const updateUser = async (data: Record<string, any>): Promise<any> => (await axios.post('/backend/core/user?_method=put', data)).data; |
||||
export const deleteUser = async (data: number[]): Promise<any> => (await axios.post('/backend/core/user?_method=delete', data)).data; |
||||
export const usernameValidation = async (username?: string): Promise<any> => (await axios.get('/backend/core/user/username-validation', { params: { username } })).data; |
||||
export const emailValidation = async (email?: string): Promise<any> => (await axios.get('/backend/core/user/email-validation', { params: { email } })).data; |
||||
export const mobileValidation = async (mobile?: string): Promise<any> => (await axios.get('/backend/core/user/mobile-validation', { params: { mobile } })).data; |
After Width: | Height: | Size: 96 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 195 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 243 B |
After Width: | Height: | Size: 321 B |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 6.7 KiB |
After Width: | Height: | Size: 687 B |
After Width: | Height: | Size: 671 B |
@ -0,0 +1,38 @@ |
||||
<template> |
||||
<el-breadcrumb separator="/"> |
||||
<transition-group name="breadcrumb"> |
||||
<el-breadcrumb-item v-for="(item, index) in itemList" :key="item.path"> |
||||
<span v-if="index === itemList.length - 1" class="text-gray-400">{{ $t(item.meta.title) }}</span> |
||||
<a v-else @click.prevent="handleLink(item)">{{ $t(item.meta.title) }}</a> |
||||
</el-breadcrumb-item> |
||||
</transition-group> |
||||
</el-breadcrumb> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import { defineComponent, ref, watchEffect } from 'vue'; |
||||
import { useRoute, useRouter } from 'vue-router'; |
||||
import { compile } from 'path-to-regexp'; |
||||
|
||||
export default defineComponent({ |
||||
setup() { |
||||
const router = useRouter(); |
||||
const route = useRoute(); |
||||
const itemList = ref<any[]>([]); |
||||
|
||||
const pathCompile = (path: string) => { |
||||
const { params } = route; |
||||
const toPath = compile(path); |
||||
return toPath(params); |
||||
}; |
||||
const handleLink = (item: any) => { |
||||
const { redirect, path } = item; |
||||
router.push(redirect || pathCompile(path)); |
||||
}; |
||||
watchEffect(() => { |
||||
itemList.value = route.matched.filter((item) => item.meta?.title); |
||||
}); |
||||
return { itemList, handleLink }; |
||||
}, |
||||
}); |
||||
</script> |
@ -0,0 +1,258 @@ |
||||
<template> |
||||
<el-dialog |
||||
:title="title" |
||||
:close-on-click-modal="!unsaved" |
||||
:model-value="modelValue" |
||||
@update:model-value="$emit('update:modelValue', $event)" |
||||
@opened="!isEdit && focus?.focus()" |
||||
:width="large ? '98%' : '683px'" |
||||
:top="large ? '16px' : undefined" |
||||
> |
||||
<div> |
||||
<el-button v-if="isEdit && addable" :disabled="perm(`${perms}:create`)" type="primary" :icon="Plus" @click="handleAdd">{{ $t('add') }}</el-button> |
||||
<el-popconfirm v-if="isEdit" :title="$t('confirmDelete')" @confirm="handleDelete"> |
||||
<template #reference> |
||||
<el-button :loading="buttonLoading" :disabled="disableDelete?.(bean) || perm(`${perms}:delete`)" :icon="Delete">{{ $t('delete') }}</el-button> |
||||
</template> |
||||
</el-popconfirm> |
||||
<el-button v-if="isEdit" @click="handlePrev" :disabled="!hasPrev">{{ $t('form.prev') }}</el-button> |
||||
<el-button v-if="isEdit" @click="handleNext" :disabled="!hasNext">{{ $t('form.next') }}</el-button> |
||||
<el-button @click="handleCancel" type="primary">{{ $t('back') }}</el-button> |
||||
<el-tooltip :content="$t('form.continuous')" placement="top"> |
||||
<el-switch v-model="continuous" size="small" class="ml-2"></el-switch> |
||||
</el-tooltip> |
||||
<el-tag v-if="unsaved" type="danger" class="ml-2">{{ $t('form.unsaved') }}</el-tag> |
||||
<slot name="header" :values="values" :bean="bean" :isEdit="isEdit"></slot> |
||||
</div> |
||||
<el-form |
||||
:class="['mt-5', 'pr-5']" |
||||
ref="form" |
||||
v-loading="loading" |
||||
:model="values" |
||||
:disabled="!editable" |
||||
:label-width="labelWidth ?? '150px'" |
||||
:label-position="labelPosition ?? 'right'" |
||||
> |
||||
<slot :values="values" :bean="bean" :isEdit="isEdit"></slot> |
||||
<div v-if="editable"> |
||||
<el-button :disabled="perm(isEdit ? `${perms}:update` : `${perms}:create`)" :loading="buttonLoading" @click.prevent="handleSubmit" type="primary" native-type="submit"> |
||||
{{ $t('submit') }} |
||||
</el-button> |
||||
</div> |
||||
</el-form> |
||||
</el-dialog> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import { computed, defineComponent, PropType, ref, toRefs, watch } from 'vue'; |
||||
import { ElMessage } from 'element-plus'; |
||||
import { Plus, Delete } from '@element-plus/icons-vue'; |
||||
import { useI18n } from 'vue-i18n'; |
||||
import _ from 'lodash'; |
||||
import { perm } from '@/store/useCurrentUser'; |
||||
|
||||
const CONTINUOUS_SETTINGS = 'ujcms_continuous_settings'; |
||||
function fetchContinuous(): Record<string, boolean> { |
||||
const settings = localStorage.getItem(CONTINUOUS_SETTINGS); |
||||
return settings ? JSON.parse(settings) : {}; |
||||
} |
||||
function storeContinuous(settings: Record<string, boolean>) { |
||||
localStorage.setItem(CONTINUOUS_SETTINGS, JSON.stringify(settings)); |
||||
} |
||||
function getContinuous(name: string) { |
||||
const settings = fetchContinuous(); |
||||
return settings[name] ?? false; |
||||
} |
||||
function setContinuous(name: string, continuous: boolean) { |
||||
const settings = fetchContinuous(); |
||||
settings[name] = continuous; |
||||
storeContinuous(settings); |
||||
} |
||||
|
||||
export default defineComponent({ |
||||
name: 'DialogForm', |
||||
props: { |
||||
modelValue: { type: Boolean, required: true }, |
||||
name: { type: String, required: true }, |
||||
beanId: { required: true }, |
||||
beanIds: { type: Array, required: true }, |
||||
initValues: { type: Function as PropType<(bean?: any) => any>, required: true }, |
||||
toValues: { type: Function as PropType<(bean: any) => any>, required: true }, |
||||
queryBean: { type: Function as PropType<(id: any) => Promise<any>>, required: true }, |
||||
createBean: { type: Function as PropType<(bean: any) => Promise<any>>, required: true }, |
||||
updateBean: { type: Function as PropType<(bean: any) => Promise<any>>, required: true }, |
||||
deleteBean: { type: Function as PropType<(ids: any[]) => Promise<any>>, required: true }, |
||||
disableDelete: { type: Function as PropType<(bean: any) => boolean> }, |
||||
addable: { type: Boolean, default: true }, |
||||
editable: { type: Boolean, default: true }, |
||||
perms: String, |
||||
focus: Object, |
||||
large: Boolean, |
||||
labelPosition: String, |
||||
labelWidth: String, |
||||
}, |
||||
emits: { |
||||
'update:modelValue': null, |
||||
finished: null, |
||||
beanChange: null, |
||||
beforeSubmit: null, |
||||
}, |
||||
setup(props, { emit }) { |
||||
const { name, beanId, beanIds, focus, modelValue: visible } = toRefs(props); |
||||
const { t } = useI18n(); |
||||
const loading = ref<boolean>(false); |
||||
const buttonLoading = ref<boolean>(false); |
||||
const continuous = ref<boolean>(getContinuous(name.value)); |
||||
const reseted = ref<boolean>(false); |
||||
const unsaved = ref<boolean>(false); |
||||
const form = ref<any>(); |
||||
const values = ref<any>(props.initValues()); |
||||
const bean = ref<any>({}); |
||||
const id = ref<any>(); |
||||
const ids = ref<Array<any>>([]); |
||||
const isEdit = computed(() => id.value != null); |
||||
const title = computed(() => `${name.value} - ${isEdit.value ? `${t('edit')} (ID: ${id.value})` : `${t('add')}`}`); |
||||
const idChanged = async () => { |
||||
loading.value = true; |
||||
unsaved.value = false; |
||||
reseted.value = true; |
||||
try { |
||||
bean.value = id.value != null ? await props.queryBean(id.value) : {}; |
||||
values.value = id.value != null ? props.toValues(bean.value) : props.initValues(values.value); |
||||
emit('beanChange', bean.value); |
||||
form.value.resetFields(); |
||||
} finally { |
||||
loading.value = false; |
||||
} |
||||
}; |
||||
watch(visible, () => { |
||||
if (visible.value) { |
||||
ids.value = beanIds.value; |
||||
if (id.value !== beanId.value) { |
||||
id.value = beanId.value; |
||||
} else if (id.value != null) { |
||||
idChanged(); |
||||
} |
||||
if (id.value == null) { |
||||
reseted.value = true; |
||||
values.value = props.initValues(); |
||||
} |
||||
} |
||||
}); |
||||
watch(id, () => { |
||||
idChanged(); |
||||
}); |
||||
watch( |
||||
// 监听对象必须使用 lodash 的深度拷贝,才能在监听里面获取旧值和新值。 |
||||
// 参考文档:https://v3.vuejs.org/guide/reactivity-computed-watchers.html#watching-reactive-objects |
||||
() => _.cloneDeep(values.value), |
||||
(curr, prev) => { |
||||
// 重置触发的bean改动,不标记为未保存。 |
||||
if (reseted.value) { |
||||
reseted.value = false; |
||||
} else if (JSON.stringify(curr) !== JSON.stringify(prev)) { |
||||
// 自定义字段改变页面时,会改变values.customs,加上字段的值,但这些值为undefined。 |
||||
// 这会触发watch监听,但json值不变。如json值不变,则不修改unsaved状态。 |
||||
unsaved.value = true; |
||||
} |
||||
}, |
||||
{ deep: true }, |
||||
); |
||||
watch(continuous, () => setContinuous(name.value, continuous.value)); |
||||
const index = computed(() => ids.value.indexOf(id.value)); |
||||
const hasPrev = computed(() => index.value > 0); |
||||
const hasNext = computed(() => index.value < ids.value.length - 1); |
||||
const handlePrev = () => { |
||||
if (hasPrev.value) { |
||||
id.value = ids.value[index.value - 1]; |
||||
} |
||||
}; |
||||
const handleNext = () => { |
||||
if (hasNext.value) { |
||||
id.value = ids.value[index.value + 1]; |
||||
} |
||||
}; |
||||
const handleAdd = () => { |
||||
// eslint-disable-next-line no-unused-expressions |
||||
focus?.value?.focus?.(); |
||||
id.value = undefined; |
||||
}; |
||||
const handleCancel = () => { |
||||
emit('update:modelValue', false); |
||||
}; |
||||
const handleSubmit = () => { |
||||
form.value.validate(async (valid: boolean) => { |
||||
if (!valid) return; |
||||
buttonLoading.value = true; |
||||
try { |
||||
emit('beforeSubmit', values.value); |
||||
if (isEdit.value) { |
||||
await props.updateBean(values.value); |
||||
unsaved.value = false; |
||||
} else { |
||||
await props.createBean(values.value); |
||||
// eslint-disable-next-line no-unused-expressions |
||||
focus?.value?.focus?.(); |
||||
unsaved.value = false; |
||||
reseted.value = true; |
||||
values.value = props.initValues(values.value); |
||||
form.value.resetFields(); |
||||
} |
||||
ElMessage.success(t('success')); |
||||
if (!continuous.value) emit('update:modelValue', false); |
||||
emit('finished', bean.value); |
||||
} finally { |
||||
buttonLoading.value = false; |
||||
} |
||||
}); |
||||
}; |
||||
const handleDelete = async () => { |
||||
buttonLoading.value = true; |
||||
try { |
||||
await props.deleteBean([id.value]); |
||||
if (!continuous.value) emit('update:modelValue', false); |
||||
if (hasNext.value) { |
||||
handleNext(); |
||||
ids.value.splice(index.value - 1, 1); |
||||
} else if (hasPrev.value) { |
||||
handlePrev(); |
||||
ids.value.splice(index.value + 1, 1); |
||||
} else { |
||||
emit('update:modelValue', false); |
||||
} |
||||
ElMessage.success(t('success')); |
||||
emit('finished'); |
||||
} finally { |
||||
buttonLoading.value = false; |
||||
} |
||||
}; |
||||
const setUnsaved = (bool: boolean) => { |
||||
unsaved.value = bool; |
||||
}; |
||||
return { |
||||
perm, |
||||
handleAdd, |
||||
handleDelete, |
||||
handleSubmit, |
||||
handleCancel, |
||||
handlePrev, |
||||
handleNext, |
||||
hasPrev, |
||||
hasNext, |
||||
loading, |
||||
buttonLoading, |
||||
continuous, |
||||
unsaved, |
||||
isEdit, |
||||
form, |
||||
values, |
||||
bean, |
||||
id, |
||||
title, |
||||
setUnsaved, |
||||
Plus, |
||||
Delete, |
||||
}; |
||||
}, |
||||
}); |
||||
</script> |
@ -0,0 +1,30 @@ |
||||
<template> |
||||
<p>{{ t('hello') }}</p> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import { defineComponent } from 'vue'; |
||||
import { useI18n } from 'vue-i18n'; |
||||
|
||||
export default defineComponent({ |
||||
name: 'HelloI18n', |
||||
setup() { |
||||
const { t } = useI18n({ |
||||
inheritLocale: true, |
||||
useScope: 'local', |
||||
}); |
||||
|
||||
// Something todo .. |
||||
|
||||
return { t }; |
||||
}, |
||||
}); |
||||
</script> |
||||
|
||||
<i18n> |
||||
{ |
||||
"en": { |
||||
"hello": "Hello i18n in SFC!" |
||||
} |
||||
} |
||||
</i18n> |
@ -0,0 +1,17 @@ |
||||
<template> |
||||
{{ label ?? $t(message) }} |
||||
<el-tooltip :content="tooltip ?? $t(message + '.tooltip')" placement="top"> |
||||
<el-icon class="text-base align-text-top"><question-filled /></el-icon> |
||||
</el-tooltip> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { defineProps } from 'vue'; |
||||
import { QuestionFilled } from '@element-plus/icons-vue'; |
||||
|
||||
defineProps({ |
||||
label: { type: String }, |
||||
tooltip: { type: String }, |
||||
message: { type: String, required: true }, |
||||
}); |
||||
</script> |
@ -0,0 +1,20 @@ |
||||
<template> |
||||
<el-button-group> |
||||
<el-button :disabled="disabled" :icon="Top" @click="$emit('move', 'top')">{{ $t('moveTop') }}</el-button> |
||||
<el-button :disabled="disabled" :icon="ArrowUp" @click="$emit('move', 'up')">{{ $t('moveUp') }}</el-button> |
||||
<el-button :disabled="disabled" :icon="ArrowDown" @click="$emit('move', 'down')">{{ $t('moveDown') }}</el-button> |
||||
<el-button :disabled="disabled" :icon="Bottom" @click="$emit('move', 'bottom')">{{ $t('moveBottom') }}</el-button> |
||||
</el-button-group> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { defineProps, defineEmits } from 'vue'; |
||||
import { Top, Bottom, ArrowUp, ArrowDown } from '@element-plus/icons-vue'; |
||||
|
||||
defineProps({ |
||||
disabled: { type: Boolean, required: true }, |
||||
}); |
||||
defineEmits({ |
||||
move: null, |
||||
}); |
||||
</script> |
@ -0,0 +1,59 @@ |
||||
<template> |
||||
<form class="flex"> |
||||
<div class="space-y-1"> |
||||
<div v-for="(name, index) in names" :key="name" class="flex"> |
||||
<el-button :icon="index == 0 ? Plus : Minus" @click="handelRow(index)" :disabled="index <= 0 && remains.length <= 0" circle></el-button> |
||||
<el-select v-model="names[index]" @change="clearParams()" class="w-36"> |
||||
<el-option v-for="item in data.filter((it) => it.name === names[index] || remains.includes(it))" :key="item.name" :label="item.label" :value="item.name"></el-option> |
||||
</el-select> |
||||
<query-input :inputs="inputs" :name="names[index]"></query-input> |
||||
</div> |
||||
</div> |
||||
<div> |
||||
<el-button-group class="ml-2"> |
||||
<el-button native-type="submit" :icon="Search" @click.prevent="$emit('search')">{{ $t('search') }}</el-button> |
||||
<el-button :icon="Refresh" @click="$emit('reset')">{{ $t('reset') }}</el-button> |
||||
</el-button-group> |
||||
</div> |
||||
</form> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { defineProps, defineEmits, useSlots, provide, computed, ref, toRefs } from 'vue'; |
||||
import { Plus, Minus, Search, Refresh } from '@element-plus/icons-vue'; |
||||
import QueryInput from './QueryInput.vue'; |
||||
|
||||
const props = defineProps({ params: { type: Object, required: true } }); |
||||
const { params } = toRefs(props); |
||||
const slots = useSlots(); |
||||
provide('params', params); |
||||
defineEmits({ |
||||
search: null, |
||||
reset: null, |
||||
}); |
||||
|
||||
const data = ref<any[]>([]); |
||||
const inputs = ref<any[]>([]); |
||||
inputs.value = slots.default?.() ?? []; |
||||
data.value = inputs.value.map((item) => ({ label: item.props?.label, name: item.props?.name })); |
||||
|
||||
const [first] = data.value; |
||||
const names = ref<string[]>([first.name]); |
||||
const remains = computed(() => data.value.filter((it) => !names.value.includes(it.name))); |
||||
const clearParams = () => { |
||||
Object.keys(params.value).forEach((key) => { |
||||
if (!names.value.includes(key) && names.value.findIndex((item) => item.split(',').includes(key)) === -1) { |
||||
delete params.value[key]; |
||||
} |
||||
}); |
||||
}; |
||||
const handelRow = (index: number) => { |
||||
if (index === 0) { |
||||
const [item] = remains.value; |
||||
names.value[names.value.length] = item.name; |
||||
} else { |
||||
names.value.splice(index, 1); |
||||
clearParams(); |
||||
} |
||||
}; |
||||
</script> |
@ -0,0 +1,16 @@ |
||||
<script lang="ts"> |
||||
import { defineComponent, computed, toRefs } from 'vue'; |
||||
|
||||
export default defineComponent({ |
||||
name: 'QueryInput', |
||||
props: { inputs: { type: Array, required: true }, name: { type: String, required: true } }, |
||||
setup(props) { |
||||
const { inputs, name } = toRefs(props); |
||||
const input = computed(() => inputs.value.find((item: any) => item.props.name === name.value)); |
||||
return { input }; |
||||
}, |
||||
render() { |
||||
return this.input; |
||||
}, |
||||
}); |
||||
</script> |
@ -0,0 +1,55 @@ |
||||
<template> |
||||
<slot> |
||||
<div v-if="type === 'number'" class="inline-block"> |
||||
<el-input-number v-model="params[first]" :placeholder="$t('begin.number')" class="w-48"></el-input-number> |
||||
<el-input-number v-model="params[second]" :placeholder="$t('end.number')" class="w-48"></el-input-number> |
||||
</div> |
||||
<el-date-picker |
||||
v-else-if="type === 'date'" |
||||
v-model="params[name]" |
||||
type="daterange" |
||||
:start-placeholder="$t('begin.date')" |
||||
:end-placeholder="$t('end.date')" |
||||
class="w-96" |
||||
></el-date-picker> |
||||
<el-date-picker |
||||
v-else-if="type === 'datetime'" |
||||
v-model="params[name]" |
||||
type="datetimerange" |
||||
:start-placeholder="$t('begin.date')" |
||||
:end-placeholder="$t('end.date')" |
||||
class="w-96" |
||||
> |
||||
</el-date-picker> |
||||
<!-- |
||||
<div v-else-if="type === 'date'" class="inline-block"> |
||||
<el-date-picker v-model="params[first]" type="date" :placeholder="$t('begin.date')" class="w-48"></el-date-picker> |
||||
<el-date-picker v-model="params[second]" type="date" :placeholder="$t('end.date')" class="w-48"></el-date-picker> |
||||
</div> |
||||
<div v-else-if="type === 'datetime'" class="inline-block"> |
||||
<el-date-picker v-model="params[first]" type="datetime" class="w-48"></el-date-picker> |
||||
<el-date-picker v-model="params[second]" type="datetime" class="w-48"></el-date-picker> |
||||
</div> |
||||
--> |
||||
<el-select v-else-if="options" v-model="params[name]" multiple class="w-96"> |
||||
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"></el-option> |
||||
</el-select> |
||||
<el-input v-else v-model="params[name]" class="w-96"></el-input> |
||||
</slot> |
||||
</template> |
||||
<script setup lang="ts"> |
||||
import { inject, defineProps, PropType, ref, toRefs } from 'vue'; |
||||
|
||||
const props = defineProps({ |
||||
label: { type: String, required: true }, |
||||
name: { type: String, required: true }, |
||||
// 'string' | 'date' | 'datetime' | 'number' |
||||
type: { type: String }, |
||||
options: { type: Object as PropType<Array<{ label: string; value: string | number }>> }, |
||||
}); |
||||
const params = inject<any>('params'); |
||||
const { name } = toRefs(props); |
||||
const [firstName, secondName] = name.value.split(','); |
||||
const first = ref<string>(firstName); |
||||
const second = ref<string>(secondName); |
||||
</script> |
@ -0,0 +1,2 @@ |
||||
export { default as QueryForm } from './QueryForm.vue'; |
||||
export { default as QueryItem } from './QueryItem.vue'; |
@ -0,0 +1,45 @@ |
||||
<script lang="ts"> |
||||
import { computed, defineComponent, toRefs } from 'vue'; |
||||
import { useI18n } from 'vue-i18n'; |
||||
import { ColumnState, getColumnSettings, setColumnOrigins } from './useColumns'; |
||||
|
||||
export default defineComponent({ |
||||
name: 'ColumnList', |
||||
props: { name: { type: String, required: true } }, |
||||
setup(props, { slots }) { |
||||
const { name } = toRefs(props); |
||||
const { t } = useI18n(); |
||||
const slotColumns = slots.default?.() ?? []; |
||||
// 获取栏目名称 |
||||
const getColumnTitle = (columnProps: any) => { |
||||
// 如果是checkbox列,则名称为“选择框” |
||||
if (columnProps?.type === 'selection') return t('table.selection'); |
||||
return columnProps?.label; |
||||
}; |
||||
// 获取el-table-column的名称、是否显示 |
||||
const origins: ColumnState[] = slotColumns.map((column) => ({ title: getColumnTitle(column.props), display: column.props?.display !== 'none' })); |
||||
setColumnOrigins(name.value, origins); |
||||
|
||||
const settings = getColumnSettings(name.value); |
||||
const columns = computed(() => |
||||
slotColumns |
||||
.filter((column) => { |
||||
const matched = settings.value.find((item) => getColumnTitle(column.props) === item.title); |
||||
return !matched || matched.display; |
||||
}) |
||||
.map((column) => ({ ...column, key: getColumnTitle(column.props) })) |
||||
.sort((a, b) => { |
||||
let indexA = settings.value.findIndex((item) => item.title === getColumnTitle(a)); |
||||
if (indexA < 0) indexA = slotColumns.findIndex((item) => getColumnTitle(item) === getColumnTitle(a)); |
||||
let indexB = settings.value.findIndex((item) => item.title === getColumnTitle(b)); |
||||
if (indexB < 0) indexB = slotColumns.findIndex((item) => getColumnTitle(item) === getColumnTitle(b)); |
||||
return indexA - indexB; |
||||
}), |
||||
); |
||||
return { columns }; |
||||
}, |
||||
render() { |
||||
return this.columns; |
||||
}, |
||||
}); |
||||
</script> |
@ -0,0 +1,32 @@ |
||||
<template> |
||||
<el-dropdown class="align-middle" trigger="click" :hide-on-click="false"> |
||||
<el-tooltip :content="$t('table.columnsSetting')" placement="top"> |
||||
<el-icon class="text-base"><setting /></el-icon> |
||||
</el-tooltip> |
||||
<template #dropdown> |
||||
<el-dropdown-menu> |
||||
<el-dropdown-item> |
||||
<el-button @click="resetColumns" type="text">{{ $t('table.columnsReset') }}</el-button> |
||||
</el-dropdown-item> |
||||
<el-dropdown-item v-for="(column, index) in settings" :key="column.title" :divided="index === 0"> |
||||
<el-checkbox v-model="column.display">{{ column.title }}</el-checkbox> |
||||
</el-dropdown-item> |
||||
</el-dropdown-menu> |
||||
</template> |
||||
</el-dropdown> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { defineProps, toRefs, watch } from 'vue'; |
||||
import { Setting } from '@element-plus/icons-vue'; |
||||
import { getColumnOrigins, getColumnSettings, mergeColumns, storeColumnSettings } from './useColumns'; |
||||
|
||||
const props = defineProps({ name: { type: String, required: true } }); |
||||
const { name } = toRefs(props); |
||||
const settings = getColumnSettings(name.value); |
||||
const origins = getColumnOrigins(name.value); |
||||
watch(settings, () => storeColumnSettings(), { deep: true }); |
||||
const resetColumns = () => { |
||||
settings.value = mergeColumns([], origins.value); |
||||
}; |
||||
</script> |
@ -0,0 +1,2 @@ |
||||
export { default as ColumnSetting } from './ColumnSetting.vue'; |
||||
export { default as ColumnList } from './ColumnList.vue'; |
@ -0,0 +1,55 @@ |
||||
import { reactive, toRef } from 'vue'; |
||||
|
||||
export interface ColumnState { |
||||
title: string; |
||||
display: boolean; |
||||
} |
||||
|
||||
const COLUMN_SETTINGS = 'ujcms_column_settings'; |
||||
|
||||
function fetchColumnSettings(): Record<string, ColumnState[]> { |
||||
const settings = localStorage.getItem(COLUMN_SETTINGS); |
||||
return settings ? JSON.parse(settings) : {}; |
||||
} |
||||
|
||||
const originStore: Record<string, ColumnState[]> = reactive({}); |
||||
const settingStore: Record<string, ColumnState[]> = reactive(fetchColumnSettings()); |
||||
|
||||
export function storeColumnSettings() { |
||||
localStorage.setItem(COLUMN_SETTINGS, JSON.stringify(settingStore)); |
||||
} |
||||
export const getColumnOrigins = (name: string) => { |
||||
if (!originStore[name]) originStore[name] = []; |
||||
return toRef(originStore, name); |
||||
}; |
||||
export const mergeColumns = (settings: ColumnState[], origins: ColumnState[]) => { |
||||
// 去除不存在的列
|
||||
for (let i = 0, len = settings.length; i < len; ) { |
||||
if (origins.findIndex((column) => column.title === settings[i].title) === -1) { |
||||
settings.splice(i, 1); |
||||
len -= 1; |
||||
} else { |
||||
i += 1; |
||||
} |
||||
} |
||||
// 增加未记录的列
|
||||
origins.forEach((column) => { |
||||
if (settings.findIndex((item) => item.title === column.title) === -1) { |
||||
settings.push({ ...column }); |
||||
} |
||||
}); |
||||
return settings; |
||||
}; |
||||
export const setColumnOrigins = (name: string, origins: ColumnState[]) => { |
||||
originStore[name] = origins; |
||||
if (!settingStore[name]) settingStore[name] = []; |
||||
const settings = settingStore[name]; |
||||
mergeColumns(settings, origins); |
||||
}; |
||||
export const getColumnSettings = (name: string) => { |
||||
if (!settingStore[name]) settingStore[name] = []; |
||||
return toRef(settingStore, name); |
||||
}; |
||||
// export const setColumnSettings = (name: string, settings: ColumnState[]) => {
|
||||
// settingStore[name] = settings;
|
||||
// };
|
@ -0,0 +1,300 @@ |
||||
<template> |
||||
<div> |
||||
<textarea :id="elementId" ref="element"></textarea> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import { defineComponent, ref, toRefs, watch, onMounted, onBeforeUnmount, onActivated, onDeactivated, PropType } from 'vue'; |
||||
import { useI18n } from 'vue-i18n'; |
||||
import { getAuthHeaders } from '@/utils/auth'; |
||||
import { imageUploadUrl, queryGlobalSettings } from '@/api/config'; |
||||
|
||||
// 参考:https://www.tiny.cloud/docs/advanced/usage-with-module-loaders/webpack/webpack_es6_npm/ |
||||
// 参考:https://github.com/tinymce/tinymce-vue/blob/main/src/main/ts/components/Editor.ts |
||||
// Import TinyMCE |
||||
import tinymce from 'tinymce'; |
||||
// Default icons are required for TinyMCE 5.3 or above |
||||
import 'tinymce/icons/default'; |
||||
// A theme is also required |
||||
import 'tinymce/themes/silver'; |
||||
// Any plugins you want to use has to be imported |
||||
import 'tinymce/plugins/advlist'; |
||||
// import 'tinymce/plugins/anchor'; |
||||
// import 'tinymce/plugins/autolink'; |
||||
import 'tinymce/plugins/autoresize'; |
||||
import 'tinymce/plugins/autosave'; |
||||
import 'tinymce/plugins/charmap'; |
||||
import 'tinymce/plugins/code'; |
||||
import 'tinymce/plugins/codesample'; |
||||
import 'tinymce/plugins/directionality'; |
||||
import 'tinymce/plugins/fullscreen'; |
||||
import 'tinymce/plugins/hr'; |
||||
// import 'tinymce/plugins/insertdatetime'; |
||||
import 'tinymce/plugins/image'; |
||||
import 'tinymce/plugins/imagetools'; |
||||
import 'tinymce/plugins/link'; |
||||
import 'tinymce/plugins/lists'; |
||||
import 'tinymce/plugins/media'; |
||||
// import 'tinymce/plugins/nonbreaking'; |
||||
// import 'tinymce/plugins/noneditable'; |
||||
import 'tinymce/plugins/pagebreak'; |
||||
import 'tinymce/plugins/paste'; |
||||
// import 'tinymce/plugins/preview'; |
||||
// import 'tinymce/plugins/print'; |
||||
import 'tinymce/plugins/quickbars'; |
||||
// import 'tinymce/plugins/save'; |
||||
import 'tinymce/plugins/searchreplace'; |
||||
// import 'tinymce/plugins/spellchecker'; |
||||
// import 'tinymce/plugins/tabfocus'; |
||||
import 'tinymce/plugins/table'; |
||||
// import 'tinymce/plugins/template'; |
||||
// import 'tinymce/plugins/textpattern'; |
||||
// import 'tinymce/plugins/toc'; |
||||
import 'tinymce/plugins/visualblocks'; |
||||
import 'tinymce/plugins/visualchars'; |
||||
// import 'tinymce/plugins/wordcount'; |
||||
|
||||
import { isTextarea, uuid, initEditor } from './utils'; |
||||
|
||||
export default defineComponent({ |
||||
name: 'Tinymce', |
||||
props: { |
||||
id: String, |
||||
initialValue: String, |
||||
modelValue: { type: String, default: '' }, |
||||
disabled: Boolean, |
||||
inline: Boolean, |
||||
init: Object, |
||||
modelEvents: [String, Array], |
||||
plugins: { type: [String, Array] as PropType<string | string[]> }, |
||||
toolbar: [String, Array], |
||||
outputFormat: { |
||||
type: String, |
||||
validator: (prop: string) => prop === 'html' || prop === 'text', |
||||
}, |
||||
}, |
||||
setup(props, ctx) { |
||||
const { disabled, modelValue } = toRefs(props); |
||||
const { t } = useI18n(); |
||||
const element = ref<any>(); |
||||
let vueEditor: any = null; |
||||
const elementId: string = props.id || uuid('tiny-vue'); |
||||
const inlineEditor: boolean = (props.init && props.init.inline) || props.inline; |
||||
const modelBind = !!ctx.attrs['onUpdate:modelValue']; |
||||
let mounting = true; |
||||
const initialValue: string = props.initialValue ? props.initialValue : ''; |
||||
let cache = ''; |
||||
const getContent = (isMounting: boolean): (() => string) => (modelBind ? () => (modelValue?.value ? modelValue.value : '') : () => (isMounting ? initialValue : cache)); |
||||
|
||||
const global = ref<any>(); |
||||
|
||||
const initWrapper = (): void => { |
||||
const content = getContent(mounting); |
||||
const publicPath = import.meta.env.VITE_PUBLIC_PATH; |
||||
const finalInit = { |
||||
language_url: `${publicPath}/tinymce/langs/zh_CN.js`, |
||||
language: 'zh_CN', |
||||
skin: 'oxide', |
||||
skin_url: `${publicPath}/tinymce/skins/ui/oxide`, |
||||
// 必须添加 '/tinymce/skins/content/default/content.min.css'。否则 fontselect 默认不显示“系统字体”。 |
||||
content_css: [`${publicPath}/tinymce/skins/ui/oxide/content.min.css`, `${publicPath}/tinymce/skins/content/default/content.min.css`], |
||||
menubar: false, |
||||
plugins: |
||||
'advlist autoresize autosave charmap code codesample directionality fullscreen hr image imagetools lists link media pagebreak paste quickbars ' + |
||||
'searchreplace table visualblocks visualchars', |
||||
toolbar: |
||||
'fullscreen code | bold italic underline strikethrough | alignleft aligncenter alignright alignjustify | selectall removeformat pastetext | ' + |
||||
'quickimage media link | blockquote codesample table | bullist numlist | outdent indent lineheight | forecolor backcolor | fontselect fontsizeselect formatselect | ' + |
||||
'superscript subscript charmap | hr | ltr rtl | visualblocks visualchars | restoredraft undo redo | searchreplace', |
||||
font_formats: |
||||
'宋体=SimSun; 微软雅黑=Microsoft YaHei; 楷体=SimKai,KaiTi; 黑体=SimHei; 隶书=SimLi,LiSu; Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;' + |
||||
'Arial Black=arial black,avant garde;Comic Sans MS=comic sans ms,sans-serif;Helvetica=helvetica;Impact=impact,chicago;Times New Roman=times new roman,times', |
||||
quickbars_selection_toolbar: 'bold italic | h2 h3 blockquote', |
||||
quickbars_insert_toolbar: false, |
||||
paste_data_images: true, |
||||
image_uploadtab: false, |
||||
image_advtab: true, |
||||
image_caption: true, |
||||
images_file_types: global.value.upload.imageTypes, |
||||
min_height: 300, |
||||
max_height: 500, |
||||
convert_urls: false, |
||||
autosave_ask_before_unload: false, |
||||
...props.init, |
||||
images_upload_handler(blobInfo: any, success: any, failure: any, progress: any) { |
||||
const fileSizeLimitByte = global.value.upload.imageLimitByte; |
||||
if (fileSizeLimitByte > 0 && blobInfo.blob().size > fileSizeLimitByte) { |
||||
failure(t('error.fileMaxSize', { size: `${fileSizeLimitByte / 1024 / 1024}MB` }), { remove: true }); |
||||
return; |
||||
} |
||||
const xhr = new XMLHttpRequest(); |
||||
xhr.open('POST', imageUploadUrl); |
||||
|
||||
xhr.upload.onprogress = (e) => { |
||||
progress((e.loaded / e.total) * 100); |
||||
}; |
||||
|
||||
xhr.onload = () => { |
||||
if (xhr.status === 403) { |
||||
failure(`HTTP Error: ${xhr.status}`, { remove: true }); |
||||
return; |
||||
} |
||||
|
||||
if (xhr.status < 200 || xhr.status >= 300) { |
||||
failure(`HTTP Error: ${xhr.status}`, { remove: true }); |
||||
return; |
||||
} |
||||
|
||||
const json = JSON.parse(xhr.responseText); |
||||
|
||||
if (!json || typeof json.url !== 'string') { |
||||
failure(`Invalid JSON: ${xhr.responseText}`, { remove: true }); |
||||
return; |
||||
} |
||||
success(json.url); |
||||
}; |
||||
|
||||
xhr.onerror = () => { |
||||
failure(`Image upload failed due to a XHR Transport error. Code: ${xhr.status}`, { remove: true }); |
||||
}; |
||||
|
||||
const formData = new FormData(); |
||||
formData.append('file', blobInfo.blob(), blobInfo.filename()); |
||||
|
||||
Object.entries(getAuthHeaders()).forEach(([key, value]: any) => xhr.setRequestHeader(key, value)); |
||||
xhr.send(formData); |
||||
}, |
||||
|
||||
file_picker_callback(callback: any, val: any, meta: any) { |
||||
const input = document.createElement('input'); |
||||
input.setAttribute('type', 'file'); |
||||
|
||||
let fileSizeLimtByte = 0; |
||||
if (meta.filetype === 'image') { |
||||
fileSizeLimtByte = global.value.upload.imageLimitByte; |
||||
input.setAttribute('accept', global.value.upload.imageInputAccept); |
||||
// input.setAttribute('accept', 'image/*'); |
||||
} else if (meta.filetype === 'media') { |
||||
fileSizeLimtByte = global.value.upload.videoLimitByte; |
||||
input.setAttribute('accept', global.value.upload.videoInputAccept); |
||||
// input.setAttribute('accept', 'video/*'); |
||||
} else { |
||||
fileSizeLimtByte = global.value.upload.fileLimitByte; |
||||
input.setAttribute('accept', global.value.upload.fileInputAccept); |
||||
} |
||||
|
||||
/* |
||||
Note: In modern browsers input[type="file"] is functional without |
||||
even adding it to the DOM, but that might not be the case in some older |
||||
or quirky browsers like IE, so you might want to add it to the DOM |
||||
just in case, and visually hide it. And do not forget do remove it |
||||
once you do not need it anymore. |
||||
*/ |
||||
|
||||
input.onchange = (event: Event) => { |
||||
const { files } = event.target as HTMLInputElement; |
||||
const file = files?.item(0); |
||||
if (!file) return; |
||||
if (fileSizeLimtByte > 0 && file.size > fileSizeLimtByte) { |
||||
tinymce.activeEditor.windowManager.alert(t('error.fileMaxSize', { size: `${fileSizeLimtByte / 1024 / 1024}MB` })); |
||||
return; |
||||
} |
||||
const xhr = new XMLHttpRequest(); |
||||
xhr.open('POST', imageUploadUrl); |
||||
|
||||
// xhr.upload.onprogress = (e) => { |
||||
// progress((e.loaded / e.total) * 100); |
||||
// }; |
||||
|
||||
xhr.onload = () => { |
||||
if (xhr.status === 403) { |
||||
tinymce.activeEditor.windowManager.alert(`HTTP Error: ${xhr.status}`); |
||||
return; |
||||
} |
||||
|
||||
if (xhr.status < 200 || xhr.status >= 300) { |
||||
tinymce.activeEditor.windowManager.alert(`HTTP Error: ${xhr.status}`); |
||||
return; |
||||
} |
||||
|
||||
const json = JSON.parse(xhr.responseText); |
||||
|
||||
if (!json || typeof json.url !== 'string') { |
||||
tinymce.activeEditor.windowManager.alert(`Invalid JSON: ${xhr.responseText}`); |
||||
return; |
||||
} |
||||
|
||||
if (meta.filetype === 'image') { |
||||
callback(json.url, { alt: '' }); |
||||
} else if (meta.filetype === 'media') { |
||||
callback(json.url); |
||||
// callback('movie.mp4', { source2: 'alt.ogg', poster: 'image.jpg' }); |
||||
} else { |
||||
callback(json.url, { text: json.name }); |
||||
} |
||||
}; |
||||
|
||||
xhr.onerror = () => { |
||||
tinymce.activeEditor.windowManager.alert(`Image upload failed due to a XHR Transport error. Code: ${xhr.status}`); |
||||
}; |
||||
|
||||
const formData = new FormData(); |
||||
formData.append('file', file, file.name); |
||||
|
||||
Object.entries(getAuthHeaders()).forEach(([key, value]: any) => xhr.setRequestHeader(key, value)); |
||||
xhr.send(formData); |
||||
}; |
||||
|
||||
input.click(); |
||||
}, |
||||
|
||||
readonly: props.disabled, |
||||
selector: `#${elementId}`, |
||||
// plugins: mergePlugins(props.init && props.init.plugins, props.plugins), |
||||
// toolbar: props.toolbar || (props.init && props.init.toolbar), |
||||
inline: inlineEditor, |
||||
setup: (editor: any) => { |
||||
vueEditor = editor; |
||||
editor.on('init', (e: Event) => initEditor(e, props, ctx, editor, modelValue, content)); |
||||
if (props.init && typeof props.init.setup === 'function') { |
||||
props.init.setup(editor); |
||||
} |
||||
}, |
||||
branding: false, |
||||
}; |
||||
if (isTextarea(element.value)) { |
||||
element.value.style.visibility = ''; |
||||
} |
||||
tinymce.init({ toolbar_mode: 'sliding', ...finalInit }); |
||||
mounting = false; |
||||
}; |
||||
watch(disabled, (disable) => { |
||||
if (vueEditor != null) { |
||||
vueEditor.setMode(disable ? 'readonly' : 'design'); |
||||
} |
||||
}); |
||||
onMounted(async () => { |
||||
global.value = await queryGlobalSettings(); |
||||
initWrapper(); |
||||
}); |
||||
onBeforeUnmount(() => { |
||||
tinymce.remove(vueEditor); |
||||
}); |
||||
if (!inlineEditor) { |
||||
onActivated(() => { |
||||
if (!mounting) { |
||||
initWrapper(); |
||||
} |
||||
}); |
||||
onDeactivated(() => { |
||||
if (!modelBind) { |
||||
cache = vueEditor.getContent(); |
||||
} |
||||
tinymce.remove(vueEditor); |
||||
}); |
||||
} |
||||
return { element, elementId }; |
||||
}, |
||||
}); |
||||
</script> |
@ -0,0 +1,128 @@ |
||||
import { Ref, watch, SetupContext } from 'vue'; |
||||
|
||||
const validEvents = [ |
||||
'onActivate', |
||||
'onAddUndo', |
||||
'onBeforeAddUndo', |
||||
'onBeforeExecCommand', |
||||
'onBeforeGetContent', |
||||
'onBeforeRenderUI', |
||||
'onBeforeSetContent', |
||||
'onBeforePaste', |
||||
'onBlur', |
||||
'onChange', |
||||
'onClearUndos', |
||||
'onClick', |
||||
'onContextMenu', |
||||
'onCopy', |
||||
'onCut', |
||||
'onDblclick', |
||||
'onDeactivate', |
||||
'onDirty', |
||||
'onDrag', |
||||
'onDragDrop', |
||||
'onDragEnd', |
||||
'onDragGesture', |
||||
'onDragOver', |
||||
'onDrop', |
||||
'onExecCommand', |
||||
'onFocus', |
||||
'onFocusIn', |
||||
'onFocusOut', |
||||
'onGetContent', |
||||
'onHide', |
||||
'onInit', |
||||
'onKeyDown', |
||||
'onKeyPress', |
||||
'onKeyUp', |
||||
'onLoadContent', |
||||
'onMouseDown', |
||||
'onMouseEnter', |
||||
'onMouseLeave', |
||||
'onMouseMove', |
||||
'onMouseOut', |
||||
'onMouseOver', |
||||
'onMouseUp', |
||||
'onNodeChange', |
||||
'onObjectResizeStart', |
||||
'onObjectResized', |
||||
'onObjectSelected', |
||||
'onPaste', |
||||
'onPostProcess', |
||||
'onPostRender', |
||||
'onPreProcess', |
||||
'onProgressState', |
||||
'onRedo', |
||||
'onRemove', |
||||
'onReset', |
||||
'onSaveContent', |
||||
'onSelectionChange', |
||||
'onSetAttrib', |
||||
'onSetContent', |
||||
'onShow', |
||||
'onSubmit', |
||||
'onUndo', |
||||
'onVisualAid', |
||||
]; |
||||
|
||||
const isValidKey = (key: string): boolean => validEvents.map((event) => event.toLowerCase()).indexOf(key.toLowerCase()) !== -1; |
||||
|
||||
const bindHandlers = (initEvent: Event, listeners: any, editor: any): void => { |
||||
Object.keys(listeners) |
||||
.filter(isValidKey) |
||||
.forEach((key: string) => { |
||||
const handler = listeners[key]; |
||||
if (typeof handler === 'function') { |
||||
if (key === 'onInit') { |
||||
handler(initEvent, editor); |
||||
} else { |
||||
editor.on(key.substring(2), (e: any) => handler(e, editor)); |
||||
} |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
const bindModelHandlers = (props: any, ctx: SetupContext, editor: any, modelValue: Ref<string>): void => { |
||||
const modelEvents = props.modelEvents ? props.modelEvents : null; |
||||
const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents; |
||||
|
||||
watch(modelValue, (val: string, prevVal: string) => { |
||||
if (editor && typeof val === 'string' && val !== prevVal && val !== editor.getContent({ format: props.outputFormat })) { |
||||
editor.setContent(val); |
||||
} |
||||
}); |
||||
|
||||
editor.on(normalizedEvents ?? 'change input undo redo', () => { |
||||
ctx.emit('update:modelValue', editor.getContent({ format: props.outputFormat })); |
||||
}); |
||||
}; |
||||
|
||||
const initEditor = (initEvent: Event, props: any, ctx: SetupContext, editor: any, modelValue: Ref<string>, content: () => string): void => { |
||||
editor.setContent(content()); |
||||
if (ctx.attrs['onUpdate:modelValue']) { |
||||
bindModelHandlers(props, ctx, editor, modelValue); |
||||
} |
||||
bindHandlers(initEvent, ctx.attrs, editor); |
||||
}; |
||||
|
||||
let unique = 0; |
||||
const uuid = (prefix: string): string => { |
||||
const time = Date.now(); |
||||
const random = Math.floor(Math.random() * 1000000000); |
||||
unique += 1; |
||||
return `${prefix}_${random + unique}${String(time)}`; |
||||
}; |
||||
|
||||
const isTextarea = (element: Element | null): element is HTMLTextAreaElement => element !== null && element.tagName.toLowerCase() === 'textarea'; |
||||
|
||||
const normalizePluginArray = (plugins?: string | string[]): string[] => { |
||||
if (typeof plugins === 'undefined' || plugins === '') { |
||||
return []; |
||||
} |
||||
|
||||
return Array.isArray(plugins) ? plugins : plugins.split(' '); |
||||
}; |
||||
|
||||
const mergePlugins = (initPlugins: string | string[], inputPlugins?: string | string[]): string[] => normalizePluginArray(initPlugins).concat(normalizePluginArray(inputPlugins)); |
||||
|
||||
export { bindHandlers, bindModelHandlers, initEditor, isValidKey, uuid, isTextarea, mergePlugins }; |
@ -0,0 +1,118 @@ |
||||
<template> |
||||
<el-upload |
||||
:action="action" |
||||
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }" |
||||
:accept="accept" |
||||
:before-upload="beforeUpload" |
||||
:on-progress="(event, file) => (progressFile = file)" |
||||
:show-file-list="false" |
||||
:disabled="disabled" |
||||
:multiple="multiple" |
||||
> |
||||
<!-- |
||||
// 用于测试上传进度条 |
||||
action="https://jsonplaceholder.typicode.com/posts/" |
||||
--> |
||||
<el-button type="primary">{{ $t('clickToUpload') }}</el-button> |
||||
</el-upload> |
||||
<el-progress v-if="progressFile.status === 'uploading'" :percentage="parseInt(progressFile.percentage, 10)"></el-progress> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import { defineComponent, onMounted, ref, toRefs, computed } from 'vue'; |
||||
import { ElMessage } from 'element-plus'; |
||||
import { useI18n } from 'vue-i18n'; |
||||
import { getAuthHeaders } from '@/utils/auth'; |
||||
import { getSiteHeaders } from '@/utils/common'; |
||||
import { imageUploadUrl, videoUploadUrl, docUploadUrl, fileUploadUrl, queryGlobalSettings } from '@/api/config'; |
||||
|
||||
export default defineComponent({ |
||||
name: 'BaseUpload', |
||||
props: { |
||||
type: { |
||||
type: String, |
||||
default: 'file', |
||||
validator: (value: string) => ['image', 'video', 'doc', 'file'].includes(value), |
||||
}, |
||||
uploadAction: { type: String }, |
||||
fileAccept: { type: String }, |
||||
fileMaxSize: { type: Number }, |
||||
multiple: { type: Boolean }, |
||||
disabled: { type: Boolean, default: false }, |
||||
'on-success': { type: Function }, |
||||
}, |
||||
setup(props) { |
||||
const { type, uploadAction, fileAccept, fileMaxSize } = toRefs(props); |
||||
const { t } = useI18n(); |
||||
const progressFile = ref<any>({}); |
||||
const global = ref<any>(); |
||||
const fetchGlobalSettings = async () => { |
||||
global.value = await queryGlobalSettings(); |
||||
}; |
||||
onMounted(() => { |
||||
fetchGlobalSettings(); |
||||
}); |
||||
const action = computed(() => { |
||||
if (uploadAction?.value != null) { |
||||
return uploadAction.value; |
||||
} |
||||
switch (type.value) { |
||||
case 'image': |
||||
return imageUploadUrl; |
||||
case 'video': |
||||
return videoUploadUrl; |
||||
case 'doc': |
||||
return docUploadUrl; |
||||
case 'file': |
||||
return fileUploadUrl; |
||||
default: |
||||
throw new Error(`Type not support: ${type.value}`); |
||||
} |
||||
}); |
||||
const accept = computed(() => { |
||||
if (fileAccept?.value != null) { |
||||
return fileAccept.value; |
||||
} |
||||
switch (type.value) { |
||||
case 'image': |
||||
return global?.value?.upload?.imageInputAccept ?? '.jpg,.jpeg,.png,.gif'; |
||||
case 'video': |
||||
return global?.value?.upload?.videoInputAccept ?? '.mp4,.m3u8'; |
||||
case 'doc': |
||||
return global?.value?.upload?.docInputAccept ?? '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx'; |
||||
case 'file': |
||||
return global?.value?.upload?.fileInputAccept ?? '.zip,.7z,.gz,.bz2,.iso,.rar,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.mp4,.m3u8,.mp3,.ogg'; |
||||
default: |
||||
throw new Error(`Type not support: ${type.value}`); |
||||
} |
||||
}); |
||||
const maxSize = computed(() => { |
||||
if (fileMaxSize?.value != null) { |
||||
return fileMaxSize.value; |
||||
} |
||||
switch (type.value) { |
||||
case 'image': |
||||
return global?.value?.upload?.imageLimitByte ?? 0; |
||||
case 'video': |
||||
return global?.value?.upload?.videoLimitByte ?? 0; |
||||
case 'doc': |
||||
return global?.value?.upload?.docLimitByte ?? 0; |
||||
case 'file': |
||||
return global?.value?.upload?.fileLimitByte ?? 0; |
||||
default: |
||||
throw new Error(`Type not support: ${type.value}`); |
||||
} |
||||
}); |
||||
const beforeUpload = (file: any) => { |
||||
if (maxSize.value > 0 && file.size > maxSize.value) { |
||||
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024}MB` })); |
||||
return false; |
||||
} |
||||
return true; |
||||
}; |
||||
return { progressFile, getAuthHeaders, getSiteHeaders, action, accept, beforeUpload }; |
||||
}, |
||||
}); |
||||
</script> |
||||
|
||||
<style lang="scss" scoped></style> |
@ -0,0 +1,107 @@ |
||||
<template> |
||||
<div class="w-full"> |
||||
<el-upload |
||||
:action="fileUploadUrl" |
||||
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }" |
||||
:accept="accept" |
||||
:before-upload="beforeUpload" |
||||
:on-success="(res) => fileList.push({ name: res.name, url: res.url, length: res.size })" |
||||
:on-progress="(event, file) => (progressFile = file)" |
||||
:show-file-list="false" |
||||
multiple |
||||
> |
||||
<!-- |
||||
action="https://jsonplaceholder.typicode.com/posts/" |
||||
--> |
||||
<el-button type="primary">{{ $t('clickToUpload') }}</el-button> |
||||
</el-upload> |
||||
<el-progress v-if="progressFile.status === 'uploading'" :percentage="parseInt(progressFile.percentage, 10)"></el-progress> |
||||
<transition-group tag="ul" :class="['el-upload-list', 'el-upload-list--text', { 'is-disabled': disabled }]" name="el-list"> |
||||
<li v-for="file in fileList" :key="file.url" class="el-upload-list__item is-success"> |
||||
<a class="el-upload-list__item-name" @click="handlePreview(file)"> |
||||
<el-icon class="el-icon--document"><Document /></el-icon>{{ file.name }} |
||||
</a> |
||||
<label class="el-upload-list__item-status-label"> |
||||
<el-icon class="el-icon--upload-success el-icon--circle-check"><CircleCheck /></el-icon> |
||||
</label> |
||||
<el-icon v-if="!disabled" class="el-icon--close" @click="fileList.splice(fileList.indexOf(file), 1)"><Close /></el-icon> |
||||
</li> |
||||
</transition-group> |
||||
<el-dialog :title="$t('article.fileList.attribute')" v-model="previewVisible" top="5vh" :width="768" append-to-body> |
||||
<el-form ref="form" :model="previewFile" label-width="150px"> |
||||
<el-form-item prop="name" :label="$t('name')" :rules="{ required: true, message: () => $t('v.required') }"> |
||||
<el-input v-model="previewFile.name" maxlength="100"></el-input> |
||||
</el-form-item> |
||||
<el-form-item prop="length" :label="$t('size')" :rules="{ required: true, message: () => $t('v.required') }"> |
||||
<el-input v-model="previewFile.length" maxlength="19"> |
||||
<template #append>Byte</template> |
||||
</el-input> |
||||
</el-form-item> |
||||
<el-form-item prop="url" label="URL" :rules="{ required: true, message: () => $t('v.required') }"> |
||||
<el-input v-model="previewFile.url" maxlength="255"></el-input> |
||||
</el-form-item> |
||||
<el-button @click.prevent="handleSubmit()" type="primary" native-type="submit">{{ $t('submit') }}</el-button> |
||||
</el-form> |
||||
</el-dialog> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { defineProps, defineEmits, onMounted, ref, toRefs, computed } from 'vue'; |
||||
import { ElMessage } from 'element-plus'; |
||||
import { Close, Document, CircleCheck } from '@element-plus/icons-vue'; |
||||
import { useI18n } from 'vue-i18n'; |
||||
import { getAuthHeaders } from '@/utils/auth'; |
||||
import { getSiteHeaders } from '@/utils/common'; |
||||
import { fileUploadUrl, queryGlobalSettings } from '@/api/config'; |
||||
|
||||
const props = defineProps({ |
||||
modelValue: { type: Array, default: () => [] }, |
||||
fileAccept: { type: String }, |
||||
fileMaxSize: { type: Number }, |
||||
disabled: { type: Boolean, default: false }, |
||||
}); |
||||
const emit = defineEmits({ 'update:modelValue': null }); |
||||
|
||||
const { fileAccept, fileMaxSize } = toRefs(props); |
||||
const { t } = useI18n(); |
||||
const { modelValue } = toRefs(props); |
||||
const progressFile = ref<any>({}); |
||||
const fileList = computed({ |
||||
get: (): any[] => modelValue.value, |
||||
set: (val) => emit('update:modelValue', val), |
||||
}); |
||||
const previewVisible = ref<boolean>(false); |
||||
const previewFile = ref<any>({}); |
||||
const form = ref<any>(); |
||||
|
||||
const handlePreview = (file: any) => { |
||||
previewFile.value = file; |
||||
previewVisible.value = true; |
||||
}; |
||||
const handleSubmit = () => { |
||||
form.value.validate(async (valid: boolean) => { |
||||
if (!valid) return; |
||||
previewVisible.value = false; |
||||
}); |
||||
}; |
||||
const global = ref<any>(); |
||||
const fetchGlobalSettings = async () => { |
||||
global.value = await queryGlobalSettings(); |
||||
}; |
||||
onMounted(() => { |
||||
fetchGlobalSettings(); |
||||
}); |
||||
const defaultAccept = '.zip,.7z,.gz,.bz2,.iso,.rar,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.mp4,.m3u8,.mp3,.ogg'; |
||||
const accept = computed(() => fileAccept?.value ?? global?.value?.upload?.fileInputAccept ?? defaultAccept); |
||||
const maxSize = computed(() => fileMaxSize?.value ?? global?.value?.upload?.fileLimitByte ?? 0); |
||||
const beforeUpload = (file: any) => { |
||||
if (maxSize.value > 0 && file.size > maxSize.value) { |
||||
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024}MB` })); |
||||
return false; |
||||
} |
||||
return true; |
||||
}; |
||||
</script> |
||||
|
||||
<style lang="scss" scoped></style> |
@ -0,0 +1,84 @@ |
||||
<template> |
||||
<el-dialog :title="$t('imageCrop')" v-model="visible" @closed="destroyCropper()" top="5vh" :width="768" destroy-on-close append-to-body> |
||||
<div class="text-center"> |
||||
<img ref="imgRef" @load="initCropper()" :src="src" alt="" class="inline" style="max-height:410px" /> |
||||
</div> |
||||
<div class="text-right"> |
||||
<el-button @click.prevent="handleSubmit()" type="primary" native-type="submit" class="mt-4">{{ $t('submit') }}</el-button> |
||||
</div> |
||||
</el-dialog> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import { computed, defineComponent, ref, toRefs } from 'vue'; |
||||
import Cropper from 'cropperjs'; |
||||
import 'cropperjs/dist/cropper.css'; |
||||
import { cropImage } from '@/api/config'; |
||||
|
||||
export default defineComponent({ |
||||
name: 'ImageCropper', |
||||
props: { |
||||
modelValue: { type: Boolean, required: true }, |
||||
src: { type: String, default: null }, |
||||
width: { type: Number }, |
||||
height: { type: Number }, |
||||
thumbnailWidth: { type: Number }, |
||||
thumbnailHeight: { type: Number }, |
||||
}, |
||||
emits: { 'update:modelValue': null, success: null }, |
||||
setup(props, { emit }) { |
||||
const { modelValue, src, width, height, thumbnailWidth, thumbnailHeight } = toRefs(props); |
||||
const visible = computed({ |
||||
get: () => modelValue.value, |
||||
set: (val) => emit('update:modelValue', val), |
||||
}); |
||||
|
||||
const imgRef = ref<any>(); |
||||
const cropper = ref<any>(); |
||||
const cropParam = ref<any>({}); |
||||
|
||||
const initCropper = () => { |
||||
if (imgRef.value) { |
||||
cropper.value = new Cropper(imgRef.value, { |
||||
aspectRatio: width?.value && height?.value ? width.value / height.value : NaN, |
||||
autoCropArea: width?.value && height?.value ? 1 : 0.8, |
||||
viewMode: 1, |
||||
minCropBoxWidth: width?.value ?? 16, |
||||
minCropBoxHeight: height?.value ?? 16, |
||||
zoomable: false, |
||||
crop(event) { |
||||
cropParam.value.url = src.value; |
||||
cropParam.value.x = Math.floor(event.detail.x); |
||||
cropParam.value.y = Math.floor(event.detail.y); |
||||
cropParam.value.width = Math.floor(event.detail.width); |
||||
cropParam.value.height = Math.floor(event.detail.height); |
||||
cropParam.value.maxWidth = width?.value; |
||||
cropParam.value.maxHeight = height?.value; |
||||
cropParam.value.thumbnailWidth = thumbnailWidth?.value; |
||||
cropParam.value.thumbnailHeight = thumbnailHeight?.value; |
||||
}, |
||||
}); |
||||
} |
||||
}; |
||||
const destroyCropper = () => { |
||||
if (cropper.value) { |
||||
cropper.value.destroy(); |
||||
} |
||||
}; |
||||
const handleSubmit = async () => { |
||||
visible.value = false; |
||||
emit('success', (await cropImage(cropParam.value)).url); |
||||
}; |
||||
return { imgRef, visible, initCropper, destroyCropper, handleSubmit }; |
||||
}, |
||||
}); |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
/* Ensure the size of the image fit the container perfectly */ |
||||
:deep(img) { |
||||
display: block; |
||||
/* This rule is very important, please don't ignore this */ |
||||
max-width: 100%; |
||||
} |
||||
</style> |
@ -0,0 +1,130 @@ |
||||
<template> |
||||
<div> |
||||
<!-- <transition-group tag="ul" :class="['el-upload-list', 'el-upload-list--picture-card', { 'is-disabled': disabled }]" name="el-list"> --> |
||||
<ul :class="['el-upload-list', 'el-upload-list--picture-card', { 'is-disabled': disabled }]"> |
||||
<li v-for="file in fileList" :key="file.url" class="el-upload-list__item is-success"> |
||||
<div class="w-full h-full bg-gray-50 flex justify-center items-center"> |
||||
<img class="max-w-full max-h-full block" :src="file.url" alt="" /> |
||||
<div class="full-flex-center absolute rounded-md cursor-default bg-black bg-opacity-50 opacity-0 hover:opacity-100 space-x-4" @click.stop> |
||||
<el-icon class="image-action" @click="(cropperVisible = true), (currentFile = file)" :title="$t('cropImage')"><Crop /></el-icon> |
||||
<el-icon class="image-action" @click="handlePreview(file)" :title="$t('previewImage')"><View /></el-icon> |
||||
<el-icon class="image-action" @click="fileList.splice(fileList.indexOf(file), 1)" :title="$t('deleteImage')"><Delete /></el-icon> |
||||
</div> |
||||
</div> |
||||
</li> |
||||
</ul> |
||||
<!-- </transition-group> --> |
||||
<el-upload |
||||
:action="imageUploadUrl" |
||||
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }" |
||||
:data="getData()" |
||||
:accept="accept" |
||||
:before-upload="beforeUpload" |
||||
:on-success="(res, file) => fileList.push({ name: res.name, url: res.url })" |
||||
:on-progress="(event, file) => (progressFile = file)" |
||||
:show-file-list="false" |
||||
multiple |
||||
class="inline-block" |
||||
> |
||||
<el-progress v-if="progressFile.status === 'uploading'" type="circle" :percentage="parseInt(progressFile.percentage, 10)" /> |
||||
<div v-else class="el-upload--picture-card"> |
||||
<el-icon><Plus /></el-icon> |
||||
</div> |
||||
</el-upload> |
||||
<div> |
||||
<el-dialog v-model="previewVisible" top="5vh" :width="768"> |
||||
<el-input v-model="previewFile.url" maxlength="255"> |
||||
<template #prepend>URL</template> |
||||
</el-input> |
||||
<el-input v-model="previewFile.description" type="textarea" :rows="2" :placeholder="$t('article.imageList.description')" class="mt-1"></el-input> |
||||
<img :src="previewFile.url" alt="" class="mt-1 border border-gray-300" /> |
||||
</el-dialog> |
||||
</div> |
||||
<image-cropper |
||||
v-model="cropperVisible" |
||||
:src="currentFile.url" |
||||
:thumbnailWidth="thumbnailWidth" |
||||
:thumbnailHeight="thumbnailHeight" |
||||
@success="(url) => (currentFile.url = url)" |
||||
></image-cropper> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { defineProps, defineEmits, onMounted, ref, toRefs, computed } from 'vue'; |
||||
import { ElMessage } from 'element-plus'; |
||||
import { Plus, Crop, View, Delete } from '@element-plus/icons-vue'; |
||||
import { useI18n } from 'vue-i18n'; |
||||
import { getAuthHeaders } from '@/utils/auth'; |
||||
import { getSiteHeaders } from '@/utils/common'; |
||||
import { imageUploadUrl, queryGlobalSettings } from '@/api/config'; |
||||
import ImageCropper from './ImageCropper.vue'; |
||||
|
||||
const props = defineProps({ |
||||
modelValue: { type: Array, default: () => [] }, |
||||
fileAccept: { type: String }, |
||||
fileMaxSize: { type: Number }, |
||||
maxWidth: { type: Number }, |
||||
maxHeight: { type: Number }, |
||||
disabled: { type: Boolean, default: false }, |
||||
}); |
||||
|
||||
const emit = defineEmits({ 'update:modelValue': null }); |
||||
|
||||
const { modelValue, maxWidth, maxHeight, fileAccept, fileMaxSize } = toRefs(props); |
||||
const { t } = useI18n(); |
||||
const progressFile = ref<any>({}); |
||||
const currentFile = ref<any>({}); |
||||
const previewVisible = ref<boolean>(false); |
||||
const cropperVisible = ref<boolean>(false); |
||||
const previewFile = ref<any>({ src: 'data:;base64,=' }); |
||||
const fileList = computed({ |
||||
get: (): any => modelValue.value, |
||||
set: (val) => emit('update:modelValue', val), |
||||
}); |
||||
const handlePreview = (file: any) => { |
||||
previewFile.value = file; |
||||
previewVisible.value = true; |
||||
}; |
||||
const thumbnailWidth = 300; |
||||
const thumbnailHeight = 300; |
||||
const getData = () => { |
||||
const data: any = { isWatermark: true, thumbnailWidth, thumbnailHeight }; |
||||
if (maxWidth?.value != null) { |
||||
data.maxWidth = maxWidth.value; |
||||
} |
||||
if (maxHeight?.value != null) { |
||||
data.maxHeight = maxHeight.value; |
||||
} |
||||
return data; |
||||
}; |
||||
const global = ref<any>(); |
||||
const fetchGlobalSettings = async () => { |
||||
global.value = await queryGlobalSettings(); |
||||
}; |
||||
onMounted(() => { |
||||
fetchGlobalSettings(); |
||||
}); |
||||
const defaultAccept = 'image/jpg,image/jpeg,image/png,image/gif'; |
||||
const accept = computed(() => fileAccept?.value ?? global?.value?.upload?.imageInputAccept ?? defaultAccept); |
||||
const maxSize = computed(() => fileMaxSize?.value ?? global?.value?.upload?.imageLimitByte ?? 0); |
||||
const beforeUpload = (file: any) => { |
||||
if (maxSize.value > 0 && file.size > maxSize.value) { |
||||
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024}MB` })); |
||||
return false; |
||||
} |
||||
return true; |
||||
}; |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
:deep(.el-dialog__headerbtn) { |
||||
top: 4px; |
||||
} |
||||
.full-flex-center { |
||||
@apply w-full h-full flex justify-center items-center; |
||||
} |
||||
.image-action { |
||||
@apply cursor-pointer text-xl text-white; |
||||
} |
||||
</style> |
@ -0,0 +1,121 @@ |
||||
<template> |
||||
<el-upload |
||||
:action="imageUploadUrl" |
||||
:headers="{ ...getAuthHeaders(), ...getSiteHeaders() }" |
||||
:accept="accept" |
||||
:before-upload="beforeUpload" |
||||
:data="data" |
||||
:show-file-list="false" |
||||
:on-success="(res) => ((src = res.url), (cropperVisible = mode === 'manual'))" |
||||
:on-progress="(event, file) => (progressFile = file)" |
||||
> |
||||
<!-- |
||||
// 用于测试上传进度条 |
||||
action="https://jsonplaceholder.typicode.com/posts/" |
||||
--> |
||||
<div v-if="src" class="full-flex-center rounded-border relative hover:border-opacity-0"> |
||||
<img :src="src" class="max-w-full max-h-full block" /> |
||||
<div class="full-flex-center absolute rounded-md cursor-default bg-black bg-opacity-50 opacity-0 hover:opacity-100 space-x-4" @click.stop> |
||||
<el-icon class="image-action" @click="cropperVisible = true" :title="$t('cropImage')"><Crop /></el-icon> |
||||
<el-icon class="image-action" @click="previewVisible = true" :title="$t('previewImage')"><View /></el-icon> |
||||
<el-icon class="image-action" @click="src = undefined" :title="$t('deleteImage')"><Delete /></el-icon> |
||||
</div> |
||||
</div> |
||||
<el-progress v-else-if="progressFile.status === 'uploading'" type="circle" :percentage="parseInt(progressFile.percentage, 10)" /> |
||||
<div v-else class="el-upload--picture-card"> |
||||
<el-icon><plus /></el-icon> |
||||
</div> |
||||
</el-upload> |
||||
<div> |
||||
<el-dialog v-model="previewVisible" top="5vh" :width="768" append-to-body destroy-on-close> |
||||
<el-input v-model="src"> |
||||
<template #prepend>URL</template> |
||||
</el-input> |
||||
<img :src="src" alt="" class="mt-1 border border-gray-300" /> |
||||
</el-dialog> |
||||
</div> |
||||
<image-cropper v-model="cropperVisible" :src="src" :width="width" :height="height" @success="(url) => (src = url)"></image-cropper> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { defineProps, defineEmits, computed, onMounted, ref, toRefs } from 'vue'; |
||||
import { ElMessage } from 'element-plus'; |
||||
import { Plus, Crop, View, Delete } from '@element-plus/icons-vue'; |
||||
import { useI18n } from 'vue-i18n'; |
||||
import { getAuthHeaders } from '@/utils/auth'; |
||||
import { getSiteHeaders } from '@/utils/common'; |
||||
import { imageUploadUrl, queryGlobalSettings } from '@/api/config'; |
||||
import ImageCropper from './ImageCropper.vue'; |
||||
|
||||
// 'image/jpg,image/jpeg,image/png,image/gif' |
||||
|
||||
const props = defineProps({ |
||||
modelValue: { type: String, default: null }, |
||||
fileAccept: { type: String }, |
||||
fileMaxSize: { type: Number }, |
||||
width: { type: Number }, |
||||
height: { type: Number }, |
||||
mode: { type: String, default: 'none' }, |
||||
}); |
||||
|
||||
const emit = defineEmits({ 'update:modelValue': null }); |
||||
|
||||
const { modelValue, width, height, mode, fileAccept, fileMaxSize } = toRefs(props); |
||||
const { t } = useI18n(); |
||||
const progressFile = ref<any>({}); |
||||
const previewVisible = ref<boolean>(false); |
||||
const cropperVisible = ref<boolean>(false); |
||||
const src = computed({ |
||||
get: (): string | undefined => modelValue.value, |
||||
set: (val: string | undefined) => emit('update:modelValue', val), |
||||
}); |
||||
const resizable = computed(() => ['cut', 'resize'].includes(mode.value)); |
||||
const data = computed(() => { |
||||
const params: any = { resizeMode: mode.value === 'cut' ? 'cut' : 'normal' }; |
||||
if (width?.value != null) { |
||||
// 为0不限制,为空则依然受全局图片宽高限制 |
||||
params.maxWidth = resizable.value ? width.value : 0; |
||||
} |
||||
if (height?.value != null) { |
||||
// 为0不限制,为空则依然受全局图片宽高限制 |
||||
params.maxHeight = resizable.value ? height.value : 0; |
||||
} |
||||
return params; |
||||
}); |
||||
const global = ref<any>(); |
||||
const fetchGlobalSettings = async () => { |
||||
global.value = await queryGlobalSettings(); |
||||
}; |
||||
onMounted(() => { |
||||
fetchGlobalSettings(); |
||||
}); |
||||
const accept = computed(() => fileAccept?.value ?? global?.value?.upload?.imageInputAccept ?? 'image/jpg,image/jpeg,image/png,image/gif'); |
||||
const maxSize = computed(() => fileMaxSize?.value ?? global?.value?.upload?.imageLimitByte ?? 0); |
||||
const beforeUpload = (file: any) => { |
||||
if (maxSize.value > 0 && file.size > maxSize.value) { |
||||
ElMessage.error(t('error.fileMaxSize', { size: `${maxSize.value / 1024 / 1024}MB` })); |
||||
return false; |
||||
} |
||||
return true; |
||||
}; |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
:deep(.el-dialog__headerbtn) { |
||||
top: 4px; |
||||
} |
||||
:deep(.el-upload) { |
||||
width: 148px; |
||||
height: 148px; |
||||
} |
||||
.full-flex-center { |
||||
@apply w-full h-full flex justify-center items-center; |
||||
} |
||||
.rounded-border { |
||||
border: 1px solid #c0ccda; |
||||
@apply rounded-md bg-gray-50; |
||||
} |
||||
.image-action { |
||||
@apply cursor-pointer text-xl text-white; |
||||
} |
||||
</style> |
@ -0,0 +1,4 @@ |
||||
export { default as ImageUpload } from './ImageUpload.vue'; |
||||
export { default as ImageListUpload } from './ImageListUpload.vue'; |
||||
export { default as FileListUpload } from './FileListUpload.vue'; |
||||
export { default as BaseUpload } from './BaseUpload.vue'; |
@ -0,0 +1,335 @@ |
||||
import i18n from '@/i18n'; |
||||
|
||||
export function getPermsTreeData(): any[] { |
||||
const { |
||||
global: { t }, |
||||
} = i18n; |
||||
return [ |
||||
{ |
||||
label: t('menu.home'), |
||||
key: 'home', |
||||
perms: ['auth'], |
||||
children: [ |
||||
{ |
||||
label: t('menu.personal'), |
||||
key: 'personal', |
||||
children: [ |
||||
{ |
||||
label: t('menu.personal.password'), |
||||
key: 'password:update', |
||||
perms: ['password:update'], |
||||
}, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.content'), |
||||
key: 'content', |
||||
children: [ |
||||
{ |
||||
label: t('menu.content.article'), |
||||
key: 'article', |
||||
perms: ['article:page', 'article:list', 'channel:list', 'dict:list', 'model:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'article:page' }, |
||||
{ label: t('add'), key: 'article:create', perms: ['article:create'] }, |
||||
{ label: t('edit'), key: 'article:update', perms: ['article:update', 'article:show'] }, |
||||
{ label: t('delete'), key: 'article:delete', perms: ['article:delete'] }, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.content.channel'), |
||||
key: 'channel', |
||||
perms: ['channel:page', 'channel:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'channel:page' }, |
||||
{ label: t('add'), key: 'channel:create', perms: ['channel:create'] }, |
||||
{ label: t('edit'), key: 'channel:update', perms: ['channel:update', 'channel:show'] }, |
||||
{ label: t('delete'), key: 'channel:delete', perms: ['channel:delete'] }, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.content.blockItem'), |
||||
key: 'blockItem', |
||||
perms: ['blockItem:page', 'blockItem:list', 'block:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'blockItem:page' }, |
||||
{ label: t('add'), key: 'blockItem:create', perms: ['blockItem:create'] }, |
||||
{ label: t('edit'), key: 'blockItem:update', perms: ['blockItem:update', 'blockItem:show'] }, |
||||
{ label: t('delete'), key: 'blockItem:delete', perms: ['blockItem:delete'] }, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.content.attachment'), |
||||
key: 'attachment', |
||||
perms: ['attachment:page', 'attachment:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'attachment:page' }, |
||||
{ label: t('add'), key: 'attachment:create', perms: ['attachment:create'] }, |
||||
{ label: t('edit'), key: 'attachment:update', perms: ['attachment:update', 'attachment:show'] }, |
||||
{ label: t('delete'), key: 'attachment:delete', perms: ['attachment:delete'] }, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.content.generator'), |
||||
key: 'generator', |
||||
perms: ['generator:show', 'siteSettings:html:show', 'task:list', 'task:show', 'task:delete'], |
||||
children: [ |
||||
{ label: t('generator.op.fulltext.reindexAll'), key: 'generator:fulltext:reindexAll', perms: ['generator:fulltext:reindexAll'] }, |
||||
{ label: t('generator.op.fulltext.reindexSite'), key: 'generator:fulltext:reindexSite', perms: ['generator:fulltext:reindexSite'] }, |
||||
{ label: t('generator.html'), key: 'generator:html', perms: ['generator:html'] }, |
||||
{ label: t('site.settings.html'), key: 'siteSettings:html:update', perms: ['siteSettings:html:update', 'generator:html'] }, |
||||
], |
||||
}, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.config'), |
||||
key: 'config', |
||||
children: [ |
||||
{ |
||||
label: t('menu.config.globalSettings'), |
||||
key: 'globalSettings', |
||||
perms: ['globalSettings:show'], |
||||
children: [ |
||||
{ label: t('global.settings.base'), key: 'globalSettings:base:update', perms: ['globalSettings:base:update'] }, |
||||
{ label: t('global.settings.upload'), key: 'globalSettings:upload:update', perms: ['globalSettings:upload:update'] }, |
||||
{ label: t('global.settings.customs'), key: 'globalSettings:customs:update', perms: ['globalSettings:customs:update'] }, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.config.siteSettings'), |
||||
key: 'siteSettings', |
||||
perms: ['siteSettings:show'], |
||||
children: [ |
||||
{ label: t('site.settings.base'), key: 'siteSettings:base:update', perms: ['siteSettings:base:update'] }, |
||||
{ label: t('site.settings.watermark'), key: 'siteSettings:watermark:update', perms: ['siteSettings:watermark:update'] }, |
||||
{ label: t('site.settings.customs'), key: 'siteSettings:customs:update', perms: ['siteSettings:customs:update'] }, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.config.model'), |
||||
key: 'model', |
||||
perms: ['model:page', 'model:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'model:page' }, |
||||
{ label: t('add'), key: 'model:create', perms: ['model:create'] }, |
||||
{ label: t('edit'), key: 'model:update', perms: ['model:update', 'model:show'] }, |
||||
{ label: t('delete'), key: 'model:delete', perms: ['model:delete'] }, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.config.block'), |
||||
key: 'block', |
||||
perms: ['block:page', 'block:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'block:page' }, |
||||
{ label: t('add'), key: 'block:create', perms: ['block:create'] }, |
||||
{ label: t('edit'), key: 'block:update', perms: ['block:update', 'block:show'] }, |
||||
{ label: t('delete'), key: 'block:delete', perms: ['block:delete'] }, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.config.dictType'), |
||||
key: 'dictType', |
||||
perms: ['dictType:page', 'dictType:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'dictType:page' }, |
||||
{ label: t('add'), key: 'dictType:create', perms: ['dictType:create'] }, |
||||
{ label: t('edit'), key: 'dictType:update', perms: ['dictType:update', 'dictType:show'] }, |
||||
{ label: t('delete'), key: 'dictType:delete', perms: ['dictType:delete'] }, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.config.dict'), |
||||
key: 'dict', |
||||
perms: ['dict:page', 'dict:list', 'dictType:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'dict:page' }, |
||||
{ label: t('add'), key: 'dict:create', perms: ['dict:create'] }, |
||||
{ label: t('edit'), key: 'dict:update', perms: ['dict:update', 'dict:show'] }, |
||||
{ label: t('delete'), key: 'dict:delete', perms: ['dict:delete'] }, |
||||
], |
||||
}, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.user'), |
||||
key: 'user', |
||||
children: [ |
||||
{ |
||||
label: t('menu.user.user'), |
||||
key: 'user', |
||||
perms: ['user:page', 'user:list', 'group:list', 'org:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'user:page' }, |
||||
{ label: t('add'), key: 'user:create', perms: ['user:create'] }, |
||||
{ label: t('edit'), key: 'user:update', perms: ['user:update', 'user:show'] }, |
||||
{ label: t('delete'), key: 'user:delete', perms: ['user:delete'] }, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.user.role'), |
||||
key: 'role', |
||||
perms: ['role:page', 'role:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'role:page' }, |
||||
{ label: t('add'), key: 'role:create', perms: ['role:create'] }, |
||||
{ label: t('edit'), key: 'role:update', perms: ['role:update', 'role:show'] }, |
||||
{ label: t('delete'), key: 'role:delete', perms: ['role:delete'] }, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.user.group'), |
||||
key: 'group', |
||||
perms: ['group:page', 'group:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'group:page' }, |
||||
{ label: t('add'), key: 'group:create', perms: ['group:create'] }, |
||||
{ label: t('edit'), key: 'group:update', perms: ['group:update', 'group:show'] }, |
||||
{ label: t('delete'), key: 'group:delete', perms: ['group:delete'] }, |
||||
], |
||||
}, |
||||
{ |
||||
// label: t('menu.user.org'),
|
||||
key: 'org', |
||||
perms: ['org:page', 'org:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'org:page' }, |
||||
{ label: t('add'), key: 'org:create', perms: ['org:create'] }, |
||||
{ label: t('edit'), key: 'org:update', perms: ['org:update', 'org:show'] }, |
||||
{ label: t('delete'), key: 'org:delete', perms: ['org:delete'] }, |
||||
], |
||||
}, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.system'), |
||||
key: 'system', |
||||
children: [ |
||||
{ |
||||
// label: t('menu.system.site'),
|
||||
key: 'site', |
||||
perms: ['site:page', 'site:list', 'org:list', 'model:list', 'storage:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'site:page' }, |
||||
{ label: t('add'), key: 'site:create', perms: ['site:create'] }, |
||||
{ label: t('edit'), key: 'site:update', perms: ['site:update', 'site:show'] }, |
||||
{ label: t('delete'), key: 'site:delete', perms: ['site:delete'] }, |
||||
], |
||||
}, |
||||
{ |
||||
label: t('menu.system.storage'), |
||||
key: 'storage', |
||||
perms: ['storage:page', 'storage:list'], |
||||
children: [ |
||||
{ label: t('list'), key: 'storage:page' }, |
||||
{ label: t('add'), key: 'storage:create', perms: ['storage:create'] }, |
||||
{ label: t('edit'), key: 'storage:update', perms: ['storage:update', 'storage:show'] }, |
||||
{ label: t('delete'), key: 'storage:delete', perms: ['storage:delete'] }, |
||||
], |
||||
}, |
||||
// {
|
||||
// label: t('menu.system.task'),
|
||||
// key: 'task',
|
||||
// perms: ['task:page', 'task:list'],
|
||||
// children: [
|
||||
// { label: t('list'), key: 'task:page' },
|
||||
// { label: t('add'), key: 'task:create', perms: ['task:create'] },
|
||||
// { label: t('edit'), key: 'task:update', perms: ['task:update', 'task:show'] },
|
||||
// { label: t('delete'), key: 'task:delete', perms: ['task:delete'] },
|
||||
// ],
|
||||
// },
|
||||
], |
||||
}, |
||||
], |
||||
}, |
||||
]; |
||||
} |
||||
|
||||
export function getModelData(): any { |
||||
return { |
||||
article: { |
||||
mains: [ |
||||
{ code: 'title', must: true, show: true, double: false, required: true }, |
||||
{ code: 'subtitle', must: false, show: false, double: false, required: false }, |
||||
{ code: 'fullTitle', must: false, show: false, double: false, required: false }, |
||||
{ code: 'linkUrl', must: false, show: true, double: false, required: false }, |
||||
{ code: 'seoKeywords', must: false, show: false, double: false, required: false }, |
||||
{ code: 'seoDescription', must: false, show: true, double: false, required: false }, |
||||
{ code: 'author', must: false, show: false, double: true, required: false }, |
||||
{ code: 'editor', must: false, show: false, double: true, required: false }, |
||||
{ code: 'image', must: false, show: true, double: false, required: false, type: 'image', imageWidth: 300, imageHeight: 200, imageMode: 'manual' }, |
||||
{ code: 'file', must: false, show: false, double: false, required: false }, |
||||
{ code: 'video', must: false, show: false, double: false, required: false }, |
||||
{ code: 'doc', must: false, show: false, double: false, required: false }, |
||||
{ code: 'imageList', must: false, show: false, double: false, required: false, type: 'imageList', imageMaxWidth: 1920, imageMaxHeight: 1920 }, |
||||
{ code: 'fileList', must: false, show: false, double: false, required: false }, |
||||
{ code: 'text', must: false, show: true, double: false, required: true }, |
||||
], |
||||
asides: [ |
||||
{ code: 'channel', must: true, show: true, required: true }, |
||||
{ code: 'org', must: false, show: true, required: true }, |
||||
{ code: 'publishDate', must: true, show: true, required: true }, |
||||
// { code: 'offlineDate', must: false, show: true, required: false },
|
||||
{ code: 'source', must: false, show: true, required: false }, |
||||
{ code: 'articleTemplate', must: false, show: true, required: false }, |
||||
{ code: 'allowComment', must: false, show: true, required: true }, |
||||
{ code: 'user', must: false, show: false, required: true }, |
||||
{ code: 'created', must: false, show: false, required: true }, |
||||
{ code: 'modifiedUser', must: false, show: false, required: false }, |
||||
{ code: 'modified', must: false, show: false, required: false }, |
||||
], |
||||
}, |
||||
channel: { |
||||
mains: [ |
||||
{ code: 'name', must: true, show: true, double: true, required: true }, |
||||
{ code: 'alias', must: true, show: true, double: true, required: true }, |
||||
{ code: 'linkUrl', must: true, show: true, double: false, required: true }, |
||||
{ code: 'seoTitle', must: false, show: true, double: true, required: false }, |
||||
{ code: 'seoKeywords', must: false, show: true, double: true, required: false }, |
||||
{ code: 'seoDescription', must: false, show: true, double: false, required: false }, |
||||
{ code: 'channelTemplate', must: false, show: true, double: true, required: true }, |
||||
{ code: 'articleTemplate', must: false, show: true, double: true, required: true }, |
||||
{ code: 'channelModel', must: true, show: true, double: true, required: true }, |
||||
{ code: 'articleModel', must: true, show: true, double: true, required: true }, |
||||
{ code: 'group', must: false, show: true, double: false, required: true }, |
||||
{ code: 'nav', must: false, show: true, double: true, required: true }, |
||||
{ code: 'allowComment', must: false, show: true, double: true, required: true }, |
||||
{ code: 'allowContribute', must: false, show: true, double: true, required: true }, |
||||
{ code: 'allowSearch', must: false, show: true, double: true, required: true }, |
||||
{ code: 'text', must: false, show: false, double: false, required: false }, |
||||
], |
||||
asides: [ |
||||
{ code: 'type', must: true, show: true, required: true }, |
||||
{ code: 'parent', must: true, show: true, required: false }, |
||||
{ code: 'pageSize', must: true, show: true, required: true }, |
||||
], |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
export function mergeModelFields(defaultFields: any[], s: string | null | undefined, type: string): any[] { |
||||
const fields = JSON.parse(s || '[]'); |
||||
const defaults = defaultFields.map((item: any) => ({ ...item, label: `${type}.${item.code}` })); |
||||
// 去除默认字段中不存在的字段
|
||||
fields.filter((field: any) => defaults.findIndex((item) => item.code === field.code) !== -1); |
||||
defaults.forEach((item) => { |
||||
const index = fields.findIndex((it: any) => it.code === item.code); |
||||
if (index !== -1) { |
||||
// 加上缺失属性,覆盖不可改属性
|
||||
fields[index] = { ...item, ...fields[index], must: item.must, label: item.label, type: item.type }; |
||||
} else { |
||||
// 加上没有的字段
|
||||
fields.push(item); |
||||
} |
||||
}); |
||||
return fields; |
||||
} |
||||
|
||||
export function arr2obj(arr: any[]) { |
||||
const obj: Record<string, any> = {}; |
||||
arr.forEach((item: any) => { |
||||
obj[item.code] = item; |
||||
}); |
||||
return obj; |
||||
} |
@ -0,0 +1,19 @@ |
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' { |
||||
import { DefineComponent } from 'vue'; |
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>; |
||||
export default component; |
||||
} |
||||
|
||||
interface ImportMetaEnv { |
||||
readonly VITE_BASE_API: string; |
||||
readonly VITE_PUBLIC_PATH: string; |
||||
readonly VITE_I18N_LOCALE: string; |
||||
readonly VITE_I18N_FALLBACK_LOCALE: string; |
||||
} |
||||
|
||||
interface ImportMeta { |
||||
readonly env: ImportMetaEnv; |
||||
} |
@ -0,0 +1,43 @@ |
||||
import { createI18n } from 'vue-i18n'; |
||||
import { Language } from 'element-plus/es/locale/index'; |
||||
import ElZhCn from 'element-plus/es/locale/lang/zh-cn'; |
||||
import ElEn from 'element-plus/es/locale/lang/en'; |
||||
import { getCookieLocale } from '@/utils/common'; |
||||
import en from './locales/en'; |
||||
import zhCn from './locales/zh-cn'; |
||||
|
||||
const messages = { |
||||
'zh-cn': { |
||||
...zhCn, |
||||
}, |
||||
en: { |
||||
...en, |
||||
}, |
||||
}; |
||||
|
||||
const elMessages: Record<string, Language> = { |
||||
'zh-cn': ElZhCn, |
||||
en: ElEn, |
||||
}; |
||||
|
||||
export const languages: Record<string, string> = { 'zh-cn': '中文', en: 'English' }; |
||||
|
||||
const i18nFallbackLocale = import.meta.env.VITE_I18N_FALLBACK_LOCALE || 'zh-cn'; |
||||
|
||||
export function getElementPlusLocale(lang: string): Language { |
||||
return elMessages[lang] ?? elMessages[i18nFallbackLocale] ?? ElZhCn; |
||||
} |
||||
|
||||
export function getLanguage(): string { |
||||
const chooseLanguage = getCookieLocale(); |
||||
if (chooseLanguage) return chooseLanguage; |
||||
return import.meta.env.VITE_I18N_LOCALE || 'zh-cn'; |
||||
} |
||||
|
||||
export default createI18n({ |
||||
legacy: false, |
||||
locale: getLanguage(), |
||||
fallbackLocale: i18nFallbackLocale, |
||||
globalInjection: true, |
||||
messages, |
||||
}); |
@ -0,0 +1,54 @@ |
||||
<template> |
||||
<div class="flex justify-between items-center py-6 px-5 overflow-hidden"> |
||||
<logo :collapse="collapse" /> |
||||
<div class="inline-flex items-center"> |
||||
<img class="mr-3 cursor-pointer" |
||||
src="@/assets/images/2.png" |
||||
alt="" /> |
||||
<img class="mr-3 cursor-pointer" |
||||
src="@/assets/images/3.png" |
||||
alt="" /> |
||||
<img class="cursor-pointer" |
||||
src="@/assets/images/4.png" |
||||
alt="" /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { ref, onMounted, computed } from 'vue'; |
||||
import { setCookieLocale, getSessionSiteId, setSessionSiteId } from '@/utils/common'; |
||||
import { toTree, flatTree } from '@/utils/tree'; |
||||
import { querySiteList } from '@/api/system'; |
||||
import { currentUser, perm, logout } from '@/store/useCurrentUser'; |
||||
import { appState, toggleSidebar } from '@/store/useAppState'; |
||||
import Logo from './Logo.vue'; |
||||
|
||||
const siteId = ref<number | null>(getSessionSiteId()); |
||||
const siteList = ref<any[]>([]); |
||||
const site = computed(() => siteList.value.find((item) => item.id === siteId.value)); |
||||
const fetchSiteList = async () => { |
||||
siteList.value = flatTree(toTree(await querySiteList())); |
||||
if (siteId.value == null) { |
||||
siteId.value = siteList.value[0]?.id; |
||||
} |
||||
}; |
||||
const changeSiteId = (id: number) => { |
||||
setSessionSiteId(id); |
||||
siteId.value = id; |
||||
window.location.reload(); |
||||
}; |
||||
onMounted(() => { |
||||
fetchSiteList(); |
||||
}); |
||||
|
||||
const handleLogout = () => { |
||||
logout(); |
||||
// router.push(`/login?redirect=${route.fullPath}`); |
||||
window.location.reload(); |
||||
}; |
||||
|
||||
const passwordFormVisible = ref<boolean>(false); |
||||
</script> |
||||
|
||||
<style lang="scss" scoped></style> |
@ -0,0 +1,19 @@ |
||||
<template> |
||||
<section class="p-3"> |
||||
<router-view v-slot="{ Component }"> |
||||
<transition name="fade-transform" |
||||
mode="out-in"> |
||||
<component :is="Component" /> |
||||
</transition> |
||||
</router-view> |
||||
</section> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import { defineComponent } from 'vue'; |
||||
|
||||
export default defineComponent({ |
||||
name: 'AppMain', |
||||
}); |
||||
</script> |
||||
<style lang="scss" scoped></style> |
@ -0,0 +1,57 @@ |
||||
<template> |
||||
<template v-if="isShow"> |
||||
<el-sub-menu v-if="subMenu" :index="resolvePath(path)" popper-append-to-body> |
||||
<template #title> |
||||
<el-icon v-if="icon"><component :is="icon"></component></el-icon> |
||||
<span>{{ $t(title) }}</span> |
||||
</template> |
||||
<menu-item v-for="item in route.children.filter((item:any) => isShowMenu(item))" :key="item.path" :route="item" :base-path="resolvePath(item.path)" /> |
||||
</el-sub-menu> |
||||
<el-menu-item v-else :index="resolvePath(path)" @click="handleClick"> |
||||
<el-icon v-if="icon"><component :is="icon"></component></el-icon> |
||||
<template #title> |
||||
<span>{{ $t(title) }}</span> |
||||
</template> |
||||
</el-menu-item> |
||||
</template> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { computed, defineProps, toRefs } from 'vue'; |
||||
import { useRouter } from 'vue-router'; |
||||
import { isShowMenu } from '@/store/useCurrentUser'; |
||||
|
||||
function isExternal(path: string): boolean { |
||||
return /^(https?:|mailto:|tel:)/.test(path); |
||||
} |
||||
|
||||
const props = defineProps({ route: { type: Object, required: true }, basePath: { type: String, default: '' } }); |
||||
const { route, basePath } = toRefs(props); |
||||
const router = useRouter(); |
||||
|
||||
// 是否显示。非隐藏路由即显示 |
||||
const isShow = computed(() => !route.value.meta?.hidden); |
||||
// 是否子菜单。有标题且有子元素即为子菜单 |
||||
const subMenu = computed(() => route.value.meta?.title && route.value.children); |
||||
// 目标路由。如果自己没有title,且有子元素,则子元素为目标路由 |
||||
const targetRoute = computed(() => (!route.value.meta?.title && route.value.children?.length > 0 ? route.value.children[0] : route.value)); |
||||
const icon = computed(() => targetRoute.value.meta?.icon); |
||||
const title = computed(() => targetRoute.value.meta?.title); |
||||
// 路由路径。如果targetItem是子元素,则使用子元素路由;使用自己的路由,则为空串,因为在basePath中已经指定了自己的路由 |
||||
const path = computed(() => (route.value !== targetRoute.value ? targetRoute.value.path : '')); |
||||
|
||||
const resolvePath = (routePath: string) => { |
||||
if (isExternal(routePath)) { |
||||
return routePath; |
||||
} |
||||
return `${basePath.value}/${routePath}`; |
||||
}; |
||||
const handleClick = () => { |
||||
const fullPath = resolvePath(path.value); |
||||
if (isExternal(fullPath)) { |
||||
window.open(fullPath, '_blank'); |
||||
} else { |
||||
router.push(resolvePath(path.value)); |
||||
} |
||||
}; |
||||
</script> |
@ -0,0 +1,126 @@ |
||||
<template> |
||||
<div> |
||||
<el-scrollbar wrap-style="height: calc(100% - 85px)"> |
||||
<div class="avatar py-3 mx-auto text-center"> |
||||
<img class="mx-auto" |
||||
src="@/assets/images/6.png" |
||||
alt="" /> |
||||
<p class="text-white text-md">产品经理</p> |
||||
<p class="my-2 text-white text-sm">产品经理</p> |
||||
<p class="text-white text-xs">操作日期:2018-02-06</p> |
||||
</div> |
||||
<ul class="switch px-7 mt-5"> |
||||
<li> |
||||
<img class="icon" |
||||
src="@/assets/images/icon1.png" |
||||
alt="" /> |
||||
<img class="icon-1" |
||||
src="@/assets/images/icon1-1.png" |
||||
alt="" /> |
||||
<p class="text">产品风控配置</p> |
||||
</li> |
||||
<li> |
||||
<img class="icon" |
||||
src="@/assets/images/icon2.png" |
||||
alt="" /> |
||||
<img class="icon-1" |
||||
src="@/assets/images/icon2-1.png" |
||||
alt="" /> |
||||
<p class="text">准入模型</p> |
||||
</li> |
||||
<li> |
||||
<img class="icon" |
||||
src="@/assets/images/icon3.png" |
||||
alt="" /> |
||||
<img class="icon-1" |
||||
src="@/assets/images/icon3-1.png" |
||||
alt="" /> |
||||
<p class="text">利率定价模型</p> |
||||
</li> |
||||
<li> |
||||
<img class="icon" |
||||
src="@/assets/images/icon4.png" |
||||
alt="" /> |
||||
<img class="icon-1" |
||||
src="@/assets/images/icon4-1.png" |
||||
alt="" /> |
||||
<p class="text">贷后管理模型</p> |
||||
</li> |
||||
</ul> |
||||
</el-scrollbar> |
||||
</div> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { computed } from 'vue'; |
||||
import { useRouter, useRoute } from 'vue-router'; |
||||
import { isShowMenu } from '@/store/useCurrentUser'; |
||||
import { appState } from '@/store/useAppState'; |
||||
|
||||
const router = useRouter(); |
||||
const route = useRoute(); |
||||
|
||||
// const routes = computed(() => router.options.routes); |
||||
|
||||
const activeMenu = computed(() => { |
||||
const { meta, path } = route; |
||||
// if set path, the sidebar will highlight the path you set |
||||
if (meta.activeMenu) { |
||||
return meta.activeMenu as string; |
||||
} |
||||
return path; |
||||
}); |
||||
|
||||
// Cool Gray 700 |
||||
const sidebarBg = '#374151'; |
||||
// Cool Gray 400 |
||||
const textColor = '#9CA3AF'; |
||||
const activeTextColor = '#FFF'; |
||||
const { routes } = router.options; |
||||
const collapse = computed(() => !appState.sidebar); |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.avatar { |
||||
background: url(../../../assets/images/5.png) 0 0/100% 100% no-repeat; |
||||
} |
||||
.switch { |
||||
li { |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: center; |
||||
align-items: center; |
||||
width: 122px; |
||||
height: 102px; |
||||
margin-bottom: 20px; |
||||
background: #ffffff; |
||||
box-shadow: 0px 0px 8px 0px rgba(162, 199, 246, 0.16); |
||||
border-radius: 10px; |
||||
cursor: pointer; |
||||
&:hover { |
||||
background: linear-gradient(-36deg, #3c65ff, #33d1ff); |
||||
box-shadow: 0px 6px 16px 0px rgba(96, 155, 255, 0.56); |
||||
.icon { |
||||
display: none; |
||||
} |
||||
.icon-1 { |
||||
display: inline-block; |
||||
} |
||||
.text { |
||||
color: #fff; |
||||
} |
||||
} |
||||
} |
||||
img { |
||||
margin: 0 auto; |
||||
} |
||||
.icon-1 { |
||||
display: none; |
||||
} |
||||
.text { |
||||
margin-top: 10px; |
||||
font-family: MiSans; |
||||
font-weight: 600; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,30 @@ |
||||
<template> |
||||
<div class="flex items-center justify-center overflow-hidden"> |
||||
<router-link class="whitespace-nowrap text-center" |
||||
to="/"> |
||||
<h1 v-if="!collapse" |
||||
class="ml-1 text-[22px] leading-[1] font-bold text-[#333]">{{ title }}</h1> |
||||
</router-link> |
||||
</div> |
||||
</template> |
||||
|
||||
<script lang="ts"> |
||||
import { defineComponent } from 'vue'; |
||||
import Settings from '@/settings'; |
||||
|
||||
export default defineComponent({ |
||||
name: 'SidebarLogo', |
||||
props: { |
||||
collapse: { |
||||
type: Boolean, |
||||
required: true, |
||||
}, |
||||
}, |
||||
data() { |
||||
return { |
||||
title: Settings.title, |
||||
logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png', |
||||
}; |
||||
}, |
||||
}); |
||||
</script> |