Merge branch 'feat/appointment-goods'
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
unpackage/
|
||||
.DS_Store
|
||||
.idea/
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
// launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
|
||||
// launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
|
||||
"version" : "0.0",
|
||||
"configurations" : [
|
||||
{
|
||||
"app-plus" : {
|
||||
"launchtype" : "remote"
|
||||
},
|
||||
"default" : {
|
||||
"launchtype" : "remote"
|
||||
},
|
||||
"h5" : {
|
||||
"launchtype" : "remote"
|
||||
},
|
||||
"mp-weixin" : {
|
||||
"launchtype" : "remote"
|
||||
},
|
||||
"type" : "uniCloud"
|
||||
},
|
||||
{
|
||||
"playground" : "standard",
|
||||
"type" : "uni-app:app-android"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
.vscode/launch.json
vendored
@@ -1,15 +0,0 @@
|
||||
{
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 悬停以查看现有属性的描述。
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:8080",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
122
App.vue
@@ -203,4 +203,126 @@ body {
|
||||
.flex1 {
|
||||
flex: 1; //必须父级设置flex
|
||||
}
|
||||
|
||||
.light-color {
|
||||
color: $light-color;
|
||||
}
|
||||
.main-color {
|
||||
color: $main-color !important;
|
||||
}
|
||||
.bg-light-color {
|
||||
background-color: $light-color !important;
|
||||
}
|
||||
.bg-main-color {
|
||||
background-color: $main-color !important;
|
||||
}
|
||||
|
||||
// 渐变主题颜色
|
||||
.bg-linear-gradient {
|
||||
background-image: linear-gradient(25deg, var(--theme-primary, #fa123b), var(--theme-light, #ff6b35), var(--theme-aider, #ff9f28), #ffcc03);
|
||||
}
|
||||
.uni-tabbar .uni-tabbar__icon {
|
||||
width: 20px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: $light-color;
|
||||
color: #ffffff;
|
||||
border-width: 0px;
|
||||
border: none;
|
||||
border-radius: 44px;
|
||||
height: 44px;
|
||||
box-shadow: none;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 44px;
|
||||
text-align: center;
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
.button-hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.margin {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background-color: rgba(242, 242, 242, 1);
|
||||
color: #333333;
|
||||
height: 28px;
|
||||
border-radius: 28px;
|
||||
box-shadow: none;
|
||||
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.c-content {
|
||||
background-color: #ffffff;
|
||||
// margin-top: 30rpx;
|
||||
border-top: 1px solid $border-color-light;
|
||||
border-bottom: 1px solid $border-color-light;
|
||||
}
|
||||
.mt-30 {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
.border-top {
|
||||
border-top: 1px solid $border-color-light;
|
||||
}
|
||||
.describe {
|
||||
color: $font-color-light;
|
||||
font-size: $font-sm;
|
||||
padding: 30rpx;
|
||||
}
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-a-c{
|
||||
align-items: center;
|
||||
}
|
||||
.flex-j-sb{
|
||||
justify-content: space-between;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wes {
|
||||
/* 多出部分用省略号表示 , 用于一行 */
|
||||
overflow: hidden;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wes-2 {
|
||||
/* 适用于webkit内核和移动端 */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wes-3 {
|
||||
/* 适用于webkit内核和移动端 */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
682
LICENSE
@@ -1,661 +1,21 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2026 Beijing Hongye Huicheng Technology Co., Ltd. (北京宏业汇成科技有限公司)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
260
README.en.md
@@ -1,180 +1,140 @@
|
||||
# Lilishop(Pickmall) - Java Open Source Mall System
|
||||
<div align="center">
|
||||
<img src="./icon.png" width="80" alt="Lilishop Logo" />
|
||||
<h1>Lilishop(PickMall) - Mobile Client (uni-app)</h1>
|
||||
|
||||
[](https://github.com/hongyehuicheng/lilishop)
|
||||
[](https://gitee.com/beijing_hongye_huicheng/lilishop)
|
||||
<p>
|
||||
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
|
||||
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
|
||||
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" /></a>
|
||||
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3-brightgreen.svg" alt="Vue 3" /></a>
|
||||
<a href="https://uniapp.dcloud.net.cn/"><img src="https://img.shields.io/badge/uni--app-Vue3-blue.svg" alt="uni-app" /></a>
|
||||
</p>
|
||||
|
||||
<p align="right">
|
||||
<a href="./README.md">简体中文</a> | English
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://pickmall.cn">🏠 Official Website</a> ·
|
||||
<a href="https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home">📖 Product Documentation</a> ·
|
||||
<a href="#2-online-demo">📱 Online Demo</a> ·
|
||||
<a href="https://github.com/lilishop/lilishop">💻 GitHub</a>
|
||||
</p>
|
||||
|
||||
LiliShop is a Java open source ecommerce system built with Spring Boot, Spring Cloud, Vue, and Uniapp.
|
||||
<p>
|
||||
<a href="./README.md">简体中文</a> | <b>English</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
It supports:
|
||||
> ⭐ **If you like this project, please click `Star` in the top-right corner to support us! Your Star is our greatest motivation for continuous open-source iteration!**
|
||||
|
||||
- B2B2C multi-vendor marketplace
|
||||
- Microservice architecture
|
||||
- WeChat mini program
|
||||
- Vue ecommerce frontend
|
||||
- Uniapp mobile app
|
||||
- Live streaming ecommerce
|
||||
- Distribution & affiliate system
|
||||
- Flash sale / coupon / promotion
|
||||
- Docker deployment
|
||||
- Private deployment
|
||||
- Multi-language ecommerce platform
|
||||
**LILISHOP** is an open-source e-commerce system built with Spring Boot, Vue 3, and uni-app. It supports B2B2C multi-vendor marketplaces, WeChat Mini Programs, H5, Android/iOS Apps, live-streaming e-commerce, affiliate distribution, and flash sales.
|
||||
|
||||
### ✨ Why Choose Lilishop
|
||||
- 🟢 **True MIT License**: Free for commercial use and private secondary development without licensing concerns.
|
||||
- 🚀 **Modern Vue 3 Stack**: Built on `uni-app + Vue 3 + uview-plus 3.x`, leaving legacy Vue 2 templates behind.
|
||||
- 🧩 **Production-Ready Completeness**: Not a static UI template—fully integrated with B2B2C multi-vendor workflows, visual storefront decoration, IM customer service, flash sales, group buying, affiliate distribution, and live streaming.
|
||||
|
||||
---
|
||||
|
||||
## Keywords
|
||||
## 1. Overview
|
||||
|
||||
Java Mall System
|
||||
Open Source Ecommerce Platform
|
||||
Spring Boot Mall System
|
||||
Spring Cloud Ecommerce
|
||||
B2B2C Marketplace
|
||||
Multi Vendor Ecommerce Platform
|
||||
Vue Ecommerce System
|
||||
Uniapp Mall System
|
||||
Microservice Mall System
|
||||
Wechat Mini Program Mall
|
||||
Docker Ecommerce Deployment
|
||||
This repository (`lilishop-uniapp`) is the **Mobile Buyer Client** of Lilishop, supporting **H5, WeChat Mini Program, Android, and iOS Apps** from a single codebase powered by **Vue 3** and **uni-app**.
|
||||
|
||||
- **Official Website**: <https://pickmall.cn>
|
||||
- **Documentation**: [Product Documentation](https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home)
|
||||
- **Gitee Repository**: <https://gitee.com/beijing_hongye_huicheng>
|
||||
- **GitHub Main Repository**: <https://github.com/lilishop/lilishop>
|
||||
|
||||
---
|
||||
|
||||
## Official Website
|
||||
## 2. Online Demo
|
||||
|
||||
- Website: https://pickmall.cn
|
||||
- Documentation: https://docs.pickmall.cn
|
||||
- GitHub: https://github.com/lilishop/lilishop
|
||||
- Gitee: https://gitee.com/beijing_hongye_huicheng/lilishop
|
||||
> **Note**: The verification code for the demo environment is `111111`.
|
||||
|
||||
| WeChat Mini Program | Mobile H5 ([Visit Directly](https://m-b2b2c.pickmall.cn)) |
|
||||
| :---: | :---: |
|
||||
| <img src="https://static.pickmall.cn/images/h5-qrcode.png" width="180" alt="WeChat Mini Program QR Code" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/h5-qrcode.png" width="180" alt="Mobile H5 QR Code" /> |
|
||||
| Scan with WeChat | Scan or visit <https://m-b2b2c.pickmall.cn> |
|
||||
|
||||
- **PC Mall**: <https://pc-b2b2c.pickmall.cn>
|
||||
- **Admin Panel**: <https://admin-b2b2c.pickmall.cn> (`admin` / `123456`)
|
||||
- **Store Panel**: <https://store-b2b2c.pickmall.cn> (`13011111111` / `111111`)
|
||||
|
||||
### Mobile UI Preview
|
||||
|
||||
| Home | Product List | Product Detail | Checkout | User Center |
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
| <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image.png" width="160" alt="Home" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image3.png" width="160" alt="Product List" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image4.png" width="160" alt="Product Detail" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image5.png" width="160" alt="Checkout" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image2.png" width="160" alt="User Center" /> |
|
||||
|
||||
---
|
||||
|
||||
## Core Features
|
||||
## 3. Core Features & Tech Stack
|
||||
|
||||
### Full Platform Support
|
||||
### Core Features
|
||||
- **Cross-Platform**: One codebase runs on H5, WeChat Mini Program, Android, and iOS.
|
||||
- **Multi-Vendor B2B2C**: Supports merchant onboarding and store-level operations.
|
||||
- **Visual Storefront Decoration**: Dynamic home page & floor layouts (`pages/floor`) and customizable theme colors.
|
||||
- **Live Streaming & IM**: Integrated with Tencent Cloud IM / MQTT and HLS video streaming.
|
||||
- **Full E-Commerce Workflow**: Product browsing, cart, promotions (flash sale, group buy, coupons, points, bargain, affiliate), order fulfillment, after-sales, and member center.
|
||||
|
||||
One codebase supports:
|
||||
|
||||
- PC
|
||||
- H5
|
||||
- WeChat Mini Program
|
||||
- Android APP
|
||||
- iOS APP
|
||||
|
||||
### Microservice Architecture
|
||||
|
||||
Built with:
|
||||
|
||||
- Spring Boot
|
||||
- Spring Cloud
|
||||
- Redis
|
||||
- RocketMQ
|
||||
- Elasticsearch
|
||||
- Docker
|
||||
- ShardingSphere
|
||||
|
||||
Supports distributed deployment and high concurrency ecommerce scenarios.
|
||||
|
||||
### Ecommerce Features
|
||||
|
||||
- Multi merchant marketplace
|
||||
- Product management
|
||||
- Order management
|
||||
- Promotion system
|
||||
- Coupon system
|
||||
- Affiliate distribution
|
||||
- Live streaming ecommerce
|
||||
- Member system
|
||||
- Points system
|
||||
- Visual decoration
|
||||
- Multi-language support
|
||||
### Mobile Tech Stack
|
||||
| Category | Technology | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| Core Framework | `uni-app` + `Vue 3` | Cross-platform framework with Vue 3 support |
|
||||
| State Management | `Vuex 4` | Global auth, cart, and user state management |
|
||||
| UI Library | `uview-plus 3.x` | Vue 3 compatible uni-app multi-platform UI library |
|
||||
| Messaging & IM | `mqtt 5.x` / `@tencentcloud/chat` | Real-time customer service and notification push |
|
||||
| Video Streaming | `hls.js` | Live streaming playback support |
|
||||
| CSS Preprocessor | `Sass (SCSS)` | Global theme variables (`uni.scss`) and styling |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
## 4. Directory Structure
|
||||
|
||||
### Backend
|
||||
|
||||
- Java
|
||||
- Spring Boot
|
||||
- Spring Cloud
|
||||
- MyBatis Plus
|
||||
- MySQL
|
||||
- Redis
|
||||
- RocketMQ
|
||||
- Elasticsearch
|
||||
- JWT
|
||||
- Docker
|
||||
|
||||
### Frontend
|
||||
|
||||
- Vue.js
|
||||
- Uniapp
|
||||
- Vuex
|
||||
- Axios
|
||||
- iView
|
||||
- uViewUI
|
||||
```text
|
||||
lilishop-uniapp
|
||||
├── api/ # API module definitions (member, goods, order, promotion, IM, etc.)
|
||||
├── components/ # Shared business and base components
|
||||
├── config/ # Global configuration
|
||||
│ ├── api.js # API base URLs (buyer / im / mqtt)
|
||||
│ └── config.js # Mall name, Mini Program AppID, AMap Key, theme colors, etc.
|
||||
├── hybrid/ # Local HTML assets for App WebView
|
||||
├── js_sdk/ # Third-party and uni-app SDKs
|
||||
├── lib/ # Base utility libraries
|
||||
├── pages/ # Pages and sub-packages
|
||||
│ ├── tabbar/ # Main tabbar pages (Home, Category, Cart, Mine)
|
||||
│ ├── passport/ # Auth sub-package (Login, Register, Reset Password)
|
||||
│ ├── product/ # Product detail, list, reviews, store homepage
|
||||
│ ├── cart/ # Shopping cart and checkout
|
||||
│ ├── order/ # Order list, detail, after-sales, logistics, invoice
|
||||
│ ├── promotion/ # Flash sale, group buy, bargain, points mall, distribution, live
|
||||
│ ├── mine/ # User center, wallet, points, favorites, addresses
|
||||
│ ├── floor/ # Visual decoration floor components and topic pages
|
||||
│ └── navigation/ # Navigation and search pages
|
||||
├── patches/ # patch-package patches for dependencies
|
||||
├── scripts/ # Build and postinstall runtime patch scripts
|
||||
├── static/ # Static images and fonts
|
||||
├── store/ # Vuex store modules
|
||||
├── styles/ # Global stylesheets
|
||||
├── uni_modules/ # uni_modules plugins (including uview-plus)
|
||||
├── utils/ # Request interceptors, encryption, validators, storage helpers
|
||||
├── App.vue # Root application component
|
||||
├── main.js # Application entry point
|
||||
├── manifest.json # uni-app multi-platform configuration
|
||||
├── pages.json # Page routes, sub-packages, and navigation bar settings
|
||||
└── uni.scss # Global SCSS variables and theme color definitions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Online Demo
|
||||
## 5. Related Repositories
|
||||
|
||||
### Admin Panel
|
||||
|
||||
https://admin-b2b2c.pickmall.cn
|
||||
|
||||
Account: admin
|
||||
Password: 123456
|
||||
|
||||
### Store Panel
|
||||
|
||||
https://store-b2b2c.pickmall.cn
|
||||
|
||||
Account: 13011111111
|
||||
Password: 111111
|
||||
|
||||
### PC Mall
|
||||
|
||||
https://pc-b2b2c.pickmall.cn
|
||||
| Repository | Description | Links |
|
||||
| :--- | :--- | :--- |
|
||||
| **`lilishop`** | Backend Services (Spring Boot 4 / Java 21) | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop) · [GitHub](https://github.com/lilishop/lilishop) |
|
||||
| **`lilishop-ui`** | PC Mall / Seller / Admin / IM Web Frontend | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop-ui) |
|
||||
| **`lilishop-uniapp`** | Mobile Buyer Client (H5 / Mini Program / App) | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp) |
|
||||
| **`docker`** | Docker Compose, Images & SQL Init Scripts | [Gitee](https://gitee.com/beijing_hongye_huicheng/docker) · [MySQL Scripts](https://gitee.com/beijing_hongye_huicheng/docker/tree/master/init/mysql) |
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## 6. License
|
||||
|
||||
Please read the deployment documentation:
|
||||
|
||||
https://docs.pickmall.cn
|
||||
|
||||
Supports:
|
||||
|
||||
- Docker deployment
|
||||
- Linux deployment
|
||||
- Kubernetes deployment
|
||||
- Microservice deployment
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Is PickMall open source?
|
||||
|
||||
Yes. PickMall is an open source Java ecommerce system.
|
||||
|
||||
### Does PickMall support microservices?
|
||||
|
||||
Yes. PickMall is built on Spring Cloud microservice architecture.
|
||||
|
||||
### Does PickMall support Docker?
|
||||
|
||||
Yes. Docker deployment is supported.
|
||||
|
||||
### Does PickMall support WeChat Mini Program?
|
||||
|
||||
Yes. It supports WeChat Mini Program and Uniapp.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
PickMall is licensed under AGPL-3.0.
|
||||
Lilishop is licensed under the [MIT](./LICENSE) License. Copyright © 2020-2026 **Beijing Hongye Huicheng Technology Co., Ltd.**
|
||||
|
||||
250
README.md
@@ -1,159 +1,179 @@
|
||||
# Lilishop(PickMall) 开源商城系统
|
||||
<div align="center">
|
||||
<img src="./icon.png" width="80" alt="Lilishop Logo" />
|
||||
<h1>Lilishop(PickMall) 开源商城系统 - 移动端 (uni-app)</h1>
|
||||
|
||||
[](https://github.com/lilishop/lilishop)
|
||||
[](https://gitee.com/beijing_hongye_huicheng/lilishop)
|
||||
[](./LICENSE)
|
||||
[](https://vuejs.org/)
|
||||
[](https://uniapp.dcloud.net.cn/)
|
||||
<p>
|
||||
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
|
||||
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
|
||||
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" /></a>
|
||||
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3-brightgreen.svg" alt="Vue 3" /></a>
|
||||
<a href="https://uniapp.dcloud.net.cn/"><img src="https://img.shields.io/badge/uni--app-Vue3-blue.svg" alt="uni-app" /></a>
|
||||
</p>
|
||||
|
||||
<p align="right">
|
||||
简体中文 | <a href="./README.en.md">English</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://pickmall.cn">🏠 官方网站</a> ·
|
||||
<a href="https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home">📖 产品文档</a> ·
|
||||
<a href="#2-在线演示">📱 在线演示</a> ·
|
||||
<a href="#7-社区与支持">💬 微信交流群</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>简体中文</b> | <a href="./README.en.md">English</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
LILISHOP 是基于 Spring Boot / Spring Cloud / Vue / Uniapp 开发的 Java 开源商城系统,支持 B2B2C 多商户商城、小程序商城、微服务商城、直播电商、分销返佣、秒杀活动、Docker 私有化部署。
|
||||
> ⭐ **如果觉得项目不错,请点击右上角 `Star` 收藏支持!您的 Star 是我们持续开源迭代的最大动力!**
|
||||
|
||||
## 核心特性
|
||||
**LILISHOP** 是基于 Spring Boot / Vue 3 / uni-app 开发的开源商城系统,支持 B2B2C 多商户商城、微信小程序商城、H5 商城、APP 商城、直播电商、分销返佣、秒杀活动。
|
||||
|
||||
- Java商城系统
|
||||
- 开源商城系统
|
||||
- Spring Boot 4商城系统
|
||||
- SpringCloud微服务商城
|
||||
- B2B2C商城源码
|
||||
- 多商户商城系统
|
||||
- 小程序商城源码
|
||||
- Uniapp商城系统
|
||||
- Vue商城系统
|
||||
- Docker商城部署
|
||||
- 多语言商城
|
||||
|
||||
适用于:
|
||||
|
||||
- 企业级商城系统
|
||||
- 私有化商城部署
|
||||
- Java商城系统二次开发
|
||||
- 开源商城源码学习
|
||||
- 多商户电商平台
|
||||
---
|
||||
|
||||
### 1. 项目简介
|
||||
|
||||
**Lilishop** 是一款功能完善的B2B2C多商户商城系统,采用前后端分离架构,全端代码开源。
|
||||
后端基于 **Spring Boot 4**(Java 21)构建,具备高内聚、低耦合的特性,支持分布式部署。
|
||||
前端覆盖PC、H5、小程序和APP,基于 **Vue** 和 **uni-app** 开发。
|
||||
|
||||
- **官方网站**: <https://pickmall.cn>
|
||||
- **官方文档**: <https://docs.pickmall.cn>
|
||||
- **Gitee 仓库**: <https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp>
|
||||
- **GitHub 仓库**: <https://github.com/lilishop/lilishop>
|
||||
|
||||
本仓库为买家端 **H5 / 小程序 / APP** 源码(`lilishop-uniapp`)。
|
||||
### ✨ 为什么选择 Lilishop
|
||||
- 🟢 **真 MIT 协议**:支持免费商用与私有化闭源二开,无版权后顾之忧。
|
||||
- 🚀 **Vue 3 全新栈**:基于 `uni-app + Vue 3 + uview-plus 3.x`,告别老旧 Vue 2 模板。
|
||||
- 🧩 **生产级完整度**:非静态切图模板,完整覆盖 B2B2C 多商户、可视化装修、IM 客服、秒杀拼团、分销直播全闭环。
|
||||
|
||||
---
|
||||
|
||||
### 2. 核心特性
|
||||
## 1. 项目简介
|
||||
|
||||
- **全端覆盖**: 一套代码库支持PC、H5、小程序、APP,降低开发和维护成本。
|
||||
- **商家入驻**: 支持多商家入驻,构建平台化电商生态。
|
||||
- **分布式架构**: 后端API服务化,支持独立部署和弹性伸缩。
|
||||
- **前后端分离**: 清晰的职责划分,便于团队协作和独立开发。
|
||||
- **容器化支持**: 提供Docker镜像和docker-compose配置,实现一键部署。
|
||||
- **功能完善**: 涵盖客户、订单、商品、促销、店铺、运营、统计等完整电商业务模块。
|
||||
**Lilishop** 是一款功能完善的 B2B2C 多商户商城系统,采用前后端分离架构,全端代码开源。
|
||||
后端基于 **Spring Boot 4**(Java 21)构建,具备高内聚、低耦合的特性,支持分布式部署;前端覆盖 PC、H5、小程序和 APP,基于 **Vue 3** 和 **uni-app** 开发。
|
||||
|
||||
本仓库(`lilishop-uniapp`)为 **买家端移动端源码**,一套代码同时支持 **H5、微信小程序、Android / iOS APP**。
|
||||
|
||||
- **官方网站**: <https://pickmall.cn>
|
||||
- **官方文档**: [产品文档](https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home)
|
||||
- **Gitee 仓库**: <https://gitee.com/beijing_hongye_huicheng>
|
||||
- **GitHub 主仓库**: <https://github.com/lilishop/lilishop>
|
||||
|
||||
---
|
||||
|
||||
### 3. 在线演示
|
||||
## 2. 在线演示
|
||||
|
||||
**注意**: 演示站手机验证码统一为 `111111`。演示环境部署于 `master` 分支。
|
||||
> **注意**: 演示站手机验证码统一为 `111111`。演示环境部署于 `master` 分支。
|
||||
|
||||
- **平台管理端**: <https://admin-b2b2c.pickmall.cn>
|
||||
- 账号: `admin`
|
||||
- 密码: `123456`
|
||||
- **店铺管理端**: <https://store-b2b2c.pickmall.cn>
|
||||
- 账号: `13011111111`
|
||||
- 密码: `111111`
|
||||
- **商城PC端**: <https://pc-b2b2c.pickmall.cn>
|
||||
- **微信小程序**:
|
||||

|
||||
| 微信小程序 | 移动端 H5([点击直接访问](https://m-b2b2c.pickmall.cn)) |
|
||||
| :---: | :---: |
|
||||
| <img src="https://static.pickmall.cn/images/h5-qrcode.png" width="180" alt="微信小程序体验码" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/h5-qrcode.png" width="180" alt="移动端 H5 体验二维码" /> |
|
||||
| 微信扫码体验小程序 | 手机扫码或访问 <https://m-b2b2c.pickmall.cn> |
|
||||
|
||||
- **商城 PC 端**: <https://pc-b2b2c.pickmall.cn>
|
||||
- **平台管理端**: <https://admin-b2b2c.pickmall.cn>
|
||||
- 账号: `admin` / 密码: `123456`
|
||||
- **店铺管理端**: <https://store-b2b2c.pickmall.cn>
|
||||
- 账号: `13011111111` / 密码: `111111`
|
||||
|
||||
### 移动端界面预览
|
||||
|
||||
| 商城首页 | 商品列表 | 商品详情 | 填写订单 | 个人中心 |
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
| <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image.png" width="160" alt="商城首页" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image3.png" width="160" alt="商品列表" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image4.png" width="160" alt="商品详情" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image5.png" width="160" alt="填写订单" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/image2.png" width="160" alt="个人中心" /> |
|
||||
|
||||
---
|
||||
|
||||
### 4. 快速开始
|
||||
## 3. 核心特性与技术栈
|
||||
|
||||
#### 环境准备与部署
|
||||
详细的本地部署指南,请参考官方文档:
|
||||
[**部署文档 -> 环境准备**](https://docs.pickmall.cn/deply/deply.html)
|
||||
### 核心特性
|
||||
- **全端覆盖**: 一套 `uni-app` 代码库支持 H5、微信小程序、Android / iOS APP,大幅降低多端开发与维护成本。
|
||||
- **商家入驻**: 支持多商家入驻,构建平台化 B2B2C 电商生态。
|
||||
- **可视化装修**: 支持首页及专题楼层(`pages/floor`)动态可视化配置与主题色切换。
|
||||
- **即时通讯与直播**: 集成腾讯云 IM / MQTT 消息推送及 HLS 视频流,支持在线客服与直播带货。
|
||||
- **完整电商闭环**: 涵盖商品浏览、购物车、多种促销(秒杀/拼团/优惠券/积分/砍价/分销)、订单履约、售后维权、会员中心等全流程。
|
||||
|
||||
#### 数据库初始化
|
||||
- **推荐方式**: 使用项目提供的 `docker-compose` 配置,可自动完成数据库(MySQL、Redis 等)的部署与初始化。
|
||||
- **手动方式**: 如果您选择手动部署,SQL脚本位于以下地址。请确保获取与您代码版本一致的SQL文件。
|
||||
[**数据库脚本 (Gitee)**](https://gitee.com/beijing_hongye_huicheng/docker/tree/master/init/mysql)
|
||||
### 移动端技术栈
|
||||
| 类别 | 技术选型 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| 核心框架 | `uni-app` + `Vue 3` | 跨端开发框架(支持 Vue 3 语法与渲染) |
|
||||
| 状态管理 | `Vuex 4` | 全局登录态、购物车与用户状态管理 |
|
||||
| UI 组件库 | `uview-plus 3.x` | 适配 Vue 3 的 uni-app 多端 UI 组件库 |
|
||||
| 即时通讯 | `mqtt 5.x` / `@tencentcloud/chat` | 客服消息与实时通知推送 |
|
||||
| 音视频流 | `hls.js` | 直播流播放支持 |
|
||||
| 样式预处理 | `Sass (SCSS)` | 全局主题变量(`uni.scss`)与组件样式定制 |
|
||||
|
||||
---
|
||||
|
||||
### 5. 架构文档边界
|
||||
## 4. 目录结构
|
||||
|
||||
本仓库首页不再维护不完整的系统架构图和技术栈表,避免与实际源码、部署形态和分支差异产生偏差。当前仓库主要说明买家端 H5 / 小程序 / APP 源码定位、在线演示和部署入口。
|
||||
|
||||
完整架构请以官方文档及四个源码仓库为准:
|
||||
|
||||
- 后端源码:`lilishop`
|
||||
- PC / 商家端 / 运营端 / IM Web:`lilishop-ui`
|
||||
- H5 / 小程序 / APP:`lilishop-uniapp`
|
||||
- 部署、SQL、镜像与启动脚本:`docker`
|
||||
```text
|
||||
lilishop-uniapp
|
||||
├── api/ # 各业务模块接口定义(会员、商品、订单、促销、IM 等)
|
||||
├── components/ # 公共业务与基础组件
|
||||
├── config/ # 全局配置目录
|
||||
│ ├── api.js # 接口域名配置(buyer / im / mqtt)
|
||||
│ └── config.js # 商城名称、小程序 AppID、高德 Key、主题色等全局参数
|
||||
├── hybrid/ # App 端本地 HTML 资源(如异常页、WebView 辅助页)
|
||||
├── js_sdk/ # 第三方及 uni-app 插件 SDK
|
||||
├── lib/ # 基础工具库封装
|
||||
├── pages/ # 页面与分包目录
|
||||
│ ├── tabbar/ # 底部导航主包页面(首页、分类、购物车、我的等)
|
||||
│ ├── passport/ # 登录、注册、找回密码分包
|
||||
│ ├── product/ # 商品详情、列表、评价、店铺主页分包
|
||||
│ ├── cart/ # 购物车与结算页分包
|
||||
│ ├── order/ # 订单列表、详情、售后、物流、发票分包
|
||||
│ ├── promotion/ # 秒杀、拼团、砍价、积分商城、分销、直播等营销分包
|
||||
│ ├── mine/ # 个人中心、钱包、积分、收藏、地址管理分包
|
||||
│ ├── floor/ # 可视化装修楼层组件与专题页
|
||||
│ └── navigation/ # 导航与搜索相关页面
|
||||
├── patches/ # patch-package 第三方依赖补丁
|
||||
├── scripts/ # 构建与依赖修补脚本(如 postinstall 补丁)
|
||||
├── static/ # 静态图片与字体资源
|
||||
├── store/ # Vuex 状态管理
|
||||
├── styles/ # 公共样式文件
|
||||
├── uni_modules/ # uni_modules 模块(含 uview-plus 等)
|
||||
├── utils/ # 请求拦截器、加密、校验、存储等工具函数
|
||||
├── App.vue # 应用入口组件
|
||||
├── main.js # 应用初始化入口
|
||||
├── manifest.json # uni-app 多端应用配置(AppID、权限、SDK 配置等)
|
||||
├── pages.json # 页面路由、分包加载与导航栏配置
|
||||
└── uni.scss # 全局 SCSS 变量与主题色定义
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 功能列表
|
||||
## 5. 配套源码仓库
|
||||
|
||||
[功能清单](https://bdwx2tfwwt.feishu.cn/sheets/FFQKscQKDhUx60to1k9cbMngnYb?from=from_copylink)
|
||||
本仓库专注于买家端 **H5 / 小程序 / APP**(`lilishop-uniapp`)。完整系统由以下四个核心仓库组成:
|
||||
|
||||
| 仓库名称 | 职责说明 | 仓库地址 |
|
||||
| :--- | :--- | :--- |
|
||||
| **`lilishop`** | 后端服务源码(Spring Boot 4 / Java 21) | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop) · [GitHub](https://github.com/lilishop/lilishop) |
|
||||
| **`lilishop-ui`** | PC 商城 / 商家端 / 运营管理端 / IM Web 前端 | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop-ui) |
|
||||
| **`lilishop-uniapp`** | 买家端 H5 / 微信小程序 / Android & iOS APP | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp) |
|
||||
| **`docker`** | 容器化部署编排、镜像与数据库脚本 | [Gitee](https://gitee.com/beijing_hongye_huicheng/docker) · [MySQL 脚本](https://gitee.com/beijing_hongye_huicheng/docker/tree/master/init/mysql) |
|
||||
|
||||
---
|
||||
|
||||
### 7. 开源与授权
|
||||
## 6. 开源与授权
|
||||
|
||||
1. **开源协议**: 本项目遵循 `AGPL-3.0` 开源协议。
|
||||
2. **使用范围**: 仅允许用于个人学习、研究和非商业用途。
|
||||
3. **禁止行为**: 禁止将本项目的代码和资源用于任何形式的商业销售。
|
||||
4. **商业授权**: 如需商业使用,必须获得官方授权。授权为一次性永久授权,并提供持续的版本升级服务。详情请联系官网客服。
|
||||
5. **软件著作权**: 本软件受国家计算机软件著作权保护(登记号:2021SR0805085)。
|
||||
本项目遵循 [MIT](./LICENSE) 开源协议,版权归 **北京宏业汇成科技有限公司** 所有。允许个人及企业免费用于学习、研究、修改和商业用途(需保留原始版权和许可声明)。
|
||||
|
||||
---
|
||||
|
||||
### 8. 社区与支持
|
||||
## 7. 社区与支持
|
||||
|
||||
我们欢迎任何形式的交流与贡献。在提问前,请先查阅 [官方文档](https://docs.pickmall.cn/) ,并参考 [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md) 以便高效沟通。
|
||||
我们欢迎任何形式的交流与贡献。在提问前,请先查阅 [产品文档](https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home),并参考 [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md) 以便高效沟通。
|
||||
|
||||
- **[在线客服](https://work.weixin.qq.com/kfid/kfc4d8dc24a73c15f44)**
|
||||
- **微信交流群**:
|
||||

|
||||
- **[在线客服](https://work.weixin.qq.com/kfid/kfc4d8dc24a73c15f44)**
|
||||
- **微信交流群**:
|
||||
<img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/wechat.jpg" width="200" alt="微信群" />
|
||||
|
||||
---
|
||||
|
||||
### 9. 升级计划
|
||||
## 8. 升级计划
|
||||
|
||||
以下功能将陆续在后续版本中发布:✅ 已完成 · 🔜 规划中
|
||||
### ✅ 已完成特性
|
||||
| 功能模块 | 功能模块 |
|
||||
| :--- | :--- |
|
||||
| ✅ 商品定时上下架 | ✅ 商品分组 / 会员分组 |
|
||||
| ✅ 商品虚拟销量 | ✅ 专用发票(发票上传) |
|
||||
| ✅ 置顶评论 | ✅ 客户等级体系 |
|
||||
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
|
||||
| ✅ 卡密商品 | ✅ 二级分销 |
|
||||
| ✅ 定时任务管理 | ✅ 预约服务商品 |
|
||||
| ✅ 主题色管理 | ✅ 商家发货改造(整单/拆单/无需配送) |
|
||||
| ✅ 多维统计(营业/商品/会员/积分/储值/营销/分销) | ✅ 经营报表(店铺业绩/商品同比环比/订单明细/销售汇总) |
|
||||
|
||||
- ✅ 商品定时上下架
|
||||
- ✅ 商品分组
|
||||
- ✅ 会员分组
|
||||
- ✅ 商品虚拟销量
|
||||
- ✅ 专用发票(发票上传)
|
||||
- ✅ 置顶评论
|
||||
- ✅ 客户等级
|
||||
- ✅ 购物卡
|
||||
- ✅ 腾讯云-云直播
|
||||
- ✅ 统计:营业概况、商品概况、会员概况、积分分析、储值分析、营销概况、分销统计
|
||||
- ✅ 经营报表(店铺业绩报表、商品同比环比报表、销售订单明细报表、商品销售汇总报表)
|
||||
- ✅ 卡密商品
|
||||
- ✅ 二级分销
|
||||
- ✅ 定时任务管理
|
||||
- ✅ 服务商品
|
||||
- ✅ 主题色管理
|
||||
- 🔜 店铺结算汇总
|
||||
- 🔜 支付方式汇总
|
||||
- 🔜 结算台账
|
||||
- 🔜 钱包流水
|
||||
- 🔜 财务汇总
|
||||
### 🔜 近期规划中
|
||||
- 🔜 店铺结算汇总 & 结算台账
|
||||
- 🔜 支付方式汇总 & 钱包流水 & 财务汇总
|
||||
- 🔜 微信小店对接
|
||||
- 🔜 抖店对接
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# Vue 3 升级说明
|
||||
|
||||
本项目已从 Vue 2 + uView 1.6 升级到 **Vue 3 + uView Plus**。
|
||||
|
||||
## 环境要求
|
||||
|
||||
- HBuilderX 3.8+
|
||||
- Node.js 16+(用于安装 `uview-plus`、`vuex@4`、`sass`)
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## 主要变更
|
||||
|
||||
| 项 | 变更 |
|
||||
|---|---|
|
||||
| `manifest.json` | 增加 `"vueVersion": "3"` |
|
||||
| `main.js` | 使用 `createSSRApp` + `export function createApp()` |
|
||||
| UI 库 | `uview-ui` → `uview-plus`(npm 依赖) |
|
||||
| 状态管理 | Vuex 4 `createStore` |
|
||||
| 全局 filter | 改为 `main.js` 全局 mixin methods |
|
||||
| 小程序分享 | `utils/mpShare.js` |
|
||||
| u-cell-item | 改为 `u-cell` |
|
||||
| u-verification-code | 改为 `u-code` |
|
||||
| u-swipe-action | 内层增加 `u-swipe-action-item` |
|
||||
| u-upload | 关闭 `auto-upload`,使用 `@afterRead` + `utils/uploadHelper.js` |
|
||||
| u-parse | `:html` 改为 `:content`,移除 `show-with-animation` / `use-cache` |
|
||||
|
||||
## 编译
|
||||
|
||||
请使用 HBuilderX 分别运行到 **H5**、**微信小程序**、**App** 进行验证。
|
||||
|
||||
## 回归测试清单
|
||||
|
||||
### 核心交易链路(H5 / 小程序)
|
||||
|
||||
- [ ] 首页装修模块加载(轮播、商品、优惠券等)
|
||||
- [ ] 分类 / 搜索 / 商品详情
|
||||
- [ ] 加购 / 购物车 / 填写订单
|
||||
- [ ] 支付(H5 微信支付、小程序支付)
|
||||
- [ ] 订单列表 / 订单详情 / 物流
|
||||
- [ ] 售后申请 / 退款 / 评价
|
||||
- [ ] 个人中心 / 地址 / 优惠券 / 积分
|
||||
|
||||
### 微商城专项(H5)
|
||||
|
||||
- [ ] 微信公众号内 JSSDK 分享
|
||||
- [ ] 网页授权登录(`wechatH5Login`)
|
||||
- [ ] `js_sdk/lili-pay/wx-pay.js` 微信支付
|
||||
- [ ] 粘贴板分享链接跳转(`h5-copy`)
|
||||
- [ ] H5 唤起 App 浮窗(`m-airbtn`)
|
||||
|
||||
### 微信小程序专项
|
||||
|
||||
- [ ] 分包加载正常
|
||||
- [ ] `onShareAppMessage` 分享
|
||||
- [ ] 定位 / 相册权限
|
||||
- [ ] 海报 / 二维码(u-draw-poster、Sansnn-uQRCode)
|
||||
|
||||
### App 端(后续阶段)
|
||||
|
||||
- [ ] 5+ 原生模块:支付 / 分享 / OAuth / 定位 / 地图 / 指纹
|
||||
- [ ] 头像裁剪(`okingtz-cropper`)
|
||||
- [ ] 启动参数 / 唤起逻辑(`App.vue` `checkArguments`)
|
||||
|
||||
## 第三方插件
|
||||
|
||||
- `Sansnn-uQRCode`:声明支持 Vue3
|
||||
- `u-draw-poster`:声明支持 Vue2/3
|
||||
- `okingtz-cropper`:需在 App 端实测头像裁剪
|
||||
@@ -1,638 +0,0 @@
|
||||
# Vue 3 语法改造计划
|
||||
|
||||
> 本文档用于按文件夹逐步改造业务代码。框架层(Vue 3 运行时、uView Plus、Vuex 4)已就绪,目标是让各模块语法与 Vue 3 完全兼容,并可选升级为 Composition API。
|
||||
|
||||
## 改造目标
|
||||
|
||||
| 层级 | 说明 | 是否必须 |
|
||||
| --- | --- | --- |
|
||||
| L1 兼容性修复 | 模板语法、uView Plus 组件、生命周期等 Vue 3 不兼容写法 | 必须 |
|
||||
| L2 代码现代化 | Options API → `<script setup>` + composables | 可选,新改模块优先 |
|
||||
|
||||
**原则**:先保证 L1 全项目通过,再按文件夹做 L2。不必一次性改完 170+ 页面。
|
||||
|
||||
---
|
||||
|
||||
## 全局前置(第 0 步,一次性)
|
||||
|
||||
### 0.1 跑自动化脚本
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run migrate:vue3
|
||||
node scripts/migrate-uview-components.js
|
||||
node scripts/migrate-navbar-props.js
|
||||
node scripts/migrate-filters-usage.js
|
||||
```
|
||||
|
||||
脚本覆盖范围:
|
||||
|
||||
- 模板过滤器 `{{ x \| unitPrice }}` → `{{ unitPrice(x) }}`
|
||||
- `v-model` → `v-model:show`(popup 类组件)
|
||||
- `slot="xxx"` → `#xxx`
|
||||
- `this.$set` / `this.$delete` 替换
|
||||
- uView 组件别名(`u-cell-item` → `u-cell` 等)
|
||||
|
||||
### 0.2 全局残留扫描
|
||||
|
||||
每完成一个文件夹,在**该文件夹**内执行:
|
||||
|
||||
```bash
|
||||
# 模板过滤器
|
||||
rg "\|\s*(unitPrice|unixToDate|beautifyTime|goodsFormatPrice)" pages/xxx --glob "*.vue"
|
||||
|
||||
# Vue 2 生命周期
|
||||
rg "beforeDestroy|destroyed" pages/xxx --glob "*.vue"
|
||||
|
||||
# 旧 uView 组件
|
||||
rg "u-cell-item|u-verification-code|u-alert-tips|<u-loading[^-]" pages/xxx --glob "*.vue"
|
||||
|
||||
# .sync 修饰符
|
||||
rg "\.sync" pages/xxx --glob "*.vue"
|
||||
|
||||
# u-parse 旧属性
|
||||
rg ":html=|show-with-animation|use-cache" pages/xxx --glob "*.vue"
|
||||
```
|
||||
|
||||
### 0.3 单文件改造 SOP
|
||||
|
||||
每个 `.vue` 文件按以下顺序处理:
|
||||
|
||||
1. **模板**
|
||||
- 过滤器改为函数调用
|
||||
- uView Plus 组件 props 对照 [VUE3_MIGRATION.md](./VUE3_MIGRATION.md)
|
||||
- 插槽:`slot="right"` → `#right`
|
||||
- `.sync` → `v-model:xxx`
|
||||
|
||||
2. **Script(L1 最小改动)**
|
||||
- `beforeDestroy` → `beforeUnmount`
|
||||
- `destroyed` → `unmounted`
|
||||
- 删除 `filters: { }` 块(改用全局 mixin methods 或本地 methods)
|
||||
- `this.$set(obj, key, val)` → `obj[key] = val`
|
||||
|
||||
3. **Script(L2 可选升级)**
|
||||
- `<script>` → `<script setup lang="ts">`
|
||||
- `data()` → `ref()` / `reactive()`
|
||||
- `computed` → `computed()`
|
||||
- `methods` → 普通函数
|
||||
- 生命周期:`onLoad` / `onShow` 从 `@dcloudio/uni-app` 引入
|
||||
- 复杂逻辑抽到同目录 `composables/useXxx.ts`
|
||||
|
||||
4. **验证**
|
||||
- H5 打开对应页面
|
||||
- 微信开发者工具编译通过
|
||||
- 核心交互点手动点一遍
|
||||
|
||||
### 0.4 进度标记
|
||||
|
||||
本文档各文件夹表格中状态列取值:
|
||||
|
||||
- `[ ]` 未开始
|
||||
- `[~]` 进行中
|
||||
- `[x]` L1 完成(兼容)
|
||||
- `[★]` L2 完成(Composition API)
|
||||
|
||||
---
|
||||
|
||||
## 批次总览
|
||||
|
||||
| 批次 | 文件夹 | 文件数 | 优先级 | 依赖 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 0 | 全局基础设施 | — | P0 | — |
|
||||
| 1 | `components/` | 22 | P0 | 被全项目引用 |
|
||||
| 2 | `pages/tabbar/` | 34 | P0 | 首页 / 购物车 / 分类 / 我的 |
|
||||
| 3 | `pages/product/` | 20 | P0 | 商品详情链路 |
|
||||
| 4 | `pages/cart/` | 8 | P0 | 下单前 |
|
||||
| 5 | `pages/order/` | 19 | P0 | 交易后 |
|
||||
| 6 | `pages/passport/` | 10 | P1 | 登录注册 |
|
||||
| 7 | `pages/mine/` | 41 | P1 | 个人中心分包 |
|
||||
| 8 | `pages/promotion/` | 10 | P1 | 营销活动 |
|
||||
| 9 | 零散页面 | 3 | P2 | 导航 / 空态 / 旧直播 |
|
||||
| 10 | `App.vue` + `store/` | 2 | P0 | 应用入口 |
|
||||
|
||||
**建议顺序**:0 → 1 → 2 → 3 → 4 → 5 → 10(穿插)→ 6 → 7 → 8 → 9
|
||||
|
||||
---
|
||||
|
||||
## 批次 0:全局基础设施
|
||||
|
||||
> 框架层已基本完成,本批次以核对为主。
|
||||
|
||||
| 文件 | 状态 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| `main.js` | [x] | `createSSRApp`、filter mixin、uView Plus |
|
||||
| `manifest.json` | [x] | `"vueVersion": "3"` |
|
||||
| `store/index.js` | [ ] | Vuex 4,可补充 `useStore()` 导出供 L2 使用 |
|
||||
| `utils/filters.js` | [x] | 已改为普通函数 |
|
||||
| `utils/mpShare.js` | [x] | 小程序分享 mixin |
|
||||
| `utils/uploadHelper.js` | [x] | u-upload `@afterRead` 配套 |
|
||||
|
||||
### 批次 0 测试
|
||||
|
||||
- [ ] `npm install` 无报错
|
||||
- [ ] H5 启动无白屏
|
||||
- [ ] 小程序编译通过
|
||||
|
||||
---
|
||||
|
||||
## 批次 1:`components/`(22 文件)
|
||||
|
||||
> 公共组件,改完收益最大。优先改被高频引用的组件。
|
||||
|
||||
### 1.1 商品与交易
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `m-goods-list/list.vue` | [ ] | 价格过滤器 |
|
||||
| `m-goods-list/base-list.vue` | [ ] | |
|
||||
| `m-goods-list/common.vue` | [ ] | |
|
||||
| `m-goods-list/promotion.vue` | [ ] | |
|
||||
| `m-goods-recommend/index.vue` | [ ] | |
|
||||
| `m-buy/goods.vue` | [ ] | popup、规格选择 |
|
||||
| `m-take-down-sale-goods/index.vue` | [ ] | |
|
||||
|
||||
### 1.2 交互与工具
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `verification/verification.vue` | [ ] | `this.$store` |
|
||||
| `verify-code/verify-code.vue` | [ ] | `u-code` |
|
||||
| `popups/popups.vue` | [ ] | `v-model:show` |
|
||||
| `uni-number-box.vue` | [ ] | |
|
||||
| `uni-load-more/uni-load-more.vue` | [ ] | |
|
||||
| `uniMap.vue` | [ ] | 地图 API |
|
||||
| `m-city/m-city.vue` | [ ] | 城市选择 |
|
||||
|
||||
### 1.3 分享与展示
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `m-share/index.vue` | [ ] | 分享逻辑 |
|
||||
| `m-placard/index.vue` | [ ] | 海报 |
|
||||
| `m-canvas/index.vue` | [ ] | 画布 |
|
||||
| `m-search-revision/m-search-revision.vue` | [ ] | |
|
||||
| `m-airbtn/index.vue` | [ ] | H5 唤起 App |
|
||||
| `default-page/default-page.vue` | [ ] | 空态页 |
|
||||
| `u-time-line/u-time-line.vue` | [ ] | 自定义 easycom |
|
||||
| `u-time-line/u-time-line-item.vue` | [ ] | |
|
||||
|
||||
### 批次 1 测试
|
||||
|
||||
- [ ] 首页商品列表渲染正常
|
||||
- [ ] 商品详情加购弹窗正常
|
||||
- [ ] 验证码 / 滑块验证正常
|
||||
|
||||
---
|
||||
|
||||
## 批次 2:`pages/tabbar/`(34 文件)
|
||||
|
||||
> 主包 Tab 页,用户入口,优先保证 L1。
|
||||
|
||||
### 2.1 首页核心(4 文件)
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `home/index.vue` | [ ] | 装修数据加载 |
|
||||
| `home/views.vue` | [ ] | 模块渲染 |
|
||||
| `home/title.vue` | [ ] | 消息 |
|
||||
| `home/web-view.vue` | [ ] | 内嵌 H5 |
|
||||
|
||||
### 2.2 首页装修模板(23 文件)
|
||||
|
||||
路径:`pages/tabbar/home/template/`
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `tpl_banner.vue` | [ ] | 轮播 |
|
||||
| `tpl_goods.vue` | [ ] | `beforeDestroy` 残留 |
|
||||
| `tpl_menu.vue` | [ ] | 宫格 |
|
||||
| `tpl_search.vue` | [ ] | 搜索框 |
|
||||
| `tpl_notice.vue` | [ ] | 公告 |
|
||||
| `tpl_title.vue` | [ ] | 标题 |
|
||||
| `tpl_spike.vue` | [ ] | 秒杀 |
|
||||
| `tpl_group.vue` | [ ] | 拼团 |
|
||||
| `tpl_join_group.vue` | [ ] | 参团 |
|
||||
| `tpl_integral.vue` | [ ] | 积分 |
|
||||
| `tpl_hot_zone.vue` | [ ] | 热区 |
|
||||
| `tpl_text_picture.vue` | [ ] | 图文 |
|
||||
| `tpl_promotions_detail.vue` | [ ] | 促销详情 |
|
||||
| `fetch_coupon.vue` | [ ] | 领券 |
|
||||
| `tpl_flex_one.vue` | [ ] | 布局 |
|
||||
| `tpl_flex_two.vue` | [ ] | |
|
||||
| `tpl_flex_three.vue` | [ ] | |
|
||||
| `tpl_flex_four.vue` | [ ] | |
|
||||
| `tpl_flex_five.vue` | [ ] | |
|
||||
| `tpl_top_one_bottom_two.vue` | [ ] | |
|
||||
| `tpl_top_two_bottom_one.vue` | [ ] | |
|
||||
| `tpl_left_one_right_two.vue` | [ ] | |
|
||||
| `tpl_left_two_right_one.vue` | [ ] | |
|
||||
|
||||
> 模板文件结构相似,建议抽一个 `template` 公共 composable 后再批量 L2。
|
||||
|
||||
### 2.3 其他 Tab(7 文件)
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `cart/cartList.vue` | [ ] | `this.$store`、SwipeAction |
|
||||
| `category/category.vue` | [ ] | 分类树 |
|
||||
| `user/my.vue` | [ ] | 个人中心入口 |
|
||||
| `user/similarGoods.vue` | [ ] | 相似商品 |
|
||||
| `user/utils/tool.vue` | [ ] | 工具函数组件 |
|
||||
| `screen/fullScreen.vue` | [ ] | 全屏广告 |
|
||||
| `special/special.vue` | [ ] | 专题页 |
|
||||
|
||||
### 批次 2 测试
|
||||
|
||||
- [ ] 首页各装修模块加载
|
||||
- [ ] 下拉刷新
|
||||
- [ ] 购物车加减 / 删除
|
||||
- [ ] 分类切换
|
||||
- [ ] 我的页面登录态展示
|
||||
|
||||
---
|
||||
|
||||
## 批次 3:`pages/product/`(20 文件)
|
||||
|
||||
> 商品详情是核心转化页,建议 `goods.vue` 优先 L2。
|
||||
|
||||
### 3.1 商品主流程(7 文件)
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `goods.vue` | [ ] | 主页面,`this.$store`,子组件多 |
|
||||
| `askList.vue` | [ ] | 问答 |
|
||||
| `comment.vue` | [ ] | 评价 |
|
||||
| `shopList.vue` | [ ] | 店铺列表 |
|
||||
| `shopPage.vue` | [ ] | 店铺首页 |
|
||||
| `shopPageGoods.vue` | [ ] | 店铺商品 |
|
||||
| `licencePhoto.vue` | [ ] | 证照 |
|
||||
| `customerservice/index.vue` | [ ] | 客服 |
|
||||
|
||||
### 3.2 商品详情子组件(12 文件)
|
||||
|
||||
路径:`pages/product/product/`
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `goods/-goods-swiper.vue` | [ ] | 轮播 |
|
||||
| `goods/-goods-intro.vue` | [ ] | **u-parse `:html`** |
|
||||
| `goods/-goods-desc.vue` | [ ] | 详情 |
|
||||
| `goods/-goods-recommend.vue` | [ ] | 推荐 |
|
||||
| `evaluation/-evaluation.vue` | [ ] | 评价列表 |
|
||||
| `shop/-shop.vue` | [ ] | 店铺信息 |
|
||||
| `popup/address.vue` | [ ] | 地址弹窗 |
|
||||
| `promotion/-promotion.vue` | [ ] | 促销 |
|
||||
| `promotion/-promotion-coupon.vue` | [ ] | 优惠券 |
|
||||
| `promotion/-promotion-details.vue` | [ ] | 促销详情 |
|
||||
| `promotion/-promotion-assemble-list.vue` | [ ] | 拼团列表 |
|
||||
| `promotion/-promotion-assemble-promotions.vue` | [ ] | 拼团促销 |
|
||||
|
||||
### 批次 3 测试
|
||||
|
||||
- [ ] 商品详情完整加载(图 / 价 / 规格 / 促销)
|
||||
- [ ] 加购 / 立即购买
|
||||
- [ ] 富文本详情渲染(u-parse)
|
||||
- [ ] 店铺跳转
|
||||
|
||||
---
|
||||
|
||||
## 批次 4:`pages/cart/`(8 文件)
|
||||
|
||||
### 4.1 优惠券(4 文件)
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `coupon/index.vue` | [ ] | `this.$store` |
|
||||
| `coupon/myCoupon.vue` | [ ] | |
|
||||
| `coupon/couponCenter.vue` | [ ] | |
|
||||
| `coupon/couponDetail.vue` | [ ] | |
|
||||
|
||||
### 4.2 支付(4 文件)
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `payment/payOrder.vue` | [ ] | 支付核心,`this.$store` |
|
||||
| `payment/success.vue` | [ ] | 支付成功 |
|
||||
| `payment/shareOrderGoods.vue` | [ ] | 分享订单 |
|
||||
| `payment/error.vue` | [ ] | 支付失败 |
|
||||
|
||||
### 批次 4 测试
|
||||
|
||||
- [ ] 选券 / 用券
|
||||
- [ ] H5 微信支付
|
||||
- [ ] 小程序支付
|
||||
- [ ] 支付成功 / 失败页
|
||||
|
||||
---
|
||||
|
||||
## 批次 5:`pages/order/`(19 文件)
|
||||
|
||||
### 5.1 订单主流程(4 文件)
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `fillorder.vue` | [ ] | 填单,`this.$store` |
|
||||
| `myOrder.vue` | [ ] | 订单列表 |
|
||||
| `orderDetail.vue` | [ ] | 订单详情 |
|
||||
| `deliverDetail.vue` | [ ] | 物流 |
|
||||
|
||||
### 5.2 售后(7 文件)
|
||||
|
||||
路径:`pages/order/afterSales/`
|
||||
|
||||
| 文件 | 状态 |
|
||||
| --- | --- |
|
||||
| `afterSales.vue` | [ ] |
|
||||
| `afterSalesSelect.vue` | [ ] |
|
||||
| `applyDetail.vue` | [ ] |
|
||||
| `applyProgress.vue` | [ ] |
|
||||
| `applySuccess.vue` | [ ] |
|
||||
| `afterSalesDetail.vue` | [ ] |
|
||||
| `afterSalesDetailExpress.vue` | [ ] |
|
||||
|
||||
### 5.3 评价(3 文件)
|
||||
|
||||
| 文件 | 状态 |
|
||||
| --- | --- |
|
||||
| `evaluate/myEvaluate.vue` | [ ] |
|
||||
| `evaluate/releaseEvaluate.vue` | [ ] |
|
||||
| `evaluate/evaluateDetail.vue` | [ ] |
|
||||
|
||||
### 5.4 投诉与发票(5 文件)
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `complain/complain.vue` | [ ] | |
|
||||
| `complain/complainList.vue` | [ ] | |
|
||||
| `complain/complainInfo.vue` | [ ] | |
|
||||
| `invoice/setInvoice.vue` | [ ] | **`.sync` 多处** |
|
||||
| `invoice/invoiceDetail.vue` | [ ] | |
|
||||
|
||||
### 批次 5 测试
|
||||
|
||||
- [ ] 提交订单 → 支付 → 订单列表
|
||||
- [ ] 申请售后全流程
|
||||
- [ ] 评价发布
|
||||
- [ ] 发票填写
|
||||
|
||||
---
|
||||
|
||||
## 批次 6:`pages/passport/`(10 文件)
|
||||
|
||||
### 6.1 登录(5 文件)
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `login.vue` | [ ] | `this.$store`、核心入口 |
|
||||
| `wechatH5Login.vue` | [ ] | H5 公众号授权 |
|
||||
| `wechatMPLogin.vue` | [ ] | 小程序登录 |
|
||||
| `scannerCodeLoginConfirm.vue` | [ ] | 扫码登录 |
|
||||
| `article.vue` | [ ] | **u-parse `:html`** |
|
||||
|
||||
### 6.2 商家入驻(5 文件)
|
||||
|
||||
路径:`pages/passport/entry/seller/`
|
||||
|
||||
| 文件 | 状态 |
|
||||
| --- | --- |
|
||||
| `index.vue` | [ ] |
|
||||
| `control.vue` | [ ] |
|
||||
| `step1.vue` | [ ] |
|
||||
| `step2.vue` | [ ] |
|
||||
| `step3.vue` | [ ] |
|
||||
|
||||
### 批次 6 测试
|
||||
|
||||
- [ ] H5 账号密码登录
|
||||
- [ ] 微信小程序授权登录
|
||||
- [ ] 登录态写入 Vuex / storage
|
||||
|
||||
---
|
||||
|
||||
## 批次 7:`pages/mine/`(41 文件)
|
||||
|
||||
> 最大分包,按子文件夹拆分推进。
|
||||
|
||||
### 7.1 根目录(4 文件)
|
||||
|
||||
| 文件 | 状态 |
|
||||
| --- | --- |
|
||||
| `signIn.vue` | [ ] |
|
||||
| `myTracks.vue` | [ ] |
|
||||
| `myCollect.vue` | [ ] |
|
||||
| `point/myPoint.vue` | [ ] |
|
||||
|
||||
### 7.2 地址(4 文件)
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `address/address.vue` | [ ] | `this.$store` |
|
||||
| `address/add.vue` | [ ] | |
|
||||
| `address/addressManage.vue` | [ ] | |
|
||||
| `address/storeAddress.vue` | [ ] | |
|
||||
|
||||
### 7.3 预存款(6 文件)
|
||||
|
||||
| 文件 | 状态 |
|
||||
| --- | --- |
|
||||
| `deposit/index.vue` | [ ] |
|
||||
| `deposit/info.vue` | [ ] |
|
||||
| `deposit/recharge.vue` | [ ] |
|
||||
| `deposit/withdrawal.vue` | [ ] |
|
||||
| `deposit/withdrawApply.vue` | [ ] |
|
||||
| `deposit/operation.vue` | [ ] |
|
||||
|
||||
### 7.4 分销(7 文件)
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `distribution/home.vue` | [ ] | `this.$store` |
|
||||
| `distribution/list.vue` | [ ] | |
|
||||
| `distribution/join.vue` | [ ] | |
|
||||
| `distribution/auth.vue` | [ ] | |
|
||||
| `distribution/achievement.vue` | [ ] | |
|
||||
| `distribution/history.vue` | [ ] | |
|
||||
| `distribution/withdrawal.vue` | [ ] | |
|
||||
|
||||
### 7.5 消息(6 文件)
|
||||
|
||||
| 文件 | 状态 |
|
||||
| --- | --- |
|
||||
| `msgTips/main.vue` | [ ] |
|
||||
| `msgTips/sysMsg/index.vue` | [ ] |
|
||||
| `msgTips/packageMsg/index.vue` | [ ] |
|
||||
| `msgTips/packageMsg/logisticsDetail.vue` | [ ] |
|
||||
| `msgTips/serviceMsg/index.vue` | [ ] |
|
||||
| `im/index.vue` | [ ] |
|
||||
| `im/list.vue` | [ ] |
|
||||
|
||||
### 7.6 设置(12 文件)
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `set/setUp.vue` | [ ] | |
|
||||
| `set/personMsg.vue` | [ ] | **`.sync`** |
|
||||
| `set/feedBack.vue` | [ ] | u-upload |
|
||||
| `set/editionIntro.vue` | [ ] | |
|
||||
| `set/versionFunctionList.vue` | [ ] | |
|
||||
| `help/tips.vue` | [ ] | **u-parse `:html`** |
|
||||
| `set/securityCenter/securityCenter.vue` | [ ] | |
|
||||
| `set/securityCenter/bindMobile.vue` | [ ] | |
|
||||
| `set/securityCenter/editPassword.vue` | [ ] | |
|
||||
| `set/securityCenter/editLoginPassword.vue` | [ ] | |
|
||||
| `set/securityCenter/faceLogin.vue` | [ ] | |
|
||||
| `set/securityCenter/fingerLogin.vue` | [ ] | |
|
||||
| `set/securityCenter/updatePwdTab.vue` | [ ] | |
|
||||
|
||||
### 批次 7 测试
|
||||
|
||||
- [ ] 地址增删改
|
||||
- [ ] 收藏 / 足迹
|
||||
- [ ] 分销中心
|
||||
- [ ] 修改密码 / 绑定手机
|
||||
|
||||
---
|
||||
|
||||
## 批次 8:`pages/promotion/`(10 文件)
|
||||
|
||||
### 8.1 直播(1 文件 + utils)
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `live/room.vue` | [ ] | 1300+ 行,**`.sync` 10 处**,建议 L2 拆 composables |
|
||||
| `live/utils/liveChat.js` | [ ] | IM 聊天服务 |
|
||||
| `live/utils/liveMqtt.js` | [ ] | MQTT |
|
||||
| `live/utils/liveRecommend.js` | [ ] | 推荐商品 |
|
||||
| `live/utils/liveSetting.js` | [ ] | 直播配置 |
|
||||
| `live/mqtt/mqtt.js` | [ ] | MQTT 客户端 |
|
||||
|
||||
> 旧路径 `pages/live/room.vue` 为冗余文件,改造完成后删除并核对 `pages.json` 路由。
|
||||
|
||||
### 8.2 营销活动(9 文件)
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `lives.vue` | [ ] | 直播列表 |
|
||||
| `seckill.vue` | [ ] | 秒杀 |
|
||||
| `joinGroup.vue` | [ ] | 拼团 |
|
||||
| `bargain/list.vue` | [ ] | 砍价列表 |
|
||||
| `bargain/detail.vue` | [ ] | **u-parse `:html`** |
|
||||
| `bargain/log.vue` | [ ] | 砍价记录 |
|
||||
| `point/pointList.vue` | [ ] | 积分商城 |
|
||||
| `point/detail.vue` | [ ] | **u-parse `:html`** |
|
||||
| `point/user.vue` | [ ] | 积分用户 |
|
||||
|
||||
### 批次 8 测试
|
||||
|
||||
- [ ] 进入直播间 / 拉流 / 聊天
|
||||
- [ ] 秒杀 / 拼团 / 砍价页面
|
||||
- [ ] 积分兑换
|
||||
|
||||
---
|
||||
|
||||
## 批次 9:零散页面(3 文件)
|
||||
|
||||
| 文件 | 状态 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| `pages/navigation/search/searchPage.vue` | [ ] | 搜索页,`this.$store` |
|
||||
| `pages/floor/empty.vue` | [ ] | 空楼层 |
|
||||
| `pages/live/room.vue` | [ ] | **待删除**,与 `promotion/live` 重复 |
|
||||
|
||||
---
|
||||
|
||||
## 批次 10:应用入口
|
||||
|
||||
| 文件 | 状态 | 关注点 |
|
||||
| --- | --- | --- |
|
||||
| `App.vue` | [ ] | `mapMutations`、`this.$store`、onLaunch |
|
||||
| `store/index.js` | [ ] | 可添加 `useStore()` 导出 |
|
||||
|
||||
### App.vue L2 参考写法
|
||||
|
||||
```ts
|
||||
// store/index.js 补充
|
||||
import { useStore as useVuexStore } from 'vuex'
|
||||
export function useStore() {
|
||||
return useVuexStore()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 已知高风险文件清单
|
||||
|
||||
以下文件含已知 Vue 2 残留,改造时优先处理:
|
||||
|
||||
| 文件 | 问题 |
|
||||
| --- | --- |
|
||||
| `pages/tabbar/home/template/tpl_goods.vue` | `beforeDestroy` |
|
||||
| `pages/order/invoice/setInvoice.vue` | `.sync` × 7 |
|
||||
| `pages/promotion/live/room.vue` | `.sync` × 10,1300+ 行 |
|
||||
| `pages/mine/set/personMsg.vue` | `.sync` |
|
||||
| `pages/product/product/goods/-goods-intro.vue` | u-parse `:html` |
|
||||
| `pages/mine/help/tips.vue` | u-parse `:html` |
|
||||
| `pages/passport/article.vue` | u-parse `:html` |
|
||||
| `pages/promotion/bargain/detail.vue` | u-parse `:html` |
|
||||
| `pages/promotion/point/detail.vue` | u-parse `:html` |
|
||||
|
||||
---
|
||||
|
||||
## 含 `this.$store` 的文件(L2 时改为 `useStore()`)
|
||||
|
||||
共 39 处 / 37 个文件,集中在:
|
||||
|
||||
- `pages/cart/`(4 文件)
|
||||
- `pages/order/`(10 文件)
|
||||
- `pages/mine/`(12 文件)
|
||||
- `pages/passport/login.vue`
|
||||
- `pages/product/goods.vue`
|
||||
- `pages/tabbar/cart/cartList.vue`
|
||||
- `App.vue`
|
||||
- `components/verification/verification.vue`
|
||||
|
||||
---
|
||||
|
||||
## 第三方模块(仅验证,不改源码)
|
||||
|
||||
| 模块 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| Sansnn-uQRCode | `uni_modules/Sansnn-uQRCode/` | 声明支持 Vue 3 |
|
||||
| okingtz-cropper | `uni_modules/okingtz-cropper/` | App 端实测头像裁剪 |
|
||||
| u-draw-poster | `js_sdk/u-draw-poster/` | 海报生成 |
|
||||
|
||||
---
|
||||
|
||||
## 推荐里程碑
|
||||
|
||||
| 里程碑 | 完成标准 | 预计工作量 |
|
||||
| --- | --- | --- |
|
||||
| M1 可运行 | 批次 0–2 L1 完成,主 Tab 无报错 | 3–5 天 |
|
||||
| M2 可交易 | 批次 3–5 L1 完成,下单支付通 | 3–5 天 |
|
||||
| M3 全兼容 | 全部 L1 完成,三端回归通过 | 5–7 天 |
|
||||
| M4 现代化 | 核心模块 L2(tabbar / product / live) | 按需持续 |
|
||||
|
||||
---
|
||||
|
||||
## 进度汇总
|
||||
|
||||
| 批次 | 文件夹 | 文件数 | L1 进度 | L2 进度 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 0 | 全局基础设施 | — | 80% | — |
|
||||
| 1 | `components/` | 22 | 0/22 | 0/22 |
|
||||
| 2 | `pages/tabbar/` | 34 | 0/34 | 0/34 |
|
||||
| 3 | `pages/product/` | 20 | 0/20 | 0/20 |
|
||||
| 4 | `pages/cart/` | 8 | 0/8 | 0/8 |
|
||||
| 5 | `pages/order/` | 19 | 0/19 | 0/19 |
|
||||
| 6 | `pages/passport/` | 10 | 0/10 | 0/10 |
|
||||
| 7 | `pages/mine/` | 41 | 0/41 | 0/41 |
|
||||
| 8 | `pages/promotion/` | 10 | 0/10 | 0/10 |
|
||||
| 9 | 零散页面 | 3 | 0/3 | 0/3 |
|
||||
| 10 | 应用入口 | 2 | 0/2 | 0/2 |
|
||||
| **合计** | | **169** | **0/169** | **0/169** |
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [VUE3_MIGRATION.md](./VUE3_MIGRATION.md) — 框架升级说明与回归清单
|
||||
- [uView Plus 文档](https://uview-plus.jiangruyi.com/) — 组件 API 对照
|
||||
@@ -39,5 +39,17 @@ export function getArticleDetail(type) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取文章列表(楼层装修用)
|
||||
* @param params { pageNumber, pageSize, categoryId? }
|
||||
*/
|
||||
export function getArticleList(params) {
|
||||
return http.request({
|
||||
url: `/other/article`,
|
||||
method: Method.GET,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
### 说明 https://ext.dcloud.net.cn/plugin?id=3237
|
||||
@@ -1,200 +0,0 @@
|
||||
<template>
|
||||
|
||||
<div class="index">
|
||||
<u-modal v-model:show="show" :show-title="false" :show-confirm-button="false" mask-close-able>
|
||||
<view class="slot-content">
|
||||
<image @click="downLoad()" class="img" :src="imgUrl" />
|
||||
<div class="canvas-hide">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<canvas id="canvas" type="2d" style="width: 600px; height: 960px" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<canvas canvas-id="canvas" id="canvas" style="width: 600px; height: 960px" />
|
||||
<!-- #endif -->
|
||||
</div>
|
||||
</view>
|
||||
</u-modal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, getCurrentInstance } from 'vue'
|
||||
// 引入绘制插件
|
||||
import DrawPoster from "@/js_sdk/u-draw-poster"
|
||||
import logoImg from "@/pages/passport/static/logo-title.png"
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
const props = defineProps<{
|
||||
res?: any
|
||||
}>()
|
||||
|
||||
const imgUrl = ref("")
|
||||
const show = ref(false)
|
||||
const dp = ref<any>({})
|
||||
const logo = logoImg
|
||||
|
||||
/**
|
||||
* 解决微信小程序中图片模糊问题
|
||||
*/
|
||||
// #ifdef MP-WEIXIN
|
||||
const st2 = (size: number) => size * 2
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
const st2 = (size: number) => size
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* 保存图片
|
||||
*/
|
||||
const downLoad = () => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: imgUrl.value,
|
||||
success: function () {
|
||||
uni.showToast({
|
||||
title: "保存成功!",
|
||||
icon: "none",
|
||||
})
|
||||
},
|
||||
fail: function () {
|
||||
uni.showToast({
|
||||
title: "保存失败,请稍后重试!",
|
||||
icon: "none",
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建canvas
|
||||
*/
|
||||
const init = async () => {
|
||||
show.value = true
|
||||
dp.value = await DrawPoster.build({
|
||||
selector: "canvas",
|
||||
componentThis: instance?.proxy,
|
||||
loading: true,
|
||||
debugging: true,
|
||||
})
|
||||
let dpVal = dp.value
|
||||
// #ifdef MP-WEIXIN
|
||||
// 用于微信小程序中画布错乱问题
|
||||
dpVal.canvas.width = st2(600)
|
||||
dpVal.canvas.height = st2(960)
|
||||
// #endif
|
||||
await draw(dpVal)
|
||||
}
|
||||
|
||||
const draw = async (dp: any) => {
|
||||
const { width, height, background, title } = props.res.container
|
||||
const { code, img, price } = props.res.bottom
|
||||
|
||||
// /** 绘制背景 */
|
||||
await dp.draw((ctx: any) => {
|
||||
ctx.fillStyle = background
|
||||
ctx.fillRoundRect(
|
||||
st2(0),
|
||||
st2(0),
|
||||
st2(width),
|
||||
st2(height),
|
||||
st2(12)
|
||||
)
|
||||
ctx.clip()
|
||||
})
|
||||
/** 绘制图片 */
|
||||
dp.draw(async (ctx: any) => {
|
||||
await Promise.all([
|
||||
// 绘制Logo
|
||||
ctx.drawImage(
|
||||
logo,
|
||||
st2(175),
|
||||
st2(0),
|
||||
st2(256),
|
||||
st2(144)
|
||||
),
|
||||
// 中间图片
|
||||
ctx.drawImage(
|
||||
img,
|
||||
st2(100),
|
||||
st2(150),
|
||||
st2(400),
|
||||
st2(400)
|
||||
),
|
||||
// 二维码
|
||||
ctx.drawImage(
|
||||
code,
|
||||
st2(39),
|
||||
st2(750),
|
||||
st2(150),
|
||||
st2(150)
|
||||
),
|
||||
])
|
||||
})
|
||||
|
||||
/** 绘制中间文字*/
|
||||
await dp.draw((ctx: any) => {
|
||||
ctx.fillStyle = "#333"
|
||||
ctx.font = `bold ${st2(24)}px PingFang SC`
|
||||
ctx.textAlign = "center"
|
||||
ctx.fillWarpText({
|
||||
text: title,
|
||||
maxWidth: st2(500),
|
||||
x: st2(300),
|
||||
y: st2(600),
|
||||
layer: 1,
|
||||
})
|
||||
|
||||
ctx.fillStyle = "#ff3c2a"
|
||||
ctx.font = `${st2(38)}px PingFang SC`
|
||||
ctx.textAlign = "center"
|
||||
ctx.fillText(price, st2(300), st2(680))
|
||||
})
|
||||
|
||||
// /** 绘制底部文字 */
|
||||
await dp.draw((ctx: any) => {
|
||||
ctx.fillStyle = "#666"
|
||||
ctx.font = `${st2(24)}px PingFang SC`
|
||||
ctx.fillText("长按图片,识别二维码", st2(200), st2(866))
|
||||
ctx.fillStyle = "#666"
|
||||
ctx.font = `${st2(24)}px PingFang SC`
|
||||
ctx.fillText("查看商品详情", st2(200), st2(900))
|
||||
})
|
||||
|
||||
imgUrl.value = await dp.createImagePath()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
page,
|
||||
.index {
|
||||
height: 100%;
|
||||
}
|
||||
.canvas-hide {
|
||||
/* 1 */
|
||||
position: fixed;
|
||||
right: 100vw;
|
||||
bottom: 100vh;
|
||||
/* 2 */
|
||||
z-index: -9999;
|
||||
/* 3 */
|
||||
opacity: 0;
|
||||
}
|
||||
.index {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba($color: grey, $alpha: 0.2);
|
||||
}
|
||||
|
||||
image {
|
||||
display: block;
|
||||
}
|
||||
.img {
|
||||
width: 600rpx;
|
||||
height: 960rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,199 +0,0 @@
|
||||
<template>
|
||||
<view class="index">
|
||||
<view v-model="show" class="slot-content">
|
||||
<image @click="downLoad()" class="img" :src="imgUrl" />
|
||||
<div class="canvas-hide">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<canvas id="canvas" type="2d" style="width: 560px; height: 800px" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<canvas canvas-id="canvas" id="canvas" style="width: 560px; height: 800px" />
|
||||
<!-- #endif -->
|
||||
</div>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, getCurrentInstance } from 'vue'
|
||||
// 引入绘制插件
|
||||
import DrawPoster from "@/js_sdk/u-draw-poster"
|
||||
// 生成二维码
|
||||
import uQRCode from '@/components/Sansnn-uQRCode/uqrcode.js'
|
||||
import logoImg from "@/pages/passport/static/logo-title.png"
|
||||
import myFaceImg from "@/pages/passport/static/missing-face.png"
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
const props = defineProps<{
|
||||
res?: any
|
||||
}>()
|
||||
|
||||
const imgUrl = ref("")
|
||||
const show = ref(false)
|
||||
const dp = ref<any>({})
|
||||
const logo = logoImg
|
||||
const myFace = myFaceImg
|
||||
const sharingLink = ref('')
|
||||
|
||||
/** 解决微信小程序中图片模糊问题 */
|
||||
// #ifdef MP-WEIXIN
|
||||
const st2 = (size: number) => size * 2
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
const st2 = (size: number) => size
|
||||
// #endif
|
||||
|
||||
/** 保存图片 */
|
||||
const downLoad = () => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: imgUrl.value,
|
||||
success: function () {
|
||||
uni.showToast({title: "保存成功!",icon: "none"})
|
||||
},
|
||||
fail: function () {
|
||||
uni.showToast({title: "保存失败,请稍后重试!",icon: "none"})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 创建canvas */
|
||||
const init = async () => {
|
||||
show.value = true
|
||||
dp.value = await DrawPoster.build({
|
||||
selector: "canvas",
|
||||
componentThis: instance?.proxy,
|
||||
loading: true,
|
||||
debugging: true,
|
||||
})
|
||||
let dpVal = dp.value
|
||||
// #ifdef MP-WEIXIN
|
||||
// 用于微信小程序中画布错乱问题
|
||||
dpVal.canvas.width = st2(560)
|
||||
dpVal.canvas.height = st2(800)
|
||||
// #endif
|
||||
await showQRCode(dpVal)
|
||||
}
|
||||
|
||||
/** 生成二维码 */
|
||||
const showQRCode = async (dp: any) => {
|
||||
await uQRCode.make({
|
||||
canvasId: 'canvas',
|
||||
componentInstance: instance?.proxy,
|
||||
text: props.res.bottom.code,
|
||||
size: 130,
|
||||
margin: 5,
|
||||
backgroundColor: '#ffffff',
|
||||
foregroundColor: '#000000',
|
||||
fileType: 'jpg',
|
||||
errorCorrectLevel: 0,
|
||||
success: (res: any) => {
|
||||
sharingLink.value = res
|
||||
draw(dp)
|
||||
},
|
||||
fail: (_res: any) => {
|
||||
// console.log('失败',res)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const draw = async (dp: any) => {
|
||||
const { face, nickName, desc } = props.res.memberInfo
|
||||
const { width, height, background, title } = props.res.container
|
||||
const { img, price } = props.res.bottom
|
||||
|
||||
/** 绘制背景 */
|
||||
await dp.draw((ctx: any) => {
|
||||
ctx.fillStyle = background
|
||||
ctx.fillRoundRect(st2(0), st2(0), st2(width), st2(height), st2(12))
|
||||
ctx.clip()
|
||||
})
|
||||
/** 绘制圆角矩形 */
|
||||
await dp.draw(async (ctx: any) => {
|
||||
ctx.fillStyle = "#ffffff"
|
||||
ctx.strokeStyle = "#ffffff"
|
||||
ctx.lineWidth = st2(1)
|
||||
ctx.fillRoundRect(30, 150, 500, 620, 0)
|
||||
})
|
||||
/** 绘制图片 */
|
||||
dp.draw(async (ctx: any) => {
|
||||
await Promise.all([
|
||||
ctx.drawImage(face ? face : myFace, st2(30), st2(30), st2(90), st2(90)),
|
||||
ctx.drawImage(img, st2(60), st2(170), st2(440), st2(440)),
|
||||
ctx.drawImage(sharingLink.value, st2(375), st2(625), st2(130), st2(130))
|
||||
])
|
||||
})
|
||||
/** 绘制顶部文字(昵称 简述) */
|
||||
await dp.draw((ctx: any) => {
|
||||
ctx.fillStyle = "#666"
|
||||
ctx.font = `${st2(24)}px PingFang SC`
|
||||
ctx.fillText(nickName, st2(150), st2(65))
|
||||
ctx.fillStyle = "#666"
|
||||
ctx.font = `${st2(24)}px PingFang SC`
|
||||
ctx.fillText(desc, st2(150), st2(105))
|
||||
})
|
||||
/** 绘制中间文字(商品名称 价格) */
|
||||
await dp.draw((ctx: any) => {
|
||||
ctx.fillStyle = "#333"
|
||||
ctx.font = `bold ${st2(24)}px PingFang SC`
|
||||
ctx.textAlign = "left"
|
||||
ctx.fillWarpText({
|
||||
text: title,
|
||||
lineHeight: st2(32),
|
||||
maxWidth: st2(280),
|
||||
x: st2(60),
|
||||
y: st2(710),
|
||||
layer: 2,
|
||||
})
|
||||
ctx.fillStyle = "#ff3c2a"
|
||||
ctx.font = `${st2(38)}px PingFang SC`
|
||||
ctx.textAlign = "left"
|
||||
ctx.fillText(price, st2(60), st2(665))
|
||||
})
|
||||
|
||||
// 绘制生成本地地址
|
||||
imgUrl.value = await dp.createImagePath()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
page,
|
||||
.index {
|
||||
height: 100%;
|
||||
}
|
||||
.canvas-hide {
|
||||
/* 1 */
|
||||
position: fixed;
|
||||
right: 100vw;
|
||||
bottom: 100vh;
|
||||
/* 2 */
|
||||
z-index: -9999;
|
||||
/* 3 */
|
||||
opacity: 0;
|
||||
}
|
||||
.index {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
background: rgba($color: grey, $alpha: 0.2);
|
||||
}
|
||||
|
||||
image {
|
||||
display: block;
|
||||
}
|
||||
.img {
|
||||
width: 560rpx;
|
||||
height: 800rpx;
|
||||
}
|
||||
|
||||
.qrcode-content {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,331 +0,0 @@
|
||||
<template>
|
||||
<view class="shadow" :class="!show?'':'shadow-show'" :style="{backgroundColor:show?maskBg:'rgba(0,0,0,0)'}" @tap="tapMask">
|
||||
<view class="popups" :class="[theme]" :style="{top: popupsTop ,left: popupsLeft,flexDirection:direction}">
|
||||
<text :class="dynPlace" :style="{width:'0px',height:'0px'}" v-if="triangle"></text>
|
||||
<view v-for="(item,index) in popData" :key="index" @tap.stop="tapItem(item)" class="itemChild view" :class="[direction=='row'?'solid-right':'solid-bottom',item.disabled?'disabledColor':'']">
|
||||
<view class="popup-icon" v-if="item.icon">
|
||||
<u-icon size="35rpx" :name="item.icon"></u-icon>
|
||||
</view>
|
||||
<span class="title">{{item.title}}</span>
|
||||
</view>
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, getCurrentInstance } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
maskBg?: string
|
||||
placement?: string
|
||||
direction?: string
|
||||
x?: number
|
||||
y?: number
|
||||
modelValue?: boolean
|
||||
popData?: any[]
|
||||
theme?: string
|
||||
dynamic?: boolean
|
||||
gap?: number
|
||||
triangle?: boolean
|
||||
}>(), {
|
||||
maskBg: "rgba(0,0,0,0)",
|
||||
placement: "default",
|
||||
direction: "column",
|
||||
x: 0,
|
||||
y: 0,
|
||||
modelValue: false,
|
||||
popData: () => [],
|
||||
theme: "light",
|
||||
dynamic: false,
|
||||
gap: 20,
|
||||
triangle: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'tapPopup'])
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
const popupsTop = ref("0rpx")
|
||||
const popupsLeft = ref("0rpx")
|
||||
const show = ref(false)
|
||||
const dynPlace = ref("")
|
||||
|
||||
onMounted(() => {
|
||||
popupsPosition()
|
||||
})
|
||||
|
||||
const tapMask = () => {
|
||||
emit("update:modelValue", !props.modelValue)
|
||||
}
|
||||
|
||||
const tapItem = (item: any) => {
|
||||
if (item.disabled) return
|
||||
emit("tapPopup", item)
|
||||
emit("update:modelValue", !props.modelValue)
|
||||
}
|
||||
|
||||
const getStatusBar = () => {
|
||||
const windowInfo = uni.getWindowInfo()
|
||||
// #ifdef H5
|
||||
return Promise.resolve(
|
||||
(windowInfo.statusBarHeight || 0) + (windowInfo.windowTop || 0)
|
||||
)
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
return Promise.resolve(windowInfo.statusBarHeight || 0)
|
||||
// #endif
|
||||
}
|
||||
|
||||
const popupsPosition = async () => {
|
||||
let statusBar = await getStatusBar()
|
||||
await new Promise<void>((resolve) => {
|
||||
let popupsDom = uni.createSelectorQuery().in(instance!.proxy).select(".popups")
|
||||
popupsDom
|
||||
.fields(
|
||||
{
|
||||
size: true,
|
||||
},
|
||||
(data: any) => {
|
||||
if (!data) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
let width = data.width
|
||||
let height = data.height
|
||||
|
||||
let y = props.dynamic
|
||||
? dynamicGetY(props.y, props.gap)
|
||||
: transformRpx(props.y)
|
||||
|
||||
let x = props.dynamic
|
||||
? dynamicGetX(props.x, props.gap)
|
||||
: transformRpx(props.x)
|
||||
|
||||
// #ifdef H5
|
||||
y = props.dynamic
|
||||
? props.y + statusBar
|
||||
: transformRpx(props.y + statusBar)
|
||||
// #endif
|
||||
|
||||
dynPlace.value =
|
||||
props.placement == "default"
|
||||
? getPlacement(x, y)
|
||||
: props.placement
|
||||
|
||||
switch (dynPlace.value) {
|
||||
case "top-start":
|
||||
popupsTop.value = `${y + 9}rpx`
|
||||
popupsLeft.value = `${x - 15}rpx`
|
||||
break
|
||||
case "top-end":
|
||||
popupsTop.value = `${y + 9}rpx`
|
||||
popupsLeft.value = `${x + 15 - width}rpx`
|
||||
break
|
||||
case "bottom-start":
|
||||
popupsTop.value = `${y - 18 - height}rpx`
|
||||
popupsLeft.value = `${x - 15}rpx`
|
||||
break
|
||||
case "bottom-end":
|
||||
popupsTop.value = `${y - 9 - height}rpx`
|
||||
popupsLeft.value = `${x + 15 - width}rpx`
|
||||
break
|
||||
}
|
||||
resolve()
|
||||
}
|
||||
)
|
||||
.exec()
|
||||
})
|
||||
}
|
||||
|
||||
const getPlacement = (x: number, y: number) => {
|
||||
const { windowWidth: width, windowHeight: height } = uni.getWindowInfo()
|
||||
if (x > width / 2 && y > height / 2) {
|
||||
return "bottom-end"
|
||||
} else if (x < width / 2 && y < height / 2) {
|
||||
return "top-start"
|
||||
} else if (x > width / 2 && y < height / 2) {
|
||||
return "top-end"
|
||||
} else if (x < width / 2 && y > height / 2) {
|
||||
return "bottom-start"
|
||||
} else if (x > width / 2) {
|
||||
return "top-end"
|
||||
} else {
|
||||
return "top-start"
|
||||
}
|
||||
}
|
||||
|
||||
const dynamicGetY = (y: number, gap: number) => {
|
||||
const { windowHeight: height } = uni.getWindowInfo()
|
||||
y = y < gap ? gap : y
|
||||
y = height - y < gap ? height - gap : y
|
||||
return y
|
||||
}
|
||||
|
||||
const dynamicGetX = (x: number, gap: number) => {
|
||||
const { windowWidth: width } = uni.getWindowInfo()
|
||||
x = x < gap ? gap : x
|
||||
x = width - x < gap ? width - gap : x
|
||||
return x
|
||||
}
|
||||
|
||||
const transformRpx = (params: number) => {
|
||||
const { screenWidth } = uni.getWindowInfo()
|
||||
return (params * screenWidth) / 375
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, async (newVal) => {
|
||||
if (newVal) await popupsPosition()
|
||||
show.value = newVal
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => props.placement, (newVal) => {
|
||||
dynPlace.value = newVal
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.title {
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
.shadow {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
transition: background 0.3s ease-in-out;
|
||||
visibility: hidden;
|
||||
|
||||
&.shadow-show {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.popups {
|
||||
position: absolute;
|
||||
padding: 20rpx;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
.view {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15rpx 10rpx;
|
||||
font-size: 25rpx;
|
||||
}
|
||||
.popup-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
.image {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
}
|
||||
.dark {
|
||||
background-color: #4c4c4c;
|
||||
color: #fff;
|
||||
.top-start:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -18rpx;
|
||||
left: 10rpx;
|
||||
border-width: 0 20rpx 20rpx;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #4c4c4c;
|
||||
}
|
||||
.top-end:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -18rpx;
|
||||
right: 10rpx;
|
||||
border-width: 0 20rpx 20rpx;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #4c4c4c;
|
||||
}
|
||||
.bottom-start:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -18rpx;
|
||||
left: 10rpx;
|
||||
border-width: 20rpx 20rpx 0;
|
||||
border-style: solid;
|
||||
border-color: #4c4c4c transparent transparent;
|
||||
}
|
||||
.bottom-end:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -18rpx;
|
||||
right: 10rpx;
|
||||
border-width: 20rpx 20rpx 0;
|
||||
border-style: solid;
|
||||
border-color: #4c4c4c transparent transparent;
|
||||
}
|
||||
.disabledColor {
|
||||
color: #c5c8ce;
|
||||
}
|
||||
}
|
||||
.light {
|
||||
color: #515a6e;
|
||||
box-shadow: 0upx 0upx 30upx rgba(0, 0, 0, 0.2);
|
||||
background: #fff;
|
||||
.top-start:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -18rpx;
|
||||
left: 10rpx;
|
||||
border-width: 0 20rpx 20rpx;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #fff;
|
||||
}
|
||||
.top-end:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -18rpx;
|
||||
right: 10rpx;
|
||||
border-width: 0 20rpx 20rpx;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #fff;
|
||||
}
|
||||
.bottom-start:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -18rpx;
|
||||
left: 10rpx;
|
||||
border-width: 20rpx 20rpx 0;
|
||||
border-style: solid;
|
||||
border-color: #fff transparent transparent;
|
||||
}
|
||||
.bottom-end:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -18rpx;
|
||||
right: 10rpx;
|
||||
border-width: 20rpx 20rpx 0;
|
||||
border-style: solid;
|
||||
border-color: #fff transparent transparent;
|
||||
}
|
||||
.disabledColor {
|
||||
color: #c5c8ce;
|
||||
}
|
||||
}
|
||||
.solid-bottom {
|
||||
border-bottom: 1px solid #f3f5f7;
|
||||
}
|
||||
.solid-right {
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
.popups .itemChild:last-child {
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
@@ -4,12 +4,9 @@
|
||||
v-if="!state.hid"
|
||||
class="flex-row-center"
|
||||
:style="{ top: state.scHight }"
|
||||
style="width: 750rpx; position: fixed; z-index: 100; left: 0"
|
||||
style="width: 750rpx; position: fixed; z-index: 100; left: 0; background-color: #ffffff; padding: 40rpx 0"
|
||||
>
|
||||
<view
|
||||
class="flex-column-center"
|
||||
style="background-color: #fcfcfc; padding: 30rpx; border-radius: 10rpx"
|
||||
>
|
||||
<view class="flex-column-center">
|
||||
<movable-area
|
||||
class="flex"
|
||||
style="width: 100%"
|
||||
@@ -43,63 +40,47 @@
|
||||
:src="state.img"
|
||||
mode="aspectFit"
|
||||
:style="{ height: state.originalHeight, width: state.originalWidth }"
|
||||
style="border-radius: 10rpx"
|
||||
style="border-radius: 24rpx"
|
||||
></image>
|
||||
</movable-area>
|
||||
|
||||
<movable-area
|
||||
class="flex-row-start"
|
||||
style="
|
||||
width: 100%;
|
||||
background-color: #efefef;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
margin-top: 30rpx;
|
||||
"
|
||||
>
|
||||
<movable-area class="v-track" animation="false">
|
||||
<view class="v-fill" :style="{ width: fillWidth }"></view>
|
||||
<text class="v-track-text" :style="{ color: state.col }">{{
|
||||
state.hasImg
|
||||
}}</text>
|
||||
<movable-view
|
||||
scale-value="1"
|
||||
animation="false"
|
||||
damping="50"
|
||||
:x="state.movePv"
|
||||
class="flex-row-center"
|
||||
style="
|
||||
border-radius: 50%;
|
||||
height: 100rpx;
|
||||
width: 100rpx;
|
||||
background-color: #ffffff;
|
||||
border: 2rpx solid #e3e3e3;
|
||||
margin-top: -13rpx;
|
||||
"
|
||||
class="v-handle"
|
||||
:style="handleStyle"
|
||||
direction="horizontal"
|
||||
@change="moveChange"
|
||||
@touchend="end"
|
||||
>
|
||||
<u-icon
|
||||
:color="mainColor"
|
||||
size="40"
|
||||
color="#ffffff"
|
||||
size="38"
|
||||
v-if="state.endLoad"
|
||||
name="arrow-right"
|
||||
></u-icon>
|
||||
<u-icon :color="mainColor" size="40" v-else name="reload"></u-icon>
|
||||
<u-icon color="#ffffff" size="38" v-else name="reload"></u-icon>
|
||||
</movable-view>
|
||||
|
||||
<text style="padding-left: 140rpx" :style="{ color: state.col }">{{
|
||||
state.hasImg
|
||||
}}</text>
|
||||
</movable-area>
|
||||
<view class="flex-row-around padding-top" style="width: 100%">
|
||||
<u-icon
|
||||
@click="hide"
|
||||
:color="mainColor"
|
||||
size="40"
|
||||
name="close"
|
||||
></u-icon>
|
||||
|
||||
<text class="cu-tag round" @click="getCode">刷新拼图</text>
|
||||
<text class="my-neirong-sm cuIcon-safe" style="color: #c1c1c1"
|
||||
>Lili-FRAMEWORK</text
|
||||
>
|
||||
<view class="v-foot">
|
||||
<view class="v-icon-btn" @click="getCode">
|
||||
<u-icon color="#888888" size="32" name="reload"></u-icon>
|
||||
</view>
|
||||
<view class="v-icon-btn" @click="hide">
|
||||
<u-icon color="#888888" size="30" name="close"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="v-mark">
|
||||
<text class="cuIcon-safe v-mark-icon"></text>
|
||||
<text>Lili-FRAMEWORK 安全验证</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -133,6 +114,13 @@ const phone = uni.getSystemInfoSync()
|
||||
const l = phone.screenWidth / 750
|
||||
|
||||
const mainColor = computed(() => store.getters.mainColor)
|
||||
const fillWidth = computed(() => `${state.moveX + 42 * l}px`)
|
||||
const handleStyle = computed(() => {
|
||||
const glow = /^#[0-9a-fA-F]{6}$/.test(mainColor.value)
|
||||
? mainColor.value + '59'
|
||||
: 'rgba(0, 0, 0, 0.18)'
|
||||
return { background: mainColor.value, boxShadow: `0 6rpx 20rpx ${glow}` }
|
||||
})
|
||||
const state = reactive({
|
||||
flage: false,
|
||||
key: '', // key
|
||||
@@ -284,6 +272,79 @@ defineExpose({
|
||||
@import "./animation.css";
|
||||
@import "./icon.css";
|
||||
// @import './main.css';
|
||||
|
||||
.v-track {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 84rpx;
|
||||
margin-top: 36rpx;
|
||||
background-color: #f7f7f8;
|
||||
border-radius: 42rpx;
|
||||
}
|
||||
|
||||
.v-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: #ffe9e5;
|
||||
border-radius: 42rpx;
|
||||
}
|
||||
|
||||
.v-track-text {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
line-height: 84rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.v-handle {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.v-foot {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.v-icon-btn {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #f4f4f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
|
||||
.v-mark {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 32rpx;
|
||||
font-size: 22rpx;
|
||||
color: #d2d2d2;
|
||||
}
|
||||
|
||||
.v-mark-icon {
|
||||
margin-right: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #d9d9d9;
|
||||
}
|
||||
.dh-wt {
|
||||
animation: at 1.1s ease;
|
||||
animation-iteration-count: infinite;
|
||||
|
||||
170
main.js
@@ -1,84 +1,86 @@
|
||||
import { createSSRApp, computed } from 'vue'
|
||||
import App from './App'
|
||||
import uviewPlus, { setConfig } from 'uview-plus'
|
||||
import store from './store'
|
||||
import config from '@/config/config'
|
||||
import airBtn from '@/components/m-airbtn/index.vue'
|
||||
import mpShare from '@/utils/mpShare.js'
|
||||
import * as filterUtils from './utils/filters.js'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
|
||||
// uview-plus 内部会触发 Vue3 开发态告警(枚举组件实例 keys),不影响运行
|
||||
app.config.warnHandler = (msg, _instance, trace) => {
|
||||
if (
|
||||
typeof msg === 'string' &&
|
||||
msg.includes('enumerating keys on a component instance')
|
||||
) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
console.warn(`[Vue warn]: ${msg}${trace || ''}`)
|
||||
} catch (e) {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信开发者工具部分版本在输出 Vue warn 时会触发
|
||||
// TypeError: Cannot read property '__global' of null
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
app.use(store)
|
||||
app.use(uviewPlus)
|
||||
|
||||
// uview-plus 的图标字体默认每个 u-icon 实例都会触发 uni.loadFontFace,
|
||||
// 导致进入页面时同一字体被反复请求。开启 loadFontOnce 后全局只加载一次。
|
||||
setConfig({
|
||||
config: {
|
||||
loadFontOnce: true,
|
||||
},
|
||||
props: {
|
||||
form: {
|
||||
labelWidth: '180rpx',
|
||||
labelPosition: 'left',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
app.config.globalProperties.$store = store
|
||||
app.config.globalProperties.$mainColor = computed(() => store.getters.mainColor)
|
||||
app.config.globalProperties.$lightColor = computed(() => store.getters.lightColor)
|
||||
app.config.globalProperties.$aiderLightColor = computed(
|
||||
() => store.getters.aiderLightColor
|
||||
)
|
||||
|
||||
const filterMethods = {}
|
||||
Object.keys(filterUtils).forEach((key) => {
|
||||
if (typeof filterUtils[key] === 'function') {
|
||||
filterMethods[key] = filterUtils[key]
|
||||
}
|
||||
})
|
||||
|
||||
app.mixin({
|
||||
methods: filterMethods,
|
||||
computed: {
|
||||
themeStyle() {
|
||||
return getThemeStyle(this.$store.state.theme)
|
||||
},
|
||||
},
|
||||
})
|
||||
app.mixin(mpShare)
|
||||
|
||||
// #ifdef H5
|
||||
if (config.enableMiniBarStartUpApp) {
|
||||
const mountPoint = document.createElement('div')
|
||||
document.body.appendChild(mountPoint)
|
||||
createSSRApp(airBtn).mount(mountPoint)
|
||||
}
|
||||
// #endif
|
||||
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
import { createSSRApp, computed } from 'vue'
|
||||
import App from './App'
|
||||
import uviewPlus, { setConfig } from 'uview-plus'
|
||||
import store from './store'
|
||||
import config from '@/config/config'
|
||||
// #ifdef H5
|
||||
import airBtn from '@/components/m-airbtn/index.vue'
|
||||
// #endif
|
||||
import mpShare from '@/utils/mpShare.js'
|
||||
import * as filterUtils from './utils/filters.js'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
|
||||
// uview-plus 内部会触发 Vue3 开发态告警(枚举组件实例 keys),不影响运行
|
||||
app.config.warnHandler = (msg, _instance, trace) => {
|
||||
if (
|
||||
typeof msg === 'string' &&
|
||||
msg.includes('enumerating keys on a component instance')
|
||||
) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
console.warn(`[Vue warn]: ${msg}${trace || ''}`)
|
||||
} catch (e) {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信开发者工具部分版本在输出 Vue warn 时会触发
|
||||
// TypeError: Cannot read property '__global' of null
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
app.use(store)
|
||||
app.use(uviewPlus)
|
||||
|
||||
// uview-plus 的图标字体默认每个 u-icon 实例都会触发 uni.loadFontFace,
|
||||
// 导致进入页面时同一字体被反复请求。开启 loadFontOnce 后全局只加载一次。
|
||||
setConfig({
|
||||
config: {
|
||||
loadFontOnce: true,
|
||||
},
|
||||
props: {
|
||||
form: {
|
||||
labelWidth: '180rpx',
|
||||
labelPosition: 'left',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
app.config.globalProperties.$store = store
|
||||
app.config.globalProperties.$mainColor = computed(() => store.getters.mainColor)
|
||||
app.config.globalProperties.$lightColor = computed(() => store.getters.lightColor)
|
||||
app.config.globalProperties.$aiderLightColor = computed(
|
||||
() => store.getters.aiderLightColor
|
||||
)
|
||||
|
||||
const filterMethods = {}
|
||||
Object.keys(filterUtils).forEach((key) => {
|
||||
if (typeof filterUtils[key] === 'function') {
|
||||
filterMethods[key] = filterUtils[key]
|
||||
}
|
||||
})
|
||||
|
||||
app.mixin({
|
||||
methods: filterMethods,
|
||||
computed: {
|
||||
themeStyle() {
|
||||
return getThemeStyle(this.$store.state.theme)
|
||||
},
|
||||
},
|
||||
})
|
||||
app.mixin(mpShare)
|
||||
|
||||
// #ifdef H5
|
||||
if (config.enableMiniBarStartUpApp) {
|
||||
const mountPoint = document.createElement('div')
|
||||
document.body.appendChild(mountPoint)
|
||||
createSSRApp(airBtn).mount(mountPoint)
|
||||
}
|
||||
// #endif
|
||||
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +192,7 @@
|
||||
"mp-weixin" : {
|
||||
/* 小程序特有相关 */
|
||||
"usingComponents" : true,
|
||||
"lazyCodeLoading" : "requiredComponents",
|
||||
"appid" : "wx6f10f29075dc1b0b",
|
||||
"optimization" : {
|
||||
"subPackages" : true
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"description": "Lilishop uni-app mobile client (Vue 3)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"migrate:vue3": "node scripts/migrate-vue3.js && node scripts/migrate-filters-usage.js",
|
||||
"postinstall": "patch-package && node scripts/patch-mqtt-abort.js",
|
||||
"test:mqtt-app-runtime": "node scripts/test-mqtt-app-runtime.js"
|
||||
},
|
||||
@@ -12,7 +11,7 @@
|
||||
"type": "git",
|
||||
"url": "https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp"
|
||||
},
|
||||
"license": "ISC",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tencentcloud/chat": "^3.5.5",
|
||||
"hls.js": "^1.6.16",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<view :style="themeStyle" class="content">
|
||||
<view class="portrait-box">
|
||||
<image src="/static/pointTrade/point_bg_1.png" mode=""></image>
|
||||
<image class="point-img" src="/static/pointTrade/tradehall.png" />
|
||||
<image src="/pages/mine/static/point_bg_1.png" mode=""></image>
|
||||
<image class="point-img" src="/pages/mine/static/tradehall.png" />
|
||||
<view class="position-point">
|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 742 B After Width: | Height: | Size: 742 B |
@@ -35,7 +35,7 @@
|
||||
<view class="select-view">
|
||||
<view class="select-cell" v-if="applyInfo.returnGoods" @click="onSelect(1)">
|
||||
<view class="select-image">
|
||||
<image class="select-icon" src="/static/order/t1.png"></image>
|
||||
<image class="select-icon" src="/pages/order/static/t1.png"></image>
|
||||
</view>
|
||||
<view class="cell-body">
|
||||
<view class="select-title">退货</view>
|
||||
@@ -45,7 +45,7 @@
|
||||
</view>
|
||||
<view class="select-cell" v-if="applyInfo.returnMoney" @click="onSelect(3)">
|
||||
<view class="select-image">
|
||||
<image class="select-icon" src="/static/order/t3.png"></image>
|
||||
<image class="select-icon" src="/pages/order/static/t3.png"></image>
|
||||
</view>
|
||||
<view class="cell-body">
|
||||
<view class="select-title">退款</view>
|
||||
|
||||
|
Before Width: | Height: | Size: 952 B After Width: | Height: | Size: 952 B |
|
Before Width: | Height: | Size: 617 B After Width: | Height: | Size: 617 B |
@@ -113,7 +113,7 @@
|
||||
<u-icon v-if="item.title != 'APPLE'" color="#fff" size="42" :name="item.icon"
|
||||
@click="navigateLogin(item)">
|
||||
</u-icon>
|
||||
<u-image v-else src="/static/appleidButton@2x.png" :lazy-load="false" @click="navigateLogin(item)"
|
||||
<u-image v-else src="/pages/passport/static/appleidButton@2x.png" :lazy-load="false" @click="navigateLogin(item)"
|
||||
width="80" height="80" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 44 KiB |
@@ -303,7 +303,7 @@ page {
|
||||
.star-item {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
background: url("/static/star.png");
|
||||
background: url("/pages/product/static/star.png");
|
||||
background-size: 100%;
|
||||
opacity: 0.25;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.group-wrapper {
|
||||
background: url("/static/exchange.png");
|
||||
background: url("/pages/product/static/exchange.png");
|
||||
background-size: cover;
|
||||
}
|
||||
.u-group-row {
|
||||
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -27,7 +27,7 @@
|
||||
</u-navbar>
|
||||
<!-- 顶部栏 -->
|
||||
<view class="header-wraper">
|
||||
<image class="header-banner" mode="aspectFit" src="/static/join-buy.png"></image>
|
||||
<image class="header-banner" mode="aspectFit" src="/pages/promotion/static/join-buy.png"></image>
|
||||
</view>
|
||||
<!-- 商品栏 -->
|
||||
<div class="swiper">
|
||||
@@ -156,7 +156,7 @@ function GET_AssembleGoods() {
|
||||
}
|
||||
|
||||
.header-wraper {
|
||||
background: url('/static/bg.png') no-repeat center center;
|
||||
background: url('/pages/promotion/static/bg.png') no-repeat center center;
|
||||
background-size: cover;
|
||||
height: 200rpx;
|
||||
display: flex;
|
||||
|
||||
@@ -32,7 +32,7 @@ async function init() {
|
||||
padding: 0 20rpx;
|
||||
height: 300rpx;
|
||||
flex-shrink: 0;
|
||||
background: url("/static/point-bg.png") no-repeat;
|
||||
background: url("/pages/promotion/static/point-bg.png") no-repeat;
|
||||
background-size: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
@left-click="back"
|
||||
></u-navbar>
|
||||
<view class="header-wraper">
|
||||
<image class="header-banner" mode="aspectFit" src="/static/seckill.png"></image>
|
||||
<image class="header-banner" mode="aspectFit" src="/pages/promotion/static/seckill.png"></image>
|
||||
</view>
|
||||
<scroll-view scroll-x>
|
||||
<view class="index-navs">
|
||||
@@ -176,7 +176,7 @@ function clickNavigateTime(type: number) {
|
||||
}
|
||||
|
||||
.header-wraper {
|
||||
background: url('/static/bg.png') no-repeat center center;
|
||||
background: url('/pages/promotion/static/bg.png') no-repeat center center;
|
||||
background-size: cover;
|
||||
height: 200rpx;
|
||||
display: flex;
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,208 +1,262 @@
|
||||
<template>
|
||||
<view class="home-page" :style="themeStyle">
|
||||
<view
|
||||
v-for="(item, index) in pageData.list"
|
||||
:key="`${item.type}-${index}`"
|
||||
class="floor-block"
|
||||
>
|
||||
<u-navbar
|
||||
class="navbar"
|
||||
v-if="item.type == 'search'"
|
||||
:auto-back="false"
|
||||
:fixed="index === 1 ? false : true"
|
||||
:placeholder="index === 1 ? false : true"
|
||||
left-icon=""
|
||||
title=""
|
||||
bg-color="#ffffff"
|
||||
>
|
||||
<template #left>
|
||||
<view class="navbar-search-wrap" :style="searchBarStyle" @click.stop>
|
||||
<search :res="item.options" />
|
||||
</view>
|
||||
</template>
|
||||
</u-navbar>
|
||||
<carousel v-if="item.type == 'carousel'" :res="item.options" />
|
||||
<titleLayout v-if="item.type == 'title'" :res="item.options" />
|
||||
<leftOneRightTwo v-if="item.type == 'leftOneRightTwo'" :res="item.options" />
|
||||
<leftTwoRightOne v-if="item.type == 'leftTwoRightOne'" :res="item.options" />
|
||||
<topOneBottomTwo v-if="item.type == 'topOneBottomTwo'" :res="item.options" />
|
||||
<topTwoBottomOne v-if="item.type == 'topTwoBottomOne'" :res="item.options" />
|
||||
<flexThree v-if="item.type == 'flexThree'" :res="item.options" />
|
||||
<flexFive v-if="item.type == 'flexFive'" :res="item.options" />
|
||||
<flexFour v-if="item.type == 'flexFour'" :res="item.options" />
|
||||
<flexTwo v-if="item.type == 'flexTwo'" :res="item.options" />
|
||||
<textPicture v-if="item.type == 'textPicture'" :res="item.options" />
|
||||
<menuLayout v-if="item.type == 'menu'" :res="item.options" />
|
||||
<flexOne v-if="item.type == 'flexOne'" :res="item.options" />
|
||||
<goods
|
||||
v-if="item.type == 'goods'"
|
||||
:enableBottomLoad="enableLoad"
|
||||
:res="item.options"
|
||||
/>
|
||||
<group v-if="item.type == 'group'" :res="item.options" />
|
||||
<notice v-if="item.type == 'notice'" :res="item.options" />
|
||||
<promotions v-if="item.type == 'promotionDetail'" :res="item.options" />
|
||||
<videoLayout v-if="item.type == 'video'" :res="item.options" />
|
||||
</view>
|
||||
<fetchCoupon ref="couponRef" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
onShow,
|
||||
onReady,
|
||||
onReachBottom,
|
||||
onPullDownRefresh,
|
||||
} from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import carousel from '@/pages/tabbar/home/template/tpl_banner.vue'
|
||||
import titleLayout from '@/pages/tabbar/home/template/tpl_title.vue'
|
||||
import leftOneRightTwo from '@/pages/tabbar/home/template/tpl_left_one_right_two.vue'
|
||||
import leftTwoRightOne from '@/pages/tabbar/home/template/tpl_left_two_right_one.vue'
|
||||
import topOneBottomTwo from '@/pages/tabbar/home/template/tpl_top_one_bottom_two.vue'
|
||||
import topTwoBottomOne from '@/pages/tabbar/home/template/tpl_top_two_bottom_one.vue'
|
||||
import flexOne from '@/pages/tabbar/home/template/tpl_flex_one.vue'
|
||||
import flexTwo from '@/pages/tabbar/home/template/tpl_flex_two.vue'
|
||||
import flexThree from '@/pages/tabbar/home/template/tpl_flex_three.vue'
|
||||
import flexFive from '@/pages/tabbar/home/template/tpl_flex_five.vue'
|
||||
import flexFour from '@/pages/tabbar/home/template/tpl_flex_four.vue'
|
||||
import textPicture from '@/pages/tabbar/home/template/tpl_text_picture.vue'
|
||||
import menuLayout from '@/pages/tabbar/home/template/tpl_menu.vue'
|
||||
import search from '@/pages/tabbar/home/template/tpl_search.vue'
|
||||
import group from '@/pages/tabbar/home/template/tpl_group.vue'
|
||||
import videoLayout from '@/pages/tabbar/home/template/tpl_video.vue'
|
||||
import goods from '@/pages/tabbar/home/template/tpl_goods.vue'
|
||||
import notice from '@/pages/tabbar/home/template/tpl_notice.vue'
|
||||
import promotions from '@/pages/tabbar/home/template/tpl_promotions_detail.vue'
|
||||
import fetchCoupon from '@/pages/tabbar/home/template/fetch_coupon.vue'
|
||||
import { getFloorData } from '@/api/home'
|
||||
|
||||
const HOME_LAYOUT_PADDING_RPX = 16
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const couponRef = ref<InstanceType<typeof fetchCoupon>>()
|
||||
const pageData = ref<{ list: any[] }>({ list: [] })
|
||||
const enableLoad = ref(false)
|
||||
const loading = ref(false)
|
||||
const searchBarWidth = ref(0)
|
||||
|
||||
const searchBarStyle = computed(() => {
|
||||
if (searchBarWidth.value) {
|
||||
return { width: `${searchBarWidth.value}px` }
|
||||
}
|
||||
return { width: '100%' }
|
||||
})
|
||||
|
||||
function setCapsuleInset() {
|
||||
try {
|
||||
const { windowWidth } = uni.getWindowInfo()
|
||||
const leftPadding = uni.upx2px(HOME_LAYOUT_PADDING_RPX)
|
||||
const rightPadding = uni.upx2px(HOME_LAYOUT_PADDING_RPX)
|
||||
// #ifdef MP-WEIXIN
|
||||
const menuButton = uni.getMenuButtonBoundingClientRect()
|
||||
searchBarWidth.value = windowWidth - (windowWidth - menuButton.left) - leftPadding
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
searchBarWidth.value = windowWidth - leftPadding - rightPadding
|
||||
// #endif
|
||||
} catch (e) {
|
||||
searchBarWidth.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
function fetchCouponFn() {
|
||||
couponRef.value?.firstGetAuto()
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
getFloorData()
|
||||
.then((res) => {
|
||||
const payload = res?.data != null ? res.data : res
|
||||
const pageDataStr = payload?.result?.pageData
|
||||
const isSuccess = payload?.success === true || payload?.code === 200
|
||||
if (!isSuccess || !pageDataStr) return
|
||||
|
||||
try {
|
||||
const result = JSON.parse(pageDataStr)
|
||||
pageData.value = Array.isArray(result?.list) ? result : { list: [] }
|
||||
const lastItem = pageData.value.list[pageData.value.list.length - 1]
|
||||
enableLoad.value =
|
||||
lastItem?.type === 'goods' || lastItem?.model === 'goods'
|
||||
} catch (parseError) {
|
||||
console.error('parse pageData failed', parseError)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('getFloorData failed', err)
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onReady(() => {
|
||||
setCapsuleInset()
|
||||
init()
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.showShareMenu({ withShareTicket: true })
|
||||
// #endif
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
setTimeout(() => {
|
||||
fetchCouponFn()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
uni.$emit('onReachBottom', true)
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
init()
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/pages/tabbar/home/template/tpl.scss';
|
||||
|
||||
.home-page {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.floor-block {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
width: 100%;
|
||||
|
||||
:deep(.u-navbar__content) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.u-navbar__content__left) {
|
||||
flex: 1;
|
||||
width: auto !important;
|
||||
max-width: 100%;
|
||||
padding: 0 0 0 $home-layout-padding !important;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-search-wrap {
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<view class="home-page" :style="[themeStyle, pageBgStyle]">
|
||||
<view
|
||||
v-for="(item, index) in pageData.list"
|
||||
:key="`${item.type}-${index}`"
|
||||
class="floor-block"
|
||||
>
|
||||
<u-navbar
|
||||
class="navbar"
|
||||
v-if="item.type == 'search'"
|
||||
:auto-back="false"
|
||||
:fixed="index === 1 ? false : true"
|
||||
:placeholder="index === 1 ? false : true"
|
||||
left-icon=""
|
||||
title=""
|
||||
:bg-color="topColor"
|
||||
>
|
||||
<template #left>
|
||||
<view class="navbar-search-wrap" :style="searchBarStyle" @click.stop>
|
||||
<search :res="item.options" />
|
||||
</view>
|
||||
</template>
|
||||
</u-navbar>
|
||||
<homeComb v-if="item.type == 'homeComb'" :res="item.options" />
|
||||
<carousel v-if="item.type == 'carousel'" :res="item.options" />
|
||||
<titleLayout v-if="item.type == 'title'" :res="item.options" />
|
||||
<leftOneRightTwo v-if="item.type == 'leftOneRightTwo'" :res="item.options" />
|
||||
<leftTwoRightOne v-if="item.type == 'leftTwoRightOne'" :res="item.options" />
|
||||
<topOneBottomTwo v-if="item.type == 'topOneBottomTwo'" :res="item.options" />
|
||||
<topTwoBottomOne v-if="item.type == 'topTwoBottomOne'" :res="item.options" />
|
||||
<flexThree v-if="item.type == 'flexThree'" :res="item.options" />
|
||||
<flexFive v-if="item.type == 'flexFive'" :res="item.options" />
|
||||
<flexFour v-if="item.type == 'flexFour'" :res="item.options" />
|
||||
<flexTwo v-if="item.type == 'flexTwo'" :res="item.options" />
|
||||
<textPicture v-if="item.type == 'textPicture'" :res="item.options" />
|
||||
<menuLayout v-if="item.type == 'menu'" :res="item.options" />
|
||||
<flexOne v-if="item.type == 'flexOne'" :res="item.options" />
|
||||
<goods
|
||||
v-if="item.type == 'goods'"
|
||||
:enableBottomLoad="enableLoad"
|
||||
:res="item.options"
|
||||
/>
|
||||
<group v-if="item.type == 'group'" :res="item.options" />
|
||||
<notice v-if="item.type == 'notice'" :res="item.options" />
|
||||
<promotions v-if="item.type == 'promotionDetail'" :res="item.options" />
|
||||
<videoLayout v-if="item.type == 'video'" :res="item.options" />
|
||||
<blank v-if="item.type == 'blank'" :res="item.options" />
|
||||
<lineLayout v-if="item.type == 'line'" :res="item.options" />
|
||||
<richText v-if="item.type == 'richText'" :res="item.options" />
|
||||
<integral v-if="item.type == 'integral'" :res="item.options" />
|
||||
<coupon v-if="item.type == 'coupon'" :res="item.options" />
|
||||
<articleList v-if="item.type == 'article'" :res="item.options" />
|
||||
<shopList v-if="item.type == 'shop'" :res="item.options" />
|
||||
</view>
|
||||
<fetchCoupon ref="couponRef" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
onShow,
|
||||
onReady,
|
||||
onReachBottom,
|
||||
onPullDownRefresh,
|
||||
} from '@dcloudio/uni-app'
|
||||
import { useStore } from '@/store'
|
||||
import { getThemeStyle } from '@/utils/theme'
|
||||
import carousel from '@/pages/tabbar/home/template/tpl_banner.vue'
|
||||
import titleLayout from '@/pages/tabbar/home/template/tpl_title.vue'
|
||||
import leftOneRightTwo from '@/pages/tabbar/home/template/tpl_left_one_right_two.vue'
|
||||
import leftTwoRightOne from '@/pages/tabbar/home/template/tpl_left_two_right_one.vue'
|
||||
import topOneBottomTwo from '@/pages/tabbar/home/template/tpl_top_one_bottom_two.vue'
|
||||
import topTwoBottomOne from '@/pages/tabbar/home/template/tpl_top_two_bottom_one.vue'
|
||||
import flexOne from '@/pages/tabbar/home/template/tpl_flex_one.vue'
|
||||
import flexTwo from '@/pages/tabbar/home/template/tpl_flex_two.vue'
|
||||
import flexThree from '@/pages/tabbar/home/template/tpl_flex_three.vue'
|
||||
import flexFive from '@/pages/tabbar/home/template/tpl_flex_five.vue'
|
||||
import flexFour from '@/pages/tabbar/home/template/tpl_flex_four.vue'
|
||||
import textPicture from '@/pages/tabbar/home/template/tpl_text_picture.vue'
|
||||
import menuLayout from '@/pages/tabbar/home/template/tpl_menu.vue'
|
||||
import search from '@/pages/tabbar/home/template/tpl_search.vue'
|
||||
import group from '@/pages/tabbar/home/template/tpl_group.vue'
|
||||
import videoLayout from '@/pages/tabbar/home/template/tpl_video.vue'
|
||||
import goods from '@/pages/tabbar/home/template/tpl_goods.vue'
|
||||
import notice from '@/pages/tabbar/home/template/tpl_notice.vue'
|
||||
import promotions from '@/pages/tabbar/home/template/tpl_promotions_detail.vue'
|
||||
import fetchCoupon from '@/pages/tabbar/home/template/fetch_coupon.vue'
|
||||
import blank from '@/pages/tabbar/home/template/tpl_blank.vue'
|
||||
import lineLayout from '@/pages/tabbar/home/template/tpl_line.vue'
|
||||
import richText from '@/pages/tabbar/home/template/tpl_rich_text.vue'
|
||||
import integral from '@/pages/tabbar/home/template/tpl_integral.vue'
|
||||
import coupon from '@/pages/tabbar/home/template/tpl_coupon.vue'
|
||||
import articleList from '@/pages/tabbar/home/template/tpl_article.vue'
|
||||
import shopList from '@/pages/tabbar/home/template/tpl_shop.vue'
|
||||
import homeComb from '@/pages/tabbar/home/template/tpl_home_comb.vue'
|
||||
import { getFloorData } from '@/api/home'
|
||||
|
||||
const HOME_LAYOUT_PADDING_RPX = 16
|
||||
|
||||
const store = useStore()
|
||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||
|
||||
const couponRef = ref<InstanceType<typeof fetchCoupon>>()
|
||||
const pageData = ref<Record<string, any>>({ list: [] })
|
||||
const enableLoad = ref(false)
|
||||
const loading = ref(false)
|
||||
const searchBarWidth = ref(0)
|
||||
|
||||
const topColor = computed(() => pageData.value?.topColor || '#ffffff')
|
||||
|
||||
// 页面设置-样式:画布背景(背景色 / 背景图 + 填充方式)
|
||||
const pageBgStyle = computed(() => {
|
||||
const data: any = pageData.value
|
||||
if (data?.bgType === 'image' && data?.bgImage) {
|
||||
const size =
|
||||
data.bgFill === 'contain'
|
||||
? 'contain'
|
||||
: data.bgFill === 'cover'
|
||||
? 'cover'
|
||||
: '100% 100%'
|
||||
return {
|
||||
backgroundImage: `url(${data.bgImage})`,
|
||||
backgroundSize: size,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundColor: data.bgColor || '#f9f9f9',
|
||||
}
|
||||
}
|
||||
if (data?.bgColor) {
|
||||
return { backgroundColor: data.bgColor }
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
const searchBarStyle = computed(() => {
|
||||
if (searchBarWidth.value) {
|
||||
return { width: `${searchBarWidth.value}px` }
|
||||
}
|
||||
return { width: '100%' }
|
||||
})
|
||||
|
||||
function setCapsuleInset() {
|
||||
try {
|
||||
const { windowWidth } = uni.getWindowInfo()
|
||||
const leftPadding = uni.upx2px(HOME_LAYOUT_PADDING_RPX)
|
||||
const rightPadding = uni.upx2px(HOME_LAYOUT_PADDING_RPX)
|
||||
// #ifdef MP-WEIXIN
|
||||
const menuButton = uni.getMenuButtonBoundingClientRect()
|
||||
searchBarWidth.value = windowWidth - (windowWidth - menuButton.left) - leftPadding
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
searchBarWidth.value = windowWidth - leftPadding - rightPadding
|
||||
// #endif
|
||||
} catch (e) {
|
||||
searchBarWidth.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
function fetchCouponFn() {
|
||||
couponRef.value?.firstGetAuto()
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
getFloorData()
|
||||
.then((res) => {
|
||||
const payload = res?.data != null ? res.data : res
|
||||
const pageDataStr = payload?.result?.pageData
|
||||
const isSuccess = payload?.success === true || payload?.code === 200
|
||||
if (!isSuccess || !pageDataStr) return
|
||||
|
||||
try {
|
||||
const result = JSON.parse(pageDataStr)
|
||||
pageData.value = Array.isArray(result?.list)
|
||||
? { ...result, list: result.list.filter((i: any) => !i?.hidden) }
|
||||
: { list: [] }
|
||||
// 页面设置-内容:页面标题同步到导航栏
|
||||
if (result?.title) {
|
||||
uni.setNavigationBarTitle({ title: result.title })
|
||||
}
|
||||
// 页面设置-样式:顶部颜色与标题颜色同步到导航栏
|
||||
if (result?.topColor || result?.titleColor) {
|
||||
uni.setNavigationBarColor({
|
||||
frontColor: result?.titleColor === 'white' ? '#ffffff' : '#000000',
|
||||
backgroundColor: result?.topColor || '#ffffff',
|
||||
})
|
||||
}
|
||||
const lastItem = pageData.value.list[pageData.value.list.length - 1]
|
||||
enableLoad.value =
|
||||
lastItem?.type === 'goods' || lastItem?.model === 'goods'
|
||||
} catch (parseError) {
|
||||
console.error('parse pageData failed', parseError)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('getFloorData failed', err)
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onReady(() => {
|
||||
setCapsuleInset()
|
||||
init()
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.showShareMenu({ withShareTicket: true })
|
||||
// #endif
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
setTimeout(() => {
|
||||
fetchCouponFn()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
uni.$emit('onReachBottom', true)
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
init()
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/pages/tabbar/home/template/tpl.scss';
|
||||
|
||||
.home-page {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.floor-block {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
width: 100%;
|
||||
|
||||
:deep(.u-navbar__content) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.u-navbar__content__left) {
|
||||
flex: 1;
|
||||
width: auto !important;
|
||||
max-width: 100%;
|
||||
padding: 0 0 0 $home-layout-padding !important;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-search-wrap {
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<view style="width: 120rpx; height: 120rpx; float: right">
|
||||
<image
|
||||
@click="enableShowCoupon = false"
|
||||
src="/static/cpauto1.png"
|
||||
src="https://lilishop-uniapp.oss-cn-beijing.aliyuncs.com/cpauto1.png"
|
||||
style="width: 100%; height: 100%"
|
||||
></image>
|
||||
</view>
|
||||
|
||||
@@ -243,3 +243,35 @@ function seacnCode() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 广告魔方各布局共用的样式字段(装修面板配置;老数据无字段时返回空对象,保持原样) */
|
||||
export function cubeWrapperStyle(res) {
|
||||
if (!res || res.paddingTop === undefined) return {};
|
||||
const r = (v) => `${Number(v) || 0}rpx`;
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
return {
|
||||
padding: `${r(res.paddingTop)} ${r(res.paddingX)} ${r(res.paddingBottom)}`,
|
||||
background: bg,
|
||||
borderRadius: r(res.borderRadius),
|
||||
marginBottom: r(res.pageGap),
|
||||
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
|
||||
};
|
||||
}
|
||||
|
||||
/** 内容圆角(作用于单张图片容器) */
|
||||
export function cubeImgStyle(res) {
|
||||
if (!res || res.imgRadius === undefined) return {};
|
||||
return {
|
||||
borderRadius: `${Number(res.imgRadius) || 0}rpx`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
}
|
||||
|
||||
/** 内容间距(作用于图片布局容器,flex gap) */
|
||||
export function cubeGapStyle(res) {
|
||||
if (!res || res.contentGap === undefined) return {};
|
||||
return { gap: `${Number(res.contentGap) || 0}rpx` };
|
||||
}
|
||||
|
||||
104
pages/tabbar/home/template/tpl_article.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<view class="tpl-article" :class="{ styled: styled }" :style="wrapperStyle" v-if="articleList.length">
|
||||
<view class="article-title" v-if="title">{{ title }}</view>
|
||||
<view
|
||||
class="article-item"
|
||||
v-for="item in articleList"
|
||||
:key="item.id"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<view class="article-text">
|
||||
<view class="article-name" :style="nameStyle">{{ item.title }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { getArticleList } from "@/api/article.js";
|
||||
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const articleList = ref<any[]>([]);
|
||||
|
||||
const title = computed(() => props.res?.list?.[0]?.title || "");
|
||||
const count = computed(() => props.res?.list?.[0]?.count || 3);
|
||||
const categoryId = computed(() => props.res?.list?.[0]?.categoryId || "");
|
||||
|
||||
// 面板保存过样式字段才启用自定义样式,老数据保持原样
|
||||
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (styled.value) {
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
const radius = Number(res.borderRadius) || 0;
|
||||
return {
|
||||
background: bg,
|
||||
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
|
||||
marginBottom: `${Number(res.pageGap) || 0}rpx`,
|
||||
borderRadius: `${radius}rpx`,
|
||||
overflow: radius > 0 ? "hidden" : "visible",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
rowGap: `${Number(res.contentGap) || 0}rpx`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
background: res.bgColor || "#FFFFFF",
|
||||
padding: `20rpx ${res.paddingX ?? 0}rpx`,
|
||||
marginBottom: `${res.pageGap ?? 10}rpx`,
|
||||
borderRadius: `${res.borderRadius ?? 0}rpx`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
});
|
||||
const nameStyle = computed(() =>
|
||||
styled.value && props.res.titleColor ? { color: props.res.titleColor } : {}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
const params: any = { pageNumber: 1, pageSize: count.value };
|
||||
if (categoryId.value) params.categoryId = categoryId.value;
|
||||
getArticleList(params).then((res) => {
|
||||
if (res.data.success && res.data.result) {
|
||||
articleList.value = (res.data.result.records || []).slice(0, count.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function goDetail(item: any) {
|
||||
uni.navigateTo({ url: `/pages/passport/article?id=${item.id}` });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.article-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
padding: 8rpx 24rpx 20rpx;
|
||||
}
|
||||
.article-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 24rpx;
|
||||
border-top: 2rpx solid #f5f5f5;
|
||||
}
|
||||
.tpl-article.styled .article-item {
|
||||
border-top: none;
|
||||
padding: 0;
|
||||
}
|
||||
.article-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.article-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,55 +1,190 @@
|
||||
<template>
|
||||
<view class="layout">
|
||||
<view class="banner-box">
|
||||
<u-swiper
|
||||
v-if="res && res.list && res.list.length"
|
||||
class="home-banner-swiper"
|
||||
:list="res.list"
|
||||
keyName="img"
|
||||
height="350rpx"
|
||||
imgMode="scaleToFill"
|
||||
interval="5000"
|
||||
duration="500"
|
||||
circular
|
||||
indicator
|
||||
@click="clickSwiper"
|
||||
>
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-swiper>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
function clickSwiper(index: number) {
|
||||
modelNavigateTo(props.res.list[index]);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
|
||||
.banner-box {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.home-banner-swiper),
|
||||
:deep(.u-swiper),
|
||||
:deep(.u-swiper__wrapper) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.u-swiper__wrapper__item),
|
||||
:deep(.u-swiper__wrapper__item__wrapper) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.u-swiper__wrapper__item__wrapper__image) {
|
||||
width: 100% !important;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<view class="layout" :style="wrapperStyle">
|
||||
<view class="banner-box" :style="cardMode ? {} : boxStyle">
|
||||
<u-swiper
|
||||
v-if="!cardMode && res && res.list && res.list.length"
|
||||
class="home-banner-swiper"
|
||||
:list="res.list"
|
||||
keyName="img"
|
||||
height="350rpx"
|
||||
imgMode="scaleToFill"
|
||||
interval="5000"
|
||||
duration="500"
|
||||
circular
|
||||
:indicator="!customIndicator"
|
||||
@change="onChange"
|
||||
@click="clickSwiper"
|
||||
>
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-swiper>
|
||||
<swiper
|
||||
v-if="cardMode && res && res.list && res.list.length"
|
||||
class="card-swiper"
|
||||
autoplay
|
||||
:interval="5000"
|
||||
:duration="500"
|
||||
circular
|
||||
previous-margin="68rpx"
|
||||
next-margin="68rpx"
|
||||
@change="onChange"
|
||||
>
|
||||
<swiper-item v-for="(item, i) in res.list" :key="i">
|
||||
<view class="card-item" :class="{ active: i === current }" @click="clickSwiper(i)">
|
||||
<image class="card-img" :style="boxStyle" :src="item.img" mode="scaleToFill" />
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view
|
||||
v-if="customIndicator && res.indicatorStyle !== 'none' && res.list.length > 1"
|
||||
class="indicator"
|
||||
:class="'pos-' + (res.indicatorPosition || 'center')"
|
||||
>
|
||||
<view
|
||||
v-for="(item, i) in res.list"
|
||||
:key="i"
|
||||
class="dot"
|
||||
:class="[res.indicatorStyle || 'round', { active: i === current }]"
|
||||
:style="i === current ? { background: indicatorColor } : {}"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const current = ref(0);
|
||||
|
||||
// 装修面板保存过样式字段才启用自定义样式/指示器,老数据保持原样
|
||||
const customIndicator = computed(() => props.res && props.res.indicatorStyle !== undefined);
|
||||
|
||||
const cardMode = computed(() => props.res && props.res.swiperStyle === "card");
|
||||
|
||||
const indicatorColor = computed(() =>
|
||||
props.res.indicatorColorMode === "custom" && props.res.indicatorColor
|
||||
? props.res.indicatorColor
|
||||
: "var(--theme-primary, #ff3c2a)"
|
||||
);
|
||||
|
||||
const toRpx = (val: any) => `${Number(val) || 0}rpx`;
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (res.paddingTop === undefined) return {};
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
return {
|
||||
padding: `${toRpx(res.paddingTop)} ${toRpx(res.paddingX)} ${toRpx(res.paddingBottom)}`,
|
||||
background: bg,
|
||||
borderRadius: toRpx(res.borderRadius),
|
||||
marginBottom: toRpx(res.pageGap),
|
||||
};
|
||||
});
|
||||
|
||||
const boxStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (res.imgRadius === undefined) return {};
|
||||
return { borderRadius: toRpx(res.imgRadius) };
|
||||
});
|
||||
|
||||
function onChange(e: any) {
|
||||
current.value = typeof e === "number" ? e : e.current;
|
||||
}
|
||||
|
||||
function clickSwiper(index: number) {
|
||||
modelNavigateTo(props.res.list[index]);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
|
||||
.banner-box {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-swiper {
|
||||
width: 100%;
|
||||
height: 350rpx;
|
||||
}
|
||||
|
||||
.card-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 8rpx;
|
||||
box-sizing: border-box;
|
||||
transform: scale(0.92);
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
|
||||
.card-item.active {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.card-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
bottom: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
|
||||
&.pos-left {
|
||||
left: 24rpx;
|
||||
}
|
||||
&.pos-center {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
&.pos-right {
|
||||
right: 24rpx;
|
||||
}
|
||||
|
||||
.dot {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
transition: background 0.3s ease;
|
||||
|
||||
&.round {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
margin: 0 6rpx;
|
||||
}
|
||||
&.line {
|
||||
width: 24rpx;
|
||||
height: 6rpx;
|
||||
border-radius: 3rpx;
|
||||
margin: 0 4rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.home-banner-swiper),
|
||||
:deep(.u-swiper),
|
||||
:deep(.u-swiper__wrapper) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.u-swiper__wrapper__item),
|
||||
:deep(.u-swiper__wrapper__item__wrapper) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.u-swiper__wrapper__item__wrapper__image) {
|
||||
width: 100% !important;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
24
pages/tabbar/home/template/tpl_blank.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<view class="tpl-blank" :style="blankStyle"></view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const blankStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
const bg =
|
||||
res.bgColor1 !== undefined
|
||||
? !res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`
|
||||
: res.bgColor || "#f5f5f5";
|
||||
return {
|
||||
background: bg,
|
||||
height: `${res.height ?? 10}rpx`,
|
||||
margin: `0 ${res.paddingX ?? 0}rpx ${res.pageGap ?? 10}rpx`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
511
pages/tabbar/home/template/tpl_coupon.vue
Normal file
@@ -0,0 +1,511 @@
|
||||
<template>
|
||||
<view class="tpl-coupon" :class="{ styled: styled }" :style="wrapperStyle" v-if="couponList.length || !styled">
|
||||
<view class="coupon-title" v-if="title">{{ title }}</view>
|
||||
|
||||
<!-- 老数据保持原样 -->
|
||||
<block v-if="!styled">
|
||||
<view
|
||||
class="coupon-item"
|
||||
v-for="item in couponList"
|
||||
:key="item.id"
|
||||
@click="receive(item)"
|
||||
>
|
||||
<view class="coupon-left">
|
||||
<view class="coupon-amount">
|
||||
<template v-if="item.couponType == 'DISCOUNT'">
|
||||
<text class="coupon-value">{{ item.couponDiscount }}</text>
|
||||
<text class="coupon-symbol">折</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="coupon-symbol">¥</text>
|
||||
<text class="coupon-value">{{ unitPrice(item.price) }}</text>
|
||||
</template>
|
||||
</view>
|
||||
<view class="coupon-threshold">满{{ unitPrice(item.consumeThreshold) }}元可用</view>
|
||||
</view>
|
||||
<view class="coupon-right">
|
||||
<view class="coupon-name">
|
||||
{{ item.storeName == 'platform' ? '全平台通用' : item.storeName + '店铺' }}
|
||||
</view>
|
||||
<view class="coupon-btn">立即领取</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 样式一:红色大卡横滑,右侧竖排领取 -->
|
||||
<scroll-view
|
||||
v-else-if="layoutStyle === '1'"
|
||||
scroll-x
|
||||
class="cp-scroll"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view
|
||||
class="cp1-item"
|
||||
v-for="item in couponList"
|
||||
:key="item.id"
|
||||
:style="itemGapStyle"
|
||||
@click="receive(item)"
|
||||
>
|
||||
<view class="cp1-left">
|
||||
<view class="cp1-amount">
|
||||
<template v-if="item.couponType == 'DISCOUNT'">
|
||||
<text class="cp1-value">{{ item.couponDiscount }}</text>
|
||||
<text class="cp1-symbol">折</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="cp1-symbol">¥</text>
|
||||
<text class="cp1-value">{{ unitPrice(item.price) }}</text>
|
||||
</template>
|
||||
</view>
|
||||
<view class="cp1-threshold">{{ thresholdText(item) }}</view>
|
||||
</view>
|
||||
<view class="cp1-right">
|
||||
<text>领</text><text>取</text>
|
||||
</view>
|
||||
<view class="cp-notch cp-notch-t"></view>
|
||||
<view class="cp-notch cp-notch-b"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 样式二:白底小卡,底部去领取按钮 -->
|
||||
<scroll-view
|
||||
v-else-if="layoutStyle === '2'"
|
||||
scroll-x
|
||||
class="cp-scroll"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view
|
||||
class="cp2-item"
|
||||
v-for="item in couponList"
|
||||
:key="item.id"
|
||||
:style="itemGapStyle"
|
||||
@click="receive(item)"
|
||||
>
|
||||
<view class="cp2-top">
|
||||
<view class="cp2-amount">
|
||||
<template v-if="item.couponType == 'DISCOUNT'">
|
||||
<text class="cp2-value">{{ item.couponDiscount }}</text>
|
||||
<text class="cp2-symbol">折</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="cp2-symbol">¥</text>
|
||||
<text class="cp2-value">{{ unitPrice(item.price) }}</text>
|
||||
</template>
|
||||
</view>
|
||||
<view class="cp2-threshold">{{ thresholdText(item) }}</view>
|
||||
</view>
|
||||
<view class="cp2-btn">去领取</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 样式三:浅粉虚线卡,右侧竖排立即领取 -->
|
||||
<scroll-view
|
||||
v-else-if="layoutStyle === '3'"
|
||||
scroll-x
|
||||
class="cp-scroll"
|
||||
:show-scrollbar="false"
|
||||
>
|
||||
<view
|
||||
class="cp3-item"
|
||||
v-for="item in couponList"
|
||||
:key="item.id"
|
||||
:style="itemGapStyle"
|
||||
@click="receive(item)"
|
||||
>
|
||||
<view class="cp3-left">
|
||||
<view class="cp3-amount">
|
||||
<template v-if="item.couponType == 'DISCOUNT'">
|
||||
<text class="cp3-value">{{ item.couponDiscount }}</text>
|
||||
<text class="cp3-symbol">折</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="cp3-symbol">¥</text>
|
||||
<text class="cp3-value">{{ unitPrice(item.price) }}</text>
|
||||
</template>
|
||||
</view>
|
||||
<view class="cp3-threshold">{{ thresholdText(item) }}</view>
|
||||
</view>
|
||||
<view class="cp3-right">
|
||||
<text>立</text><text>即</text><text>领</text><text>取</text>
|
||||
</view>
|
||||
<view class="cp-notch cp-notch-t"></view>
|
||||
<view class="cp-notch cp-notch-b"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 样式四:浅粉渐变 + 右侧渐变竖条领取 -->
|
||||
<scroll-view v-else scroll-x class="cp-scroll" :show-scrollbar="false">
|
||||
<view
|
||||
class="cp4-item"
|
||||
v-for="item in couponList"
|
||||
:key="item.id"
|
||||
:style="itemGapStyle"
|
||||
@click="receive(item)"
|
||||
>
|
||||
<view class="cp4-left">
|
||||
<view class="cp4-amount">
|
||||
<template v-if="item.couponType == 'DISCOUNT'">
|
||||
<text class="cp4-value">{{ item.couponDiscount }}</text>
|
||||
<text class="cp4-symbol">折</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="cp4-symbol">¥</text>
|
||||
<text class="cp4-value">{{ unitPrice(item.price) }}</text>
|
||||
</template>
|
||||
</view>
|
||||
<view class="cp4-threshold">{{ thresholdText(item) }}</view>
|
||||
</view>
|
||||
<view class="cp4-right">
|
||||
<text>领</text><text>取</text>
|
||||
</view>
|
||||
<view class="cp-notch cp-notch-t"></view>
|
||||
<view class="cp-notch cp-notch-b"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { getAllCoupons } from "@/api/promotions.js";
|
||||
import { receiveCoupons } from "@/api/members.js";
|
||||
import { isLogin, navigateToLogin, unitPrice } from "@/utils/filters.js";
|
||||
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const couponList = ref<any[]>([]);
|
||||
|
||||
const title = computed(() => props.res?.list?.[0]?.title || "");
|
||||
const count = computed(() => props.res?.list?.[0]?.count || 5);
|
||||
const layoutStyle = computed(() => (props.res && props.res.layoutStyle) || "1");
|
||||
|
||||
// 面板保存过样式字段才启用自定义样式,老数据保持原样
|
||||
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
|
||||
const itemGapStyle = computed(() => {
|
||||
if (!styled.value) return {};
|
||||
const gap = props.res?.contentGap !== undefined ? Number(props.res.contentGap) || 0 : 5;
|
||||
return { marginRight: `${gap}rpx` };
|
||||
});
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (styled.value) {
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
const radius = Number(res.borderRadius) || 0;
|
||||
return {
|
||||
background: bg,
|
||||
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
|
||||
marginBottom: `${Number(res.pageGap) || 0}rpx`,
|
||||
borderRadius: `${radius}rpx`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
}
|
||||
return {
|
||||
background: res.bgColor || "#FFFFFF",
|
||||
padding: `20rpx ${res.paddingX ?? 0}rpx`,
|
||||
marginBottom: `${res.pageGap ?? 10}rpx`,
|
||||
borderRadius: `${res.borderRadius ?? 0}rpx`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getAllCoupons({ pageNumber: 1, pageSize: count.value }).then((res) => {
|
||||
if (res.data.code == 200 && res.data.result) {
|
||||
couponList.value = (res.data.result.records || []).slice(0, count.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function thresholdText(item: any) {
|
||||
const t = Number(unitPrice(item.consumeThreshold));
|
||||
return t > 0 ? `满${unitPrice(item.consumeThreshold)}元可用` : "无门槛使用";
|
||||
}
|
||||
|
||||
function receive(item: any) {
|
||||
if (!isLogin("auth")) {
|
||||
navigateToLogin("redirectTo");
|
||||
return;
|
||||
}
|
||||
receiveCoupons(item.id).then((res) => {
|
||||
if (res.data.code == 200) {
|
||||
uni.showToast({ title: "领取成功", icon: "none" });
|
||||
} else {
|
||||
uni.showToast({ title: res.data.message, icon: "none" });
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-coupon {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.coupon-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
padding: 8rpx 24rpx 20rpx;
|
||||
}
|
||||
.coupon-item {
|
||||
display: flex;
|
||||
margin: 0 20rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(90deg, #ffece8, #fff5f4);
|
||||
}
|
||||
.tpl-coupon.styled .coupon-item {
|
||||
margin: 0;
|
||||
}
|
||||
.coupon-left {
|
||||
width: 180rpx;
|
||||
padding: 24rpx 0;
|
||||
text-align: center;
|
||||
color: var(--theme-primary, #e1251b);
|
||||
border-right: 2rpx dashed #f0c0bb;
|
||||
}
|
||||
.coupon-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.coupon-symbol {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.coupon-threshold {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.coupon-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
.coupon-name {
|
||||
color: #333;
|
||||
}
|
||||
.coupon-btn {
|
||||
background: var(--theme-primary, #e1251b);
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
/* 参考 CRMEB 优惠券组件 */
|
||||
.tpl-coupon.styled .coupon-title {
|
||||
padding: 0 0 16rpx;
|
||||
}
|
||||
.cp-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cp1-item:last-child,
|
||||
.cp2-item:last-child,
|
||||
.cp3-item:last-child,
|
||||
.cp4-item:last-child {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.cp-notch {
|
||||
position: absolute;
|
||||
right: 60rpx;
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
}
|
||||
.cp-notch-t {
|
||||
top: -8rpx;
|
||||
}
|
||||
.cp-notch-b {
|
||||
bottom: -8rpx;
|
||||
}
|
||||
|
||||
/* 样式一:红色大卡 */
|
||||
.cp1-item {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: 264rpx;
|
||||
height: 132rpx;
|
||||
margin-right: 16rpx;
|
||||
border-radius: 12rpx;
|
||||
background: linear-gradient(135deg, #ff5a36, #e1251b);
|
||||
color: #fff;
|
||||
vertical-align: top;
|
||||
}
|
||||
.cp1-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 24rpx;
|
||||
}
|
||||
.cp1-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.cp1-symbol {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.cp1-threshold {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.cp1-right {
|
||||
width: 60rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
border-left: 2rpx dashed rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.cp1-item .cp-notch {
|
||||
right: 52rpx;
|
||||
}
|
||||
|
||||
/* 样式二:白底小卡 */
|
||||
.cp2-item {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
width: 148rpx;
|
||||
margin-right: 16rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #fff;
|
||||
border-left: 4rpx solid #f5483a;
|
||||
border-right: 4rpx solid #f5483a;
|
||||
overflow: hidden;
|
||||
vertical-align: top;
|
||||
}
|
||||
.cp2-top {
|
||||
padding: 16rpx 0 12rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.cp2-value {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp2-symbol {
|
||||
font-size: 20rpx;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp2-threshold {
|
||||
font-size: 18rpx;
|
||||
color: #999;
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
.cp2-btn {
|
||||
background: linear-gradient(90deg, #ff6a3d, #e1251b);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
text-align: center;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
/* 样式三:浅粉虚线卡 */
|
||||
.cp3-item {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: 276rpx;
|
||||
height: 120rpx;
|
||||
margin-right: 16rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #fdf1ec;
|
||||
border: 2rpx dashed #f5483a;
|
||||
vertical-align: top;
|
||||
}
|
||||
.cp3-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 24rpx;
|
||||
}
|
||||
.cp3-value {
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp3-symbol {
|
||||
font-size: 20rpx;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp3-threshold {
|
||||
font-size: 20rpx;
|
||||
color: #e1251b;
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
.cp3-right {
|
||||
width: 64rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
color: #e1251b;
|
||||
border-left: 2rpx dashed #f5483a;
|
||||
}
|
||||
.cp3-item .cp-notch {
|
||||
right: 56rpx;
|
||||
}
|
||||
|
||||
/* 样式四:浅粉渐变 + 渐变竖条 */
|
||||
.cp4-item {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: 232rpx;
|
||||
height: 108rpx;
|
||||
margin-right: 16rpx;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
vertical-align: top;
|
||||
}
|
||||
.cp4-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 20rpx;
|
||||
background: linear-gradient(135deg, #fff3ec, #ffe0d3);
|
||||
}
|
||||
.cp4-value {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp4-symbol {
|
||||
font-size: 20rpx;
|
||||
color: #e1251b;
|
||||
}
|
||||
.cp4-threshold {
|
||||
font-size: 18rpx;
|
||||
color: #e1251b;
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
.cp4-right {
|
||||
width: 52rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
background: linear-gradient(180deg, #ff6a3d, #e1251b);
|
||||
}
|
||||
.cp4-item .cp-notch {
|
||||
right: 44rpx;
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
}
|
||||
.cp4-item .cp-notch-t {
|
||||
top: -6rpx;
|
||||
}
|
||||
.cp4-item .cp-notch-b {
|
||||
bottom: -6rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +1,21 @@
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<u-image width="140rpx" mode="aspectFit" height="140rpx" @click="modelNavigateTo(item)" class="image-mode" v-for="(item,index) in res.list" :key="index" :src="item.img" alt="">
|
||||
<div class="layout" :style="[wrapperStyle, gapStyle]">
|
||||
<u-image width="100%" mode="aspectFit" height="140rpx" @click="modelNavigateTo(item)" class="image-mode flex-img" :style="imgStyle" v-for="(item,index) in res.list" :key="index" :src="item.img" alt="">
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
import { computed } from "vue";
|
||||
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
|
||||
|
||||
defineProps<{ res: any }>();
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const gapStyle = computed(() => cubeGapStyle(props.res));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -20,4 +25,8 @@ defineProps<{ res: any }>();
|
||||
justify-content: center;
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
.flex-img {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<u-image height="175rpx" mode="aspectFit" width="175rpx" @click="modelNavigateTo(item)" class="image-mode" :src="item.img" v-for="(item,index) in res.list" :key="index">
|
||||
<div class="layout" :style="[wrapperStyle, gapStyle]">
|
||||
<u-image height="175rpx" mode="aspectFit" width="100%" @click="modelNavigateTo(item)" class="image-mode flex-img" :style="imgStyle" :src="item.img" v-for="(item,index) in res.list" :key="index">
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
import { computed } from "vue";
|
||||
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
|
||||
|
||||
defineProps<{ res: any }>();
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const gapStyle = computed(() => cubeGapStyle(props.res));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
.layout {
|
||||
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
.flex-img {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="layout">
|
||||
<view class="flex-one">
|
||||
<view class="layout" :style="wrapperStyle">
|
||||
<view class="flex-one" :style="contentStyle">
|
||||
<u-image
|
||||
v-if="!hasHotZone"
|
||||
@click="modelNavigateTo(res.list[0])"
|
||||
@@ -25,6 +25,27 @@ const hasHotZone = computed(() => {
|
||||
const zoneInfo = props.res?.list?.[0]?.zoneInfo;
|
||||
return Array.isArray(zoneInfo) ? zoneInfo.length > 0 : !!zoneInfo;
|
||||
});
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const r = props.res || {};
|
||||
const bg =
|
||||
r.bgColor1 !== undefined
|
||||
? !r.bgColor2 || r.bgColor1 === r.bgColor2
|
||||
? r.bgColor1
|
||||
: `linear-gradient(180deg, ${r.bgColor1}, ${r.bgColor2})`
|
||||
: r.bgColor ?? "transparent";
|
||||
return {
|
||||
background: bg,
|
||||
padding: `${r.paddingTop ?? 0}rpx ${r.paddingX ?? 0}rpx ${r.paddingBottom ?? 0}rpx`,
|
||||
marginBottom: `${r.pageGap ?? 0}rpx`,
|
||||
borderRadius: `${r.borderRadius ?? 0}rpx`,
|
||||
overflow: (r.borderRadius ?? 0) > 0 ? "hidden" : "visible",
|
||||
};
|
||||
});
|
||||
|
||||
const contentStyle = computed(() => ({
|
||||
borderRadius: `${props.res?.imgRadius ?? 0}rpx`,
|
||||
}));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -33,4 +54,4 @@ const hasHotZone = computed(() => {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<u-image @click="modelNavigateTo(item)" height="240rpx" width="240rpx" class="image-mode" :src="item.img" v-for="(item, index) in res.list" :key="index">
|
||||
<div class="layout" :style="[wrapperStyle, gapStyle]">
|
||||
<u-image @click="modelNavigateTo(item)" height="240rpx" width="100%" class="image-mode flex-img" :style="imgStyle" :src="item.img" v-for="(item, index) in res.list" :key="index">
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
import { computed } from "vue";
|
||||
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
|
||||
|
||||
defineProps<{ res: any }>();
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const gapStyle = computed(() => cubeGapStyle(props.res));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -22,4 +27,8 @@ defineProps<{ res: any }>();
|
||||
justify-content: center;
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
.flex-img {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<view class="layout">
|
||||
<view class="flex-two">
|
||||
<view class="flex-item" @click="modelNavigateTo(res.list[0])">
|
||||
<view class="layout" :style="wrapperStyle">
|
||||
<view class="flex-two" :style="gapStyle">
|
||||
<view class="flex-item" :style="imgStyle" @click="modelNavigateTo(res.list[0])">
|
||||
<u-image height="250rpx" width="100%" mode="scaleToFill" :src="res.list[0].img" alt>
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</view>
|
||||
<view class="flex-item" @click="modelNavigateTo(res.list[1])">
|
||||
<view class="flex-item" :style="imgStyle" @click="modelNavigateTo(res.list[1])">
|
||||
<u-image height="250rpx" width="100%" mode="scaleToFill" :src="res.list[1].img" alt>
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
@@ -15,9 +15,14 @@
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
import { computed } from "vue";
|
||||
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
|
||||
|
||||
defineProps<{ res: any }>();
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const gapStyle = computed(() => cubeGapStyle(props.res));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -27,6 +32,7 @@ defineProps<{ res: any }>();
|
||||
overflow: hidden;
|
||||
}
|
||||
.flex-item {
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,265 +1,704 @@
|
||||
<template>
|
||||
<view class="layout">
|
||||
<u-sticky>
|
||||
<view class="goods-cell-title">
|
||||
<view
|
||||
class="goods-item-title"
|
||||
:class="{ 'selected-title': selected.index == index }"
|
||||
@click="handleClickTitle(title, index)"
|
||||
v-for="(title, index) in res.list[0].titleWay"
|
||||
:key="index"
|
||||
>
|
||||
<h4 class="h4">{{ title.title }}</h4>
|
||||
<view class="title-desc">{{ title.desc }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-sticky>
|
||||
<view class="goods-list">
|
||||
<template v-for="(item, item_index) in res.list[0].listWay" :key="item_index">
|
||||
<view
|
||||
v-if="
|
||||
item.___index != undefined
|
||||
? selected.index == item.___index
|
||||
: selected.val == item.type
|
||||
"
|
||||
@click="handleClick(item)"
|
||||
class="goods-item"
|
||||
>
|
||||
<view class="goods-img">
|
||||
<u-image
|
||||
:src="item.img"
|
||||
height="350rpx"
|
||||
mode="aspectFit"
|
||||
width="100%"
|
||||
>
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</view>
|
||||
<view class="goods-desc">
|
||||
<view class="goods-title">
|
||||
{{ item.title }}
|
||||
</view>
|
||||
<view class="goods-bottom">
|
||||
<view class="goods-price">
|
||||
¥<span class="price-int"
|
||||
>{{ goodsFormatPrice(item.price)[0] }} </span
|
||||
>.{{ goodsFormatPrice(item.price)[1] }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="currentTitleWay?.bindCategory && goodsData.length"
|
||||
>
|
||||
<view
|
||||
v-for="(item, index) in goodsData"
|
||||
:key="index"
|
||||
class="goods-item"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
<view class="goods-img">
|
||||
<u-image
|
||||
:src="item.thumbnail"
|
||||
height="350rpx"
|
||||
mode="aspectFit"
|
||||
width="100%"
|
||||
>
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</view>
|
||||
<view class="goods-desc">
|
||||
<view class="goods-title">
|
||||
{{ item.goodsName }}
|
||||
</view>
|
||||
<view class="goods-bottom">
|
||||
<view class="goods-price">
|
||||
¥<span class="price-int"
|
||||
>{{ goodsFormatPrice(item.price)[0] }} </span
|
||||
>.{{ goodsFormatPrice(item.price)[1] }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, onMounted, onUnmounted, computed } from "vue";
|
||||
import { getGoodsList } from "@/api/goods.js";
|
||||
import { goodsFormatPrice } from "@/utils/filters.js";
|
||||
|
||||
const props = defineProps<{ res: any; enableBottomLoad?: boolean }>();
|
||||
|
||||
const selected = reactive({
|
||||
index: 0,
|
||||
val: "",
|
||||
});
|
||||
|
||||
const currentTitleWay = computed(() => {
|
||||
return props.res?.list?.[0]?.titleWay?.[selected.index]
|
||||
})
|
||||
const params = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 100,
|
||||
categoryId: "",
|
||||
});
|
||||
const goodsData = ref<any[]>([]);
|
||||
const goodsResult = ref<any>("");
|
||||
|
||||
watch(
|
||||
() => props.res,
|
||||
(val) => {
|
||||
if (!val?.list?.[0]) return
|
||||
const firstListWay = val.list[0].listWay?.[0]
|
||||
selected.val = firstListWay?.type || ''
|
||||
const firstTitleWay = val.list[0].titleWay?.[0]
|
||||
if (firstTitleWay?.bindCategory) {
|
||||
initGoods(firstTitleWay)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
uni.$on("onReachBottom", () => {
|
||||
if (
|
||||
props.enableBottomLoad &&
|
||||
goodsResult.value.totalElements >=
|
||||
params.pageNumber * params.pageSize
|
||||
) {
|
||||
params.pageNumber++;
|
||||
initGoods(props.res.list[0].titleWay[selected.index]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off("onReachBottom");
|
||||
});
|
||||
|
||||
function handleClick(item: any) {
|
||||
if (!item?.id || !item?.goodsId) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
|
||||
})
|
||||
}
|
||||
|
||||
async function initGoods(val: any) {
|
||||
if (props.enableBottomLoad) params.pageSize = 20;
|
||||
val ? (params.categoryId = val.bindCategory.id) : "";
|
||||
const res = await getGoodsList(params);
|
||||
if (res.data.success) {
|
||||
goodsResult.value = res.data.result;
|
||||
const result = res.data.result.records;
|
||||
goodsData.value.push(...result);
|
||||
console.log(goodsData.value);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickTitle(val: any, index: number) {
|
||||
selected.index = index;
|
||||
selected.val = val.title;
|
||||
if (val.bindCategory) {
|
||||
params.pageNumber = 1;
|
||||
goodsData.value = [];
|
||||
initGoods(val);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
$w_94: 94%;
|
||||
|
||||
.layout {
|
||||
padding: 8px 0;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.selected-title {
|
||||
.h4 {
|
||||
font-size: 30rpx;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.title-desc {
|
||||
font-weight: bold;
|
||||
color: $main-color !important;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-cell-title {
|
||||
background: #f9f9f9;
|
||||
padding: 10px;
|
||||
transition: 0.35s;
|
||||
display: flex;
|
||||
|
||||
> .goods-item-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
|
||||
.h4 {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.title-desc {
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-list {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
width: 50%;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 0.4em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-img {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
// width: 158px;
|
||||
width: $w_94;
|
||||
border-top-left-radius: 20rpx;
|
||||
border-top-right-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-desc {
|
||||
border-bottom-left-radius: 20rpx;
|
||||
border-bottom-right-radius: 20rpx;
|
||||
width: $w_94;
|
||||
background: #fff;
|
||||
padding: 8rpx 0 8rpx 8rpx;
|
||||
margin: 0 auto;
|
||||
> .goods-title {
|
||||
font-size: 24rpx;
|
||||
height: 67rpx;
|
||||
display: -webkit-box;
|
||||
font-weight: 500;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> .goods-bottom {
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
> .goods-price {
|
||||
line-height: 2;
|
||||
color: $main-color;
|
||||
.price-int {
|
||||
font-size: 42rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<!-- 新版商品列表 -->
|
||||
<view v-if="isNew" class="goods-new" :style="wrapperStyle">
|
||||
<view v-if="displayGoods.length" class="g-list" :class="'mode-' + layout" :style="listStyle">
|
||||
<view
|
||||
class="g-item"
|
||||
v-for="(g, i) in displayGoods"
|
||||
:key="i"
|
||||
:style="itemStyle"
|
||||
@click="goDetail(g)"
|
||||
>
|
||||
<view class="g-imgbox">
|
||||
<image :src="g.img" mode="aspectFill" class="g-img" />
|
||||
</view>
|
||||
<view class="g-info">
|
||||
<view v-if="showName" class="g-name" :style="nameStyle">{{ g.name }}</view>
|
||||
<view v-if="showPrice" class="g-price" :style="priceStyle">
|
||||
¥{{ goodsFormatPrice(g.price)[0] }}.{{ goodsFormatPrice(g.price)[1] }}
|
||||
</view>
|
||||
<view class="g-bottom">
|
||||
<text
|
||||
v-if="showSales && g.sales != null"
|
||||
class="g-sales"
|
||||
:style="salesStyle"
|
||||
>已售{{ g.sales }}件</text
|
||||
>
|
||||
<view
|
||||
v-if="cartShow"
|
||||
class="g-cart"
|
||||
:class="'cart-' + cartStyle"
|
||||
:style="{ background: priceColor }"
|
||||
@click.stop="onCartBtn(g)"
|
||||
>
|
||||
<text v-if="cartStyle === 'text'" class="cart-text-label">加购</text>
|
||||
<text v-else-if="cartStyle === 'plus'" class="cart-plus-label">+</text>
|
||||
<u-icon v-else name="shopping-cart" color="#ffffff" size="28rpx"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="g-empty">暂无商品</view>
|
||||
</view>
|
||||
|
||||
<!-- 旧版:分类标签 + 绑定商品 -->
|
||||
<view v-else class="layout">
|
||||
<u-sticky>
|
||||
<view class="goods-cell-title">
|
||||
<view
|
||||
class="goods-item-title"
|
||||
:class="{ 'selected-title': selected.index == index }"
|
||||
@click="handleClickTitle(title, index)"
|
||||
v-for="(title, index) in res.list[0].titleWay"
|
||||
:key="index"
|
||||
>
|
||||
<h4 class="h4">{{ title.title }}</h4>
|
||||
<view class="title-desc">{{ title.desc }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-sticky>
|
||||
<view class="goods-list">
|
||||
<template v-for="(item, item_index) in res.list[0].listWay" :key="item_index">
|
||||
<view
|
||||
v-if="
|
||||
item.___index != undefined
|
||||
? selected.index == item.___index
|
||||
: selected.val == item.type
|
||||
"
|
||||
@click="handleClick(item)"
|
||||
class="goods-item"
|
||||
>
|
||||
<view class="goods-img">
|
||||
<u-image
|
||||
:src="item.img"
|
||||
height="350rpx"
|
||||
mode="aspectFit"
|
||||
width="100%"
|
||||
>
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</view>
|
||||
<view class="goods-desc">
|
||||
<view class="goods-title">
|
||||
{{ item.title }}
|
||||
</view>
|
||||
<view class="goods-bottom">
|
||||
<view class="goods-price">
|
||||
¥<span class="price-int"
|
||||
>{{ goodsFormatPrice(item.price)[0] }} </span
|
||||
>.{{ goodsFormatPrice(item.price)[1] }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="currentTitleWay?.bindCategory && goodsData.length"
|
||||
>
|
||||
<view
|
||||
v-for="(item, index) in goodsData"
|
||||
:key="index"
|
||||
class="goods-item"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
<view class="goods-img">
|
||||
<u-image
|
||||
:src="item.thumbnail"
|
||||
height="350rpx"
|
||||
mode="aspectFit"
|
||||
width="100%"
|
||||
>
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</view>
|
||||
<view class="goods-desc">
|
||||
<view class="goods-title">
|
||||
{{ item.goodsName }}
|
||||
</view>
|
||||
<view class="goods-bottom">
|
||||
<view class="goods-price">
|
||||
¥<span class="price-int"
|
||||
>{{ goodsFormatPrice(item.price)[0] }} </span
|
||||
>.{{ goodsFormatPrice(item.price)[1] }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, onMounted, onUnmounted, computed } from "vue";
|
||||
import { getGoodsList } from "@/api/goods.js";
|
||||
import { addToCart } from "@/api/trade.js";
|
||||
import { goodsFormatPrice } from "@/utils/filters.js";
|
||||
import storage from "@/utils/storage.js";
|
||||
|
||||
const props = defineProps<{ res: any; enableBottomLoad?: boolean }>();
|
||||
|
||||
const selected = reactive({
|
||||
index: 0,
|
||||
val: "",
|
||||
});
|
||||
|
||||
// ---------- 新版商品列表 ----------
|
||||
const first = computed(() => props.res?.list?.[0] || {});
|
||||
const isNew = computed(() => first.value.layout !== undefined);
|
||||
const layout = computed(() => first.value.layout || "list");
|
||||
const showName = computed(() => first.value.showName !== false);
|
||||
const showPrice = computed(() => first.value.showPrice !== false);
|
||||
const showSales = computed(() => first.value.showSales !== false);
|
||||
const cartShow = computed(() => first.value.cartShow !== false);
|
||||
const cartStyle = computed(() => first.value.cartStyle || "plus");
|
||||
const priceColor = computed(() =>
|
||||
props.res?.toneMode === "custom" && props.res?.priceColor
|
||||
? props.res.priceColor
|
||||
: "var(--theme-primary, #ff3c2a)"
|
||||
);
|
||||
const priceStyle = computed(() => ({ color: priceColor.value }));
|
||||
const nameStyle = computed(() =>
|
||||
props.res?.titleColor ? { color: props.res.titleColor } : {}
|
||||
);
|
||||
const salesStyle = computed(() =>
|
||||
props.res?.salesColor ? { color: props.res.salesColor } : {}
|
||||
);
|
||||
const rpx = (v: any) => `${Number(v) || 0}rpx`;
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1 || "#ffffff"
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
return {
|
||||
background: bg,
|
||||
padding: `${rpx(res.paddingTop)} ${rpx(res.paddingX)} ${rpx(res.paddingBottom)}`,
|
||||
marginBottom: rpx(res.pageGap),
|
||||
borderRadius: rpx(res.borderRadius),
|
||||
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
|
||||
};
|
||||
});
|
||||
const listStyle = computed(() => {
|
||||
const gap = rpx(props.res?.contentGap ?? 10);
|
||||
return { gap, "--g-gap": gap };
|
||||
});
|
||||
const itemStyle = computed(() => ({
|
||||
borderRadius: rpx(props.res?.contentRadius ?? 7),
|
||||
overflow: Number(props.res?.contentRadius ?? 7) > 0 ? "hidden" : "visible",
|
||||
}));
|
||||
|
||||
const newGoodsData = ref<any[]>([]);
|
||||
|
||||
const displayGoods = computed(() => {
|
||||
if (!isNew.value) return [];
|
||||
return newGoodsData.value;
|
||||
});
|
||||
|
||||
// 触底加载(新版)
|
||||
const newParams = reactive({ pageNumber: 1 });
|
||||
const newTotal = ref(0);
|
||||
|
||||
function buildQuery() {
|
||||
const item = first.value;
|
||||
const params: Record<string, any> = {
|
||||
pageNumber: newParams.pageNumber,
|
||||
pageSize: item.count || 6,
|
||||
};
|
||||
if (item.dataMode === "brand" && item.brandId) params.brandId = item.brandId;
|
||||
if (item.dataMode === "store" && item.storeId) params.storeId = item.storeId;
|
||||
if (item.dataMode === "category" && item.categoryId) params.categoryId = item.categoryId;
|
||||
if (item.dataMode === "goods") {
|
||||
const ids = (item.goodsList || []).map((g: any) => g.id).filter(Boolean);
|
||||
if (!ids.length) return null;
|
||||
params.ids = ids.join(",");
|
||||
params.pageSize = ids.length;
|
||||
}
|
||||
if (item.sortType === "sales") {
|
||||
params.sort = "buyCount";
|
||||
params.order = "desc";
|
||||
} else if (item.sortType === "price") {
|
||||
params.sort = "price";
|
||||
params.order = "asc";
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
async function loadNewGoods(reset = true) {
|
||||
const params = buildQuery();
|
||||
if (!params) {
|
||||
newGoodsData.value = [];
|
||||
newTotal.value = 0;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await getGoodsList(params);
|
||||
if (!res.data.success) return;
|
||||
const result = res.data.result || {};
|
||||
const records = result.records || [];
|
||||
const mapped = records.map((g: any) => ({
|
||||
id: g.id,
|
||||
goodsId: g.goodsId,
|
||||
name: g.goodsName,
|
||||
price: g.price,
|
||||
img: g.thumbnail || g.small,
|
||||
sales: g.buyCount != null ? g.buyCount : null,
|
||||
}));
|
||||
if (reset) {
|
||||
// 指定商品:按选择顺序排列(兼容 SKU id 与 goodsId)
|
||||
if (first.value.dataMode === "goods") {
|
||||
const byId: Record<string, any> = {};
|
||||
mapped.forEach((g: any) => {
|
||||
if (g.id) byId[g.id] = g;
|
||||
if (g.goodsId) byId[g.goodsId] = g;
|
||||
});
|
||||
newGoodsData.value = (first.value.goodsList || [])
|
||||
.map((g: any) => byId[g.id] || byId[g.goodsId])
|
||||
.filter(Boolean);
|
||||
} else {
|
||||
newGoodsData.value = mapped;
|
||||
}
|
||||
} else {
|
||||
newGoodsData.value.push(...mapped);
|
||||
}
|
||||
newTotal.value = result.totalElements ?? newGoodsData.value.length;
|
||||
} catch (e) {
|
||||
// 静默失败,保持已有数据
|
||||
}
|
||||
}
|
||||
|
||||
function goDetail(g: any) {
|
||||
if (!g?.id || !g?.goodsId) return;
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${g.id}&goodsId=${g.goodsId}`,
|
||||
});
|
||||
}
|
||||
|
||||
function onCartBtn(g: any) {
|
||||
if (first.value.cartAction !== "add") {
|
||||
goDetail(g);
|
||||
return;
|
||||
}
|
||||
if (!g?.id) return;
|
||||
if (!storage.getAccessToken()) {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.navigateTo({ url: "/pages/passport/wechatMPLogin" });
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.navigateTo({ url: "/pages/passport/login" });
|
||||
// #endif
|
||||
return;
|
||||
}
|
||||
addToCart({ skuId: g.id, num: 1 }).then((res: any) => {
|
||||
uni.showToast({
|
||||
title: res.data.success ? "已加入购物车" : res.data.message || "加购失败",
|
||||
icon: "none",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [
|
||||
first.value.dataMode,
|
||||
first.value.categoryId,
|
||||
first.value.brandId,
|
||||
first.value.storeId,
|
||||
first.value.count,
|
||||
first.value.sortType,
|
||||
first.value.goodsList,
|
||||
],
|
||||
() => {
|
||||
if (!isNew.value) return;
|
||||
newParams.pageNumber = 1;
|
||||
loadNewGoods(true);
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
// ---------- 旧版逻辑 ----------
|
||||
const currentTitleWay = computed(() => {
|
||||
return props.res?.list?.[0]?.titleWay?.[selected.index]
|
||||
})
|
||||
const params = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 100,
|
||||
categoryId: "",
|
||||
});
|
||||
const goodsData = ref<any[]>([]);
|
||||
const goodsResult = ref<any>("");
|
||||
|
||||
watch(
|
||||
() => props.res,
|
||||
(val) => {
|
||||
if (!val?.list?.[0]) return
|
||||
if (val.list[0].layout !== undefined) return
|
||||
const firstListWay = val.list[0].listWay?.[0]
|
||||
selected.val = firstListWay?.type || ''
|
||||
const firstTitleWay = val.list[0].titleWay?.[0]
|
||||
if (firstTitleWay?.bindCategory) {
|
||||
initGoods(firstTitleWay)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
uni.$on("onReachBottom", () => {
|
||||
if (!props.enableBottomLoad) return;
|
||||
if (isNew.value) {
|
||||
// 新版:仅分类/品牌/商户模式支持触底加载
|
||||
if (first.value.dataMode === "goods") return;
|
||||
if (newTotal.value > newGoodsData.value.length) {
|
||||
newParams.pageNumber++;
|
||||
loadNewGoods(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
goodsResult.value.totalElements >=
|
||||
params.pageNumber * params.pageSize
|
||||
) {
|
||||
params.pageNumber++;
|
||||
initGoods(props.res.list[0].titleWay[selected.index]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
uni.$off("onReachBottom");
|
||||
});
|
||||
|
||||
function handleClick(item: any) {
|
||||
if (!item?.id || !item?.goodsId) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/product/goods?id=${item.id}&goodsId=${item.goodsId}`,
|
||||
})
|
||||
}
|
||||
|
||||
async function initGoods(val: any) {
|
||||
if (props.enableBottomLoad) params.pageSize = 20;
|
||||
val ? (params.categoryId = val.bindCategory.id) : "";
|
||||
const res = await getGoodsList(params);
|
||||
if (res.data.success) {
|
||||
goodsResult.value = res.data.result;
|
||||
const result = res.data.result.records;
|
||||
goodsData.value.push(...result);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickTitle(val: any, index: number) {
|
||||
selected.index = index;
|
||||
selected.val = val.title;
|
||||
if (val.bindCategory) {
|
||||
params.pageNumber = 1;
|
||||
goodsData.value = [];
|
||||
initGoods(val);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
$w_94: 94%;
|
||||
|
||||
// ---------- 新版 ----------
|
||||
.goods-new {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.g-empty {
|
||||
padding: 60rpx 0;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.g-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.g-item {
|
||||
background: #fff;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.g-imgbox {
|
||||
flex-shrink: 0;
|
||||
background: #f5f5f5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.g-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.g-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.g-name {
|
||||
font-size: 26rpx;
|
||||
color: #282828;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.g-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.g-price {
|
||||
color: $main-color;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.g-sales {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.g-cart {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: $main-color;
|
||||
}
|
||||
|
||||
.cart-plus,
|
||||
.cart-cart {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.cart-plus-label {
|
||||
font-size: 30rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cart-text {
|
||||
border-radius: 24rpx;
|
||||
padding: 6rpx 20rpx;
|
||||
}
|
||||
|
||||
.cart-text-label {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* 卡片:左图右文(参考 CRMEB 详细列表,左侧 224rpx 缩略图,右侧标题置顶、价格销量置底) */
|
||||
.mode-card {
|
||||
flex-direction: column;
|
||||
.g-item {
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
padding: 20rpx;
|
||||
}
|
||||
.g-imgbox {
|
||||
width: 224rpx;
|
||||
height: 224rpx;
|
||||
margin-right: 24rpx;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.g-info {
|
||||
min-height: 224rpx;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.g-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
line-height: 40rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.g-price {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
margin-top: auto;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.g-bottom {
|
||||
margin-top: 0;
|
||||
}
|
||||
.g-sales {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.g-cart {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* 列表:两列网格 */
|
||||
.mode-list {
|
||||
.g-item {
|
||||
width: calc((100% - var(--g-gap, 10rpx)) / 2);
|
||||
flex-direction: column;
|
||||
}
|
||||
.g-imgbox {
|
||||
width: 100%;
|
||||
height: 330rpx;
|
||||
}
|
||||
.g-info {
|
||||
padding: 12rpx;
|
||||
min-height: 120rpx;
|
||||
}
|
||||
.g-name {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 三列 */
|
||||
.mode-three {
|
||||
.g-item {
|
||||
width: calc((100% - var(--g-gap, 10rpx) * 2) / 3);
|
||||
flex-direction: column;
|
||||
}
|
||||
.g-imgbox {
|
||||
width: 100%;
|
||||
height: 220rpx;
|
||||
}
|
||||
.g-info {
|
||||
padding: 10rpx;
|
||||
min-height: 96rpx;
|
||||
}
|
||||
.g-name {
|
||||
-webkit-line-clamp: 1;
|
||||
font-size: 24rpx;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.g-price {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.g-sales {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
/* 大图:一行一个 */
|
||||
.mode-big {
|
||||
flex-direction: column;
|
||||
.g-item {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
.g-imgbox {
|
||||
width: 100%;
|
||||
height: 420rpx;
|
||||
}
|
||||
.g-info {
|
||||
padding: 16rpx;
|
||||
min-height: 110rpx;
|
||||
}
|
||||
.g-name {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 旧版 ----------
|
||||
.layout {
|
||||
padding: 8px 0;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.selected-title {
|
||||
.h4 {
|
||||
font-size: 30rpx;
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.title-desc {
|
||||
font-weight: bold;
|
||||
color: $main-color !important;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-cell-title {
|
||||
background: #f9f9f9;
|
||||
padding: 10px;
|
||||
transition: 0.35s;
|
||||
display: flex;
|
||||
|
||||
> .goods-item-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
|
||||
.h4 {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.title-desc {
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-list {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.goods-item {
|
||||
width: 50%;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 0.4em;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-img {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
width: $w_94;
|
||||
border-top-left-radius: 20rpx;
|
||||
border-top-right-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-desc {
|
||||
border-bottom-left-radius: 20rpx;
|
||||
border-bottom-right-radius: 20rpx;
|
||||
width: $w_94;
|
||||
background: #fff;
|
||||
padding: 8rpx 0 8rpx 8rpx;
|
||||
margin: 0 auto;
|
||||
> .goods-title {
|
||||
font-size: 24rpx;
|
||||
height: 67rpx;
|
||||
display: -webkit-box;
|
||||
font-weight: 500;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> .goods-bottom {
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
> .goods-price {
|
||||
line-height: 2;
|
||||
color: $main-color;
|
||||
.price-int {
|
||||
font-size: 42rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
497
pages/tabbar/home/template/tpl_home_comb.vue
Normal file
@@ -0,0 +1,497 @@
|
||||
<template>
|
||||
<view class="tpl-home-comb" :style="wrapperStyle">
|
||||
<!-- 高斯模糊轮播图背景 + 底部渐变遮罩 -->
|
||||
<view class="comb-bg">
|
||||
<image
|
||||
v-if="currentBannerImg"
|
||||
class="comb-bg-img"
|
||||
:src="currentBannerImg"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="comb-mask"></view>
|
||||
</view>
|
||||
|
||||
<!-- 顶部状态栏占位(小程序/App) -->
|
||||
<view v-if="statusBarHeight" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
|
||||
<!-- 1. 搜索栏 -->
|
||||
<view class="comb-search-row" :style="searchRowStyle">
|
||||
<image
|
||||
v-if="res.searType === 'logo' && res.logoUrl"
|
||||
class="comb-logo"
|
||||
:src="res.logoUrl"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="comb-search-box" :style="searchBoxStyle" @click="onSearchTap">
|
||||
<u-icon name="search" :color="res.textColor || '#303133'" size="28rpx" />
|
||||
<text class="comb-search-text">{{ displayHotWord }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 2. 分类选项卡 -->
|
||||
<view v-if="res.showTabs !== false" class="comb-tabs-wrap">
|
||||
<view class="comb-tabs">
|
||||
<scroll-view scroll-x class="comb-tabs-scroll" :show-scrollbar="false">
|
||||
<view class="comb-tabs-inner">
|
||||
<view
|
||||
v-for="(tab, idx) in displayTabs"
|
||||
:key="idx"
|
||||
class="comb-tab-item"
|
||||
:class="{ active: activeTabIndex === idx }"
|
||||
:style="{
|
||||
color:
|
||||
activeTabIndex === idx
|
||||
? res.tabActiveColor || '#E93323'
|
||||
: res.tabColor || '#FFFFFF',
|
||||
}"
|
||||
@click="onTabTap(tab, idx)"
|
||||
>
|
||||
<text>{{ tab.title || "未命名" }}</text>
|
||||
<view
|
||||
v-if="activeTabIndex === idx"
|
||||
class="comb-tab-line"
|
||||
:style="{ background: res.tabActiveColor || '#E93323' }"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="comb-tab-toggle" @click="showMoreTabs = !showMoreTabs">
|
||||
<u-icon
|
||||
:name="showMoreTabs ? 'arrow-up' : 'arrow-down'"
|
||||
:color="res.tabColor || '#FFFFFF'"
|
||||
size="28rpx"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 下拉展开全部分类面板 -->
|
||||
<view v-if="showMoreTabs" class="comb-tabs-dropdown">
|
||||
<view
|
||||
v-for="(tab, idx) in displayTabs"
|
||||
:key="idx"
|
||||
class="dropdown-chip"
|
||||
:class="{ active: activeTabIndex === idx }"
|
||||
:style="
|
||||
activeTabIndex === idx
|
||||
? {
|
||||
color: res.tabActiveColor || '#E93323',
|
||||
borderColor: res.tabActiveColor || '#E93323',
|
||||
}
|
||||
: {}
|
||||
"
|
||||
@click="onTabTap(tab, idx)"
|
||||
>
|
||||
{{ tab.title || "未命名" }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 3. 轮播图区域 -->
|
||||
<view
|
||||
v-if="bannerList.length"
|
||||
class="comb-banner"
|
||||
:class="{ 'no-tabs': res.showTabs === false }"
|
||||
>
|
||||
<swiper
|
||||
class="comb-swiper"
|
||||
autoplay
|
||||
:interval="4500"
|
||||
:duration="500"
|
||||
circular
|
||||
:previous-margin="cardMode ? '48rpx' : '0rpx'"
|
||||
:next-margin="cardMode ? '48rpx' : '0rpx'"
|
||||
@change="onSwiperChange"
|
||||
>
|
||||
<swiper-item v-for="(item, i) in bannerList" :key="i">
|
||||
<view
|
||||
class="comb-slide-item"
|
||||
:class="{ 'is-card': cardMode, active: i === currentSlide }"
|
||||
@click="onBannerTap(item)"
|
||||
>
|
||||
<image
|
||||
class="comb-slide-img"
|
||||
:style="bannerImgStyle"
|
||||
:src="item.img"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- 指示器 -->
|
||||
<view
|
||||
v-if="res.indicatorStyle !== 'none' && bannerList.length > 1"
|
||||
class="comb-indicator"
|
||||
:class="'pos-' + (res.indicatorPosition || 'center')"
|
||||
>
|
||||
<view
|
||||
v-for="(item, i) in bannerList"
|
||||
:key="i"
|
||||
class="dot"
|
||||
:class="[res.indicatorStyle || 'round', { active: i === currentSlide }]"
|
||||
:style="
|
||||
i === currentSlide
|
||||
? { background: res.indicatorColor || '#E93323' }
|
||||
: {}
|
||||
"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const toRpx = (v: any) => `${Number(v) || 0}rpx`;
|
||||
|
||||
// 状态栏与胶囊避让
|
||||
const statusBarHeight = ref(0);
|
||||
const capsuleRightPad = ref(0);
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
const info = uni.getWindowInfo();
|
||||
// #ifdef MP-WEIXIN || APP-PLUS
|
||||
statusBarHeight.value = info.statusBarHeight || 0;
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
const menu = uni.getMenuButtonBoundingClientRect();
|
||||
capsuleRightPad.value = Math.max(0, info.windowWidth - menu.left + 8);
|
||||
// #endif
|
||||
} catch (e) {
|
||||
statusBarHeight.value = 0;
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索热词轮换
|
||||
const hotIndex = ref(0);
|
||||
let hotTimer: ReturnType<typeof setInterval> | null = null;
|
||||
const validHotWords = computed(() =>
|
||||
(props.res?.hotWords || []).filter((w: string) => w && String(w).trim())
|
||||
);
|
||||
const displayHotWord = computed(() => {
|
||||
if (validHotWords.value.length) {
|
||||
return validHotWords.value[hotIndex.value % validHotWords.value.length];
|
||||
}
|
||||
return props.res?.placeWords || "搜索商品名称";
|
||||
});
|
||||
|
||||
function stopHotTimer() {
|
||||
if (hotTimer) {
|
||||
clearInterval(hotTimer);
|
||||
hotTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [validHotWords.value.length, props.res?.hotInterval],
|
||||
() => {
|
||||
stopHotTimer();
|
||||
hotIndex.value = 0;
|
||||
if (validHotWords.value.length > 1) {
|
||||
hotTimer = setInterval(() => {
|
||||
hotIndex.value = (hotIndex.value + 1) % validHotWords.value.length;
|
||||
}, (Number(props.res?.hotInterval) || 3) * 1000);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onBeforeUnmount(stopHotTimer);
|
||||
|
||||
// 选项卡
|
||||
const activeTabIndex = ref(0);
|
||||
const showMoreTabs = ref(false);
|
||||
const displayTabs = computed(() => {
|
||||
const custom = Array.isArray(props.res?.tabList) ? props.res.tabList : [];
|
||||
return [{ title: "首页", isHome: true }, ...custom];
|
||||
});
|
||||
|
||||
function onTabTap(tab: any, idx: number) {
|
||||
activeTabIndex.value = idx;
|
||||
showMoreTabs.value = false;
|
||||
if (tab.isHome || !tab.value) return;
|
||||
if (tab.dataType === "special") {
|
||||
uni.navigateTo({
|
||||
url: `/pages/tabbar/special/special?id=${tab.value}`,
|
||||
});
|
||||
} else if (tab.dataType === "category") {
|
||||
uni.navigateTo({
|
||||
url: `/pages/navigation/search/searchPage?category=${tab.value}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 轮播图
|
||||
const currentSlide = ref(0);
|
||||
const bannerList = computed(() =>
|
||||
Array.isArray(props.res?.list) ? props.res.list : []
|
||||
);
|
||||
const cardMode = computed(() => (props.res?.swiperStyle || "card") === "card");
|
||||
const currentBannerImg = computed(() => {
|
||||
const list = bannerList.value;
|
||||
if (!list.length) return "";
|
||||
const item = list[currentSlide.value % list.length] || list[0];
|
||||
return item?.img || "";
|
||||
});
|
||||
|
||||
function onSwiperChange(e: any) {
|
||||
currentSlide.value = e?.detail?.current ?? 0;
|
||||
}
|
||||
|
||||
function onBannerTap(item: any) {
|
||||
if (item) modelNavigateTo(item);
|
||||
}
|
||||
|
||||
function onSearchTap() {
|
||||
const keyword = validHotWords.value.length ? displayHotWord.value : "";
|
||||
const query = keyword ? `?keyword=${encodeURIComponent(keyword)}` : "";
|
||||
uni.navigateTo({
|
||||
url: `/pages/navigation/search/searchPage${query}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 样式计算
|
||||
const wrapperStyle = computed(() => ({
|
||||
marginBottom: toRpx(props.res?.pageGap ?? 0),
|
||||
}));
|
||||
|
||||
const searchRowStyle = computed(() =>
|
||||
capsuleRightPad.value ? { paddingRight: `${capsuleRightPad.value}px` } : {}
|
||||
);
|
||||
|
||||
const searchBoxStyle = computed(() => {
|
||||
const r = props.res || {};
|
||||
const align = r.textAlign || "left";
|
||||
return {
|
||||
background: r.boxColor || "#FFFFFF",
|
||||
color: r.textColor || "#303133",
|
||||
borderRadius: toRpx(r.searchRadius ?? 30),
|
||||
justifyContent:
|
||||
align === "center"
|
||||
? "center"
|
||||
: align === "right"
|
||||
? "flex-end"
|
||||
: "flex-start",
|
||||
};
|
||||
});
|
||||
|
||||
const bannerImgStyle = computed(() => ({
|
||||
borderRadius: toRpx(props.res?.bannerRadius ?? 14),
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tpl-home-comb {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 16rpx 20rpx 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.comb-bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #e93323 0%, #f5f5f5 100%);
|
||||
.comb-bg-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: scale(1.25);
|
||||
filter: blur(40rpx);
|
||||
display: block;
|
||||
}
|
||||
.comb-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 180rpx;
|
||||
background: linear-gradient(180deg, rgba(245, 245, 245, 0) 0%, #f5f5f5 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.comb-search-row {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.comb-logo {
|
||||
width: 138rpx;
|
||||
height: 48rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.comb-search-box {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 60rpx;
|
||||
padding: 0 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 24rpx;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comb-search-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.comb-tabs-wrap {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.comb-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
.comb-tabs-scroll {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.comb-tabs-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.comb-tab-item {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 68rpx;
|
||||
font-size: 26rpx;
|
||||
white-space: nowrap;
|
||||
&.active {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.comb-tab-line {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 4rpx;
|
||||
transform: translateX(-50%);
|
||||
width: 36rpx;
|
||||
height: 4rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.comb-tab-toggle {
|
||||
flex-shrink: 0;
|
||||
padding-left: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comb-tabs-dropdown {
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
margin-top: 8rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dropdown-chip {
|
||||
padding: 8rpx 24rpx;
|
||||
background: #f5f5f5;
|
||||
border: 2rpx solid transparent;
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.comb-banner {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-top: 12rpx;
|
||||
height: 300rpx;
|
||||
&.no-tabs {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.comb-swiper {
|
||||
width: 100%;
|
||||
height: 300rpx;
|
||||
}
|
||||
|
||||
.comb-slide-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
&.is-card {
|
||||
padding: 0 8rpx;
|
||||
transform: scale(0.92);
|
||||
transition: transform 0.35s ease;
|
||||
&.active {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comb-slide-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comb-indicator {
|
||||
position: absolute;
|
||||
bottom: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 3;
|
||||
&.pos-left {
|
||||
left: 32rpx;
|
||||
}
|
||||
&.pos-center {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
&.pos-right {
|
||||
right: 32rpx;
|
||||
}
|
||||
.dot {
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
transition: all 0.25s ease;
|
||||
&.round {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
margin: 0 6rpx;
|
||||
}
|
||||
&.line {
|
||||
width: 24rpx;
|
||||
height: 6rpx;
|
||||
border-radius: 4rpx;
|
||||
margin: 0 4rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,54 +1,188 @@
|
||||
<template>
|
||||
<div class="layout" v-if="goodsList.length">
|
||||
<div class="join-list">
|
||||
<div class="join-title" @click="goMore">
|
||||
<div class="join-title-name">{{ res.list[0].title }}</div>
|
||||
<div class="join-title-more">更多</div>
|
||||
</div>
|
||||
<div class="join-box">
|
||||
<div
|
||||
class="join-item"
|
||||
@click="goDetail(item)"
|
||||
v-for="(item, index) in goodsList"
|
||||
:key="index"
|
||||
>
|
||||
<div class="item-img-box">
|
||||
<u-image
|
||||
class="item-img"
|
||||
width="150rpx"
|
||||
height="150rpx"
|
||||
:src="item.goodsImage || item.thumbnail"
|
||||
/>
|
||||
</div>
|
||||
<div class="item-price">
|
||||
<span class="price-text">{{ item.points != null ? item.points : 0 }}积分</span>
|
||||
</div>
|
||||
<div class="item-line-through" v-if="item.originalPrice">
|
||||
<span class="price-text">¥{{ item.originalPrice }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<view class="layout" :style="wrapperStyle" v-if="goodsList.length">
|
||||
<view class="integral-head" :style="headStyle" @click="goMore">
|
||||
<image
|
||||
v-if="styled && options.titleType === 'image' && options.titleImage"
|
||||
class="integral-head-img"
|
||||
:src="options.titleImage"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view v-else class="integral-head-title" :style="headTitleStyle">{{ title }}</view>
|
||||
<view class="integral-head-space"></view>
|
||||
<view class="integral-head-more" :style="moreStyle">更多 ></view>
|
||||
</view>
|
||||
<!-- 老数据:原始四列样式 -->
|
||||
<view v-if="!styled" class="join-box">
|
||||
<view
|
||||
class="join-item"
|
||||
@click="goDetail(item)"
|
||||
v-for="(item, index) in goodsList"
|
||||
:key="index"
|
||||
>
|
||||
<view class="item-img-box">
|
||||
<u-image
|
||||
class="item-img"
|
||||
width="150rpx"
|
||||
height="150rpx"
|
||||
:src="item.goodsImage || item.thumbnail"
|
||||
/>
|
||||
</view>
|
||||
<view class="item-price">
|
||||
<span class="price-text">{{ item.points != null ? item.points : 0 }}积分</span>
|
||||
</view>
|
||||
<view class="item-line-through" v-if="item.originalPrice">
|
||||
<span class="price-text">¥{{ item.originalPrice }}</span>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 样式一:两列大卡片 -->
|
||||
<view v-else-if="layoutStyle === '1'" class="ig-grid2" :style="gapStyle">
|
||||
<view
|
||||
class="ig-g2-item"
|
||||
v-for="(item, index) in goodsList"
|
||||
:key="index"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<view class="ig-g2-img-box" :style="imgRadiusStyle">
|
||||
<image class="ig-img" :src="item.goodsImage || item.thumbnail" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="ig-g2-name" :style="nameStyle">{{ item.goodsName || item.promotionName || '' }}</view>
|
||||
<view class="ig-price-row">
|
||||
<view class="ig-price" :style="pointStyle">
|
||||
<text class="ig-coin">¥</text>{{ item.points != null ? item.points : 0 }}积分
|
||||
</view>
|
||||
<text class="ig-original" v-if="item.originalPrice">¥{{ item.originalPrice }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 样式二:三列,积分价在名称上方 -->
|
||||
<view v-else-if="layoutStyle === '2'" class="ig-grid3" :style="gapStyle">
|
||||
<view
|
||||
class="ig-g3-item"
|
||||
v-for="(item, index) in goodsList"
|
||||
:key="index"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<view class="ig-g3-img-box" :style="imgRadiusStyle">
|
||||
<image class="ig-img" :src="item.goodsImage || item.thumbnail" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="ig-price ig-price-sm" :style="pointStyle">
|
||||
<text class="ig-coin ig-coin-sm">¥</text>{{ item.points != null ? item.points : 0 }}积分
|
||||
</view>
|
||||
<view class="ig-g3-name" :style="nameStyle">{{ item.goodsName || item.promotionName || '' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 样式三:三列,仅积分价 -->
|
||||
<view v-else class="ig-grid3" :style="gapStyle">
|
||||
<view
|
||||
class="ig-g3-item"
|
||||
v-for="(item, index) in goodsList"
|
||||
:key="index"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<view class="ig-g3-img-box" :style="imgRadiusStyle">
|
||||
<image class="ig-img" :src="item.goodsImage || item.thumbnail" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="ig-price ig-price-sm" :style="pointStyle">
|
||||
<text class="ig-coin ig-coin-sm">¥</text>{{ item.points != null ? item.points : 0 }}积分
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import * as API_Promotions from "@/api/promotions";
|
||||
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const goodsList = ref<any[]>([]);
|
||||
|
||||
const options = computed(() => props.res || {});
|
||||
const title = computed(() => props.res?.list?.[0]?.title || "积分兑好礼");
|
||||
const count = computed(() => props.res?.list?.[0]?.count || 6);
|
||||
const layoutStyle = computed(() => props.res?.layoutStyle || "1");
|
||||
|
||||
// 面板保存过样式字段才启用新样式,老数据保持原样
|
||||
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (styled.value) {
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
const radius = Number(res.borderRadius) || 0;
|
||||
return {
|
||||
background: bg,
|
||||
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
|
||||
marginBottom: `${Number(res.pageGap) || 0}rpx`,
|
||||
borderRadius: `${radius}rpx`,
|
||||
overflow: radius > 0 ? "hidden" : "visible",
|
||||
};
|
||||
}
|
||||
return {
|
||||
background: res.bgColor || "#FFFFFF",
|
||||
padding: `0 ${res.paddingX ?? 0}rpx`,
|
||||
marginBottom: `${res.pageGap ?? 10}rpx`,
|
||||
borderRadius: `${res.borderRadius ?? 0}rpx`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
});
|
||||
|
||||
const headStyle = computed(() => {
|
||||
if (!styled.value) return {};
|
||||
const res = props.res;
|
||||
if (res.headBgType === "image" && res.headBgImage) {
|
||||
return {
|
||||
backgroundImage: `url(${res.headBgImage})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
};
|
||||
}
|
||||
const bg =
|
||||
!res.headBg2 || res.headBg1 === res.headBg2
|
||||
? res.headBg1
|
||||
: `linear-gradient(90deg, ${res.headBg1}, ${res.headBg2})`;
|
||||
return { background: bg };
|
||||
});
|
||||
|
||||
const headTitleStyle = computed(() => {
|
||||
if (!styled.value) return {};
|
||||
const res = props.res;
|
||||
return {
|
||||
color: res.titleColor || "#FFFFFF",
|
||||
fontWeight: res.titleStyle === "bold" ? "bold" : "normal",
|
||||
fontStyle: res.titleStyle === "italic" ? "italic" : "normal",
|
||||
};
|
||||
});
|
||||
|
||||
const moreStyle = computed(() =>
|
||||
styled.value ? { color: props.res.moreColor || "#999999" } : {}
|
||||
);
|
||||
const pointStyle = computed(() =>
|
||||
styled.value && props.res.pointColor ? { color: props.res.pointColor } : {}
|
||||
);
|
||||
const nameStyle = computed(() =>
|
||||
styled.value && props.res.nameColor ? { color: props.res.nameColor } : {}
|
||||
);
|
||||
const gapStyle = computed(() => {
|
||||
if (!styled.value) return {};
|
||||
const gap = `${Number(props.res.contentGap ?? 10) || 0}rpx`;
|
||||
return { gap, "--ig-gap": gap };
|
||||
});
|
||||
const imgRadiusStyle = computed(() =>
|
||||
styled.value ? { borderRadius: `${Number(props.res.imgRadius) || 0}rpx`, overflow: "hidden" } : {}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
API_Promotions.getPointsGoods({
|
||||
pageNumber: 1,
|
||||
pageSize: props.res.list[0].count || 3,
|
||||
pageSize: count.value,
|
||||
}).then((res) => {
|
||||
if (res.data.success && res.data.result && res.data.result.records) {
|
||||
goodsList.value = res.data.result.records.slice(
|
||||
0,
|
||||
props.res.list[0].count || 3
|
||||
);
|
||||
goodsList.value = res.data.result.records.slice(0, count.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -63,12 +197,39 @@ function goDetail(item: any) {
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
.integral-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 96rpx;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
.integral-head-img {
|
||||
width: 154rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
.integral-head-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.integral-head-space {
|
||||
flex: 1;
|
||||
}
|
||||
.integral-head-more {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
.join-box {
|
||||
display: flex;
|
||||
}
|
||||
.item-price {
|
||||
.price-text {
|
||||
font-size: 15px;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: $main-color;
|
||||
}
|
||||
@@ -77,36 +238,116 @@ function goDetail(item: any) {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.item-img {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
.item-img-box {
|
||||
position: relative;
|
||||
}
|
||||
.item-line-through {
|
||||
.price-text {
|
||||
font-size: 10px;
|
||||
font-size: 20rpx;
|
||||
font-weight: 400;
|
||||
text-decoration: line-through;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
.join-title {
|
||||
|
||||
/* 积分商品卡片(参考 CRMEB 积分商城) */
|
||||
.ig-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.ig-grid2 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ig-g2-item {
|
||||
width: calc((100% - var(--ig-gap, 10rpx)) / 2);
|
||||
background: #fff;
|
||||
height: 50px;
|
||||
.join-title-name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.join-title-more {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
padding-bottom: 16rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ig-g2-img-box {
|
||||
width: 100%;
|
||||
height: 300rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.ig-g2-name {
|
||||
margin: 16rpx 16rpx 0;
|
||||
font-size: 26rpx;
|
||||
color: #282828;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ig-price-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin: 10rpx 16rpx 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.ig-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #f5483a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ig-price-sm {
|
||||
font-size: 24rpx;
|
||||
margin: 10rpx 16rpx 0;
|
||||
}
|
||||
.ig-coin {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #ffe29a, #f5a623);
|
||||
color: #fff;
|
||||
font-size: 18rpx;
|
||||
font-weight: bold;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 6rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ig-coin-sm {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
font-size: 16rpx;
|
||||
}
|
||||
.ig-original {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-left: 10rpx;
|
||||
white-space: nowrap;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.ig-grid3 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ig-g3-item {
|
||||
width: calc((100% - var(--ig-gap, 10rpx) * 2) / 3);
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
padding-bottom: 16rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ig-g3-img-box {
|
||||
width: 100%;
|
||||
height: 184rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.ig-g3-name {
|
||||
margin: 8rpx 16rpx 0;
|
||||
font-size: 24rpx;
|
||||
color: #282828;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
|
||||
<template>
|
||||
|
||||
<view class="layout">
|
||||
<view class="view-height-150" @click="modelNavigateTo(res.list[0])">
|
||||
<view class="layout" :style="[wrapperStyle, gapStyle]">
|
||||
<view class="view-height-150" :style="imgStyle" @click="modelNavigateTo(res.list[0])">
|
||||
<u-image width="100%" height="340rpx" class="image-mode" :src="res.list[0].img">
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</view>
|
||||
<view class="view-height-150">
|
||||
<view class="view-height-75" @click="modelNavigateTo(res.list[1])">
|
||||
<view class="view-height-75" :style="imgStyle" @click="modelNavigateTo(res.list[1])">
|
||||
<u-image width="100%" height="170rpx" class="image-mode" :src="res.list[1].img" alt>
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</view>
|
||||
<view class="view-height-75" @click="modelNavigateTo(res.list[2])">
|
||||
<view class="view-height-75" :style="imgStyle" @click="modelNavigateTo(res.list[2])">
|
||||
<u-image width="100%" height="170rpx" class="image-mode" :src="res.list[2].img" alt>
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
@@ -22,9 +22,14 @@
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
import { computed } from "vue";
|
||||
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
|
||||
|
||||
defineProps<{ res: any }>();
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const gapStyle = computed(() => cubeGapStyle(props.res));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -35,4 +40,7 @@ defineProps<{ res: any }>();
|
||||
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
.view-height-150 {
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
|
||||
<template>
|
||||
<div class="layout ">
|
||||
<div class="layout " :style="[wrapperStyle, gapStyle]">
|
||||
<div class="view-height-150">
|
||||
<div class="view-height-75" @click="modelNavigateTo(res.list[0])">
|
||||
<div class="view-height-75" :style="imgStyle" @click="modelNavigateTo(res.list[0])">
|
||||
<u-image class="image-mode" width="100%" height="150rpx" :src="res.list[0].img" ></u-image>
|
||||
</div>
|
||||
<div class="view-height-75" @click="modelNavigateTo(res.list[1])">
|
||||
<div class="view-height-75" :style="imgStyle" @click="modelNavigateTo(res.list[1])">
|
||||
<u-image class="image-mode" width="100%" height="150rpx" :src="res.list[1].img" ></u-image>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-height-150" @click="modelNavigateTo(res.list[2])">
|
||||
<div class="view-height-150" :style="imgStyle" @click="modelNavigateTo(res.list[2])">
|
||||
<u-image class="image-mode" width="100%" height="300rpx" :src="res.list[2].img" ></u-image>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
import { computed } from "vue";
|
||||
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
|
||||
|
||||
defineProps<{ res: any }>();
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const gapStyle = computed(() => cubeGapStyle(props.res));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -30,5 +35,8 @@ defineProps<{ res: any }>();
|
||||
|
||||
background-size: cover;
|
||||
}
|
||||
.view-height-150 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
30
pages/tabbar/home/template/tpl_line.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<view class="tpl-line" :style="wrapperStyle">
|
||||
<view :style="lineStyle"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
const bg =
|
||||
res.bgColor1 !== undefined
|
||||
? !res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`
|
||||
: res.bgColor || "#FFFFFF";
|
||||
return {
|
||||
background: bg,
|
||||
padding: `0 ${res.paddingX ?? 0}rpx`,
|
||||
marginBottom: `${res.pageGap ?? 10}rpx`,
|
||||
};
|
||||
});
|
||||
|
||||
const lineStyle = computed(() => ({
|
||||
borderTop: `${props.res?.lineHeight ?? 1}rpx ${props.res?.lineStyle || "dashed"} ${props.res?.lineColor || "#979797"}`,
|
||||
}));
|
||||
</script>
|
||||
@@ -1,31 +1,61 @@
|
||||
<template>
|
||||
<view class="layout">
|
||||
<view class="menu-list">
|
||||
<view class="layout" :style="wrapperStyle">
|
||||
<view
|
||||
class="menu-list"
|
||||
:class="{ styled: styled, single: isSingle }"
|
||||
:style="listStyle"
|
||||
>
|
||||
<view
|
||||
class="menu-item"
|
||||
:style="itemStyle"
|
||||
@click="modelNavigateTo(item)"
|
||||
v-for="(item, index) in res.list"
|
||||
v-for="(item, index) in visibleList"
|
||||
:key="index"
|
||||
>
|
||||
<view class="menu-img-wrap">
|
||||
<u-image
|
||||
width="88rpx"
|
||||
height="88rpx"
|
||||
class="menu-img"
|
||||
:src="item.img"
|
||||
>
|
||||
<view class="menu-img-wrap" :style="imgStyle">
|
||||
<u-image width="88rpx" height="88rpx" class="menu-img" :src="item.img">
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</view>
|
||||
<view class="menu-title">{{ item.title }}</view>
|
||||
<view class="menu-title" :style="titleStyle">{{ item.title }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
import { computed } from "vue";
|
||||
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle } from "./tpl";
|
||||
|
||||
defineProps<{ res: any }>();
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
// 面板保存过样式字段才启用自定义样式,老数据保持原样
|
||||
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const listStyle = computed(() =>
|
||||
styled.value ? { rowGap: `${Number(props.res.contentGap) || 0}rpx` } : {}
|
||||
);
|
||||
const titleStyle = computed(() =>
|
||||
styled.value && props.res.textColor ? { color: props.res.textColor } : {}
|
||||
);
|
||||
|
||||
// 展示设置:单行/多行、行数、每行个数,超出部分不展示
|
||||
const isSingle = computed(
|
||||
() => styled.value && props.res.menuDisplay === "single"
|
||||
);
|
||||
const cols = computed(() =>
|
||||
styled.value ? Number(props.res.menuCols) || 4 : 4
|
||||
);
|
||||
const rows = computed(() => Number(props.res.menuRows) || 3);
|
||||
const itemStyle = computed(() =>
|
||||
styled.value ? { flex: `0 0 ${100 / cols.value}%` } : {}
|
||||
);
|
||||
const visibleList = computed(() => {
|
||||
const list = props.res.list || [];
|
||||
if (!styled.value) return list;
|
||||
const limit = isSingle.value ? cols.value : rows.value * cols.value;
|
||||
return list.slice(0, limit);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -34,17 +64,28 @@ defineProps<{ res: any }>();
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
&.single {
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> .menu-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: 20%;
|
||||
flex: 0 0 20%;
|
||||
width: 25%;
|
||||
flex: 0 0 25%;
|
||||
box-sizing: border-box;
|
||||
margin: 20rpx 0;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
&.styled > .menu-item,
|
||||
&.single > .menu-item {
|
||||
margin: 0;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-img-wrap {
|
||||
@@ -72,4 +113,4 @@ defineProps<{ res: any }>();
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,55 +1,103 @@
|
||||
<template>
|
||||
<div class="layout" v-if="list.length">
|
||||
<div class="background">
|
||||
<u-notice-bar
|
||||
direction="column"
|
||||
:bg-color="noticeStyle.bk_color"
|
||||
:color="noticeStyle.color"
|
||||
:text="list"
|
||||
></u-notice-bar>
|
||||
</div>
|
||||
</div>
|
||||
<view class="layout" :class="{ styled: styled }" v-if="texts.length" :style="wrapperStyle">
|
||||
<view class="notice-bar">
|
||||
<image
|
||||
v-if="styled && item.img"
|
||||
class="notice-icon"
|
||||
:src="item.img"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view class="notice-body" :style="{ textAlign: alignValue }">
|
||||
<u-notice-bar
|
||||
:direction="directionValue"
|
||||
:bg-color="styled ? 'transparent' : item.bk_color"
|
||||
:color="item.color"
|
||||
:text="texts"
|
||||
@click="clickNotice"
|
||||
></u-notice-bar>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const noticeStyle = computed(() => props.res?.list?.[0] || {});
|
||||
const item = computed(() => props.res?.list?.[0] || {});
|
||||
|
||||
const list = computed(() => {
|
||||
const titles = props.res?.list?.[0]?.title;
|
||||
if (!Array.isArray(titles)) return [];
|
||||
return titles
|
||||
.map((item: { context?: string }) => (item?.context == null ? "" : String(item.context)))
|
||||
.filter((text: string) => text.length > 0);
|
||||
// 面板保存过样式字段才启用自定义样式,老数据保持原样
|
||||
const styled = computed(() => props.res && props.res.paddingX !== undefined);
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (!styled.value) return {};
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
const radius = Number(res.borderRadius) || 0;
|
||||
return {
|
||||
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
|
||||
background: bg,
|
||||
borderRadius: `${radius}rpx`,
|
||||
marginBottom: `${Number(res.pageGap) || 0}rpx`,
|
||||
overflow: radius > 0 ? "hidden" : "visible",
|
||||
};
|
||||
});
|
||||
|
||||
const directionValue = computed(() =>
|
||||
styled.value && item.value.direction === "horizontal" ? "row" : "column"
|
||||
);
|
||||
const alignValue = computed(() =>
|
||||
styled.value ? item.value.textAlign || "left" : "left"
|
||||
);
|
||||
|
||||
const titleList = computed(() => {
|
||||
const titles = item.value?.title;
|
||||
if (!Array.isArray(titles)) return [];
|
||||
return titles.filter(
|
||||
(t: any) => t && t.context != null && String(t.context).length > 0
|
||||
);
|
||||
});
|
||||
|
||||
const texts = computed(() =>
|
||||
titleList.value.map((t: any) => String(t.context))
|
||||
);
|
||||
|
||||
function clickNotice(index: number) {
|
||||
const target = titleList.value[index];
|
||||
if (target && target.url) {
|
||||
modelNavigateTo(target);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
.background {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 84rpx;
|
||||
text-align: left;
|
||||
font-size: 20rpx;
|
||||
background-size: cover;
|
||||
}
|
||||
.layout {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
height: 84rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background: #ffffff;
|
||||
}
|
||||
.title {
|
||||
line-height: 84rpx;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
@import "./tpl.scss";
|
||||
.layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 84rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
.layout.styled {
|
||||
height: auto;
|
||||
}
|
||||
.notice-bar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.notice-icon {
|
||||
width: 130rpx;
|
||||
height: 36rpx;
|
||||
flex-shrink: 0;
|
||||
margin: 0 16rpx;
|
||||
}
|
||||
.notice-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
35
pages/tabbar/home/template/tpl_rich_text.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<view class="tpl-rich-text" :style="wrapperStyle">
|
||||
<rich-text :nodes="processedHtml"></rich-text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
const bg =
|
||||
res.bgColor1 !== undefined
|
||||
? !res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`
|
||||
: res.bgColor || "#FFFFFF";
|
||||
return {
|
||||
background: bg,
|
||||
padding: `0 ${res.paddingX ?? 12}rpx`,
|
||||
marginBottom: `${res.pageGap ?? 10}rpx`,
|
||||
borderRadius: `${res.borderRadius ?? 0}rpx`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
});
|
||||
|
||||
// 防止富文本内图片超宽撑破布局
|
||||
const processedHtml = computed(() => {
|
||||
const html = props.res?.content;
|
||||
if (!html) return "";
|
||||
return String(html).replace(/<img/gi, '<img style="max-width:100%;height:auto;"');
|
||||
});
|
||||
</script>
|
||||
@@ -1,57 +1,123 @@
|
||||
<template>
|
||||
<view class="layout">
|
||||
<view class="search" @click="handleSearch">
|
||||
<u-icon name="search"></u-icon>
|
||||
<text class="search-text">{{ res.list[0].title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ res: any; storeId?: string }>();
|
||||
|
||||
function handleSearch() {
|
||||
if (props.storeId) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/navigation/search/searchPage?storeId=${props.storeId}`,
|
||||
});
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: `/pages/navigation/search/searchPage`,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
height: 64rpx;
|
||||
border-radius: 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0 10rpx;
|
||||
background: #ededed;
|
||||
color: #999;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-text {
|
||||
margin-left: 12rpx;
|
||||
font-size: 26rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<view class="layout" :style="wrapperStyle">
|
||||
<view class="search" :style="boxStyle" @click="handleSearch">
|
||||
<u-icon name="search"></u-icon>
|
||||
<text class="search-text">{{ displayText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onBeforeUnmount } from "vue";
|
||||
|
||||
const props = defineProps<{ res: any; storeId?: string }>();
|
||||
|
||||
// 面板保存过样式字段才启用自定义样式,老数据保持原样
|
||||
const styled = computed(() => props.res && props.res.paddingX !== undefined);
|
||||
|
||||
// 搜索热词轮播:有热词时按切换时间轮换展示,否则展示提示文字
|
||||
const hotIndex = ref(0);
|
||||
let hotTimer: ReturnType<typeof setInterval> | null = null;
|
||||
const validWords = computed(() =>
|
||||
(props.res.hotWords || []).filter((w: string) => w && w.trim())
|
||||
);
|
||||
const displayText = computed(() => {
|
||||
if (validWords.value.length) {
|
||||
return validWords.value[hotIndex.value % validWords.value.length];
|
||||
}
|
||||
return props.res.list?.[0]?.title || "";
|
||||
});
|
||||
|
||||
function stopHot() {
|
||||
if (hotTimer) {
|
||||
clearInterval(hotTimer);
|
||||
hotTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [validWords.value.length, props.res.hotInterval],
|
||||
() => {
|
||||
stopHot();
|
||||
hotIndex.value = 0;
|
||||
if (validWords.value.length > 1) {
|
||||
hotTimer = setInterval(() => {
|
||||
hotIndex.value = (hotIndex.value + 1) % validWords.value.length;
|
||||
}, (Number(props.res.hotInterval) || 3) * 1000);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onBeforeUnmount(stopHot);
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (!styled.value) return {};
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
return {
|
||||
padding: `0 ${Number(res.paddingX) || 0}rpx`,
|
||||
background: bg,
|
||||
borderRadius: `${Number(res.borderRadius) || 0}rpx`,
|
||||
overflow: Number(res.borderRadius) > 0 ? "hidden" : "visible",
|
||||
};
|
||||
});
|
||||
const boxStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (!styled.value) return {};
|
||||
return {
|
||||
background: res.boxColor || "#EDEDED",
|
||||
color: res.textColor || "#999999",
|
||||
borderRadius: `${Number(res.boxRadius) || 0}rpx`,
|
||||
justifyContent: res.textAlign === "center" ? "center" : "flex-start",
|
||||
};
|
||||
});
|
||||
|
||||
function handleSearch() {
|
||||
if (props.storeId) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/navigation/search/searchPage?storeId=${props.storeId}`,
|
||||
});
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: `/pages/navigation/search/searchPage`,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
height: 64rpx;
|
||||
border-radius: 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0 10rpx;
|
||||
background: #ededed;
|
||||
color: #999;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-text {
|
||||
margin-left: 12rpx;
|
||||
font-size: 26rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
378
pages/tabbar/home/template/tpl_shop.vue
Normal file
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<view class="tpl-shop" :style="wrapperStyle" v-if="shopList.length">
|
||||
<view class="shop-head" v-if="headIcon || title || moreUrl">
|
||||
<image v-if="headIcon" class="shop-head-icon" :src="headIcon" mode="aspectFit" />
|
||||
<view class="shop-title" :style="titleStyle">{{ title }}</view>
|
||||
<view v-if="moreUrl" class="shop-more" :style="moreStyle" @click="goMore">更多 ></view>
|
||||
</view>
|
||||
|
||||
<!-- 老数据:两列网格 -->
|
||||
<view v-if="!styled" class="shop-grid-legacy">
|
||||
<view
|
||||
class="shop-item"
|
||||
v-for="item in shopList"
|
||||
:key="item.id"
|
||||
@click="goShop(item)"
|
||||
>
|
||||
<image
|
||||
v-if="showLogo"
|
||||
class="shop-logo"
|
||||
:src="item.storeLogo"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-if="showName" class="shop-name">{{ item.storeName }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 样式一:店铺卡片(商品图背景 + Logo + 名称 + 类型标签) -->
|
||||
<view v-else-if="shopStyle === 'one'" class="shop-grid">
|
||||
<view
|
||||
class="shop-card"
|
||||
v-for="item in shopList"
|
||||
:key="item.id"
|
||||
@click="goShop(item)"
|
||||
>
|
||||
<image
|
||||
v-if="(item.goodsImages && item.goodsImages.length) || item.storeBanner"
|
||||
class="shop-card-bg"
|
||||
:style="logoStyle"
|
||||
:src="item.goodsImages && item.goodsImages.length ? item.goodsImages[0] : item.storeBanner"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="shop-card-bg shop-card-bg-empty" :style="logoStyle"></view>
|
||||
<view class="shop-card-body">
|
||||
<image
|
||||
v-if="showLogo"
|
||||
class="shop-card-logo"
|
||||
:src="item.storeLogo"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view v-if="showName" class="shop-card-name" :style="nameStyle">{{ item.storeName }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 样式二:店铺卡片 + 商品列表 -->
|
||||
<view v-else class="shop-street-list">
|
||||
<view
|
||||
class="shop-street-card"
|
||||
v-for="item in shopList"
|
||||
:key="item.id"
|
||||
@click="goShop(item)"
|
||||
>
|
||||
<view class="shop-street-head">
|
||||
<image v-if="showLogo" class="shop-street-logo" :src="item.storeLogo" mode="aspectFit" />
|
||||
<view v-if="showName" class="shop-street-name" :style="nameStyle">{{ item.storeName }}</view>
|
||||
<image
|
||||
v-if="(item.goods && item.goods.length) || item.storeBanner"
|
||||
class="shop-street-banner"
|
||||
:style="logoStyle"
|
||||
:src="item.goods && item.goods.length ? item.goods[0].img : item.storeBanner"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
<view class="shop-street-goods">
|
||||
<view
|
||||
class="street-goods-item"
|
||||
v-for="(g, gi) in item.goods || []"
|
||||
:key="gi"
|
||||
>
|
||||
<image class="street-goods-img" :src="g.img" mode="aspectFill" />
|
||||
<view class="street-goods-name">{{ g.name }}</view>
|
||||
<view class="street-goods-price">
|
||||
¥{{ goodsFormatPrice(g.price)[0] }}.{{ goodsFormatPrice(g.price)[1] }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { getStoreList } from "@/api/store.js";
|
||||
import { getGoodsList } from "@/api/goods.js";
|
||||
import { goodsFormatPrice } from "@/utils/filters.js";
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const shopList = ref<any[]>([]);
|
||||
|
||||
const first = computed(() => props.res?.list?.[0] || {});
|
||||
const title = computed(() => first.value.title || "");
|
||||
const count = computed(() => first.value.count || 4);
|
||||
const headIcon = computed(() => first.value.icon || "");
|
||||
const moreUrl = computed(() => first.value.moreUrl || "");
|
||||
const showLogo = computed(() => first.value.showLogo !== false);
|
||||
const showName = computed(() => first.value.showName !== false);
|
||||
const shopStyle = computed(() => first.value.shopStyle || "one");
|
||||
|
||||
// 面板保存过样式字段才启用自定义样式,老数据保持原样
|
||||
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (styled.value) {
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
const radius = Number(res.borderRadius) || 0;
|
||||
return {
|
||||
background: bg,
|
||||
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
|
||||
marginBottom: `${Number(res.pageGap) || 0}rpx`,
|
||||
borderRadius: `${radius}rpx`,
|
||||
overflow: radius > 0 ? "hidden" : "visible",
|
||||
};
|
||||
}
|
||||
return {
|
||||
background: res.bgColor || "#FFFFFF",
|
||||
padding: `20rpx ${res.paddingX ?? 0}rpx`,
|
||||
marginBottom: `${res.pageGap ?? 10}rpx`,
|
||||
borderRadius: `${res.borderRadius ?? 0}rpx`,
|
||||
overflow: "hidden",
|
||||
};
|
||||
});
|
||||
const titleStyle = computed(() =>
|
||||
styled.value && props.res.titleColor ? { color: props.res.titleColor } : {}
|
||||
);
|
||||
const nameStyle = computed(() =>
|
||||
styled.value && props.res.nameColor ? { color: props.res.nameColor } : {}
|
||||
);
|
||||
const moreStyle = computed(() =>
|
||||
styled.value && props.res.moreColor ? { color: props.res.moreColor } : {}
|
||||
);
|
||||
const logoStyle = computed(() =>
|
||||
styled.value ? { borderRadius: `${Number(props.res.imgRadius) || 0}rpx` } : {}
|
||||
);
|
||||
|
||||
async function loadShops() {
|
||||
const item = first.value;
|
||||
if (item.dataMode === "custom" && Array.isArray(item.shopList) && item.shopList.length) {
|
||||
shopList.value = item.shopList;
|
||||
} else {
|
||||
const res = await getStoreList({ pageNumber: 1, pageSize: count.value });
|
||||
if (res.data.success && res.data.result) {
|
||||
shopList.value = (res.data.result.records || []).slice(0, count.value);
|
||||
}
|
||||
}
|
||||
loadGoodsImages();
|
||||
}
|
||||
|
||||
// 样式二:按店铺拉取商品(图片、名称、价格)
|
||||
function loadGoodsImages() {
|
||||
if (shopStyle.value !== "two") return;
|
||||
shopList.value.forEach((shop: any, i: number) => {
|
||||
getGoodsList({ storeId: shop.id, pageNumber: 1, pageSize: 3 }).then((res: any) => {
|
||||
const records = res?.data?.result?.records || res?.data?.result || [];
|
||||
const goods = (Array.isArray(records) ? records : [])
|
||||
.map((g: any) => ({
|
||||
name: g.goodsName,
|
||||
price: g.price,
|
||||
img: g.thumbnail || g.small || g.goodsImage,
|
||||
}))
|
||||
.filter((g: any) => g.img);
|
||||
shopList.value[i].goods = goods;
|
||||
shopList.value[i].goodsImages = goods.map((g: any) => g.img);
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [first.value.dataMode, first.value.shopList, count.value],
|
||||
() => loadShops(),
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
function goShop(item: any) {
|
||||
uni.navigateTo({ url: `/pages/product/shopPage?id=${item.id}` });
|
||||
}
|
||||
|
||||
function goMore() {
|
||||
modelNavigateTo({ url: moreUrl.value });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shop-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 24rpx 20rpx;
|
||||
}
|
||||
.shop-head-icon {
|
||||
width: 124rpx;
|
||||
height: 32rpx;
|
||||
flex-shrink: 0;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
.shop-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.shop-more {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
// 样式一:三列店铺卡片
|
||||
.shop-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 24rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.shop-card {
|
||||
width: calc((100% - 32rpx) / 3);
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.shop-card-bg {
|
||||
width: 100%;
|
||||
height: 140rpx;
|
||||
display: block;
|
||||
background: #eee;
|
||||
}
|
||||
.shop-card-bg-empty {
|
||||
background: linear-gradient(135deg, #f5f5f5, #e8e8e8);
|
||||
}
|
||||
.shop-card-body {
|
||||
padding: 8rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.shop-card-logo {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
width: 96rpx;
|
||||
height: 32rpx;
|
||||
margin: -16rpx auto 0;
|
||||
background: #fff;
|
||||
}
|
||||
.shop-card-name {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
// 样式二:店铺卡片 + 商品列表
|
||||
.shop-street-list {
|
||||
padding: 0 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.shop-street-card {
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.shop-street-head {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 68rpx;
|
||||
padding: 0 16rpx;
|
||||
box-sizing: border-box;
|
||||
background: var(--theme-primary, #ff3c2a);
|
||||
overflow: hidden;
|
||||
}
|
||||
.shop-street-logo {
|
||||
flex-shrink: 0;
|
||||
width: 88rpx;
|
||||
height: 32rpx;
|
||||
padding: 2rpx 6rpx;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
.shop-street-name {
|
||||
max-width: calc(100% - 220rpx);
|
||||
margin-left: 12rpx;
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.shop-street-banner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 160rpx;
|
||||
height: 68rpx;
|
||||
}
|
||||
.shop-street-goods {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
padding: 12rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.street-goods-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
.street-goods-img {
|
||||
width: 100%;
|
||||
height: 116rpx;
|
||||
display: block;
|
||||
background: #eee;
|
||||
}
|
||||
.street-goods-name {
|
||||
padding: 8rpx 8rpx 0;
|
||||
color: #282828;
|
||||
font-size: 22rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.street-goods-price {
|
||||
padding: 4rpx 8rpx 10rpx;
|
||||
color: var(--theme-primary, #ff3c2a);
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// 老数据两列网格(未配置样式时)
|
||||
.shop-grid-legacy {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
.shop-item {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.shop-logo {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.shop-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-left: 16rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +1,30 @@
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="view-list">
|
||||
<div class="layout" :style="[wrapperStyle, gapStyle]">
|
||||
<div class="view-list" :style="gapStyle">
|
||||
<div class="view-item" @click="modelNavigateTo(res.list[0])">
|
||||
<div class="-item-tilte">{{res.list[0].title}}</div>
|
||||
<div class="-item-image">
|
||||
<div class="-item-image" :style="imgStyle">
|
||||
<u-image width="120rpx" mode="scaleToFill" height="120rpx" :src="res.list[0].img" alt ></u-image>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-item" @click="modelNavigateTo(res.list[1])">
|
||||
<div class="-item-tilte">{{res.list[1].title}}</div>
|
||||
<div class="-item-image">
|
||||
<div class="-item-image" :style="imgStyle">
|
||||
<u-image width="120rpx" mode="scaleToFill" height="120rpx" :src="res.list[1].img" alt ></u-image>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-list">
|
||||
<div class="view-list" :style="gapStyle">
|
||||
<div class="view-item" @click="modelNavigateTo(res.list[2])">
|
||||
<div class="-item-tilte">{{res.list[2].title}}</div>
|
||||
<div class="-item-image">
|
||||
<div class="-item-image" :style="imgStyle">
|
||||
<u-image width="120rpx" mode="scaleToFill" height="120rpx" :src="res.list[2].img" alt ></u-image>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-item" @click="modelNavigateTo(res.list[3])">
|
||||
<div class="-item-tilte">{{res.list[3].title}}</div>
|
||||
<div class="-item-image">
|
||||
<div class="-item-image" :style="imgStyle">
|
||||
<u-image width="120rpx" mode="scaleToFill" height="120rpx" :src="res.list[3].img" alt ></u-image>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,9 +33,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
import { computed } from "vue";
|
||||
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
|
||||
|
||||
defineProps<{ res: any }>();
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const gapStyle = computed(() => cubeGapStyle(props.res));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -48,15 +53,15 @@ defineProps<{ res: any }>();
|
||||
padding: 0;
|
||||
}
|
||||
.-item-image {
|
||||
padding: 10px;
|
||||
padding: 20rpx;
|
||||
}
|
||||
.-item-tilte {
|
||||
background: $aider-light-color;
|
||||
height: 60rpx;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-size: 28rpx;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
line-height: 60rpx;
|
||||
}
|
||||
|
||||
.view-list {
|
||||
@@ -66,10 +71,11 @@ defineProps<{ res: any }>();
|
||||
background: #fff;
|
||||
border-top-left-radius: 0.8em;
|
||||
border-top-right-radius: 0.8em;
|
||||
border: 1px solid #ededed;
|
||||
|
||||
border: 2rpx solid #ededed;
|
||||
|
||||
> .view-item {
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
> .view-item:nth-of-type(1) {
|
||||
> .-item-tilte {
|
||||
@@ -82,4 +88,4 @@ defineProps<{ res: any }>();
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,50 +1,149 @@
|
||||
<template>
|
||||
<div class="layout" :style="{textAlign: res.list[0].textAlign}" @click="modelNavigateTo(res.list[0])" >
|
||||
<div class="background" :style="{ backgroundColor: res.list[0].bk_color}">
|
||||
<div class="title" :style="{ color: res.list[0].color }">
|
||||
{{ res.list[0].title }}
|
||||
</div>
|
||||
<div style="position: absolute;right: 10px;top:2px;color: #fff;line-height: 42px;font-size: 10px">
|
||||
<a :style="{ color: res.list[0].color1 }" style="text-decoration: none">{{ res.list[0].title1 }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
|
||||
defineProps<{ res: any }>();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
.background {
|
||||
// background: url("/static/title.png") no-repeat;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 84rpx;
|
||||
background-position-x: center;
|
||||
background-position-y: center;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.layout {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
height: 84rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 84rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<view class="layout" :style="wrapperStyle">
|
||||
<!-- 新样式:左标题+副标题,右侧更多 -->
|
||||
<view v-if="styled" class="title-bar" :style="barStyle">
|
||||
<view class="title-left">
|
||||
<text class="title-main" :style="mainStyle">{{ item.title }}</text>
|
||||
<text class="title-sub" :style="subStyle" v-if="item.title1">{{ item.title1 }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="item.showMore"
|
||||
class="title-more"
|
||||
:style="moreStyle"
|
||||
@click="goMore"
|
||||
>
|
||||
{{ item.moreText || "更多" }} >
|
||||
</view>
|
||||
</view>
|
||||
<!-- 老数据:保持原样 -->
|
||||
<view
|
||||
v-else
|
||||
class="background"
|
||||
:style="{ backgroundColor: item.bk_color, textAlign: item.textAlign }"
|
||||
@click="modelNavigateTo(item)"
|
||||
>
|
||||
<view class="title" :style="{ color: item.color }">{{ item.title }}</view>
|
||||
<view class="legacy-sub">
|
||||
<a :style="{ color: item.color1 }" style="text-decoration: none">{{ item.title1 }}</a>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const item = computed(() => props.res?.list?.[0] || {});
|
||||
|
||||
// 面板保存过样式字段才启用新样式,老数据保持原样
|
||||
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (!styled.value) return {};
|
||||
return {
|
||||
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
|
||||
marginBottom: `${Number(res.pageGap) || 0}rpx`,
|
||||
borderRadius: `${Number(res.radiusTop) || 0}rpx ${Number(res.radiusTop) || 0}rpx ${Number(res.radiusBottom) || 0}rpx ${Number(res.radiusBottom) || 0}rpx`,
|
||||
overflow:
|
||||
Number(res.radiusTop) > 0 || Number(res.radiusBottom) > 0 ? "hidden" : "visible",
|
||||
background: "#ffffff",
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
});
|
||||
|
||||
const barStyle = computed(() => {
|
||||
const it = item.value;
|
||||
if (it.bgType === "image" && it.bgImage) {
|
||||
return {
|
||||
backgroundImage: `url(${it.bgImage})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
};
|
||||
}
|
||||
return { backgroundColor: it.bk_color || "#FFFFFF" };
|
||||
});
|
||||
|
||||
const mainStyle = computed(() => ({
|
||||
color: item.value.color || "#282828",
|
||||
fontSize: `${(Number(item.value.fontSize) || 20) * 2}rpx`,
|
||||
fontWeight: item.value.textStyle === "bold" ? "bold" : "normal",
|
||||
fontStyle: item.value.textStyle === "italic" ? "italic" : "normal",
|
||||
}));
|
||||
const subStyle = computed(() => ({
|
||||
color: item.value.color1 || "#282828",
|
||||
fontSize: `${(Number(item.value.subFontSize) || 14) * 2}rpx`,
|
||||
}));
|
||||
const moreStyle = computed(() => ({
|
||||
color: item.value.moreColor || "#282828",
|
||||
fontSize: `${(Number(item.value.moreFontSize) || 12) * 2}rpx`,
|
||||
}));
|
||||
|
||||
function goMore() {
|
||||
if (item.value.moreUrl) {
|
||||
modelNavigateTo({ url: item.value.moreUrl });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
.layout {
|
||||
background: #ffffff;
|
||||
}
|
||||
.title-bar {
|
||||
width: 100%;
|
||||
min-height: 84rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.title-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
overflow: hidden;
|
||||
}
|
||||
.title-main {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.title-sub {
|
||||
margin-left: 12rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.title-more {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.background {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 84rpx;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.legacy-sub {
|
||||
position: absolute;
|
||||
right: 20rpx;
|
||||
top: 4rpx;
|
||||
color: #fff;
|
||||
line-height: 84rpx;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.title {
|
||||
line-height: 84rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="view-width-100" @click="modelNavigateTo(res.list[0])">
|
||||
<div class="layout" :style="[wrapperStyle, gapStyle]">
|
||||
<div class="view-width-100" :style="imgStyle" @click="modelNavigateTo(res.list[0])">
|
||||
<u-image class="image-mode" width="100%" height="200rpx" :src="res.list[0].img">
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</div>
|
||||
<div class="view-width-100" @click="modelNavigateTo(res.list[1])">
|
||||
<div class="view-height-85">
|
||||
<div class="view-width-100" :style="gapStyle" @click="modelNavigateTo(res.list[1])">
|
||||
<div class="view-height-85" :style="imgStyle">
|
||||
<u-image class="image-mode" width="100%" height="170rpx" :src="res.list[1].img">
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</div>
|
||||
<div class="view-height-85" @click="modelNavigateTo(res.list[2])">
|
||||
<div class="view-height-85" :style="imgStyle" @click="modelNavigateTo(res.list[2])">
|
||||
<u-image class="image-mode" width="100%" height="170rpx" :src="res.list[2].img">
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
@@ -22,9 +22,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
import { computed } from "vue";
|
||||
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
|
||||
|
||||
defineProps<{ res: any }>();
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const gapStyle = computed(() => cubeGapStyle(props.res));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -41,16 +46,17 @@ defineProps<{ res: any }>();
|
||||
// height: 85px !important;
|
||||
// }
|
||||
.view-width-100 {
|
||||
padding: 1px 0;
|
||||
padding: 2rpx 0;
|
||||
display: flex;
|
||||
height: 200rpx;
|
||||
width: 100%;
|
||||
}
|
||||
.view-height-85 {
|
||||
padding: 0 1px;
|
||||
width: 50%;
|
||||
padding: 0 2rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.image-mode{
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="view-width-100">
|
||||
<div class="view-height-85" @click="modelNavigateTo(res.list[0])">
|
||||
<div class="layout" :style="[wrapperStyle, gapStyle]">
|
||||
<div class="view-width-100" :style="gapStyle">
|
||||
<div class="view-height-85" :style="imgStyle" @click="modelNavigateTo(res.list[0])">
|
||||
<u-image
|
||||
class="image-mode"
|
||||
height="170rpx"
|
||||
@@ -12,7 +12,7 @@
|
||||
<template #loading><u-loading-icon></u-loading-icon></template>
|
||||
</u-image>
|
||||
</div>
|
||||
<div class="view-height-85" @click="modelNavigateTo(res.list[1])">
|
||||
<div class="view-height-85" :style="imgStyle" @click="modelNavigateTo(res.list[1])">
|
||||
<u-image
|
||||
class="image-mode"
|
||||
height="170rpx"
|
||||
@@ -24,7 +24,7 @@
|
||||
</u-image>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-width-100" @click="modelNavigateTo(res.list[2])">
|
||||
<div class="view-width-100" :style="imgStyle" @click="modelNavigateTo(res.list[2])">
|
||||
<u-image
|
||||
class="image-mode"
|
||||
height="200rpx"
|
||||
@@ -38,9 +38,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { modelNavigateTo } from "./tpl";
|
||||
import { computed } from "vue";
|
||||
import { modelNavigateTo, cubeWrapperStyle, cubeImgStyle, cubeGapStyle } from "./tpl";
|
||||
|
||||
defineProps<{ res: any }>();
|
||||
const props = defineProps<{ res: any }>();
|
||||
|
||||
const wrapperStyle = computed(() => cubeWrapperStyle(props.res));
|
||||
const imgStyle = computed(() => cubeImgStyle(props.res));
|
||||
const gapStyle = computed(() => cubeGapStyle(props.res));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./tpl.scss";
|
||||
@@ -57,14 +62,15 @@ defineProps<{ res: any }>();
|
||||
// height: 85px !important;
|
||||
// }
|
||||
.view-width-100 {
|
||||
padding: 1px 0;
|
||||
padding: 2rpx 0;
|
||||
display: flex;
|
||||
height: 200rpx;
|
||||
width: 100%;
|
||||
}
|
||||
.view-height-85 {
|
||||
padding: 0 1px;
|
||||
width: 50%;
|
||||
padding: 0 2rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.image-mode{
|
||||
width: 100%;
|
||||
|
||||
@@ -91,7 +91,26 @@ const item = computed(() => {
|
||||
return data;
|
||||
});
|
||||
|
||||
// 面板保存过样式字段(options 级)才启用新样式,老数据走 item.style 体系
|
||||
const styled = computed(() => props.res && props.res.paddingTop !== undefined);
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
const res = props.res || {};
|
||||
if (styled.value) {
|
||||
const bg =
|
||||
!res.bgColor2 || res.bgColor1 === res.bgColor2
|
||||
? res.bgColor1
|
||||
: `linear-gradient(180deg, ${res.bgColor1}, ${res.bgColor2})`;
|
||||
const radius = Number(res.borderRadius) || 0;
|
||||
return {
|
||||
padding: `${Number(res.paddingTop) || 0}rpx ${Number(res.paddingX) || 0}rpx ${Number(res.paddingBottom) || 0}rpx`,
|
||||
background: bg,
|
||||
borderRadius: `${radius}rpx`,
|
||||
marginBottom: `${Number(res.pageGap) || 0}rpx`,
|
||||
overflow: radius > 0 ? "hidden" : "visible",
|
||||
boxSizing: "border-box",
|
||||
};
|
||||
}
|
||||
const style = item.value.style;
|
||||
return {
|
||||
margin: `${style.margin || 0}px`,
|
||||
@@ -102,6 +121,14 @@ const wrapperStyle = computed(() => {
|
||||
});
|
||||
|
||||
const boxStyle = computed(() => {
|
||||
if (styled.value) {
|
||||
return {
|
||||
borderRadius: `${Number(props.res.imgRadius) || 0}rpx`,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
};
|
||||
}
|
||||
const style = item.value.style;
|
||||
const box: Record<string, string> = {
|
||||
borderRadius: `${style.bgRadius || 0}px`,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div class="wrapper" :style="pageBgStyle">
|
||||
<!-- uni 中不能使用 vue component 所以用if判断每个组件 -->
|
||||
<div v-for="(item, index) in pageData.list" :key="index">
|
||||
<!-- 搜索栏,如果在楼层装修顶部则会自动浮动,否则不浮动 -->
|
||||
<div class="navbar" v-if="item.type == 'search'">
|
||||
<div class="navbar" v-if="item.type == 'search'" :style="{ background: topColor }">
|
||||
<search style="width: 100%" :res="item.options" />
|
||||
<!-- #ifndef H5 -->
|
||||
<!-- 扫码功能 不兼容h5 详情文档: https://uniapp.dcloud.io/api/system/barcode?id=scancode -->
|
||||
@@ -12,6 +12,7 @@
|
||||
</div>
|
||||
<!-- #endif -->
|
||||
</div>
|
||||
<homeComb v-if="item.type == 'homeComb'" :res="item.options" />
|
||||
<carousel v-if="item.type == 'carousel'" :res="item.options" />
|
||||
<titleLayout v-if="item.type == 'title'" :res="item.options" />
|
||||
<leftOneRightTwo
|
||||
@@ -41,16 +42,22 @@
|
||||
<group v-if="item.type == 'group'" :res="item.options" />
|
||||
<notice v-if="item.type == 'notice'" :res="item.options" />
|
||||
<promotions v-if="item.type == 'promotionDetail'" :res="item.options" />
|
||||
<!-- <integral v-if="item.type == 'integral'" :res="item.options" /> -->
|
||||
<integral v-if="item.type == 'integral'" :res="item.options" />
|
||||
<!-- <spike v-if="item.type == 'spike'" :res="item.options" /> -->
|
||||
<videoLayout v-if="item.type == 'video'" :res="item.options" />
|
||||
<blank v-if="item.type == 'blank'" :res="item.options" />
|
||||
<lineLayout v-if="item.type == 'line'" :res="item.options" />
|
||||
<richText v-if="item.type == 'richText'" :res="item.options" />
|
||||
<coupon v-if="item.type == 'coupon'" :res="item.options" />
|
||||
<articleList v-if="item.type == 'article'" :res="item.options" />
|
||||
<shopList v-if="item.type == 'shop'" :res="item.options" />
|
||||
</div>
|
||||
<u-no-network></u-no-network>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import carousel from '@/pages/tabbar/home/template/tpl_banner'
|
||||
import titleLayout from '@/pages/tabbar/home/template/tpl_title'
|
||||
@@ -71,6 +78,14 @@ import videoLayout from '@/pages/tabbar/home/template/tpl_video'
|
||||
import goods from '@/pages/tabbar/home/template/tpl_goods'
|
||||
import notice from '@/pages/tabbar/home/template/tpl_notice'
|
||||
import promotions from '@/pages/tabbar/home/template/tpl_promotions_detail'
|
||||
import blank from '@/pages/tabbar/home/template/tpl_blank'
|
||||
import lineLayout from '@/pages/tabbar/home/template/tpl_line'
|
||||
import richText from '@/pages/tabbar/home/template/tpl_rich_text'
|
||||
import integral from '@/pages/tabbar/home/template/tpl_integral'
|
||||
import coupon from '@/pages/tabbar/home/template/tpl_coupon'
|
||||
import articleList from '@/pages/tabbar/home/template/tpl_article'
|
||||
import shopList from '@/pages/tabbar/home/template/tpl_shop'
|
||||
import homeComb from '@/pages/tabbar/home/template/tpl_home_comb.vue'
|
||||
import { toSpecial, getSpecial } from '@/api/home'
|
||||
import permision from '@/js_sdk/wa-permission/permission.js'
|
||||
import config from '@/config/config'
|
||||
@@ -80,6 +95,42 @@ const pageData = ref<any>('')
|
||||
const bodyParam = ref('')
|
||||
const isIos = ref('')
|
||||
|
||||
const topColor = computed(() => pageData.value?.topColor || '#ffffff')
|
||||
const pageBgStyle = computed(() => {
|
||||
const data: any = pageData.value
|
||||
if (data?.bgType === 'image' && data?.bgImage) {
|
||||
const size =
|
||||
data.bgFill === 'contain'
|
||||
? 'contain'
|
||||
: data.bgFill === 'cover'
|
||||
? 'cover'
|
||||
: '100% 100%'
|
||||
return {
|
||||
backgroundImage: `url(${data.bgImage})`,
|
||||
backgroundSize: size,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundColor: data.bgColor || '#f9f9f9',
|
||||
}
|
||||
}
|
||||
if (data?.bgColor) {
|
||||
return { backgroundColor: data.bgColor }
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
function applyPageSettings(parsed: any) {
|
||||
if (!parsed) return
|
||||
if (parsed.title) {
|
||||
uni.setNavigationBarTitle({ title: parsed.title })
|
||||
}
|
||||
if (parsed.topColor || parsed.titleColor) {
|
||||
uni.setNavigationBarColor({
|
||||
frontColor: parsed.titleColor === 'white' ? '#ffffff' : '#000000',
|
||||
backgroundColor: parsed.topColor || '#ffffff',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
// #ifdef MP-WEIXIN
|
||||
@@ -98,13 +149,23 @@ function init() {
|
||||
if (bodyParam.value) {
|
||||
toSpecial({ body: bodyParam.value }).then((res) => {
|
||||
if (res.data.success) {
|
||||
pageData.value = JSON.parse(res.data.result.pageData)
|
||||
const parsed = JSON.parse(res.data.result.pageData)
|
||||
if (parsed && Array.isArray(parsed.list)) {
|
||||
parsed.list = parsed.list.filter((i: any) => !i?.hidden)
|
||||
}
|
||||
pageData.value = parsed
|
||||
applyPageSettings(parsed)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
getSpecial(id.value).then((res) => {
|
||||
if (res.data.success) {
|
||||
pageData.value = JSON.parse(res.data.result.pageData)
|
||||
const parsed = JSON.parse(res.data.result.pageData)
|
||||
if (parsed && Array.isArray(parsed.list)) {
|
||||
parsed.list = parsed.list.filter((i: any) => !i?.hidden)
|
||||
}
|
||||
pageData.value = parsed
|
||||
applyPageSettings(parsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -190,4 +251,4 @@ async function scan() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -181,7 +181,7 @@ body {
|
||||
background-size: cover;
|
||||
border-bottom-left-radius: 30rpx;
|
||||
border-bottom-right-radius: 30rpx;
|
||||
background-image: url("/static/img/main-bg.png");
|
||||
background-image: url("https://lilishop-uniapp.oss-cn-beijing.aliyuncs.com/main-bg.png");
|
||||
background-position: bottom;
|
||||
background-repeat: no-repeat;
|
||||
color: #ffffff;
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const root = path.join(__dirname, '..')
|
||||
|
||||
function walk(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (['node_modules', 'uview-ui', '.git', 'scripts'].includes(entry.name)) continue
|
||||
const full = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) walk(full, files)
|
||||
else if (entry.name.endsWith('.vue') || entry.name.endsWith('.js')) files.push(full)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
function process(content) {
|
||||
let result = content
|
||||
result = result.replace(/\$options\.filters\.([a-zA-Z][a-zA-Z0-9_]*)/g, '$1')
|
||||
result = result.replace(/this\.([a-zA-Z][a-zA-Z0-9_]*)\(\)/g, (m, name) => {
|
||||
const filterNames = ['isLogin', 'tipsToLogin', 'forceLogin', 'quiteLoginOut', 'logoff', 'navigateToLogin', 'talkIm', 'callPhone', 'unitPrice', 'goodsFormatPrice']
|
||||
if (filterNames.includes(name) && result.includes(`this.${name}(`)) return m
|
||||
return m
|
||||
})
|
||||
result = result.replace(/([a-zA-Z0-9_.\[\]'"\s]+)\s\|\s(refundWayFilter|accountTypeFilter|statusFilter|paymentTypeFilter)/g, (_, expr, name) => {
|
||||
return `${name}(${expr.trim()})`
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
walk(root).forEach((file) => {
|
||||
const original = fs.readFileSync(file, 'utf8')
|
||||
const updated = process(original)
|
||||
if (updated !== original) {
|
||||
fs.writeFileSync(file, updated)
|
||||
console.log('updated', path.relative(root, file))
|
||||
}
|
||||
})
|
||||
|
||||
console.log('done')
|
||||
@@ -1,51 +0,0 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const root = path.join(__dirname, '..')
|
||||
|
||||
function walk(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (['node_modules', '.git', 'scripts'].includes(entry.name)) continue
|
||||
const full = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) walk(full, files)
|
||||
else if (entry.name.endsWith('.vue')) files.push(full)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
function fixNavbarLine(line) {
|
||||
if (!line.includes('u-navbar')) return line
|
||||
let result = line
|
||||
result = result.replace(/:border-bottom="false"/g, ':border="false"')
|
||||
result = result.replace(/:is-back="/g, ':auto-back="')
|
||||
result = result.replace(/:custom-back="back"/g, ':auto-back="false" @leftClick="back"')
|
||||
result = result.replace(/back-icon-color="/g, 'left-icon-color="')
|
||||
result = result.replace(/:background="navbarOnlyBack"/g, ':bg-color="navbarOnlyBack.background"')
|
||||
result = result.replace(/:background="navbar"/g, ':bg-color="navbar.background"')
|
||||
result = result.replace(/:background="navObj"/g, ':bg-color="navObj.background"')
|
||||
result = result.replace(/:background="background"/g, ':bg-color="background.backgroundColor"')
|
||||
return result
|
||||
}
|
||||
|
||||
function fixCellLine(line) {
|
||||
if (!line.includes('u-cell') || !line.includes(':border-bottom')) return line
|
||||
return line.replace(/:border-bottom="false"/g, ':border="false"')
|
||||
}
|
||||
|
||||
function fixInputLine(line) {
|
||||
if (!line.includes('u-input') || !line.includes(':border-bottom')) return line
|
||||
return line.replace(/:border-bottom="false"/g, 'border="none"')
|
||||
}
|
||||
|
||||
function processFile(file) {
|
||||
const lines = fs.readFileSync(file, 'utf8').split('\n')
|
||||
const updated = lines.map((line) => fixInputLine(fixCellLine(fixNavbarLine(line)))).join('\n')
|
||||
const original = lines.join('\n')
|
||||
if (updated !== original) {
|
||||
fs.writeFileSync(file, updated)
|
||||
console.log('updated', path.relative(root, file))
|
||||
}
|
||||
}
|
||||
|
||||
walk(root).forEach(processFile)
|
||||
console.log('navbar props migration done')
|
||||
@@ -1,37 +0,0 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const root = path.join(__dirname, '..')
|
||||
|
||||
function walk(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (['node_modules', '.git', 'scripts'].includes(entry.name)) continue
|
||||
const full = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) walk(full, files)
|
||||
else if (entry.name.endsWith('.vue')) files.push(full)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
function process(content) {
|
||||
return content
|
||||
.replace(/u-verification-code/g, 'u-code')
|
||||
.replace(/<u-cell-item/g, '<u-cell')
|
||||
.replace(/<\/u-cell-item>/g, '</u-cell>')
|
||||
.replace(/:arrow="false"/g, ':isLink="false"')
|
||||
.replace(/<u-loading(\s|>|\/)/g, '<u-loading-icon$1')
|
||||
.replace(/<\/u-loading>/g, '</u-loading-icon>')
|
||||
.replace(/<u-alert-tips/g, '<u-alert')
|
||||
.replace(/<\/u-alert-tips>/g, '</u-alert>')
|
||||
}
|
||||
|
||||
walk(root).forEach((file) => {
|
||||
const original = fs.readFileSync(file, 'utf8')
|
||||
const updated = process(original)
|
||||
if (updated !== original) {
|
||||
fs.writeFileSync(file, updated)
|
||||
console.log('updated', path.relative(root, file))
|
||||
}
|
||||
})
|
||||
|
||||
console.log('uview component aliases updated')
|
||||
@@ -1,145 +0,0 @@
|
||||
/**
|
||||
* Vue 3 migration helpers for template and script patterns.
|
||||
*/
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const root = path.join(__dirname, '..')
|
||||
const filterNames = [
|
||||
'unitPrice',
|
||||
'goodsFormatPrice',
|
||||
'secrecyMobile',
|
||||
'unixToDate',
|
||||
'beautifyTime',
|
||||
'formatTime',
|
||||
'orderStatusList',
|
||||
'serviceStatusList',
|
||||
'noPassByName',
|
||||
'setClipboard',
|
||||
'clearStrComma'
|
||||
]
|
||||
|
||||
function walk(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === 'node_modules' || entry.name === 'uview-ui' || entry.name === '.git') continue
|
||||
const full = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) walk(full, files)
|
||||
else if (entry.name.endsWith('.vue')) files.push(full)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
function convertFilterPipes(content) {
|
||||
let result = content
|
||||
for (const name of filterNames) {
|
||||
const re = new RegExp(`([\\w\\[\\].'"()\\s]+?)\\s\\|\\s${name}(\\([^)]*\\))?`, 'g')
|
||||
result = result.replace(re, (_, expr, args = '') => {
|
||||
const trimmed = expr.trim()
|
||||
return `${name}(${trimmed}${args ? args.slice(1, -1) ? ', ' + args.slice(1, -1) : '' : ''})`
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function convertComponentFilters(content) {
|
||||
return content.replace(/\|\s*([a-zA-Z][a-zA-Z0-9_]*)(\([^)]*\))?/g, (match, name, args = '') => {
|
||||
if (filterNames.includes(name)) return match
|
||||
return match
|
||||
})
|
||||
}
|
||||
|
||||
function convertPopupVModel(content) {
|
||||
const tags = ['u-popup', 'u-modal', 'u-action-sheet', 'u-picker', 'u-select', 'u-keyboard', 'u-calendar']
|
||||
let result = content
|
||||
for (const tag of tags) {
|
||||
result = result.replace(new RegExp(`(<${tag}[^>]*?)\\bv-model="([^"]+)"`, 'g'), '$1v-model:show="$2"')
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function convertNavbarProps(content) {
|
||||
return content
|
||||
.replace(/:is-back="false"/g, ':auto-back="false"')
|
||||
.replace(/:isBack="false"/g, ':auto-back="false"')
|
||||
.replace(/:is-back="true"/g, ':auto-back="true"')
|
||||
.replace(/:isBack="true"/g, ':auto-back="true"')
|
||||
}
|
||||
|
||||
function convertSlotSyntax(content) {
|
||||
let result = content
|
||||
result = result.replace(/\sslot="right-icon"/g, ' #right-icon')
|
||||
result = result.replace(/\sslot="loading"/g, ' #loading')
|
||||
result = result.replace(/\sslot="([^"]+)"/g, ' #$1')
|
||||
result = result.replace(/slot-scope="([^"]+)"/g, '#default="$1"')
|
||||
return result
|
||||
}
|
||||
|
||||
function convertSetDelete(content) {
|
||||
return content
|
||||
.replace(/this\.\$set\(([^,]+),\s*([^,]+),\s*([^)]+)\)/g, '$1[$2] = $3')
|
||||
.replace(/this\.\$delete\(([^,]+),\s*([^)]+)\)/g, 'delete $1[$2]')
|
||||
}
|
||||
|
||||
function moveLocalFiltersToMethods(content) {
|
||||
const match = content.match(/filters:\s*\{([\s\S]*?)\n\s*\},/)
|
||||
if (!match) return content
|
||||
const filtersBlock = match[1]
|
||||
const methodEntries = []
|
||||
const re = /([a-zA-Z][a-zA-Z0-9_]*)\s*\(([^)]*)\)\s*\{([\s\S]*?)\n\s*\},/g
|
||||
let m
|
||||
while ((m = re.exec(filtersBlock + ',')) !== null) {
|
||||
methodEntries.push(` ${m[1]}(${m[2]}) {${m[3]}\n },`)
|
||||
}
|
||||
if (!methodEntries.length) return content
|
||||
|
||||
let updated = content.replace(/filters:\s*\{[\s\S]*?\n\s*\},\n?/, '')
|
||||
updated = convertComponentFiltersInTemplate(updated, methodEntries.map(e => e.trim().split('(')[0].trim()))
|
||||
|
||||
if (updated.includes('methods: {')) {
|
||||
updated = updated.replace('methods: {', `methods: {\n${methodEntries.join('\n')}`)
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
function convertComponentFiltersInTemplate(content, localNames) {
|
||||
let result = content
|
||||
for (const name of localNames) {
|
||||
const clean = name.replace(/^\s+/, '')
|
||||
const re = new RegExp(`([\\w\\[\\].'"()\\s]+?)\\s\\|\\s${clean}(\\([^)]*\\))?`, 'g')
|
||||
result = result.replace(re, (_, expr, args = '') => {
|
||||
const trimmed = expr.trim()
|
||||
const argText = args ? args.slice(1, -1) : ''
|
||||
return `${clean}(${trimmed}${argText ? ', ' + argText : ''})`
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function fixShareComponent(content) {
|
||||
return content
|
||||
.replace(/import mpShare from "uview-ui\/libs\/mixin\/mpShare.js";/, 'import mpShare from "@/utils/mpShare.js";')
|
||||
.replace(/this\.\$options\.filters\.setClipboard\(([^)]+)\)/g, 'setClipboard($1)')
|
||||
}
|
||||
|
||||
function processFile(file) {
|
||||
let content = fs.readFileSync(file, 'utf8')
|
||||
const original = content
|
||||
content = convertFilterPipes(content)
|
||||
content = convertPopupVModel(content)
|
||||
content = convertNavbarProps(content)
|
||||
content = convertSlotSyntax(content)
|
||||
content = convertSetDelete(content)
|
||||
content = moveLocalFiltersToMethods(content)
|
||||
content = fixShareComponent(content)
|
||||
content = content.replace(/import uFormItem from "@\/uview-ui\/components\/u-form-item\/u-form-item.vue";?\n?/g, '')
|
||||
content = content.replace(/import uImage from "@\/uview-ui\/components\/u-image\/u-image.vue";?\n?/g, '')
|
||||
content = content.replace(/components:\s*\{\s*uFormItem,?\s*\},?\n?/g, '')
|
||||
content = content.replace(/components:\s*\{\s*uImage,?\s*\},?\n?/g, '')
|
||||
if (content !== original) {
|
||||
fs.writeFileSync(file, content)
|
||||
console.log('updated', path.relative(root, file))
|
||||
}
|
||||
}
|
||||
|
||||
walk(root).forEach(processFile)
|
||||
console.log('migration complete')
|
||||
BIN
static/bg1.png
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 290 B |
|
Before Width: | Height: | Size: 327 B |
|
Before Width: | Height: | Size: 335 B |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
BIN
static/logo.png
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
BIN
static/title.png
|
Before Width: | Height: | Size: 2.2 KiB |
239
uni.scss
@@ -1,180 +1,59 @@
|
||||
/* 页面左右间距 */
|
||||
$page-color-base: #f8f8f8;
|
||||
@import "uview-plus/theme.scss";
|
||||
|
||||
/* 文字尺寸 */
|
||||
$font-sm: 24rpx;
|
||||
$font-base: 28rpx;
|
||||
$font-lg: 32rpx;
|
||||
$font-sl: 36rpx;
|
||||
/*文字颜色*/
|
||||
$font-color-dark: #303133;
|
||||
$font-color-white: #ffffff;
|
||||
$font-color-base: #606266;
|
||||
$font-color-light: #909399;
|
||||
$font-color-disabled: #c0c4cc;
|
||||
/* 边框颜色 */
|
||||
$border-color-dark: #dcdfe6;
|
||||
$border-color-base: #e4e7ed;
|
||||
$border-color-light: #ebeef5;
|
||||
/* 图片加载中颜色 */
|
||||
$image-bg-color: #eee;
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #fa436a;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/*主题颜色 具体参考 https://ant.design/docs/spec/colors-cn */
|
||||
/**微信登录按钮颜色 */
|
||||
$weChat-color:#00a327;
|
||||
/**主颜色*/
|
||||
$main-color: var(--theme-primary, #ff3c2a);
|
||||
/*用于金钱等颜色 */
|
||||
$price-color: var(--theme-primary, #ff3c2a);
|
||||
/*主题高亮颜色*/
|
||||
$light-color: var(--theme-light, #ff6b35);
|
||||
/*辅助高亮颜色*/
|
||||
$aider-light-color: var(--theme-aider, #ff9f28);
|
||||
|
||||
/*主题高亮背景颜色*/
|
||||
$main-light-color: #edfcf7;
|
||||
/*价格红高亮背景颜色*/
|
||||
$price-light-color: #fcedeb;
|
||||
|
||||
/*辅助色*/
|
||||
$aider-color-red: #ff6262;
|
||||
$aider-color-yellow: #ffc71c;
|
||||
$aider-color-green: #28d094;
|
||||
$aider-color-blue: #1e9ff2;
|
||||
// 中性色
|
||||
$neutral-color: #333333;
|
||||
//背景色
|
||||
$bg-color: #f9f9f9;
|
||||
|
||||
// 字体
|
||||
$font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
$font-weight-b: 500;
|
||||
$font-weight: 400;
|
||||
.light-color {
|
||||
color: $light-color;
|
||||
}
|
||||
.main-color {
|
||||
color: $main-color !important;
|
||||
}
|
||||
.bg-light-color {
|
||||
background-color: $light-color !important;
|
||||
}
|
||||
.bg-main-color {
|
||||
background-color: $main-color !important;
|
||||
}
|
||||
|
||||
// 渐变主题颜色
|
||||
.bg-linear-gradient {
|
||||
background-image: linear-gradient(25deg, var(--theme-primary, #fa123b), var(--theme-light, #ff6b35), var(--theme-aider, #ff9f28), #ffcc03);
|
||||
}
|
||||
.uni-tabbar .uni-tabbar__icon {
|
||||
width: 20px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: $light-color;
|
||||
color: #ffffff;
|
||||
border-width: 0px;
|
||||
border: none;
|
||||
border-radius: 44px;
|
||||
height: 44px;
|
||||
box-shadow: none;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 44px;
|
||||
text-align: center;
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
.button-hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.margin {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background-color: rgba(242, 242, 242, 1);
|
||||
color: #333333;
|
||||
height: 28px;
|
||||
border-radius: 28px;
|
||||
box-shadow: none;
|
||||
font-family: "PingFangSC-Regular", "PingFang SC", sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.c-content {
|
||||
background-color: #ffffff;
|
||||
// margin-top: 30rpx;
|
||||
border-top: 1px solid $border-color-light;
|
||||
border-bottom: 1px solid $border-color-light;
|
||||
}
|
||||
.mt-30 {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
.border-top {
|
||||
border-top: 1px solid $border-color-light;
|
||||
}
|
||||
.describe {
|
||||
color: $font-color-light;
|
||||
font-size: $font-sm;
|
||||
padding: 30rpx;
|
||||
}
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-a-c{
|
||||
align-items: center;
|
||||
}
|
||||
.flex-j-sb{
|
||||
justify-content: space-between;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wes {
|
||||
/* 多出部分用省略号表示 , 用于一行 */
|
||||
overflow: hidden;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wes-2 {
|
||||
/* 适用于webkit内核和移动端 */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wes-3 {
|
||||
/* 适用于webkit内核和移动端 */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* 页面左右间距 */
|
||||
$page-color-base: #f8f8f8;
|
||||
@import "uview-plus/theme.scss";
|
||||
|
||||
/* 文字尺寸 */
|
||||
$font-sm: 24rpx;
|
||||
$font-base: 28rpx;
|
||||
$font-lg: 32rpx;
|
||||
$font-sl: 36rpx;
|
||||
/*文字颜色*/
|
||||
$font-color-dark: #303133;
|
||||
$font-color-white: #ffffff;
|
||||
$font-color-base: #606266;
|
||||
$font-color-light: #909399;
|
||||
$font-color-disabled: #c0c4cc;
|
||||
/* 边框颜色 */
|
||||
$border-color-dark: #dcdfe6;
|
||||
$border-color-base: #e4e7ed;
|
||||
$border-color-light: #ebeef5;
|
||||
/* 图片加载中颜色 */
|
||||
$image-bg-color: #eee;
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #fa436a;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/*主题颜色 具体参考 https://ant.design/docs/spec/colors-cn */
|
||||
/**微信登录按钮颜色 */
|
||||
$weChat-color:#00a327;
|
||||
/**主颜色*/
|
||||
$main-color: var(--theme-primary, #ff3c2a);
|
||||
/*用于金钱等颜色 */
|
||||
$price-color: var(--theme-primary, #ff3c2a);
|
||||
/*主题高亮颜色*/
|
||||
$light-color: var(--theme-light, #ff6b35);
|
||||
/*辅助高亮颜色*/
|
||||
$aider-light-color: var(--theme-aider, #ff9f28);
|
||||
|
||||
/*主题高亮背景颜色*/
|
||||
$main-light-color: #edfcf7;
|
||||
/*价格红高亮背景颜色*/
|
||||
$price-light-color: #fcedeb;
|
||||
|
||||
/*辅助色*/
|
||||
$aider-color-red: #ff6262;
|
||||
$aider-color-yellow: #ffc71c;
|
||||
$aider-color-green: #28d094;
|
||||
$aider-color-blue: #1e9ff2;
|
||||
// 中性色
|
||||
$neutral-color: #333333;
|
||||
//背景色
|
||||
$bg-color: #f9f9f9;
|
||||
|
||||
// 字体
|
||||
$font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
$font-weight-b: 500;
|
||||
$font-weight: 400;
|
||||
|
||||