mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-07 17:38:55 +08:00
Compare commits
467 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f21ce6e252 | ||
|
|
da95f17885 | ||
|
|
0f9593d5ed | ||
|
|
088a2bb308 | ||
|
|
d29ad9c99b | ||
|
|
d87e0c5f6b | ||
|
|
5e74e356e0 | ||
|
|
cba624f480 | ||
|
|
ba964ff478 | ||
|
|
45338c9d70 | ||
|
|
6b6dc2c4f3 | ||
|
|
8fd353a766 | ||
|
|
3cb60139d5 | ||
|
|
7d9bf776db | ||
|
|
0c814ec956 | ||
|
|
322e357398 | ||
|
|
cc4a94da40 | ||
|
|
36648f065b | ||
|
|
9893494cf1 | ||
|
|
906cda0385 | ||
|
|
92dd179525 | ||
|
|
fefb748864 | ||
|
|
0b644a1722 | ||
|
|
2a2c3f8174 | ||
|
|
88fc1b0b2a | ||
|
|
b45910a35f | ||
|
|
758f09a291 | ||
|
|
fce063642d | ||
|
|
c832350dd3 | ||
|
|
5169d4ac43 | ||
|
|
d80ead4d94 | ||
|
|
62700162d5 | ||
|
|
a568131b7c | ||
|
|
27ca53068b | ||
|
|
1d946a9616 | ||
|
|
f31669cba6 | ||
|
|
005006adf7 | ||
|
|
2f097ace49 | ||
|
|
654839b247 | ||
|
|
8ed67847dd | ||
|
|
47974dddab | ||
|
|
76cc10c6af | ||
|
|
ae08a568bd | ||
|
|
2bb3a71460 | ||
|
|
ce03b13718 | ||
|
|
0ad79157d3 | ||
|
|
bdae07e585 | ||
|
|
7fa6c2d84c | ||
|
|
59bd1b8c78 | ||
|
|
18eb79054f | ||
|
|
b5047a7a7a | ||
|
|
45b83d0fe4 | ||
|
|
a804120fa2 | ||
|
|
ebbf42cb76 | ||
|
|
1e7e78a841 | ||
|
|
350e79895d | ||
|
|
2bee689198 | ||
|
|
de333c2d34 | ||
|
|
0c7ecbe039 | ||
|
|
7a1ae5d5da | ||
|
|
2bbec5b9e5 | ||
|
|
b4d67859da | ||
|
|
15c229fa62 | ||
|
|
3278726a25 | ||
|
|
5df1ab6993 | ||
|
|
870bacdd8c | ||
|
|
bbe336d902 | ||
|
|
1bd50cee45 | ||
|
|
b639bc36eb | ||
|
|
89711af0e6 | ||
|
|
3ec3f11d5b | ||
|
|
5fcc26a866 | ||
|
|
8484936e3d | ||
|
|
1b275cf592 | ||
|
|
4c232284f8 | ||
|
|
326ccea1b4 | ||
|
|
8410179885 | ||
|
|
4777eb2dd6 | ||
|
|
867b03ebfc | ||
|
|
a6063874c7 | ||
|
|
037fd0caa0 | ||
|
|
ea897b154b | ||
|
|
54c72239d4 | ||
|
|
1bad9a14db | ||
|
|
50ebc7d6df | ||
|
|
ff38cf52b4 | ||
|
|
2fdab71d14 | ||
|
|
b51148f520 | ||
|
|
87db9dbb12 | ||
|
|
fb979ea514 | ||
|
|
8d18f05085 | ||
|
|
dfb790e713 | ||
|
|
62d55e2b92 | ||
|
|
f4a50cd854 | ||
|
|
47a2c40ffd | ||
|
|
fddb82e680 | ||
|
|
2d80a795a7 | ||
|
|
ffdae4a332 | ||
|
|
3ca9aad9be | ||
|
|
085674dfa7 | ||
|
|
9540a1b98b | ||
|
|
b9d48fab05 | ||
|
|
6552767823 | ||
|
|
f42f5903d3 | ||
|
|
0c94b67ad9 | ||
|
|
3b665f6d8c | ||
|
|
420e8a84e5 | ||
|
|
05b14ada18 | ||
|
|
aed9d86ada | ||
|
|
6933fb79ec | ||
|
|
4be36d61c7 | ||
|
|
d4cd6e0936 | ||
|
|
c0b00999c5 | ||
|
|
1accdf5826 | ||
|
|
1ae7ab3036 | ||
|
|
36878e6f7f | ||
|
|
09b735e82a | ||
|
|
24f829033f | ||
|
|
a72ff640cc | ||
|
|
68a791a439 | ||
|
|
c0ca9672a1 | ||
|
|
6ee49ce9ac | ||
|
|
c83d715234 | ||
|
|
4bcaf14bad | ||
|
|
e91d783e22 | ||
|
|
4bb3d33f89 | ||
|
|
12291ad4d5 | ||
|
|
829d3e65e7 | ||
|
|
3be9aa5c14 | ||
|
|
6c9a3820e8 | ||
|
|
d172a1c9b8 | ||
|
|
59c98d1289 | ||
|
|
b7235f5644 | ||
|
|
9dc4829f1f | ||
|
|
843134c82a | ||
|
|
7e87214b6d | ||
|
|
192a9ca53f | ||
|
|
1df821aa0c | ||
|
|
3e3644f4a4 | ||
|
|
b1f62d599b | ||
|
|
4e0ee8e899 | ||
|
|
7804d4eba6 | ||
|
|
1fa3a91979 | ||
|
|
29d5ba98ff | ||
|
|
c25c16adf3 | ||
|
|
de9bf1902a | ||
|
|
f2ec4530ed | ||
|
|
7bbcab7679 | ||
|
|
f646022926 | ||
|
|
f6037dc18b | ||
|
|
29acc7f0ee | ||
|
|
5ef5adf9be | ||
|
|
9647e7614b | ||
|
|
fb7c0c9ca2 | ||
|
|
0002a39fc4 | ||
|
|
e67ea56651 | ||
|
|
4cd96980ba | ||
|
|
d648855533 | ||
|
|
9bed373ec1 | ||
|
|
c355194aba | ||
|
|
15fa1a5e52 | ||
|
|
ecf11ae2d2 | ||
|
|
fb2a9e7a9f | ||
|
|
106e059d1a | ||
|
|
f6ddcf4f99 | ||
|
|
a4659ed1a2 | ||
|
|
c90559981e | ||
|
|
b299507df9 | ||
|
|
cf92d9d939 | ||
|
|
6b61b86585 | ||
|
|
ae896ff42c | ||
|
|
709d114986 | ||
|
|
cc86430ea4 | ||
|
|
295aaf1a01 | ||
|
|
548335b658 | ||
|
|
df35a26f9e | ||
|
|
c277cf1121 | ||
|
|
297bdd95dd | ||
|
|
e13ca6281d | ||
|
|
235362d5b0 | ||
|
|
fc5fb3832d | ||
|
|
bd9a5f75b0 | ||
|
|
99de0d4722 | ||
|
|
726d563e1a | ||
|
|
7ce1ea8fa4 | ||
|
|
2e5be29a43 | ||
|
|
ac77acd888 | ||
|
|
f4b17254ec | ||
|
|
28a857249a | ||
|
|
22f7b1a343 | ||
|
|
ec948b5496 | ||
|
|
1b2e980fb6 | ||
|
|
8a60e23214 | ||
|
|
ff55a0fade | ||
|
|
c1447b4376 | ||
|
|
734825ba9b | ||
|
|
0da6ef99af | ||
|
|
832fda813b | ||
|
|
615ee91511 | ||
|
|
e7350899bf | ||
|
|
ad0522335e | ||
|
|
38da0d9f3a | ||
|
|
020b413e20 | ||
|
|
1750f04d0b | ||
|
|
14d53fa48d | ||
|
|
aa4bc81da4 | ||
|
|
fe4e0ce75c | ||
|
|
052c6e153d | ||
|
|
60479b44b1 | ||
|
|
8e0d9148b1 | ||
|
|
fc471a17a0 | ||
|
|
b6a2dbc23a | ||
|
|
b854f40cc7 | ||
|
|
a2cd9aebe8 | ||
|
|
2e8d257140 | ||
|
|
379fed15b3 | ||
|
|
afa3290f11 | ||
|
|
5593ebe574 | ||
|
|
b18999900d | ||
|
|
03ccdf2d43 | ||
|
|
40dc2b5d2f | ||
|
|
50960ed899 | ||
|
|
ede1de664a | ||
|
|
6884a89fe5 | ||
|
|
676410a108 | ||
|
|
b37e12b7a7 | ||
|
|
8a0f5ff120 | ||
|
|
c5fecda212 | ||
|
|
e5d98d022e | ||
|
|
799184fd21 | ||
|
|
756cb28daa | ||
|
|
5f6eb6a26d | ||
|
|
4efbec5741 | ||
|
|
86dbc0d86e | ||
|
|
c32d011851 | ||
|
|
311ca35afc | ||
|
|
1a3fae6501 | ||
|
|
3432530de7 | ||
|
|
af85493863 | ||
|
|
ae85f0d612 | ||
|
|
16c4a78e29 | ||
|
|
8fb87aa64d | ||
|
|
c5d675b6d2 | ||
|
|
f4d06b2d7b | ||
|
|
bb610a7cd8 | ||
|
|
bb864e72b3 | ||
|
|
c6c4797d87 | ||
|
|
25fed42395 | ||
|
|
66c4676493 | ||
|
|
f8747b5d5d | ||
|
|
0eb58d4b8a | ||
|
|
7b30ea343f | ||
|
|
7db8484a7c | ||
|
|
78b058009b | ||
|
|
05abad3905 | ||
|
|
ad178b1806 | ||
|
|
075c20ae41 | ||
|
|
56420bd4d0 | ||
|
|
a18fc729c0 | ||
|
|
099941a54e | ||
|
|
3867d51d92 | ||
|
|
f1be18b7e3 | ||
|
|
ef46380f9e | ||
|
|
d711273cf0 | ||
|
|
5e7cf2d679 | ||
|
|
11319ba21f | ||
|
|
9c51523352 | ||
|
|
b4a706820e | ||
|
|
376a3223dc | ||
|
|
a85ec69549 | ||
|
|
ebf4d6d3eb | ||
|
|
2ccd8265b9 | ||
|
|
fb1671d062 | ||
|
|
95e37df2ec | ||
|
|
cfde72f1ab | ||
|
|
efc778acbc | ||
|
|
37384ebadd | ||
|
|
9a7aee7e97 | ||
|
|
1a56534d28 | ||
|
|
79e7e5f087 | ||
|
|
aea03cf975 | ||
|
|
8ef6f944dc | ||
|
|
e18d243961 | ||
|
|
765b633cea | ||
|
|
9c402a3878 | ||
|
|
86426b087a | ||
|
|
c5aaa2f3e2 | ||
|
|
cc59bb6094 | ||
|
|
5662da1210 | ||
|
|
863725d3ba | ||
|
|
b1acfbdd4b | ||
|
|
001f9c3f30 | ||
|
|
8d4df4b66e | ||
|
|
604447508a | ||
|
|
6f30949620 | ||
|
|
10e8e82fe4 | ||
|
|
5227e0ae13 | ||
|
|
cf193f4eab | ||
|
|
708ae53fe2 | ||
|
|
aed57177dd | ||
|
|
e916a03ff8 | ||
|
|
859d60c0bf | ||
|
|
1eeaf38638 | ||
|
|
ca09bbe3fc | ||
|
|
57b42bdc66 | ||
|
|
88c31d157e | ||
|
|
c738274423 | ||
|
|
1c62ee6efe | ||
|
|
4fcb24607d | ||
|
|
52ce1e795c | ||
|
|
39c4593d9e | ||
|
|
6ae288cf01 | ||
|
|
bab54570fe | ||
|
|
2551c76d97 | ||
|
|
e3a5583802 | ||
|
|
b03adc1b1f | ||
|
|
6e9d725f6d | ||
|
|
92559ca9e7 | ||
|
|
3bfa99f578 | ||
|
|
30b85163b7 | ||
|
|
ef528a2952 | ||
|
|
907e9d35a7 | ||
|
|
d003e5da70 | ||
|
|
19530988ce | ||
|
|
4e9baebcf5 | ||
|
|
3a1cbfe8bb | ||
|
|
c16b37c8bc | ||
|
|
0c6703f04d | ||
|
|
3471caf00a | ||
|
|
ad28e70a07 | ||
|
|
47315963f1 | ||
|
|
fb8fa9e787 | ||
|
|
223fa7ca05 | ||
|
|
b568c242e3 | ||
|
|
e4283e1c53 | ||
|
|
25f9cc1095 | ||
|
|
85729e3761 | ||
|
|
fa0a0cb1d8 | ||
|
|
e7ee256b80 | ||
|
|
36190438f0 | ||
|
|
ce629a1fb6 | ||
|
|
f989d1ef4e | ||
|
|
99455dee58 | ||
|
|
c7bd7223bc | ||
|
|
8166f90f0f | ||
|
|
8b2587daa7 | ||
|
|
82e047c797 | ||
|
|
3cefb40ccb | ||
|
|
8bba4a1e48 | ||
|
|
a12f317aa8 | ||
|
|
1d133af38d | ||
|
|
9cab354cbb | ||
|
|
c0385414ca | ||
|
|
4f7846dc4e | ||
|
|
01beac9265 | ||
|
|
77ad146e24 | ||
|
|
6c1c47de1d | ||
|
|
b3cac070df | ||
|
|
daf1b20f67 | ||
|
|
2316fb8d11 | ||
|
|
43159335f8 | ||
|
|
0e56c2038d | ||
|
|
0840740e2c | ||
|
|
e2404f0f3b | ||
|
|
5e9a258cc8 | ||
|
|
8cfc420085 | ||
|
|
83593bd015 | ||
|
|
c1cdf97edb | ||
|
|
eca8370c70 | ||
|
|
54502b331f | ||
|
|
0ec5955ea0 | ||
|
|
b70a9bf0c0 | ||
|
|
fb20cb32da | ||
|
|
a314373e64 | ||
|
|
4976b87c4e | ||
|
|
3b9e54375a | ||
|
|
8b15cfb0b1 | ||
|
|
ff02cce191 | ||
|
|
83beb81a3b | ||
|
|
de89249d3e | ||
|
|
77732e766a | ||
|
|
21c91cfcf1 | ||
|
|
ebaf5e34d8 | ||
|
|
94e389ab23 | ||
|
|
7ba94c7fe1 | ||
|
|
c47394a162 | ||
|
|
bf8afdc0f4 | ||
|
|
3df17ed33f | ||
|
|
dc6d3b3743 | ||
|
|
6e626f6e11 | ||
|
|
d20da31f2c | ||
|
|
2182cd012a | ||
|
|
71c629cb06 | ||
|
|
185d675abf | ||
|
|
6221b59b21 | ||
|
|
07f320ca2f | ||
|
|
9908b83b18 | ||
|
|
9ce1c966eb | ||
|
|
c81eff58d3 | ||
|
|
92b61a203f | ||
|
|
b1f9c0adf2 | ||
|
|
c5d75a3570 | ||
|
|
42c64920b1 | ||
|
|
77e7d095a7 | ||
|
|
560199944d | ||
|
|
21ae217369 | ||
|
|
4565297fe6 | ||
|
|
118e68b8ac | ||
|
|
a03f908e96 | ||
|
|
0b6dcf17d1 | ||
|
|
5136f8b3dd | ||
|
|
286295393d | ||
|
|
195ea35798 | ||
|
|
053279f60d | ||
|
|
9bfb7e8b69 | ||
|
|
c82f8a6de3 | ||
|
|
749147cfad | ||
|
|
4429227665 | ||
|
|
3ec0e9d997 | ||
|
|
904046f02c | ||
|
|
5a29215b80 | ||
|
|
508a00d103 | ||
|
|
5385d10bc8 | ||
|
|
08b4a04791 | ||
|
|
218cee2aef | ||
|
|
63b22f4c86 | ||
|
|
709a3f71fd | ||
|
|
f7589e42ab | ||
|
|
0e7008862e | ||
|
|
7e88ee930b | ||
|
|
e935f2a3e3 | ||
|
|
9d442a2aa1 | ||
|
|
eed6ba43d9 | ||
|
|
b2c286f0f5 | ||
|
|
2aae5a4bff | ||
|
|
c88c1865e7 | ||
|
|
7152e339e5 | ||
|
|
46143beb83 | ||
|
|
2c66eaae2b | ||
|
|
b8c74ac84c | ||
|
|
cb210916eb | ||
|
|
f968f3d20d | ||
|
|
4ef2496221 | ||
|
|
b2b56178a2 | ||
|
|
525838468c | ||
|
|
ef92bcfe3a | ||
|
|
ed72c2ffc2 | ||
|
|
9f94c2013a | ||
|
|
8666254ffe | ||
|
|
e13fc57ae7 | ||
|
|
c6cf90fb4e | ||
|
|
fff086a3a1 | ||
|
|
b7a2a5af6d | ||
|
|
1786dfeee2 | ||
|
|
2c32088f4c | ||
|
|
71f61461ce | ||
|
|
54be87efea | ||
|
|
76fc55fdd0 | ||
|
|
5d5a845523 | ||
|
|
4f915a32b6 | ||
|
|
78df54d153 | ||
|
|
24554a0768 | ||
|
|
ffe03fd785 | ||
|
|
4b635735ae | ||
|
|
79c1e2c5bb | ||
|
|
cf9f9fb7dd | ||
|
|
2bcefc1306 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@
|
||||
node_modules/
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
!seller/yarn.lock
|
||||
|
||||
682
LICENSE
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.
|
||||
|
||||
201
MIGRATION-VUE3.md
Normal file
201
MIGRATION-VUE3.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# Lilishop UI — Vue 3 迁移与上线前检查清单
|
||||
|
||||
四个子项目(buyer / seller / manager / im)已完成 **Vue 3 + Element Plus** 迁移,并完成依赖小版本对齐。本文档供上线前统一回归使用。
|
||||
|
||||
## 技术栈(四端一致)
|
||||
|
||||
| 类别 | 包 | 版本(package.json) |
|
||||
|------|-----|---------------------|
|
||||
| 框架 | vue | ^3.5.35 |
|
||||
| 编译 | @vue/compiler-sfc | ^3.5.35 |
|
||||
| 路由 | vue-router | ^4.6.4 |
|
||||
| 状态 | pinia | ^4.0.3(四端已全部从 vuex 迁移完成) |
|
||||
| 组件写法 | `<script setup>` | 四端 597 个组件已全部完成 Options API → 组合式 API 迁移;各端 eslint 已启用 `vue/component-api-style: ['warn', ['script-setup']]` 防止回退 |
|
||||
| UI | element-plus | ^2.14.1 |
|
||||
| 图标 | @element-plus/icons-vue | ^2.3.2 |
|
||||
| 请求 | axios | ^1.7.9 |
|
||||
| 兼容 | core-js | ^3.49.0 |
|
||||
| 构建 | @vue/cli-service | ^5.0.9 |
|
||||
|
||||
> 使用 `^` 范围,实际安装版本以各目录 `yarn.lock` 为准。
|
||||
> 要求 **Node >= 16**,包管理统一 **yarn**。
|
||||
|
||||
## 本地启动
|
||||
|
||||
```bash
|
||||
# 买家端
|
||||
cd buyer && yarn install && yarn dev # 端口 10000
|
||||
|
||||
# 商家端
|
||||
cd seller && yarn install && yarn dev # 端口 10002
|
||||
|
||||
# 管理端
|
||||
cd manager && yarn install && yarn dev # 端口 10003
|
||||
|
||||
# IM 客服
|
||||
cd im && yarn install && yarn dev # 端口 8000
|
||||
```
|
||||
|
||||
生产构建(上线前必跑):
|
||||
|
||||
```bash
|
||||
cd buyer && yarn build
|
||||
cd seller && yarn build
|
||||
cd manager && yarn build
|
||||
cd im && yarn build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 一、通用检查(四端都要过)
|
||||
|
||||
### 1. 认证与请求(axios 1.x 重点)
|
||||
|
||||
各端请求封装位置:
|
||||
|
||||
| 端 | 文件 |
|
||||
|----|------|
|
||||
| buyer | `buyer/src/plugins/request.js` |
|
||||
| seller | `seller/src/libs/axios.js` |
|
||||
| manager | `manager/src/libs/axios.js` |
|
||||
| im | `im/src/utils/request.js` |
|
||||
|
||||
- [ ] **登录**:账号密码登录成功,token 写入正常
|
||||
- [ ] **登出**:清除 token,跳转登录页
|
||||
- [ ] **401**:token 过期后跳转登录 / 刷新 token(buyer/manager/seller 有刷新逻辑)
|
||||
- [ ] **403**:无权限时提示文案正确、不白屏
|
||||
- [ ] **400 / 5xx**:接口报错能弹出 `Message` / `ElMessage` / `Notification`
|
||||
- [ ] **网络超时**:断网或超时后有错误提示,页面不卡死
|
||||
- [ ] **GET 带参**:列表筛选、分页参数序列化正常
|
||||
- [ ] **POST JSON**:`Content-Type: application/json` 请求体正确
|
||||
- [ ] **POST 表单**:`application/x-www-form-urlencoded` 提交正常(im 等)
|
||||
|
||||
### 2. Element Plus UI(2.6 → 2.14 重点)
|
||||
|
||||
- [ ] **表格** `el-table`:列展示、排序、空数据、加载态
|
||||
- [ ] **分页** `el-pagination`:翻页、每页条数切换
|
||||
- [ ] **表单** `el-form`:必填校验、错误提示位置
|
||||
- [ ] **弹窗** `el-dialog` / `ElMessageBox`:打开、确认、取消、关闭
|
||||
- [ ] **下拉** `el-select` / `el-dropdown`:选项、点击外部关闭
|
||||
- [ ] **消息** `ElMessage` / `$Message` 兼容层:success / error / warning
|
||||
- [ ] **加载** `ElLoading` / `v-loading`:请求期间遮罩正常消失
|
||||
- [ ] **日期** `el-date-picker`:选择、清空、范围选择
|
||||
- [ ] **上传** `el-upload`:选文件、进度、成功 / 失败回调
|
||||
|
||||
### 3. 路由与页面
|
||||
|
||||
- [ ] 浏览器 **刷新** 当前路由不 404(history 模式 + 服务端配置)
|
||||
- [ ] **动态路由** 加载正常(seller / manager 菜单权限路由)
|
||||
- [ ] 浏览器 **前进 / 后退** 正常
|
||||
- [ ] 各端 **404 页** 可回到首页
|
||||
|
||||
### 4. 构建与部署
|
||||
|
||||
- [ ] 四端 `yarn build` 无 error(warning 可记录,不阻断)
|
||||
- [ ] 生产环境 API 地址配置正确(`config` / 环境变量)
|
||||
- [ ] 静态资源 `publicPath` 与 Nginx / CDN 路径一致
|
||||
- [ ] gzip 压缩包(如有)可正常访问
|
||||
|
||||
---
|
||||
|
||||
## 二、分端专项检查
|
||||
|
||||
### Buyer 买家端(10000)
|
||||
|
||||
详见 [buyer/MIGRATION-VUE3.md](./buyer/MIGRATION-VUE3.md)
|
||||
|
||||
- [ ] 首页装修模块渲染(轮播、商品列表、秒杀区等)
|
||||
- [ ] 商品详情:规格选择、加购、收藏、`el-image` 预览
|
||||
- [ ] 购物车 → 结算 → 支付流程
|
||||
- [ ] 用户中心:订单列表、地址、优惠券、售后
|
||||
- [ ] 登录 / 注册 / 忘记密码
|
||||
- [ ] 店铺入驻、商户页
|
||||
- [ ] 地图(高德 `@amap/amap-jsapi-loader`)定位与选点
|
||||
- [ ] 侧边栏 `el-menu`、头部 `el-dropdown`、动态图标 `iconMap.js`
|
||||
|
||||
### Seller 商家端(10002)
|
||||
|
||||
详见 [seller/MIGRATION-VUE3.md](./seller/MIGRATION-VUE3.md)
|
||||
|
||||
- [ ] 商家登录、店铺信息
|
||||
- [ ] 商品发布三步流程(`goodsOperation` 步骤条)
|
||||
- [ ] 订单管理:发货、退款、详情
|
||||
- [ ] 促销 / 分销 / 统计图表(`@antv/g2`)
|
||||
- [ ] 页面装修(wap / modelList,从 manager 同步部分)
|
||||
- [ ] 图片懒加载 `vue-lazyload`
|
||||
- [ ] Excel 导出 `vue-json-excel`
|
||||
|
||||
### Manager 管理端(10003)
|
||||
|
||||
详见 [manager/MIGRATION-VUE3.md](./manager/MIGRATION-VUE3.md)
|
||||
|
||||
- [ ] 管理员登录、权限菜单、动态路由
|
||||
- [ ] 系统设置(`sys/setting-manage` 各 Tab)
|
||||
- [ ] 菜单 / 部门管理
|
||||
- [ ] 页面装修(PC + H5)
|
||||
- [ ] 文章管理
|
||||
- [ ] 商品 / 订单 / 会员 / 促销等核心列表 CRUD
|
||||
- [ ] 富文本 TinyMCE 编辑、图片上传
|
||||
- [ ] 多语言 `vue-i18n` 切换(如有启用)
|
||||
|
||||
### IM 客服端(8000)
|
||||
|
||||
详见 [im/MIGRATION-VUE3.md](./im/MIGRATION-VUE3.md)
|
||||
|
||||
- [ ] 带 token 参数进入会话页
|
||||
- [ ] 对话列表加载、切换会话、未读数
|
||||
- [ ] 发送文字、表情、图片消息
|
||||
- [ ] 商品 / 订单卡片链接跳转(`linkToGoods` / `linkToOrders`)
|
||||
- [ ] 右键菜单(复制、撤回、删除等)
|
||||
- [ ] WebSocket 连接 / 断线重连 / 新消息通知
|
||||
- [ ] 最近浏览、订单列表侧栏(商家场景)
|
||||
- [ ] 用户名片弹层 `$user()`
|
||||
|
||||
---
|
||||
|
||||
## 三、已知可忽略项
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| `vue-awesome-swiper` peer warning | 历史依赖,Vue 3 下仍有 peer 告警,功能正常可暂忽略 |
|
||||
| im 内 `<i class="el-icon-*">` | Element Plus 无字体图标,部分旧图标可能不显示,不影响核心功能 |
|
||||
| `vue-prism-editor@2.0.0-alpha.2` | im 代码块编辑器刻意锁定 alpha 版 |
|
||||
| 打包体积 warning | chunk-vendors 偏大,属 Element Plus 全量引入,可后续做按需优化 |
|
||||
|
||||
---
|
||||
|
||||
## 四、问题记录模板
|
||||
|
||||
上线测试时建议按端记录:
|
||||
|
||||
```markdown
|
||||
### [端名] 问题标题
|
||||
- 页面/路由:
|
||||
- 操作步骤:
|
||||
- 预期:
|
||||
- 实际:
|
||||
- 控制台报错:
|
||||
- 优先级:P0 / P1 / P2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、子项目文档索引
|
||||
|
||||
| 端 | 迁移说明 |
|
||||
|----|----------|
|
||||
| buyer | [buyer/MIGRATION-VUE3.md](./buyer/MIGRATION-VUE3.md) |
|
||||
| seller | [seller/MIGRATION-VUE3.md](./seller/MIGRATION-VUE3.md) |
|
||||
| manager | [manager/MIGRATION-VUE3.md](./manager/MIGRATION-VUE3.md) |
|
||||
| im | [im/MIGRATION-VUE3.md](./im/MIGRATION-VUE3.md) |
|
||||
|
||||
---
|
||||
|
||||
## 六、建议上线顺序
|
||||
|
||||
1. **manager**(后台配置源头)
|
||||
2. **seller**(商家经营)
|
||||
3. **buyer**(C 端流量最大)
|
||||
4. **im**(客服,依赖前后端 WebSocket)
|
||||
|
||||
每上一端,至少完成本文 **第一节通用检查** + 对应 **第二节分端检查** 后再切下一端。
|
||||
144
README.en.md
Normal file
144
README.en.md
Normal file
@@ -0,0 +1,144 @@
|
||||
<div align="center">
|
||||
<img src="./manager/src/assets/logo.png" width="180" alt="Lilishop Logo" />
|
||||
<h1>Lilishop(PickMall) - Web Frontend (Vue 3)</h1>
|
||||
|
||||
<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.5-brightgreen.svg" alt="Vue 3" /></a>
|
||||
<a href="https://element-plus.org/"><img src="https://img.shields.io/badge/Element%20Plus-2.x-409EFF.svg" alt="Element Plus" /></a>
|
||||
<a href="https://pinia.vuejs.org/"><img src="https://img.shields.io/badge/Pinia-4.x-yellow.svg" alt="Pinia" /></a>
|
||||
</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>
|
||||
|
||||
<p>
|
||||
<a href="./README.md">简体中文</a> | <b>English</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
> ⭐ **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!**
|
||||
|
||||
**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 `Vue 3.5 + Pinia 4 + Vue Router 4 + Element Plus`, leaving legacy Vue 2 templates behind.
|
||||
- 🧩 **4-in-1 Production Suite**: Covers **PC Buyer Mall (`buyer`)**, **Seller Portal (`seller`)**, **Platform Admin Panel (`manager`)**, and **IM Customer Service Web (`im`)**, complete with drag-and-drop visual storefront decoration and rich analytics.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
This repository (`lilishop-ui`) is the **Web Frontend Suite** of Lilishop, containing four standalone **Vue 3** web applications: **PC Buyer Mall, Seller Panel, Platform Admin Panel, and IM Customer Service Web**.
|
||||
|
||||
- **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>
|
||||
|
||||
---
|
||||
|
||||
## 2. Online Demo
|
||||
|
||||
> **Note**: The verification code for the demo environment is `111111`.
|
||||
|
||||
| Portal | URL | Demo Credentials |
|
||||
| :--- | :--- | :--- |
|
||||
| **Platform Admin Panel (`manager`)** | <https://admin-b2b2c.pickmall.cn> | `admin` / `123456` |
|
||||
| **Seller / Store Panel (`seller`)** | <https://store-b2b2c.pickmall.cn> | `13011111111` / `111111` |
|
||||
| **PC Buyer Mall (`buyer`)** | <https://pc-b2b2c.pickmall.cn> | SMS code: `111111` |
|
||||
| **Mobile H5 (`uni-app`)** | <https://m-b2b2c.pickmall.cn> | SMS code: `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> |
|
||||
|
||||
---
|
||||
|
||||
## 3. Core Features & Tech Stack
|
||||
|
||||
### Core Features
|
||||
- **Four-Portal Synergy**: Includes Platform Admin, Seller Panel, PC Buyer Mall, and IM Web Workbench with clear role boundaries.
|
||||
- **Drag-and-Drop Visual Decoration**: Built-in visual floor editor (`vuedraggable`) for customizing both PC and mobile storefronts.
|
||||
- **Multi-Vendor B2B2C Operations**: Merchant onboarding, product auditing, split-order fulfillment, promotions, and financial settlement ledgers.
|
||||
- **Rich Business Analytics**: Powered by `@antv/g2` for sales overview, YoY/MoM product reports, member analytics, and distribution metrics.
|
||||
- **IM Customer Service**: Integrated with Tencent Cloud IM / WebSocket customer service for real-time communication.
|
||||
|
||||
### Web Tech Stack
|
||||
| Category | Technology | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| Core Framework | `Vue 3.5` + `Vue Router 4` | Modern reactive frontend framework |
|
||||
| State Management | `Pinia 4` | Official Vue 3 state management library |
|
||||
| UI Library | `Element Plus 2.x` | Enterprise desktop UI component library |
|
||||
| HTTP Client | `Axios 1.x` | Unified request/response interceptors & auth handling |
|
||||
| Data Visualization | `@antv/g2` | Interactive charts and business analytics |
|
||||
| Visual Decoration | `vuedraggable 4` | Drag-and-drop page floor builder |
|
||||
| Messaging & IM | `@tencentcloud/chat` / `sockjs-client` | Real-time customer service messaging |
|
||||
| Video Player | `dplayer` + `hls.js` | Product video and live streaming playback |
|
||||
| Build & Styling | `Vue CLI 5` + `Webpack 5` + `Sass / Less` | Modular build pipeline and theme customization |
|
||||
|
||||
---
|
||||
|
||||
## 4. Directory Structure
|
||||
|
||||
```text
|
||||
lilishop-ui
|
||||
├── buyer/ # [PC Buyer Mall]
|
||||
│ ├── public/ # Static assets & global config (config.js)
|
||||
│ └── src/
|
||||
│ ├── api/ # Buyer API modules (goods, cart, order, member, promotion)
|
||||
│ ├── components/ # Shared components (floor renderer, navbar, goods card)
|
||||
│ ├── pages/ # PC Mall pages (Home, Product Detail, Checkout, User Center)
|
||||
│ ├── router/ # Route definitions & guards
|
||||
│ ├── stores/ # Pinia stores
|
||||
│ └── utils/ # Request interceptors & helpers
|
||||
├── seller/ # [Seller / Store Management Portal]
|
||||
│ ├── public/ # Static assets & global config
|
||||
│ └── src/
|
||||
│ ├── api/ # Seller API modules
|
||||
│ ├── components/ # Seller shared & decoration components
|
||||
│ ├── views/ # Seller views (goods, orders, after-sales, promotions, store settings)
|
||||
│ ├── router/ # Dynamic permission routes
|
||||
│ └── stores/ # Pinia stores
|
||||
├── manager/ # [Platform Admin Portal]
|
||||
│ ├── public/ # Static assets & global config
|
||||
│ └── src/
|
||||
│ ├── api/ # Admin API modules
|
||||
│ ├── components/ # Admin components, rich-text editor, visual floor builder
|
||||
│ ├── views/ # Admin views (member, goods, order, seller, marketing, finance, stats, system)
|
||||
│ ├── router/ # Menu & permission routes
|
||||
│ └── stores/ # Pinia stores
|
||||
├── im/ # [IM Customer Service Web Client]
|
||||
│ └── src/
|
||||
│ ├── api/ # Chat session & message APIs
|
||||
│ ├── components/ # Chat window, message list, product/order cards
|
||||
│ ├── im-server/ # WebSocket / IM connection manager
|
||||
│ └── views/ # Customer service workbench
|
||||
├── build.sh # Unified build script
|
||||
└── docker-build.sh # Docker image build script
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Related Repositories
|
||||
|
||||
| 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) |
|
||||
|
||||
---
|
||||
|
||||
## 6. License
|
||||
|
||||
Lilishop is licensed under the [MIT](./LICENSE) License. Copyright © 2020-2026 **Beijing Hongye Huicheng Technology Co., Ltd.**
|
||||
279
README.md
279
README.md
@@ -1,176 +1,181 @@
|
||||
## Lilishop B2B2C商城系统
|
||||
<div align="center">
|
||||
<img src="./manager/src/assets/logo.png" width="180" alt="Lilishop Logo" />
|
||||
<h1>Lilishop(PickMall) 开源商城系统 - Web 前端 (Vue 3)</h1>
|
||||
|
||||
#### 欢迎交流需求,交流业务,交流技术(基础问题自行解决,其他问题先看文档后提问)
|
||||
<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.5-brightgreen.svg" alt="Vue 3" /></a>
|
||||
<a href="https://element-plus.org/"><img src="https://img.shields.io/badge/Element%20Plus-2.x-409EFF.svg" alt="Element Plus" /></a>
|
||||
<a href="https://pinia.vuejs.org/"><img src="https://img.shields.io/badge/Pinia-4.x-yellow.svg" alt="Pinia" /></a>
|
||||
</p>
|
||||
|
||||
#### 不用削尖脑袋往老群里加,老群活跃度较低,很多潜水党,新群相对而言活跃一些 :tw-1f606: :tw-1f606: :tw-1f606: :tw-1f606: :tw-1f606: :tw-1f606:
|
||||
<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>
|
||||
|
||||
#### PS: **演示站点所有环境均部署master分支。如果有演示站点问题,可以反馈,如果演示站点没问题本地运行有问题,需自行处理**
|
||||
<p>
|
||||
<b>简体中文</b> | <a href="./README.en.md">English</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
##### 交流 qq 1群 961316482(已满)
|
||||
##### 交流 qq 2群 875294241(已满)
|
||||
##### 交流 qq 3群 263785057(已满)
|
||||
##### 交流 qq 4群 674617534 (已满)
|
||||
##### 交流 qq 5群 594675235
|
||||
> ⭐ **如果觉得项目不错,请点击右上角 `Star` 收藏支持!您的 Star 是我们持续开源迭代的最大动力!**
|
||||
|
||||
##### 体验 公众号/小程序/APP 体验,扫描二维码
|
||||
**LILISHOP** 是基于 Spring Boot / Vue 3 / uni-app 开发的开源商城系统,支持 B2B2C 多商户商城、微信小程序商城、H5 商城、APP 商城、直播电商、分销返佣、秒杀活动。
|
||||
|
||||

|
||||
### ✨ 为什么选择 Lilishop
|
||||
- 🟢 **真 MIT 协议**:支持免费商用与私有化闭源二开,无版权后顾之忧。
|
||||
- 🚀 **Vue 3 全新栈**:全面基于 `Vue 3.5 + Pinia 4 + Vue Router 4 + Element Plus` 构建,告别老旧 Vue 2 架构。
|
||||
- 🧩 **四端合一生产级闭环**:完整覆盖 **PC 买家端 (`buyer`)**、**商家端 (`seller`)**、**平台运营管理端 (`manager`)** 与 **在线客服端 (`im`)**,支持拖拽式可视化楼层装修与多维数据统计报表。
|
||||
|
||||
[](https://gitee.com/beijing_hongye_huicheng/lilishop/stargazers)
|
||||

|
||||
---
|
||||
|
||||
## 1. 项目简介
|
||||
|
||||
### 商城介绍
|
||||
**官网**:https://pickmall.cn
|
||||
**Lilishop** 是一款功能完善的 B2B2C 多商户商城系统,采用前后端分离架构,全端代码开源。
|
||||
后端基于 **Spring Boot 4**(Java 21)构建,具备高内聚、低耦合的特性,支持分布式部署;前端覆盖 PC、管理后台、H5、小程序和 APP,基于 **Vue 3** 和 **uni-app** 开发。
|
||||
|
||||
Lilishop商城系统支持商家入驻,后端基于SpringBoot 研发,前端使用 Vue、uniapp开发, **系统全端全部代码开源**
|
||||
本仓库(`lilishop-ui`)为 **Web 前端源码仓库**,包含 **PC 商城端、商家后台、平台管理后台、IM 在线客服** 四个独立子工程。
|
||||
|
||||
前后端分离,支持分布式部署,支持Docker,各个API独立,并且有独立的消费者。
|
||||
- **官方网站**: <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>
|
||||
|
||||
### 商城 API/消费者 聚合版
|
||||
api不需要单独部署,只需启动一个jar包就可以正常运转 如有需要,可以点击跳转
|
||||
https://gitee.com/beijing_hongye_huicheng/lilishop-simplify
|
||||
---
|
||||
|
||||
### 开发/使用/常见问题 帮助文档
|
||||
## 2. 在线演示
|
||||
|
||||
https://docs.pickmall.cn
|
||||
> **注意**: 演示站手机验证码统一为 `111111`。演示环境部署于 `master` 分支。
|
||||
|
||||
### 项目地址
|
||||
| 演示站点 | 访问地址 | 演示账号 |
|
||||
| :--- | :--- | :--- |
|
||||
| **平台运营管理端 (`manager`)** | <https://admin-b2b2c.pickmall.cn> | 账号:`admin` / 密码:`123456` |
|
||||
| **商家店铺管理端 (`seller`)** | <https://store-b2b2c.pickmall.cn> | 账号:`13011111111` / 密码:`111111` |
|
||||
| **商城 PC 买家端 (`buyer`)** | <https://pc-b2b2c.pickmall.cn> | 可使用手机号验证码 `111111` 登录 |
|
||||
| **移动端 H5 (`uni-app`)** | <https://m-b2b2c.pickmall.cn> | 可使用手机号验证码 `111111` 登录 |
|
||||
|
||||
gitee : https://gitee.com/beijing_hongye_huicheng
|
||||
| 微信小程序 | 移动端 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> |
|
||||
|
||||
github 镜像: https://github.com/lilishop?tab=repositories
|
||||
---
|
||||
|
||||
商城UI 项目下3个文件夹
|
||||
buyer:买家PC端,seller:商家端,manager:后台管理端
|
||||
## 3. 核心特性与技术栈
|
||||
|
||||
### 核心特性
|
||||
- **四端协同**: 涵盖平台管理端、商家管理端、PC 买家商城端、IM 客服端,角色边界清晰,开箱即用。
|
||||
- **可视化拖拽装修**: 管理端与商家端内置可视化楼层编辑器(基于 `vuedraggable`),可同步配置 PC 端与移动端首页/专题页。
|
||||
- **多商户入驻与结算**: 完整的商家入驻审核、商品审核、拆单发货、促销报名、财务对账与结算台账体系。
|
||||
- **多维经营分析**: 集成 `@antv/g2` 图表引擎,提供营业概况、商品同比环比、会员分析、营销与分销数据统计。
|
||||
- **即时通讯客服**: 集成腾讯云 IM / WebSocket 在线客服系统,支持买家、商家与平台实时沟通。
|
||||
|
||||
### 演示地址
|
||||
PS:手机验证码为 ‘111111’
|
||||
### Web 前端技术栈
|
||||
| 类别 | 技术选型 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| 核心框架 | `Vue 3.5` + `Vue Router 4` | 基于 Composition API 的现代响应式前端框架 |
|
||||
| 状态管理 | `Pinia 4` | 轻量、类型友好的 Vue 3 官方状态管理库 |
|
||||
| UI 组件库 | `Element Plus 2.x` | 企业级中后台与桌面端 UI 组件库 |
|
||||
| 网络请求 | `Axios 1.x` | 统一请求/响应拦截、Token 刷新与异常提示 |
|
||||
| 数据可视化 | `@antv/g2` | 运营端与商家端数据统计图表渲染 |
|
||||
| 可视化拖拽 | `vuedraggable 4` | 商城首页及专题楼层可视化拖拽装修 |
|
||||
| 即时通讯 | `@tencentcloud/chat` / `sockjs-client` | 在线客服消息收发与实时通知 |
|
||||
| 音视频播放 | `dplayer` + `hls.js` | 商品视频与直播流播放支持 |
|
||||
| 构建与样式 | `Vue CLI 5` + `Webpack 5` + `Sass / Less` | 模块化工程构建与全局主题样式定制 |
|
||||
|
||||
**平台管理端**:https://admin-b2b2c.pickmall.cn 账号:admin/123456
|
||||
---
|
||||
|
||||
**店铺管理端**:https://store-b2b2c.pickmall.cn 账号:13011111111/111111
|
||||
## 4. 目录结构
|
||||
|
||||
**商城PC页面**:https://pc-b2b2c.pickmall.cn
|
||||
|
||||
**商城 小程序/公众号/APP**:扫描二维码
|
||||
|
||||

|
||||
|
||||
### 快速本地部署
|
||||
|
||||
[点击跳转](https://docs.pickmall.cn/deploy/%E8%BF%90%E8%A1%8C%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87.html)
|
||||
|
||||
**商城数据库**
|
||||
使用docker-compose部署数据库,自动初始化数据库,不需要手动下载等操作
|
||||
|
||||
如果手动部署,才需要获取sql [点击跳转](https://gitee.com/beijing_hongye_huicheng/docker/tree/master/init/mysql) (PS:这里有与tag版本一致的sql,如果是历史版本,则docker项目也切换至历史版本获取sql即可,历史版本升级则根据java相聚的根目录DB目录下的升级sql,按需执行)。
|
||||
|
||||
|
||||
|
||||
### 功能列表
|
||||
|
||||
|
||||
|
||||
#### 平台管理端功能
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
#### 卖家功能
|
||||
|
||||

|
||||
|
||||
|
||||
### 商城前端功能展示
|
||||
|
||||
#### 商城移动端
|
||||
|
||||
<img src="https://static.pickmall.cn/images/other/app.gif" alt="移动端功能展示" style="zoom:50%;" />
|
||||
|
||||
#### 平台管理端
|
||||
|
||||

|
||||
|
||||
|
||||
### 技术选型
|
||||
|
||||
#### 架构图
|
||||
|
||||

|
||||
|
||||
##### 后台技术选型
|
||||
|
||||
| 说明 | 框架 | 说明 | |
|
||||
| -------------- | --------------- | -------------- | ------------- |
|
||||
| 基础框架 | Spring Boot | MVC框架 | Spring MVC |
|
||||
| 持久框架 | Mybatis-Plus | 程序构建 | Maven |
|
||||
| 关系型数据库 | MySQL | 消息中间件AMQP | RocketMQ |
|
||||
| 缓存 | Redis +MongoDB | 搜索引擎 | Elasticsearch |
|
||||
| 安全框架 | Spring Security | 数据库连接池 | Druid |
|
||||
| 数据库分库分表 | sharding | 定时任务 | xxl-job |
|
||||
| 负载均衡 | Nginx | 静态资源 | 阿里云OSS |
|
||||
| 短信 | 阿里云短信 | 认证 | JWT |
|
||||
| 日志处理 | Log4j | 接口规范 | RESTful |
|
||||
|
||||
##### 前端-运营后台、店铺后台
|
||||
|
||||
| 说明 | 框架 | 说明 | 框架 |
|
||||
| ---------- | ---------- | ---------- | ------- |
|
||||
| 构建工具 | webpack | JS版本 | ES6 |
|
||||
| 基础JS框架 | Vue.js | 视频播放器 | Dplayer |
|
||||
| 路由管理 | Vue Router | 状态管理 | Vuex |
|
||||
| 基础UI库 | iView | UI界面基于 | iView |
|
||||
| 网络请求 | axios | | |
|
||||
|
||||
##### 前端-移动端
|
||||
|
||||
| 说明 | 架构 | 说明 | 架构 |
|
||||
| --------- | ------- | -------- | ------- |
|
||||
| 基础UI库 | uViewui | 基础框架 | uni-app |
|
||||
| CSS预处理 | scss | 地图引擎 | amap |
|
||||
|
||||
### 版本升级
|
||||
|
||||
```
|
||||
系统后续会提供多场景解决方案。
|
||||
更多架构:微服务、Saas、中台等,都会支持。 支持差价升级商业授权
|
||||
```text
|
||||
lilishop-ui
|
||||
├── buyer/ # 【PC 买家商城端】源码工程
|
||||
│ ├── public/ # 静态资源与全局配置文件 (config.js)
|
||||
│ └── src/
|
||||
│ ├── api/ # 买家端接口定义(商品、购物车、订单、会员、促销等)
|
||||
│ ├── components/ # 公共业务组件(楼层渲染、导航栏、商品卡片等)
|
||||
│ ├── pages/ # PC 商城各业务页面(首页、商品详情、结算、个人中心等)
|
||||
│ ├── router/ # 路由配置与守卫
|
||||
│ ├── stores/ # Pinia 状态管理
|
||||
│ └── utils/ # 请求封装与通用工具函数
|
||||
├── seller/ # 【商家店铺管理端】源码工程
|
||||
│ ├── public/ # 静态资源与全局配置
|
||||
│ └── src/
|
||||
│ ├── api/ # 商家端接口定义
|
||||
│ ├── components/ # 商家端公共组件与装修组件
|
||||
│ ├── views/ # 商家端业务视图(商品管理、订单发货、售后、促销、店铺设置、统计等)
|
||||
│ ├── router/ # 动态权限路由
|
||||
│ └── stores/ # Pinia 状态管理
|
||||
├── manager/ # 【平台运营管理端】源码工程
|
||||
│ ├── public/ # 静态资源与全局配置
|
||||
│ └── src/
|
||||
│ ├── api/ # 运营端接口定义
|
||||
│ ├── components/ # 平台公共组件、富文本、可视化楼层装修编辑器
|
||||
│ ├── views/ # 运营端业务视图(会员、商品、订单、商家、营销、分销、财务、统计、系统设置)
|
||||
│ ├── router/ # 菜单与权限路由
|
||||
│ └── stores/ # Pinia 状态管理
|
||||
├── im/ # 【IM 在线客服 Web 端】源码工程
|
||||
│ └── src/
|
||||
│ ├── api/ # 客服会话与消息接口
|
||||
│ ├── components/ # 聊天窗口、消息列表、商品/订单卡片组件
|
||||
│ ├── im-server/ # WebSocket / IM 连接服务管理
|
||||
│ └── views/ # 客服工作台页面
|
||||
├── build.sh # 多端统一构建脚本
|
||||
└── docker-build.sh # Docker 镜像构建脚本
|
||||
```
|
||||
|
||||
### 商业授权
|
||||
商业版本与开源版本代码一致,没有区分
|
||||
---
|
||||
|
||||
商业使用需要授权,授权方式可选择联系官网客服,或者qq群联系群主。
|
||||
## 5. 配套源码仓库
|
||||
|
||||
商业授权模式为永久授权,支持永久升级。
|
||||
本仓库专注于 **PC / 商家端 / 运营端 / IM Web**(`lilishop-ui`)。完整系统由以下四个核心仓库组成:
|
||||
|
||||
商业案例由于涉及部分多层二开关系,如需了解可以咨询销售。
|
||||
| 仓库名称 | 职责说明 | 仓库地址 |
|
||||
| :--- | :--- | :--- |
|
||||
| **`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) |
|
||||
|
||||
---
|
||||
|
||||
### 开源须知
|
||||
1.仅允许用于个人学习研究使用.
|
||||
## 6. 开源与授权
|
||||
|
||||
2.禁止将本开源的代码和资源进行任何形式任何名义的出售.
|
||||
本项目遵循 [MIT](./LICENSE) 开源协议,版权归 **北京宏业汇成科技有限公司** 所有。允许个人及企业免费用于学习、研究、修改和商业用途(需保留原始版权和许可声明)。
|
||||
|
||||
3.软件受国家计算机软件著作权保护(登记号:2021SR0805085)。
|
||||
---
|
||||
|
||||
4.限制商用,如果需要商业使用请联系我们。QQ3409056806.或者加入qq群联系群主。
|
||||
## 7. 社区与支持
|
||||
|
||||
我们欢迎任何形式的交流与贡献。在提问前,请先查阅 [产品文档](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)**
|
||||
- **微信交流群**:
|
||||
<img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/wechat.jpg" width="200" alt="微信群" />
|
||||
|
||||
### 交流群
|
||||
---
|
||||
|
||||
##### 交流 qq 1群 961316482(已满)
|
||||
##### 交流 qq 2群 875294241(已满)
|
||||
##### 交流 qq 3群 263785057(已满)
|
||||
##### 交流 qq 4群 674617534(已满)
|
||||
##### 交流 qq 5群 594675235
|
||||
## 8. 升级计划
|
||||
|
||||
### 附录
|
||||
有人有自己的学习视频、学习记录文档、希望宣传关联开源项目等均可以私聊仓库所有者。
|
||||
### ✅ 已完成特性
|
||||
| 功能模块 | 功能模块 |
|
||||
| :--- | :--- |
|
||||
| ✅ 商品定时上下架 | ✅ 商品分组 / 会员分组 |
|
||||
| ✅ 商品虚拟销量 | ✅ 专用发票(发票上传) |
|
||||
| ✅ 置顶评论 | ✅ 客户等级体系 |
|
||||
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
|
||||
| ✅ 卡密商品 | ✅ 二级分销 |
|
||||
| ✅ 定时任务管理 | ✅ 预约服务商品 |
|
||||
| ✅ 主题色管理 | ✅ 商家发货改造(整单/拆单/无需配送) |
|
||||
| ✅ 多维统计(营业/商品/会员/积分/储值/营销/分销) | ✅ 经营报表(店铺业绩/商品同比环比/订单明细/销售汇总) |
|
||||
|
||||
类似:
|
||||
|
||||
清晨敲代码同学的分析: https://blog.csdn.net/vaevaevae233/category_12103567.html
|
||||
### 🔜 近期规划中
|
||||
- 🔜 店铺结算汇总 & 结算台账
|
||||
- 🔜 支付方式汇总 & 钱包流水 & 财务汇总
|
||||
- 🔜 微信小店对接
|
||||
- 🔜 抖店对接
|
||||
|
||||
1
build.sh
1
build.sh
@@ -1,7 +1,6 @@
|
||||
#代码目录
|
||||
code_path=$PWD
|
||||
|
||||
git checkout master
|
||||
git pull
|
||||
|
||||
cd ${code_path}/manager
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["env", {
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
|
||||
}
|
||||
}],
|
||||
"stage-2"
|
||||
],
|
||||
"plugins": ["transform-vue-jsx", "transform-runtime"]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/build/
|
||||
/config/
|
||||
/dist/
|
||||
/*.js
|
||||
@@ -1,33 +0,0 @@
|
||||
// https://eslint.org/docs/user-guide/configuring
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
extends: [
|
||||
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
|
||||
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
|
||||
'plugin:vue/essential',
|
||||
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
|
||||
'standard'
|
||||
],
|
||||
// required to lint *.vue files
|
||||
plugins: [
|
||||
'vue'
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
"semi": [0, "never"],
|
||||
// allow async-await
|
||||
'generator-star-spacing': 'off',
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
"vue/no-parsing-error": [2, {
|
||||
"x-invalid-end-tag": false
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1 @@
|
||||
# 默认依赖包指定淘宝源
|
||||
registry=https://registry.npm.taobao.org/
|
||||
sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
|
||||
phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/
|
||||
electron_mirror=https://npm.taobao.org/mirrors/electron/
|
||||
engine-strict=false
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
registry "https://registry.npmmirror.com"
|
||||
sass_binary_site "https://npmmirror.com/mirrors/node-sass"
|
||||
phantomjs_cdnurl "https://npmmirror.com/mirrors/phantomjs"
|
||||
electron_mirror "https://npmmirror.com/mirrors/electron"
|
||||
|
||||
--ignore-engines true
|
||||
|
||||
55
buyer/MIGRATION-VUE3.md
Normal file
55
buyer/MIGRATION-VUE3.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Buyer 买家端 Vue 3 迁移说明
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vue 3 + Vue Router 4 + Pinia(vuex 已移除,见下文"组合式 API 迁移")
|
||||
- Element Plus(替代 view-design / iView)
|
||||
- 保留 `$Message` / `$Modal` / `$Notice` 兼容层(`src/utils/message.js`)
|
||||
|
||||
## 启动
|
||||
|
||||
```bash
|
||||
cd buyer
|
||||
yarn install
|
||||
yarn dev # 默认端口 10000
|
||||
yarn build
|
||||
```
|
||||
|
||||
## 迁移要点
|
||||
|
||||
1. **入口** `src/main.js` 使用 `createApp`,全局过滤器改为 `$filters`
|
||||
2. **路由** `src/router/index.js` 使用 `createRouter` + 动态 `import()`
|
||||
3. **请求** `src/plugins/request.js` 使用 Element Plus `ElLoading`
|
||||
4. **iView 组件已全部替换**:轮播 `el-carousel`、下拉 `el-dropdown`、侧栏 `el-menu`、头像 `el-avatar` 等
|
||||
5. **图标映射** `src/utils/iconMap.js` — 动态图标(如侧边栏菜单)通过 `resolveIcon()` 使用
|
||||
6. **自定义字体图标** `icomoon` 保留为 `<i class="icomoon icon-*">`(购物车步骤条、钱包等)
|
||||
7. **首页装修** `indexDecorate/modelList` 保留买家端展示逻辑(非 manager 编辑版)
|
||||
8. **特殊替换**
|
||||
- `vue-piczoom` → `el-image` 预览
|
||||
- `mv-count-down` → 内置倒计时
|
||||
- 秒杀轮播 → 横向滚动列表
|
||||
|
||||
## 组合式 API 迁移(2026-10 完成)
|
||||
|
||||
全部 95 个 `.vue` 组件已从 Options API 转换为 `<script setup>`,同时完成 vuex → pinia:
|
||||
|
||||
- **状态管理**:`src/stores/app.js`(`useAppStore`:navList / cartNum / unreadMsg / logoImg / siteName / hotWordsList / category + 对应 setXxx actions)。原 `src/vuex/` 已删除,package.json 不再依赖 vuex。
|
||||
- **路由守卫**:`beforeRouteEnter` 无组合式等价物,5 个页面(`GoodsList` / `GoodsDetail` / `PointGoodsDetail` / `Cart` / `ShoppingCart`)保留普通 `<script>` 块存放 `export default { name, beforeRouteEnter }`——这是官方支持的合并写法,统计 Options API 残留时需排除。
|
||||
- **defineExpose**:以下组件的方法/状态被父组件通过模板 ref 调用,已显式暴露:`verify`(init / close)、`multiple-map`(open)、`addressManage`(show / hide)、`invoiceModal`(invoiceAvailable)。新增此类父子调用时必须加 `defineExpose`。
|
||||
- **模板 ref 与响应式变量同名会互相覆盖**:同名时模板 ref 统一改名 `xxxRef`(如 `formRef` / `verifyRef`)。
|
||||
- **mixin 已消除**:原 `components/mixes/talkIm.vue` 的 IM 客服逻辑已内联进 `Merchant.vue` 和 `goodsDetail/StoreSide.vue`,文件已删除。
|
||||
- **全局属性照旧**:模板中的 `$filters.*` / `linkTo` / `connectCs` / `$inputMaxLength` 是 `main.js` 注册的全局属性,`<script setup>` 下模板可直接使用;script 内使用 `$filters` 函数需从 `@/plugins/filters.js` 具名导入,`this.Cookies` 等同 `storage`(`@/plugins/storage`)。
|
||||
- **其余三端(seller / manager / im)仍为 Options API + vuex**,迁移时参照本端模式。
|
||||
|
||||
## 批量脚本
|
||||
|
||||
- `scripts/migrate-iview-to-element.js` — iView → Element 模板替换
|
||||
- `scripts/fix-filters.js` — Vue 2 过滤器语法修复
|
||||
- `scripts/replace-page.js` — `<Page>` → `el-pagination`
|
||||
- `scripts/replace-icon.js` — 静态 `<Icon type>` → `el-icon`
|
||||
- `scripts/fix-el-icon-click.js` — 修复 `@click` 绑定
|
||||
- `scripts/fix-broken-tags.js` — 闭合标签修复
|
||||
|
||||
## 上线前检查
|
||||
|
||||
四端统一清单(认证、axios、Element Plus、分端专项):[../MIGRATION-VUE3.md](../MIGRATION-VUE3.md)
|
||||
@@ -1,19 +0,0 @@
|
||||
# new
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
||||
36
buyer/eslint.config.js
Normal file
36
buyer/eslint.config.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const globals = require('globals')
|
||||
const pluginVue = require('eslint-plugin-vue')
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
ignores: [
|
||||
'dist/**',
|
||||
'node_modules/**',
|
||||
'build/**',
|
||||
'config/**',
|
||||
'src/scripts/**', // 一次性迁移脚本,不参与 lint
|
||||
],
|
||||
},
|
||||
...pluginVue.configs['flat/essential'],
|
||||
{
|
||||
files: ['src/**/*.{js,vue}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||
'no-empty': ['error', { allowEmptyCatch: false }],
|
||||
'vue/no-parsing-error': ['error', { 'x-invalid-end-tag': false }],
|
||||
// 页面组件通过路由按文件路径引用,组件名不参与解析,存量单词组件名不再强制多词
|
||||
'vue/multi-word-component-names': 'off',
|
||||
// 存量代码大量使用与 HTML 保留名同名的组件(element-plus 图标 Search/View/Camera/Link 及全局注册的 empty/drawer 等),重命名成本高,暂不启用
|
||||
'vue/no-reserved-component-names': 'off',
|
||||
// 全端已完成 Options API → <script setup> 迁移,防止新代码回退(路由守卫的普通 script 块等有意保留的除外,故用 warn)
|
||||
'vue/component-api-style': ['warn', ['script-setup']],
|
||||
},
|
||||
},
|
||||
]
|
||||
11525
buyer/package-lock.json
generated
11525
buyer/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,42 +3,54 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vue-cli-service serve",
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build"
|
||||
"build": "vue-cli-service build",
|
||||
"dev": "vue-cli-service serve",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amap/amap-jsapi-loader": "0.0.7",
|
||||
"axios": "^0.19.2",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"axios": "^1.7.9",
|
||||
"core-js": "^3.49.0",
|
||||
"dplayer": "^1.27.1",
|
||||
"element-plus": "^2.14.1",
|
||||
"js-cookie": "^2.2.1",
|
||||
"less": "^2.7.0",
|
||||
"less-loader": "^5.0.0",
|
||||
"mv-count-down": "^0.1.15",
|
||||
"node-sass": "^4.14.1",
|
||||
"postcss-loader": "^7.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^4.0.3",
|
||||
"psl": "^1.8.0",
|
||||
"qs": "^6.9.4",
|
||||
"sass-loader": "^7.3.1",
|
||||
"sass": "^1.63.6",
|
||||
"sass-loader": "^10.4.1",
|
||||
"uuid": "^8.3.2",
|
||||
"view-design": "^4.3.2",
|
||||
"vue": "^2.6.11",
|
||||
"vue-awesome-swiper": "^3.1.3",
|
||||
"vue-piczoom": "^1.0.6",
|
||||
"vue-qr": "^2.3.0",
|
||||
"vue-router": "^3.0.1",
|
||||
"vuex": "^3.0.1"
|
||||
"vue": "^3.5.35",
|
||||
"vue-qr": "^5.0.3",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"compression-webpack-plugin": "^5.0.0",
|
||||
"sass-loader": "^7.3.1",
|
||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
"@vue/cli-plugin-babel": "^5.0.9",
|
||||
"@vue/cli-service": "^5.0.9",
|
||||
"@vue/compiler-sfc": "^3.5.35",
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-vue": "^10.11.1",
|
||||
"globals": "^17.13.0",
|
||||
"less": "^4.2.0",
|
||||
"less-loader": "^11.1.4",
|
||||
"vue-eslint-parser": "^10.4.1",
|
||||
"webpack": "^5.95.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
]
|
||||
],
|
||||
"resolutions": {
|
||||
"minimatch": "^3.1.2",
|
||||
"@achrinza/node-ipc": "9.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
8658
buyer/pnpm-lock.yaml
generated
Normal file
8658
buyer/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,15 +3,9 @@ var BASE = {
|
||||
* @description api请求基础路径
|
||||
*/
|
||||
API_DEV: {
|
||||
common: "https://common-api.pickmall.cn",
|
||||
buyer: "https://buyer-api.pickmall.cn",
|
||||
seller: "https://store-api.pickmall.cn",
|
||||
manager: "https://admin-api.pickmall.cn"
|
||||
},
|
||||
API_PROD: {
|
||||
common: "https://common-api.pickmall.cn",
|
||||
buyer: "https://buyer-api.pickmall.cn",
|
||||
seller: "https://store-api.pickmall.cn",
|
||||
manager: "https://admin-api.pickmall.cn"
|
||||
},
|
||||
};
|
||||
|
||||
@@ -7,20 +7,12 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>logo.ico">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
|
||||
<link rel="stylesheet" href="<%=css%>" />
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
|
||||
<script src="<%=js%>"></script>
|
||||
<% } %>
|
||||
<script src="/config.js"></script>
|
||||
<script src="<%= BASE_URL %>config.js"></script>
|
||||
<noscript>
|
||||
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
54
buyer/scripts/fix-broken-tags.js
Normal file
54
buyer/scripts/fix-broken-tags.js
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = path.join(__dirname, "..");
|
||||
|
||||
function walkDir(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walkDir(full, files);
|
||||
else if (entry.name.endsWith(".vue")) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function fix(content) {
|
||||
let c = content;
|
||||
c = c.replace(/<\/Button>/g, "</el-button>");
|
||||
c = c.replace(/<\/Button\b/g, "</el-button");
|
||||
c = c.replace(/<\/Input>/g, "</el-input>");
|
||||
c = c.replace(/<\/Input\b/g, "</el-input");
|
||||
c = c.replace(/<\/Option>/g, "</el-option>");
|
||||
c = c.replace(/<\/Option\b/g, "</el-option");
|
||||
c = c.replace(/<\/Checkbox>/g, "</el-checkbox>");
|
||||
c = c.replace(/<\/Checkbox\b/g, "</el-checkbox");
|
||||
c = c.replace(/<\/Select>/g, "</el-select>");
|
||||
c = c.replace(/<\/Select\b/g, "</el-select");
|
||||
c = c.replace(/<\/Form>/g, "</el-form>");
|
||||
c = c.replace(/<\/Modal>/g, "</el-dialog>");
|
||||
c = c.replace(/@click\.native/g, "@click");
|
||||
c = c.replace(/@on-search/g, "@keyup.enter");
|
||||
c = c.replace(/\bsearch\b(?=\s+enter-button)/g, "");
|
||||
c = c.replace(/\benter-button\b/g, "");
|
||||
c = c.replace(/<BreadcrumbItem/g, "<el-breadcrumb-item");
|
||||
c = c.replace(/<\/BreadcrumbItem>/g, "</el-breadcrumb-item>");
|
||||
c = c.replace(/<Breadcrumb/g, "<el-breadcrumb");
|
||||
c = c.replace(/<\/Breadcrumb>/g, "</el-breadcrumb>");
|
||||
c = c.replace(/type="error"/g, 'type="danger"');
|
||||
c = c.replace(/<i-col/g, "<div");
|
||||
c = c.replace(/<\/i-col>/g, "</div>");
|
||||
c = c.replace(/\{\{([^}|]+)\s*\|\s*secrecyMobile\s*\}\}/g, "{{ $filters.secrecyMobile($1) }}");
|
||||
return c;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const file of walkDir(path.join(ROOT, "src"))) {
|
||||
const original = fs.readFileSync(file, "utf8");
|
||||
const fixed = fix(original);
|
||||
if (fixed !== original) {
|
||||
fs.writeFileSync(file, fixed);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
console.log(`Fixed tags in ${count} files`);
|
||||
52
buyer/scripts/fix-dialog-slots.js
Normal file
52
buyer/scripts/fix-dialog-slots.js
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = path.join(__dirname, "..");
|
||||
|
||||
function walkDir(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walkDir(full, files);
|
||||
else if (entry.name.endsWith(".vue")) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function fix(content) {
|
||||
let c = content;
|
||||
|
||||
c = c.replace(/<p #header>/g, "<template #header><p>");
|
||||
c = c.replace(
|
||||
/(<template #header><p>[\s\S]*?<\/p>)(\s*<div)/g,
|
||||
"$1</template>$2"
|
||||
);
|
||||
|
||||
c = c.replace(
|
||||
/<template #footer><div([^>]*)>([\s\S]*?)<\/div><\/template><\/el-dialog>/g,
|
||||
"<template #footer><div$1>$2</div></template>\n </el-dialog>"
|
||||
);
|
||||
|
||||
c = c.replace(
|
||||
/<template #default\s*>([\s\S]*?)<\/template\s*>/g,
|
||||
(match, inner) => {
|
||||
if (match.includes("el-alert")) {
|
||||
return `<template #title>${inner}</template>`;
|
||||
}
|
||||
return match;
|
||||
}
|
||||
);
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const file of walkDir(path.join(ROOT, "src"))) {
|
||||
const original = fs.readFileSync(file, "utf8");
|
||||
const fixed = fix(original);
|
||||
if (fixed !== original) {
|
||||
fs.writeFileSync(file, fixed);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
console.log(`Fixed dialog slots in ${count} files`);
|
||||
26
buyer/scripts/fix-duplicate-size.js
Normal file
26
buyer/scripts/fix-duplicate-size.js
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "src");
|
||||
|
||||
function walk(dir, files = []) {
|
||||
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const f = path.join(dir, e.name);
|
||||
if (e.isDirectory()) walk(f, files);
|
||||
else if (e.name.endsWith(".vue")) files.push(f);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
let n = 0;
|
||||
for (const file of walk(ROOT)) {
|
||||
let c = fs.readFileSync(file, "utf8");
|
||||
const next = c.replace(/:size="(\d+)"\s+:size="\1"/g, ':size="$1"');
|
||||
if (next !== c) {
|
||||
fs.writeFileSync(file, next);
|
||||
n++;
|
||||
console.log(path.relative(ROOT, file));
|
||||
}
|
||||
}
|
||||
console.log(`Fixed duplicate :size in ${n} files`);
|
||||
28
buyer/scripts/fix-el-icon-click.js
Normal file
28
buyer/scripts/fix-el-icon-click.js
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "src");
|
||||
|
||||
function walk(dir, files = []) {
|
||||
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const f = path.join(dir, e.name);
|
||||
if (e.isDirectory()) walk(f, files);
|
||||
else if (e.name.endsWith(".vue")) files.push(f);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
let n = 0;
|
||||
for (const file of walk(ROOT)) {
|
||||
let c = fs.readFileSync(file, "utf8");
|
||||
const next = c
|
||||
.replace(/<el-icon([^>]*?):click=/g, "<el-icon$1@click=")
|
||||
.replace(/:class="refresh"/g, 'class="refresh"')
|
||||
.replace(/:size="20" @click="show = false" :size="20"/g, ':size="20" @click="show = false"');
|
||||
if (next !== c) {
|
||||
fs.writeFileSync(file, next);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
console.log(`Fixed el-icon click in ${n} files`);
|
||||
77
buyer/scripts/fix-filters.js
Normal file
77
buyer/scripts/fix-filters.js
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = path.join(__dirname, "..");
|
||||
|
||||
function walkDir(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walkDir(full, files);
|
||||
else if (entry.name.endsWith(".vue")) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function fixFilters(content) {
|
||||
let c = content;
|
||||
|
||||
// Fix broken: {{ expr {{ $filters.unitPrice(args) }}
|
||||
c = c.replace(
|
||||
/\{\{\s*([^}{]+?)\s*\{\{\s*\$filters\.unitPrice\(([^)]*)\)\s*\}\}/g,
|
||||
"{{ $filters.unitPrice($1, $2) }}"
|
||||
);
|
||||
|
||||
// Fix: {{ expr | unitPrice }} or {{ expr | unitPrice('¥') }}
|
||||
c = c.replace(
|
||||
/\{\{\s*([^}|]+?)\s*\|\s*unitPrice(?:\(([^)]*)\))?\s*\}\}/g,
|
||||
(_, expr, args) => {
|
||||
const e = expr.trim();
|
||||
return args !== undefined
|
||||
? `{{ $filters.unitPrice(${e}, ${args}) }}`
|
||||
: `{{ $filters.unitPrice(${e}) }}`;
|
||||
}
|
||||
);
|
||||
|
||||
// Fix inline without braces: expr | unitPrice("¥")
|
||||
c = c.replace(
|
||||
/(?<!\{\{)\s*([a-zA-Z0-9_.?()[\]'"\s]+?)\s*\|\s*unitPrice\(([^)]*)\)/g,
|
||||
(match, expr, args) => {
|
||||
if (match.includes("{{")) return match;
|
||||
return `{{ $filters.unitPrice(${expr.trim()}, ${args}) }}`;
|
||||
}
|
||||
);
|
||||
|
||||
// Fix inline without args: expr | unitPrice
|
||||
c = c.replace(
|
||||
/(?<!\{\{)\s*([a-zA-Z0-9_.?()[\]'"\s]+?)\s*\|\s*unitPrice(?!\()/g,
|
||||
(match, expr) => {
|
||||
if (match.includes("{{") || match.includes("$filters")) return match;
|
||||
return `{{ $filters.unitPrice(${expr.trim()}) }}`;
|
||||
}
|
||||
);
|
||||
|
||||
// Fix unixToDate filter
|
||||
c = c.replace(
|
||||
/\{\{\s*([^}|]+?)\s*\|\s*unixToDate(?:\(([^)]*)\))?\s*\}\}/g,
|
||||
(_, expr, args) => {
|
||||
const e = expr.trim();
|
||||
return args !== undefined
|
||||
? `{{ $filters.unixToDate(${e}, ${args}) }}`
|
||||
: `{{ $filters.unixToDate(${e}) }}`;
|
||||
}
|
||||
);
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const file of walkDir(path.join(ROOT, "src"))) {
|
||||
const original = fs.readFileSync(file, "utf8");
|
||||
const fixed = fixFilters(original);
|
||||
if (fixed !== original) {
|
||||
fs.writeFileSync(file, fixed);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
console.log(`Fixed filters in ${count} files`);
|
||||
55
buyer/scripts/fix-runtime-compat.js
Normal file
55
buyer/scripts/fix-runtime-compat.js
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = path.join(__dirname, "..");
|
||||
|
||||
function walkDir(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walkDir(full, files);
|
||||
else if (entry.name.endsWith(".vue")) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function fix(content) {
|
||||
let c = content;
|
||||
|
||||
c = c.replace(/<el-pagination\b/g, "<Page");
|
||||
c = c.replace(/<\/el-pagination>/g, "</Page>");
|
||||
|
||||
c = c.replace(
|
||||
/<Icon([^>]*)\s+#prepend\s*>\s*<\/Icon>/g,
|
||||
"<template #prepend><Icon$1></Icon></template>"
|
||||
);
|
||||
c = c.replace(
|
||||
/<Icon([^>]*)\s+#prepend\s*>\s*<\/Icon>/g,
|
||||
"<template #prepend><Icon$1></Icon></template>"
|
||||
);
|
||||
c = c.replace(
|
||||
/<template #prepend><Icon([^>]*)\s*\/>/g,
|
||||
"<template #prepend><Icon$1></Icon></template>"
|
||||
);
|
||||
|
||||
c = c.replace(/@keyup\.enter\.native/g, "@keyup.enter");
|
||||
c = c.replace(/slot="append"/g, "#append");
|
||||
c = c.replace(/(<div[^>]*)\s+#append/g, "<template #append>$1");
|
||||
|
||||
c = c.replace(/(<el-button[^>]*)\slong/g, '$1 style="width:100%"');
|
||||
c = c.replace(/@on-select/g, "@select");
|
||||
c = c.replace(/@on-click/g, "@click");
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const file of walkDir(path.join(ROOT, "src"))) {
|
||||
const original = fs.readFileSync(file, "utf8");
|
||||
const fixed = fix(original);
|
||||
if (fixed !== original) {
|
||||
fs.writeFileSync(file, fixed);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
console.log(`Runtime compat fixes in ${count} files`);
|
||||
54
buyer/scripts/fix-slots.js
Normal file
54
buyer/scripts/fix-slots.js
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = path.join(__dirname, "..");
|
||||
|
||||
function walkDir(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walkDir(full, files);
|
||||
else if (entry.name.endsWith(".vue")) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function fix(content) {
|
||||
let c = content;
|
||||
|
||||
c = c.replace(
|
||||
/<template #default="(\{[^"]+\})" #([a-zA-Z0-9_-]+)>/g,
|
||||
'<template #$2="$1">'
|
||||
);
|
||||
|
||||
c = c.replace(
|
||||
/<div([^>]*) #footer([^>]*)>/g,
|
||||
'<template #footer><div$1$2>'
|
||||
);
|
||||
c = c.replace(/<\/div>\s*(?=<\/el-dialog>)/g, "</div></template>");
|
||||
|
||||
c = c.replace(/<span #append>/g, "<template #append><span>");
|
||||
c = c.replace(/<\/span><\/el-input/g, "</span></template></el-input");
|
||||
c = c.replace(/><span #append>/g, "><template #append><span>");
|
||||
|
||||
c = c.replace(/<el-button #append/g, "<template #append><el-button");
|
||||
c = c.replace(
|
||||
/<\/el-button>(\s*<\/el-input>)/g,
|
||||
"</el-button></template>$1"
|
||||
);
|
||||
|
||||
c = c.replace(/<template #desc\b/g, "<template #default");
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const file of walkDir(path.join(ROOT, "src"))) {
|
||||
const original = fs.readFileSync(file, "utf8");
|
||||
const fixed = fix(original);
|
||||
if (fixed !== original) {
|
||||
fs.writeFileSync(file, fixed);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
console.log(`Fixed slots in ${count} files`);
|
||||
46
buyer/scripts/fix-span-tags.js
Normal file
46
buyer/scripts/fix-span-tags.js
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = path.join(__dirname, "..");
|
||||
|
||||
function walkDir(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walkDir(full, files);
|
||||
else if (entry.name.endsWith(".vue")) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function fix(content) {
|
||||
let c = content;
|
||||
c = c.replace(/<\/span\s*\n\s*>/g, "</span>");
|
||||
c = c.replace(/<\/span\s+>/g, "</span>");
|
||||
c = c.replace(/<i-input/g, "<el-input");
|
||||
c = c.replace(/<\/i-input>/g, "</el-input>");
|
||||
c = c.replace(
|
||||
/<Icon([^>]*)\s+#prepend\s*\/>/g,
|
||||
"<template #prepend><Icon$1 /></template>"
|
||||
);
|
||||
c = c.replace(
|
||||
/<template #append><el-button([^>]*)>([\s\S]*?)<\/el-button>\s*<\/i-input>/g,
|
||||
"<template #append><el-button$1>$2</el-button></template></el-input>"
|
||||
);
|
||||
c = c.replace(
|
||||
/<template #append><el-button([^>]*)>([\s\S]*?)<\/el-button>\s*<\/el-input>/g,
|
||||
"<template #append><el-button$1>$2</el-button></template></el-input>"
|
||||
);
|
||||
return c;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const file of walkDir(path.join(ROOT, "src"))) {
|
||||
const original = fs.readFileSync(file, "utf8");
|
||||
const fixed = fix(original);
|
||||
if (fixed !== original) {
|
||||
fs.writeFileSync(file, fixed);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
console.log(`Fixed span/input tags in ${count} files`);
|
||||
55
buyer/scripts/fix-stray-templates.js
Normal file
55
buyer/scripts/fix-stray-templates.js
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = path.join(__dirname, "..");
|
||||
|
||||
function walkDir(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walkDir(full, files);
|
||||
else if (entry.name.endsWith(".vue")) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function fix(content) {
|
||||
let c = content;
|
||||
|
||||
c = c.replace(
|
||||
/(<div[^>]*)\s+#content>/g,
|
||||
"<template #content><div$1>"
|
||||
);
|
||||
c = c.replace(
|
||||
/(<\/el-tooltip>)/g,
|
||||
(match, _m, offset, str) => {
|
||||
return match;
|
||||
}
|
||||
);
|
||||
|
||||
c = c.replace(
|
||||
/(<div style="white-space:[^"]*"[^>]*>[\s\S]*?<\/div>)(\s*<\/el-tooltip>)/g,
|
||||
"$1</template>$2"
|
||||
);
|
||||
|
||||
c = c.replace(/<\/div><\/template><\/el-dialog>/g, (m, offset, str) => {
|
||||
const before = str.slice(Math.max(0, offset - 200), offset);
|
||||
if (before.includes("#footer")) return m;
|
||||
return "</div></el-dialog>";
|
||||
});
|
||||
|
||||
c = c.replace(/<i(\s[^>]*)\/>/g, "<i$1></i>");
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const file of walkDir(path.join(ROOT, "src"))) {
|
||||
const original = fs.readFileSync(file, "utf8");
|
||||
const fixed = fix(original);
|
||||
if (fixed !== original) {
|
||||
fs.writeFileSync(file, fixed);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
console.log(`Fixed stray templates in ${count} files`);
|
||||
227
buyer/scripts/migrate-iview-to-element.js
Normal file
227
buyer/scripts/migrate-iview-to-element.js
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Migrate buyer Vue files from iView to Element Plus.
|
||||
* Usage: node scripts/migrate-iview-to-element.js
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const BUYER_ROOT = path.join(__dirname, "..");
|
||||
const MANAGER_ROOT = path.join(__dirname, "../../manager");
|
||||
|
||||
const MODEL_LIST = [
|
||||
["Carousel.vue", "carousel.vue"],
|
||||
["Carousel1.vue", "carousel1.vue"],
|
||||
["Carousel2.vue", "carousel2.vue"],
|
||||
["FirstPageAdvert.vue", "firstPageAdvert.vue"],
|
||||
["NewGoodsSort.vue", "newGoodsSort.vue"],
|
||||
["NotEnough.vue", "notEnough.vue"],
|
||||
["Recommend.vue", "recommend.vue"],
|
||||
["Seckill.vue", "seckill.vue"],
|
||||
["forYour.vue", "forYour.vue"],
|
||||
["goodsAndType.vue", "goodsAndType.vue"],
|
||||
["mixModel.vue", "mixModel.vue"],
|
||||
["oneRowThreeColumns.vue", "oneRowThreeColumns.vue"],
|
||||
["onlyGoodsModel.vue", "onlyGoodsModel.vue"],
|
||||
["mixs/mix-brand.vue", "mixs/mix-brand.vue"],
|
||||
["mixs/mix-goods.vue", "mixs/mix-goods.vue"],
|
||||
];
|
||||
|
||||
const DECORATE_COPY_MAP = [
|
||||
[
|
||||
"src/components/indexDecorate/ModelForm.vue",
|
||||
"src/views/page-decoration/modelForm.vue",
|
||||
],
|
||||
[
|
||||
"src/components/indexDecorate/ModelFormItem.vue",
|
||||
"src/views/page-decoration/modelFormItem.vue",
|
||||
],
|
||||
];
|
||||
|
||||
MODEL_LIST.forEach(([buyerName, managerName]) => {
|
||||
DECORATE_COPY_MAP.push([
|
||||
`src/components/indexDecorate/modelList/${buyerName}`,
|
||||
`src/views/page-decoration/modelList/${managerName}`,
|
||||
]);
|
||||
});
|
||||
|
||||
const TARGET_DIRS = ["src/pages", "src/components"];
|
||||
|
||||
function adaptBuyerContent(content) {
|
||||
return content
|
||||
.replace(/@\/views\/page-decoration\//g, "@/components/indexDecorate/")
|
||||
.replace(/admin-setting/g, "buyer-setting")
|
||||
.replace(/adminPCPageCache/g, "buyerPCPageCache")
|
||||
.replace(/getStore\('adminPCPageCache'\)/g, "getStore('buyerPCPageCache')")
|
||||
.replace(/setStore\('adminPCPageCache'/g, "setStore('buyerPCPageCache'");
|
||||
}
|
||||
|
||||
function applyScriptTransforms(content) {
|
||||
let c = content;
|
||||
c = c.replace(/\$options\.filters\./g, "$filters.");
|
||||
c = c.replace(/\|\s*unitPrice\(/g, "{{ $filters.unitPrice(");
|
||||
c = c.replace(/\|\s*unixToDate\(/g, "{{ $filters.unixToDate(");
|
||||
c = c.replace(/\bbeforeDestroy\b/g, "beforeUnmount");
|
||||
c = c.replace(/this\.\$set\(([^,]+),\s*([^,]+),\s*([^)]+)\)/g, "$1[$2] = $3");
|
||||
c = c.replace(/::v-deep/g, ":deep");
|
||||
c = c.replace(/\/deep\//g, ":deep");
|
||||
c = c.replace(/@on-change/g, "@change");
|
||||
c = c.replace(/@on-click/g, "@click");
|
||||
c = c.replace(/@on-ok/g, "@ok");
|
||||
c = c.replace(/@on-cancel/g, "@cancel");
|
||||
c = c.replace(/@on-clear/g, "@clear");
|
||||
c = c.replace(/@on-open-change/g, "@visible-change");
|
||||
c = c.replace(/@on-page-size-change/g, "@size-change");
|
||||
c = c.replace(/@on-select/g, "@select");
|
||||
c = c.replace(/@keydown\.enter\.native/g, "@keyup.enter");
|
||||
c = c.replace(/slot-scope="([^"]+)"/g, '#default="$1"');
|
||||
c = c.replace(/slot="([^"]+)"/g, '#$1');
|
||||
c = c.replace(/from ['"]view-design['"]/g, 'from "@/utils/message"');
|
||||
c = c.replace(/import\s*\{([^}]*)\}\s*from\s*['"]view-design['"]/g, 'import {$1} from "@/utils/message"');
|
||||
return c;
|
||||
}
|
||||
|
||||
function applyTemplateTransforms(content) {
|
||||
let c = content;
|
||||
const replacements = [
|
||||
[/<\/?Card>/g, (m) => m.replace("Card", "el-card")],
|
||||
[/<Form-item/g, "<el-form-item"],
|
||||
[/<\/Form-item>/g, "</el-form-item>"],
|
||||
[/<FormItem/g, "<el-form-item"],
|
||||
[/<\/FormItem>/g, "</el-form-item>"],
|
||||
[/<Form(\s|>)/g, "<el-form$1"],
|
||||
[/<\/Form>/g, "</el-form>"],
|
||||
[/<InputNumber/g, "<el-input-number"],
|
||||
[/<\/InputNumber>/g, "</el-input-number>"],
|
||||
[/<Input(\s|>)/g, "<el-input$1"],
|
||||
[/<\/Input>/g, "</el-input>"],
|
||||
[/<Button(\s|>)/g, "<el-button$1"],
|
||||
[/<\/Button>/g, "</el-button>"],
|
||||
[/<Select(\s|>)/g, "<el-select$1"],
|
||||
[/<\/Select>/g, "</el-select>"],
|
||||
[/<Option(\s)/g, "<el-option "],
|
||||
[/<\/Option>/g, "</el-option>"],
|
||||
[/<DatePicker/g, "<el-date-picker"],
|
||||
[/<\/DatePicker>/g, "</el-date-picker>"],
|
||||
[/<TimePicker/g, "<el-time-picker"],
|
||||
[/<\/TimePicker>/g, "</el-time-picker>"],
|
||||
[/<Modal/g, "<el-dialog"],
|
||||
[/<\/Modal>/g, "</el-dialog>"],
|
||||
[/<Page(\s)/g, "<el-pagination "],
|
||||
[/<\/Page>/g, "</el-pagination>"],
|
||||
[/<Tabs(\s|>)/g, "<el-tabs$1"],
|
||||
[/<\/Tabs>/g, "</el-tabs>"],
|
||||
[/<TabPane/g, "<el-tab-pane"],
|
||||
[/<\/TabPane>/g, "</el-tab-pane>"],
|
||||
[/<Row(\s|>)/g, "<div$1"],
|
||||
[/<\/Row>/g, "</div>"],
|
||||
[/<Col(\s|>)/g, "<div$1"],
|
||||
[/<\/Col>/g, "</div>"],
|
||||
[/<Tag(\s|>)/g, "<el-tag$1"],
|
||||
[/<\/Tag>/g, "</el-tag>"],
|
||||
[/<Switch(\s|>)/g, "<el-switch$1"],
|
||||
[/<\/Switch>/g, "</el-switch>"],
|
||||
[/<RadioGroup/g, "<el-radio-group"],
|
||||
[/<\/RadioGroup>/g, "</el-radio-group>"],
|
||||
[/<Radio(\s)/g, "<el-radio "],
|
||||
[/<\/Radio>/g, "</el-radio>"],
|
||||
[/<CheckboxGroup/g, "<el-checkbox-group"],
|
||||
[/<\/CheckboxGroup>/g, "</el-checkbox-group>"],
|
||||
[/<Checkbox/g, "<el-checkbox"],
|
||||
[/<\/Checkbox>/g, "</el-checkbox>"],
|
||||
[/<Poptip/g, "<el-popover"],
|
||||
[/<\/Poptip>/g, "</el-popover>"],
|
||||
[/<Tooltip/g, "<el-tooltip"],
|
||||
[/<\/Tooltip>/g, "</el-tooltip>"],
|
||||
[/<Drawer/g, "<el-drawer"],
|
||||
[/<\/Drawer>/g, "</el-drawer>"],
|
||||
[/<Alert/g, "<el-alert"],
|
||||
[/<\/Alert>/g, "</el-alert>"],
|
||||
[/<Badge/g, "<el-badge"],
|
||||
[/<\/Badge>/g, "</el-badge>"],
|
||||
[/<Divider/g, "<el-divider"],
|
||||
[/<\/Divider>/g, "</el-divider>"],
|
||||
[/<Rate/g, "<el-rate"],
|
||||
[/<\/Rate>/g, "</el-rate>"],
|
||||
[/<Steps/g, "<el-steps"],
|
||||
[/<\/Steps>/g, "</el-steps>"],
|
||||
[/<Step(\s)/g, "<el-step "],
|
||||
[/<\/Step>/g, "</el-step>"],
|
||||
[/<Upload/g, "<el-upload"],
|
||||
[/<\/Upload>/g, "</el-upload>"],
|
||||
[/<Cascader/g, "<el-cascader"],
|
||||
[/<\/Cascader>/g, "</el-cascader>"],
|
||||
[/<Affix/g, "<el-affix"],
|
||||
[/<\/Affix>/g, "</el-affix>"],
|
||||
[/<Spin/g, "<el-skeleton"],
|
||||
[/<\/Spin>/g, "</el-skeleton>"],
|
||||
[/:label-width="(\d+)"/g, 'label-width="$1px"'],
|
||||
[/:mask-closable="false"/g, ':close-on-click-modal="false"'],
|
||||
[/:width="(\d+)"/g, 'width="$1"'],
|
||||
[/type="primary"/g, 'type="primary"'],
|
||||
];
|
||||
for (const [pattern, replacement] of replacements) {
|
||||
c = c.replace(pattern, replacement);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
function walkDir(dir, files = []) {
|
||||
if (!fs.existsSync(dir)) return files;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkDir(full, files);
|
||||
} else if (entry.name.endsWith(".vue")) {
|
||||
files.push(full);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const copied = [];
|
||||
const transformed = [];
|
||||
const skipped = [];
|
||||
|
||||
for (const [buyerRel, managerRel] of DECORATE_COPY_MAP) {
|
||||
const managerPath = path.join(MANAGER_ROOT, managerRel);
|
||||
const buyerPath = path.join(BUYER_ROOT, buyerRel);
|
||||
if (!fs.existsSync(managerPath)) {
|
||||
skipped.push({ file: buyerRel, reason: "manager counterpart missing" });
|
||||
continue;
|
||||
}
|
||||
let content = fs.readFileSync(managerPath, "utf8");
|
||||
content = adaptBuyerContent(content);
|
||||
fs.mkdirSync(path.dirname(buyerPath), { recursive: true });
|
||||
fs.writeFileSync(buyerPath, content);
|
||||
copied.push(buyerRel);
|
||||
}
|
||||
|
||||
const copiedSet = new Set(copied);
|
||||
for (const dir of TARGET_DIRS) {
|
||||
const absDir = path.join(BUYER_ROOT, dir);
|
||||
for (const filePath of walkDir(absDir)) {
|
||||
const rel = path.relative(BUYER_ROOT, filePath).replace(/\\/g, "/");
|
||||
if (copiedSet.has(rel)) continue;
|
||||
|
||||
let content = fs.readFileSync(filePath, "utf8");
|
||||
const hasIView =
|
||||
/<(Card|Form|Form-item|FormItem|Table|Button|Modal|Page|Tabs|TabPane|Input|Select|Option|DatePicker|Row|Poptip|Drawer|Upload|Checkbox|Radio|Switch|Tag|Alert|Rate|Steps|Step|Spin|Affix|TimePicker|Cascader|Divider|Badge|InputNumber)\b/.test(
|
||||
content
|
||||
);
|
||||
if (!hasIView) {
|
||||
skipped.push({ file: rel, reason: "no iView components" });
|
||||
continue;
|
||||
}
|
||||
|
||||
content = applyTemplateTransforms(content);
|
||||
content = applyScriptTransforms(content);
|
||||
fs.writeFileSync(filePath, content);
|
||||
transformed.push(rel);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("=== Buyer Migration Summary ===");
|
||||
console.log(`Copied from manager: ${copied.length}`);
|
||||
console.log(`Script-transformed: ${transformed.length}`);
|
||||
console.log(`Skipped: ${skipped.length}`);
|
||||
149
buyer/scripts/replace-icon.js
Normal file
149
buyer/scripts/replace-icon.js
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 将静态 <Icon type="..." /> 替换为 <el-icon><Xxx /></el-icon>
|
||||
* 动态 :type / custom 属性保留,由人工或 resolveIcon 处理。
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "src");
|
||||
|
||||
const TYPE_TO_COMPONENT = {
|
||||
"md-checkmark": "Check",
|
||||
"md-close": "Close",
|
||||
"md-trash": "Delete",
|
||||
"md-add": "Plus",
|
||||
"md-arrow-dropdown": "ArrowDown",
|
||||
"md-arrow-dropup": "ArrowUp",
|
||||
"md-person": "User",
|
||||
"md-lock": "Lock",
|
||||
"md-phone-portrait": "Iphone",
|
||||
"md-key": "Key",
|
||||
"md-link": "Link",
|
||||
"md-refresh": "Refresh",
|
||||
"md-qr-scanner": "Iphone",
|
||||
"ios-arrow-down": "ArrowDown",
|
||||
"ios-arrow-up": "ArrowUp",
|
||||
"ios-arrow-forward": "ArrowRight",
|
||||
"ios-arrow-back": "ArrowLeft",
|
||||
"ios-arrow-round-down": "ArrowDown",
|
||||
"ios-arrow-round-forward": "ArrowRight",
|
||||
"ios-cart-outline": "ShoppingCart",
|
||||
"ios-camera": "Camera",
|
||||
"ios-eye-outline": "View",
|
||||
"ios-trash-outline": "Delete",
|
||||
"ios-alert-outline": "Warning",
|
||||
"ios-help-circle-outline": "Help",
|
||||
"ios-add-circle-outline": "Plus",
|
||||
"ios-information-circle": "InfoFilled",
|
||||
"ios-search": "Search",
|
||||
"ios-text-outline": "Document",
|
||||
"ios-alarm-outline": "AlarmClock",
|
||||
"ios-heart": "StarFilled",
|
||||
"md-close-circle": "CircleClose",
|
||||
edit: "Edit",
|
||||
"trash-a": "Delete",
|
||||
location: "Location",
|
||||
};
|
||||
|
||||
function walkDir(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory() && entry.name !== "compat") walkDir(full, files);
|
||||
else if (entry.name.endsWith(".vue")) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function parseAttrs(attrStr) {
|
||||
const attrs = {};
|
||||
const re = /(?::([\w-]+)|@([\w-]+)|([\w-]+))=(?:"([^"]*)"|'([^']*)')|(?::([\w-]+)|@([\w-]+)|([\w-]+))(?=\s|\/|>)/g;
|
||||
let m;
|
||||
while ((m = re.exec(attrStr)) !== null) {
|
||||
const name = m[1] || m[2] || m[3] || m[6] || m[7] || m[8];
|
||||
const value = m[4] ?? m[5] ?? true;
|
||||
attrs[name] = value;
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function attrsToString(attrs, exclude = []) {
|
||||
return Object.entries(attrs)
|
||||
.filter(([k]) => !exclude.includes(k))
|
||||
.map(([k, v]) => {
|
||||
if (v === true) return k;
|
||||
if (k.startsWith("@")) return `${k}="${v}"`;
|
||||
return `:${k}="${v}"`;
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function transform(content, filePath) {
|
||||
if (filePath.includes("/compat/")) return content;
|
||||
let c = content;
|
||||
const used = new Set();
|
||||
|
||||
c = c.replace(/<Icon\b([^>]*?)\/>/g, (match, attrStr) => {
|
||||
if (/\bcustom=/.test(attrStr) || /:type=/.test(attrStr)) return match;
|
||||
const attrs = parseAttrs(attrStr);
|
||||
const type = attrs.type;
|
||||
if (!type || !TYPE_TO_COMPONENT[type]) return match;
|
||||
const comp = TYPE_TO_COMPONENT[type];
|
||||
used.add(comp);
|
||||
const rest = attrsToString(attrs, ["type"]);
|
||||
const size = attrs.size ? ` :size="${attrs.size}"` : "";
|
||||
const open = rest ? `<el-icon ${rest}${size}>` : `<el-icon${size}>`;
|
||||
return `${open}<${comp} /></el-icon>`;
|
||||
});
|
||||
|
||||
c = c.replace(/<Icon\b([^>]*?)><\/Icon>/g, (match, attrStr) => {
|
||||
if (/\bcustom=/.test(attrStr) || /:type=/.test(attrStr)) return match;
|
||||
const attrs = parseAttrs(attrStr);
|
||||
const type = attrs.type;
|
||||
if (!type || !TYPE_TO_COMPONENT[type]) return match;
|
||||
const comp = TYPE_TO_COMPONENT[type];
|
||||
used.add(comp);
|
||||
const rest = attrsToString(attrs, ["type"]);
|
||||
const size = attrs.size ? ` :size="${attrs.size}"` : "";
|
||||
const open = rest ? `<el-icon ${rest}${size}>` : `<el-icon${size}>`;
|
||||
return `${open}<${comp} /></el-icon>`;
|
||||
});
|
||||
|
||||
if (used.size && !c.includes("@element-plus/icons-vue")) {
|
||||
const imports = [...used].sort().join(", ");
|
||||
c = c.replace(
|
||||
/(<script[^>]*>\s*\n)/,
|
||||
`$1import { ${imports} } from '@element-plus/icons-vue';\n`
|
||||
);
|
||||
c = c.replace(/export default \{/, (m) => {
|
||||
if (/components:\s*\{/.test(c)) {
|
||||
return m;
|
||||
}
|
||||
return `${m}\n components: { ${imports} },`;
|
||||
});
|
||||
if (/components:\s*\{/.test(c) && !c.includes(`${[...used][0]},`)) {
|
||||
c = c.replace(/components:\s*\{([^}]*)\}/, (m, inner) => {
|
||||
const existing = inner.trim();
|
||||
const add = [...used].filter((n) => !inner.includes(n)).join(", ");
|
||||
if (!add) return m;
|
||||
const merged = existing ? `${existing}, ${add}` : add;
|
||||
return `components: { ${merged} }`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const file of walkDir(ROOT)) {
|
||||
const original = fs.readFileSync(file, "utf8");
|
||||
if (!/<Icon\b/.test(original)) continue;
|
||||
const fixed = transform(original, file);
|
||||
if (fixed !== original) {
|
||||
fs.writeFileSync(file, fixed);
|
||||
count++;
|
||||
console.log(path.relative(ROOT, file));
|
||||
}
|
||||
}
|
||||
console.log(`Icon replacements in ${count} files`);
|
||||
63
buyer/scripts/replace-page.js
Normal file
63
buyer/scripts/replace-page.js
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 将全局 <Page> 封装替换为原生 el-pagination
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "src");
|
||||
|
||||
function walkDir(dir, files = []) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walkDir(full, files);
|
||||
else if (entry.name.endsWith(".vue")) files.push(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function transformPage(content) {
|
||||
return content.replace(
|
||||
/<Page\b([^>]*?)(\/>|>(\s*)<\/Page>)/gs,
|
||||
(match, attrs, close) => {
|
||||
let a = attrs;
|
||||
const hasTotal = /\bshow-total\b/.test(a);
|
||||
const hasSizer = /\bshow-sizer\b/.test(a);
|
||||
const hasElevator = /\bshow-elevator\b/.test(a);
|
||||
|
||||
a = a.replace(/\s+show-total\b/g, "");
|
||||
a = a.replace(/\s+show-sizer\b/g, "");
|
||||
a = a.replace(/\s+show-elevator\b/g, "");
|
||||
a = a.replace(/:current=/g, ":current-page=");
|
||||
a = a.replace(/:page-size-opts=/g, ":page-sizes=");
|
||||
a = a.replace(/@change=/g, "@current-change=");
|
||||
a = a.replace(/\ssize="small"/g, " small");
|
||||
|
||||
const layoutParts = [];
|
||||
if (hasTotal) layoutParts.push("total");
|
||||
if (hasSizer) layoutParts.push("sizes");
|
||||
layoutParts.push("prev", "pager", "next");
|
||||
if (hasElevator) layoutParts.push("jumper");
|
||||
|
||||
const layoutAttr = ` layout="${layoutParts.join(", ")}"`;
|
||||
|
||||
if (close.startsWith("/>")) {
|
||||
return `<el-pagination${a}${layoutAttr} />`;
|
||||
}
|
||||
return `<el-pagination${a}${layoutAttr}></el-pagination>`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const file of walkDir(ROOT)) {
|
||||
const original = fs.readFileSync(file, "utf8");
|
||||
if (!/<Page\b/.test(original)) continue;
|
||||
const fixed = transformPage(original);
|
||||
if (fixed !== original) {
|
||||
fs.writeFileSync(file, fixed);
|
||||
count++;
|
||||
console.log(path.relative(ROOT, file));
|
||||
}
|
||||
}
|
||||
console.log(`Page → el-pagination in ${count} files`);
|
||||
@@ -4,60 +4,78 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import storage from "@/plugins/storage";
|
||||
import { getBaseSite } from "@/api/common.js";
|
||||
export default {
|
||||
name: "App",
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods:{
|
||||
init(){
|
||||
if(!storage.getItem("siteName")||!storage.getItem("logoImg")||!storage.getItem("sitelogo_expiration_time")) {
|
||||
this.getSite();
|
||||
}else{
|
||||
// 如果缓存过期,则获取最新的信息
|
||||
if (new Date() > storage.getItem("sitelogo_expiration_time")) {
|
||||
this.getSite();
|
||||
return;
|
||||
}else{
|
||||
window.document.title = storage.getItem("siteName");
|
||||
//动态获取icon
|
||||
let link =document.querySelector("link[rel*='icon']") ||document.createElement("link");
|
||||
link.type = "image/x-icon";
|
||||
link.href = storage.getItem("siteIcon");
|
||||
link.rel = "shortcut icon";
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
getSite(){
|
||||
//获取基本站点信息
|
||||
getBaseSite().then((res) => {
|
||||
if (res.success && res.result.settingValue) {
|
||||
let data = JSON.parse(res.result.settingValue);
|
||||
// 过期时间
|
||||
var expirationTime = new Date().setHours(new Date().getHours() + 1);
|
||||
// 存放过期时间
|
||||
storage.setItem("sitelogo_expiration_time", expirationTime);
|
||||
// 存放信息
|
||||
storage.setItem('siteName', data.siteName);
|
||||
storage.setItem('logoImg', data.buyerSideLogo);
|
||||
storage.setItem("siteIcon",data.buyerSideIcon);
|
||||
window.document.title = data.siteName;
|
||||
//动态获取icon
|
||||
let link =document.querySelector("link[rel*='icon']") ||document.createElement("link");
|
||||
link.type = "image/x-icon";
|
||||
link.href = data.buyerSideIcon;
|
||||
link.rel = "shortcut icon";
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
}
|
||||
});
|
||||
}
|
||||
import { getBaseSite, getThemeSetting } from "@/api/common.js";
|
||||
import { fetchAndApplyTheme } from "@/utils/theme";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
defineOptions({ name: "App" });
|
||||
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
|
||||
function applySiteInfo(data) {
|
||||
const expirationTime = new Date().setHours(new Date().getHours() + 1);
|
||||
storage.setItem("sitelogo_expiration_time", expirationTime);
|
||||
storage.setItem("siteName", data.siteName);
|
||||
storage.setItem("logoImg", data.buyerSideLogo);
|
||||
storage.setItem("siteIcon", data.buyerSideIcon);
|
||||
appStore.setSiteName(data.siteName);
|
||||
appStore.setLogoImg(data.buyerSideLogo);
|
||||
if (route.meta.title === undefined) {
|
||||
window.document.title = data.siteName;
|
||||
}
|
||||
};
|
||||
let link =
|
||||
document.querySelector("link[rel*='icon']") ||
|
||||
document.createElement("link");
|
||||
link.type = "image/x-icon";
|
||||
link.href = data.buyerSideIcon;
|
||||
link.rel = "shortcut icon";
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
}
|
||||
|
||||
function init() {
|
||||
const expirationTime = storage.getItem("sitelogo_expiration_time");
|
||||
const cacheValid =
|
||||
expirationTime && new Date() <= new Date(Number(expirationTime));
|
||||
if (cacheValid) {
|
||||
applySiteInfo({
|
||||
siteName: storage.getItem("siteName"),
|
||||
buyerSideLogo: storage.getItem("logoImg"),
|
||||
buyerSideIcon: storage.getItem("siteIcon"),
|
||||
});
|
||||
}
|
||||
getSite();
|
||||
getTheme();
|
||||
}
|
||||
|
||||
function getSite() {
|
||||
getBaseSite().then((res) => {
|
||||
if (res.success && res.result.settingValue) {
|
||||
try {
|
||||
const data = JSON.parse(res.result.settingValue);
|
||||
applySiteInfo(data);
|
||||
} catch {
|
||||
// 站点配置解析失败时使用缓存
|
||||
}
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.warn('获取站点配置失败', err);
|
||||
});
|
||||
}
|
||||
|
||||
function getTheme() {
|
||||
fetchAndApplyTheme(getThemeSetting).catch((err) => {
|
||||
console.warn('获取主题配置失败', err);
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#app {
|
||||
|
||||
@@ -44,7 +44,8 @@ export function editMemberInfo (params) {
|
||||
url: '/buyer/passport/member/editOwn',
|
||||
method: Method.PUT,
|
||||
needToken: true,
|
||||
data: params
|
||||
data: params,
|
||||
loading: false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,7 +55,9 @@ export function editPwd (params) {
|
||||
url: `/buyer/passport/member/modifyPass`,
|
||||
method: Method.PUT,
|
||||
needToken: true,
|
||||
data: params
|
||||
data: params,
|
||||
loading: false,
|
||||
skipMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import request, {
|
||||
Method, commonUrl
|
||||
Method, buyerUrl
|
||||
} from '@/plugins/request.js';
|
||||
|
||||
// 会员收货地址列表
|
||||
@@ -17,7 +17,8 @@ export function newMemberAddress (params) {
|
||||
url: '/buyer/member/address',
|
||||
needToken: true,
|
||||
method: Method.POST,
|
||||
data: params
|
||||
data: params,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,7 +28,8 @@ export function editMemberAddress (params) {
|
||||
url: '/buyer/member/address',
|
||||
needToken: true,
|
||||
method: Method.PUT,
|
||||
params
|
||||
params,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,7 +38,8 @@ export function delMemberAddress (id) {
|
||||
return request({
|
||||
url: `/buyer/member/address/delById/${id}`,
|
||||
needToken: true,
|
||||
method: Method.DELETE
|
||||
method: Method.DELETE,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -52,7 +55,7 @@ export function getAddrDetail (id) {
|
||||
// 传给后台citycode 获取城市街道等id
|
||||
export function handleRegion (params) {
|
||||
return request({
|
||||
url: `${commonUrl}/common/common/region/region`,
|
||||
url: `${buyerUrl}/buyer/common/region/region`,
|
||||
needToken: true,
|
||||
method: Method.GET,
|
||||
params
|
||||
|
||||
@@ -20,7 +20,8 @@ export function cartGoodsAll () {
|
||||
return request({
|
||||
url: '/buyer/trade/carts/all',
|
||||
method: Method.GET,
|
||||
needToken: true
|
||||
needToken: true,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ export function addCartGoods (params) {
|
||||
/**
|
||||
* 创建交易
|
||||
* @param client 客户端:H5/移动端 PC/PC端,WECHAT_MP/小程序端,APP/移动应用端
|
||||
* @param way 购物车购买:CART/立即购买:BUY_NOW / 积分购买:POINT
|
||||
* @param way 购物车:CART / 立即购买:BUY_NOW / 积分:POINTS / 拼团:PINTUAN / 砍价:KANJIA
|
||||
* @param remark 备注 非必填
|
||||
*/
|
||||
export function createTrade (data) {
|
||||
@@ -80,7 +81,7 @@ export function createTrade (data) {
|
||||
/**
|
||||
* 选择优惠券
|
||||
* @param memberCouponId 优惠券id
|
||||
* @param way 购物车购买:CART/立即购买:BUY_NOW/ 积分购买:POINT
|
||||
* @param way 购物车:CART / 立即购买:BUY_NOW / 积分:POINTS
|
||||
* @param used 使用true 弃用 false
|
||||
*/
|
||||
export function selectCoupon (params) {
|
||||
@@ -92,6 +93,21 @@ export function selectCoupon (params) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择礼品卡
|
||||
* @param way 购物车:CART / 立即购买:BUY_NOW / 拼团:PINTUAN / 积分:POINTS / 砍价:KANJIA
|
||||
* @param credentialId 礼品卡凭证 id;used=false 且不传则清空全部
|
||||
* @param used 使用 true / 弃用 false
|
||||
*/
|
||||
export function selectGiftCard (params) {
|
||||
return request({
|
||||
url: '/buyer/trade/carts/select/giftCard',
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 可用优惠券数量
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import request, {Method, commonUrl} from '@/plugins/request.js';
|
||||
import request, {Method, buyerUrl} from '@/plugins/request.js';
|
||||
|
||||
/**
|
||||
* 获取拼图验证
|
||||
*/
|
||||
export function getVerifyImg (verificationEnums) {
|
||||
return request({
|
||||
url: `${commonUrl}/common/common/slider/${verificationEnums}`,
|
||||
url: `${buyerUrl}/buyer/common/slider/${verificationEnums}`,
|
||||
method: Method.GET,
|
||||
needToken: false
|
||||
});
|
||||
@@ -15,7 +15,7 @@ export function getVerifyImg (verificationEnums) {
|
||||
*/
|
||||
export function postVerifyImg (params) {
|
||||
return request({
|
||||
url: `${commonUrl}/common/common/slider/${params.verificationEnums}`,
|
||||
url: `${buyerUrl}/buyer/common/slider/${params.verificationEnums}`,
|
||||
method: Method.POST,
|
||||
needToken: false,
|
||||
params
|
||||
@@ -26,7 +26,7 @@ export function postVerifyImg (params) {
|
||||
*/
|
||||
export function sendSms (params) {
|
||||
return request({
|
||||
url: `${commonUrl}/common/common/sms/${params.verificationEnums}/${params.mobile}`,
|
||||
url: `${buyerUrl}/buyer/common/sms/${params.verificationEnums}/${params.mobile}`,
|
||||
method: Method.GET,
|
||||
needToken: false,
|
||||
params
|
||||
@@ -36,9 +36,10 @@ export function sendSms (params) {
|
||||
// 地区数据,用于三级联动
|
||||
export function getRegion (id) {
|
||||
return request({
|
||||
url: `${commonUrl}/common/common/region/item/${id}`,
|
||||
url: `${buyerUrl}/buyer/common/region/item/${id}`,
|
||||
needToken: true,
|
||||
method: Method.GET
|
||||
method: Method.GET,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,7 +80,7 @@ export function articleDetail (id) {
|
||||
// 获取IM接口前缀
|
||||
export function getIMDetail () {
|
||||
return request({
|
||||
url: `${commonUrl}/common/common/IM`,
|
||||
url: `${buyerUrl}/buyer/common/IM`,
|
||||
method: Method.GET
|
||||
});
|
||||
}
|
||||
@@ -88,8 +89,17 @@ export function getIMDetail () {
|
||||
//获取图片logo
|
||||
export function getBaseSite(){
|
||||
return request ({
|
||||
url:`${commonUrl}/common/common/site`,
|
||||
url:`${buyerUrl}/buyer/common/site`,
|
||||
method: Method.GET,
|
||||
needToken: false
|
||||
})
|
||||
}
|
||||
|
||||
// 获取主题色配置
|
||||
export function getThemeSetting() {
|
||||
return request({
|
||||
url: `${buyerUrl}/buyer/common/site/theme`,
|
||||
method: Method.GET,
|
||||
needToken: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ import request, {
|
||||
// 商品列表
|
||||
export function goodsList (params) {
|
||||
return request({
|
||||
url: '/buyer/goods/goods/es',
|
||||
url: '/buyer/goods/goods/search',
|
||||
method: Method.GET,
|
||||
needToken: false,
|
||||
optionalToken: true,
|
||||
params
|
||||
});
|
||||
}
|
||||
@@ -15,9 +16,10 @@ export function goodsList (params) {
|
||||
// 商品分类 筛选条件 品牌, 尺寸等
|
||||
export function filterList (params) {
|
||||
return request({
|
||||
url: '/buyer/goods/goods/es/related',
|
||||
url: '/buyer/goods/goods/search/related',
|
||||
method: Method.GET,
|
||||
needToken: false,
|
||||
optionalToken: true,
|
||||
params
|
||||
});
|
||||
}
|
||||
@@ -31,12 +33,13 @@ export function goodsDetail (goodsId) {
|
||||
});
|
||||
}
|
||||
|
||||
// id获取商品Sku详情
|
||||
// id获取商品Sku详情(支持未登录浏览,登录后可选携带 token)
|
||||
export function goodsSkuDetail (params) {
|
||||
return request({
|
||||
url: `/buyer/goods/goods/sku/${params.goodsId}/${params.skuId}`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
needToken: false,
|
||||
optionalToken: true,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import request, { Method } from "@/plugins/request.js";
|
||||
|
||||
/**
|
||||
* 获取首页专题数据
|
||||
*/
|
||||
export function getTopicData(id) {
|
||||
return request({
|
||||
url: `/buyer/other/pageData/get/${id}`,
|
||||
method: Method.GET,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 获取首页楼层装修数据
|
||||
export function indexData(params) {
|
||||
return request({
|
||||
@@ -52,6 +63,9 @@ export function handleRefreshToken(token) {
|
||||
url: `/buyer/passport/member/refresh/${token}`,
|
||||
method: Method.GET,
|
||||
needToken: false,
|
||||
// 刷新令牌请求自身不再进入 401/403 刷新分支,避免拦截器递归死锁;失败也不弹全局错误提示
|
||||
skipAuthHandler: true,
|
||||
skipMessage: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,32 @@ export function loginCallback (uuid) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getThirdAccountBindList () {
|
||||
return request({
|
||||
url: '/buyer/passport/connect/bind/list',
|
||||
method: Method.GET,
|
||||
needToken: true
|
||||
});
|
||||
}
|
||||
|
||||
export function bindThirdAccount (data) {
|
||||
return request({
|
||||
url: '/buyer/passport/connect/bind',
|
||||
method: Method.POST,
|
||||
needToken: true,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function unbindThirdAccount (data) {
|
||||
return request({
|
||||
url: '/buyer/passport/connect/bind/unbind',
|
||||
method: Method.POST,
|
||||
needToken: true,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码 验证手机验证码
|
||||
*/
|
||||
@@ -98,6 +124,7 @@ export function getSCLoginCode(params) {
|
||||
url: `/buyer/passport/member/pc_session`,
|
||||
method: Method.POST,
|
||||
needToken: false,
|
||||
loading: false,
|
||||
params
|
||||
});
|
||||
}
|
||||
@@ -106,6 +133,19 @@ export function sCLogin(token,params) {
|
||||
url: `/buyer/passport/member/session_login/`+token,
|
||||
method: Method.POST,
|
||||
needToken: false,
|
||||
loading: false,
|
||||
skipMessage: true,
|
||||
// 长轮询后端最长挂起 20s,需大于该值,避免 axios 10s 默认超时把二维码请求掐断
|
||||
timeout: 25000,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
export function getWechatH5QrCode (params) {
|
||||
return request({
|
||||
url: '/buyer/other/wechat/h5/qrcode',
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ export function addEvaluation (params) {
|
||||
url: `/buyer/member/evaluation`,
|
||||
method: Method.POST,
|
||||
needToken: true,
|
||||
params
|
||||
data: params
|
||||
});
|
||||
}
|
||||
|
||||
@@ -180,6 +180,15 @@ export function receiptList () {
|
||||
});
|
||||
}
|
||||
|
||||
// 发票详情
|
||||
export function receiptDetail (id) {
|
||||
return request({
|
||||
url: `/buyer/trade/receipt/${id}`,
|
||||
method: Method.GET,
|
||||
needToken: true
|
||||
});
|
||||
}
|
||||
|
||||
// 保存发票信息
|
||||
export function saveReceipt (params) {
|
||||
return request({
|
||||
@@ -196,6 +205,7 @@ export function couponList (params) {
|
||||
url: `/buyer/promotion/coupon`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
loading: false,
|
||||
params
|
||||
});
|
||||
}
|
||||
@@ -288,7 +298,8 @@ export function cancelAfterSale (afterSaleSn) {
|
||||
return request({
|
||||
url: `/buyer/order/afterSale/cancel/${afterSaleSn}`,
|
||||
method: Method.POST,
|
||||
needToken: true
|
||||
needToken: true,
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -333,93 +344,6 @@ export function clearComplain (id) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会员分销信息
|
||||
*/
|
||||
export function distribution () {
|
||||
return request({
|
||||
url: `/buyer/distribution/distribution`,
|
||||
method: Method.GET,
|
||||
needToken: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请成为分销商
|
||||
* @param idNumber 身份证号
|
||||
* @param name 名字
|
||||
*/
|
||||
export function applyDistribution (params) {
|
||||
return request({
|
||||
url: `/buyer/distribution/distribution`,
|
||||
method: Method.POST,
|
||||
needToken: true,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分销商订单列表
|
||||
*/
|
||||
export function getDistOrderList (params) {
|
||||
return request({
|
||||
url: `/buyer/distribution/order`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分销商商品列表
|
||||
*/
|
||||
export function getDistGoodsList (params) {
|
||||
return request({
|
||||
url: `/buyer/distribution/goods`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定、解绑分销商品
|
||||
* @param distributionGoodsId 分销商品id
|
||||
* @param checked 分销商品id,true为绑定,false为解绑
|
||||
*/
|
||||
export function selectDistGoods (params) {
|
||||
return request({
|
||||
url: `/buyer/distribution/goods/checked/${params.distributionGoodsId}`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分销商提现历史
|
||||
*/
|
||||
export function distCashHistory (params) {
|
||||
return request({
|
||||
url: `/buyer/distribution/cash`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分销商提现
|
||||
*/
|
||||
export function distCash (params) {
|
||||
return request({
|
||||
url: `/buyer/distribution/cash`,
|
||||
method: Method.POST,
|
||||
needToken: true,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的足迹
|
||||
* @param {Number} pageNumber 页码
|
||||
@@ -480,6 +404,31 @@ export function memberPointHistory (params) {
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
export function memberGradeCurrent (params) {
|
||||
return request({
|
||||
url: `/buyer/member/memberGrade/current`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function memberGradeRules () {
|
||||
return request({
|
||||
url: `/buyer/member/memberGrade/rules`,
|
||||
method: Method.GET,
|
||||
needToken: true
|
||||
})
|
||||
}
|
||||
|
||||
export function memberGradeList () {
|
||||
return request({
|
||||
url: `/buyer/member/memberGrade/list`,
|
||||
method: Method.GET,
|
||||
needToken: true
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 分页获取会员站内信
|
||||
* @param {Object} params 请求参数,包括pageNumber、pageSize、status
|
||||
@@ -497,6 +446,15 @@ export function memberMsgList (params) {
|
||||
* @param {String} messageId 消息id
|
||||
*/
|
||||
|
||||
export function getMemberMessage (id) {
|
||||
return request({
|
||||
url: `/buyer/message/member/${id}`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
skipMessage: true
|
||||
});
|
||||
}
|
||||
|
||||
export function readMemberMsg (id) {
|
||||
return request({
|
||||
url: `/buyer/message/member/${id}`,
|
||||
|
||||
@@ -115,3 +115,21 @@ export function getLogisticsCompany () {
|
||||
params: { pageNumber: 1, pageSize: 200, disabled: 'OPEN' }
|
||||
});
|
||||
}
|
||||
|
||||
//查询包裹列表
|
||||
export const getPackage = (sn) => {
|
||||
return request({
|
||||
url: `/buyer/order/order/getPackage/${sn}`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
})
|
||||
}
|
||||
|
||||
//查询物流
|
||||
export const getTracesList = (sn, params) => {
|
||||
return request({
|
||||
url: `/buyer/order/order/getTracesList/${sn}`,
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
})
|
||||
};
|
||||
|
||||
@@ -24,12 +24,13 @@ export function tradeDetail (params) {
|
||||
* @param payClient 调起方式 可用值:APP,NATIVE,JSAPI,H5
|
||||
* @param sn 订单编号
|
||||
*/
|
||||
export function pay (params) {
|
||||
export function pay (params, config = {}) {
|
||||
return request({
|
||||
url: `/buyer/payment/cashier/pay/${params.paymentMethod}/${params.paymentClient}`,
|
||||
needToken: true,
|
||||
method: Method.GET,
|
||||
params
|
||||
params,
|
||||
skipMessage: !!config.skipMessage,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -57,3 +57,50 @@ export function pointGoodsDetail (id) {
|
||||
id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询当前客户礼品卡
|
||||
*/
|
||||
export function getGiftCardCashMemberCardPage (params) {
|
||||
return request({
|
||||
url: '/buyer/promotion/giftCardCash/memberCard',
|
||||
method: Method.GET,
|
||||
needToken: true,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定礼品卡(卡号 + 兑换码/卡密)
|
||||
*/
|
||||
export function bindGiftCardCashMemberCard (data) {
|
||||
return request({
|
||||
url: '/buyer/promotion/giftCardCash/memberCard/bind',
|
||||
method: Method.POST,
|
||||
needToken: true,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活礼品卡
|
||||
*/
|
||||
export function activateGiftCardCashMemberCard (memberCardId) {
|
||||
return request({
|
||||
url: `/buyer/promotion/giftCardCash/memberCard/${memberCardId}/activate`,
|
||||
method: Method.POST,
|
||||
needToken: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 下单页可用礼品卡列表
|
||||
*/
|
||||
export function getAvailableGiftCardCashMemberCards () {
|
||||
return request({
|
||||
url: '/buyer/promotion/giftCardCash/memberCard/available',
|
||||
method: Method.GET,
|
||||
needToken: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,11 +42,21 @@ export function applyThird (params) {
|
||||
})
|
||||
}
|
||||
|
||||
// 店铺信息(含评分)
|
||||
export function getStoreById (id) {
|
||||
return request({
|
||||
url: `/buyer/store/store/store/${id}`,
|
||||
needToken: false,
|
||||
method: Method.GET
|
||||
})
|
||||
}
|
||||
|
||||
// 店铺详情
|
||||
export function getDetailById (id) {
|
||||
return request({
|
||||
url: `/buyer/store/store/get/detail/${id}`,
|
||||
needToken: true,
|
||||
needToken: false,
|
||||
optionalToken: true,
|
||||
method: Method.GET
|
||||
})
|
||||
}
|
||||
@@ -72,7 +82,8 @@ export function applyStatus () {
|
||||
return request({
|
||||
url: `/buyer/store/store/apply`,
|
||||
needToken: true,
|
||||
method: Method.GET
|
||||
method: Method.GET,
|
||||
loading: false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
29
buyer/src/assets/iconfont/iconMap.js
Normal file
29
buyer/src/assets/iconfont/iconMap.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import back from "./back.png";
|
||||
import carts from "./carts.png";
|
||||
import collage from "./collage.png";
|
||||
import feedback from "./feedback.png";
|
||||
import notice from "./notice.png";
|
||||
import notification from "./notification.png";
|
||||
import shop from "./shop.png";
|
||||
import story from "./story.png";
|
||||
import support from "./support.png";
|
||||
import user from "./user.png";
|
||||
|
||||
const iconMap = {
|
||||
back,
|
||||
carts,
|
||||
collage,
|
||||
feedback,
|
||||
notice,
|
||||
notification,
|
||||
shop,
|
||||
story,
|
||||
support,
|
||||
user,
|
||||
};
|
||||
|
||||
export function getIconUrl(name) {
|
||||
return iconMap[name] || "";
|
||||
}
|
||||
|
||||
export default iconMap;
|
||||
40
buyer/src/assets/styles/element.scss
Normal file
40
buyer/src/assets/styles/element.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
:root {
|
||||
--el-color-primary: var(--theme-color, #ff3c2a);
|
||||
--el-color-danger: var(--theme-color, #ff3c2a);
|
||||
--el-font-size-base: 14px;
|
||||
}
|
||||
|
||||
.el-table--border {
|
||||
.el-table__cell {
|
||||
border-right: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: var(--theme-color, #ff3c2a);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.op-split {
|
||||
margin: 0 8px;
|
||||
color: #dcdfe6;
|
||||
}
|
||||
|
||||
/* 次要强调按钮,使用高亮主题色(如「立即购买」) */
|
||||
.confirm-receipt-btn {
|
||||
background-color: var(--theme-light, #ff6b35) !important;
|
||||
border-color: var(--theme-light, #ff6b35) !important;
|
||||
color: #fff !important;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: color-mix(in srgb, var(--theme-light, #ff6b35) 88%, black) !important;
|
||||
border-color: color-mix(in srgb, var(--theme-light, #ff6b35) 88%, black) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
25
buyer/src/assets/styles/global-layout.scss
Normal file
25
buyer/src/assets/styles/global-layout.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
// 全局布局工具类(非 scoped,避免子组件继承 class 时宽度失效)
|
||||
.width_1200_auto {
|
||||
width: 1200px;
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// 首页主体:分类导航 + 楼层装修共用同一 1200px 容器,保证列对齐
|
||||
.index-main {
|
||||
width: 1200px;
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// 与 CateNav 商品分类栏、Carousel 左占位一致的列宽
|
||||
$index-main-width: 1200px;
|
||||
$index-side-col-width: 263.2px;
|
||||
$index-col-gap: 10px;
|
||||
// 1200 - 263.2*2 - 10*2 = 653.6px
|
||||
$index-center-col-width: $index-main-width - ($index-side-col-width * 2) - ($index-col-gap * 2);
|
||||
@@ -10,7 +10,7 @@ $warning_color: #ff9900;
|
||||
$error_color: #ed3f14;
|
||||
$handle-btn-color: #438cde;
|
||||
|
||||
$theme_color: #F31947;
|
||||
$theme_color: var(--theme-color, #ff3c2a);
|
||||
|
||||
$border_color: #dddee1;
|
||||
$title_color: #8c8c8c;
|
||||
@@ -30,6 +30,7 @@ $dark_content_color: #d5d5d5;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
/***** 封装一些方法可用于 黑暗主题 ,明亮主题 *****/
|
||||
@@ -305,9 +306,11 @@ li {
|
||||
}
|
||||
|
||||
.width_1200_auto {
|
||||
width: 1184px;
|
||||
width: 1200px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
@@ -325,18 +328,24 @@ li {
|
||||
.goods-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
column-gap: 10px;
|
||||
}
|
||||
.goods-show-info {
|
||||
width: 235px;
|
||||
width: calc(20% - 8px);
|
||||
padding: 6px;
|
||||
margin: 10px 0px;
|
||||
margin-left: 5px;
|
||||
margin: 10px 0;
|
||||
position: relative;
|
||||
border: 1px solid #fff;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.goods-show-img img {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
.goods-show-info:hover {
|
||||
border: 1px solid #ccc;
|
||||
@@ -346,16 +355,17 @@ li {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.goods-show-detail {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
margin: 6px 0px;
|
||||
}
|
||||
.goods-show-num {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
color: #666;
|
||||
}
|
||||
.goods-show-num {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
color: #009688;
|
||||
}
|
||||
@@ -364,11 +374,16 @@ li {
|
||||
font-weight: bold;
|
||||
}
|
||||
.goods-show-seller {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
color: $theme_color;
|
||||
}
|
||||
.goods-page {
|
||||
margin:10px auto ;
|
||||
text-align: right;
|
||||
width: 1200px;
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
:deep(.el-pagination) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,18 +9,24 @@
|
||||
.goods-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
column-gap: 10px;
|
||||
}
|
||||
.goods-show-info {
|
||||
width: 235px;
|
||||
width: calc(20% - 8px);
|
||||
padding: 6px;
|
||||
margin: 10px 0px;
|
||||
margin-left: 5px;
|
||||
margin: 10px 0;
|
||||
position: relative;
|
||||
border: 1px solid #fff;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.goods-show-img img {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
}
|
||||
.goods-show-info:hover {
|
||||
border: 1px solid #ccc;
|
||||
@@ -30,13 +36,15 @@
|
||||
margin-top: 6px;
|
||||
}
|
||||
.goods-show-detail {
|
||||
height: 36px;
|
||||
font-size: 12px;
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
margin: 6px 0px;
|
||||
overflow:hidden;
|
||||
text-overflow: ellipsis;
|
||||
display:-webkit-box;
|
||||
-webkit-line-clamp:2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient:vertical;
|
||||
color:#808080;
|
||||
}
|
||||
@@ -44,12 +52,12 @@
|
||||
color:#E23A3A;
|
||||
}
|
||||
.goods-show-num {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
color: #666;
|
||||
}
|
||||
.goods-show-num {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
color: #009688;
|
||||
}
|
||||
@@ -58,11 +66,16 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
.goods-show-seller {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
color: $theme_color;
|
||||
}
|
||||
.goods-page {
|
||||
margin:10px auto ;
|
||||
text-align: right;
|
||||
width: 1200px;
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
:deep(.el-pagination) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
@import "~view-design/src/styles/index.less";
|
||||
// iview 自定义样式
|
||||
|
||||
// Element Plus 主题变量(原 view-design less 已移除)
|
||||
@primary-color: #ff5c58;
|
||||
@info-color: #fa6419;
|
||||
@success-color: #68cabe;
|
||||
@@ -11,9 +9,7 @@
|
||||
@table-td-highlight-bg: #ededed;
|
||||
@font-size-base: 12px;
|
||||
|
||||
.ivu-drawer,
|
||||
.drawer,
|
||||
.ivu-drawer-wrap {
|
||||
.el-drawer,
|
||||
.drawer {
|
||||
z-index: 2600 !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,217 +1,386 @@
|
||||
<template>
|
||||
<div class="navbar" :class="{'small-search-box':useClass == 'fixed-show'}">
|
||||
<div class="container width_1200_auto flex flex-a-c">
|
||||
<img
|
||||
:src="$store.state.logoImg"
|
||||
v-if="showLogo"
|
||||
class="logo-img"
|
||||
alt=""
|
||||
@click="$router.push('/')"
|
||||
/>
|
||||
<div :class="{'small-search-box':useClass == 'fixed-show'}" class="search-box">
|
||||
<i-input
|
||||
<div class="navbar" ref="rootRef" :class="{'small-search-box': useClass == 'fixed-show'}">
|
||||
<template v-if="useClass == 'fixed-show'">
|
||||
<div class="search search-embedded">
|
||||
<el-input
|
||||
v-model="searchData"
|
||||
size="large"
|
||||
class="search "
|
||||
class="search-input"
|
||||
placeholder="输入你想查找的商品"
|
||||
@keyup.enter.native="search"
|
||||
>
|
||||
|
||||
<div class="search-icon" slot="append" @click="search">
|
||||
<Icon type="ios-search" size="21"/>
|
||||
</div>
|
||||
</i-input>
|
||||
<template v-if="showTag">
|
||||
<div class="only-store" v-if="storeId" @click="research()">
|
||||
切换为{{!onlyStore ? '店铺内' : '平台'}}搜索
|
||||
|
||||
</div>
|
||||
<div v-if="promotionTags.length === 0"></div>
|
||||
<div v-else class="history-list flex">
|
||||
<div
|
||||
v-for="(item, index) in promotionTags"
|
||||
:key="index"
|
||||
class="mr_10"
|
||||
>
|
||||
<span class="history-item" @click="selectTags(item)">{{ item }}</span>
|
||||
@focus="openPanel"
|
||||
@input="queueSuggest"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<div class="search-btn" @click="search">
|
||||
<el-icon :size="21"><SearchIcon /></el-icon>
|
||||
</div>
|
||||
<div v-if="showPanel" class="suggest-panel" @mousedown.prevent>
|
||||
<SearchSuggest
|
||||
:keyword="trimmedKeyword"
|
||||
:history="history"
|
||||
:hot-words="promotionTags"
|
||||
:suggestions="suggestions"
|
||||
:suggesting="suggesting"
|
||||
@pick="selectTags"
|
||||
@clear="clearHistory"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="container width_1200_auto flex flex-a-c">
|
||||
<img
|
||||
:src="logoImg"
|
||||
v-if="showLogo"
|
||||
class="logo-img"
|
||||
alt=""
|
||||
@click="$router.push('/')"
|
||||
/>
|
||||
<div class="search-box">
|
||||
<div class="search">
|
||||
<el-input
|
||||
v-model="searchData"
|
||||
size="large"
|
||||
class="search-input"
|
||||
placeholder="输入你想查找的商品"
|
||||
@focus="openPanel"
|
||||
@input="queueSuggest"
|
||||
@keyup.enter="search"
|
||||
/>
|
||||
<div class="search-btn" @click="search">
|
||||
<el-icon :size="21"><SearchIcon /></el-icon>
|
||||
</div>
|
||||
<div v-if="showPanel" class="suggest-panel" @mousedown.prevent>
|
||||
<SearchSuggest
|
||||
:keyword="trimmedKeyword"
|
||||
:history="history"
|
||||
:hot-words="promotionTags"
|
||||
:suggestions="suggestions"
|
||||
:suggesting="suggesting"
|
||||
@pick="selectTags"
|
||||
@clear="clearHistory"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="showTag">
|
||||
<div v-if="promotionTags.length === 0"></div>
|
||||
<div v-else class="history-list flex">
|
||||
<div
|
||||
v-for="(item, index) in promotionTags"
|
||||
:key="index"
|
||||
class="mr_10"
|
||||
>
|
||||
<span class="history-item" @click="selectTags(item)">{{ item }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import storage from '@/plugins/storage.js'
|
||||
import {hotWords} from '@/api/goods.js'
|
||||
import { goodsList, hotWords } from '@/api/goods.js'
|
||||
import { Search as SearchIcon } from '@element-plus/icons-vue'
|
||||
import SearchSuggest from '@/components/SearchSuggest.vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
export default {
|
||||
name: 'search',
|
||||
props: {
|
||||
showTag: { // 是否展示搜索栏下方热门搜索
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showLogo: { // 是否展示左侧logo
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
storeId: { // 是否为店铺页面
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
hover: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
useClass:{
|
||||
type:null,
|
||||
default:''
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
storeId(val){
|
||||
this.onlyStore = val ? true : false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchData: '', // 搜索内容
|
||||
onlyStore:false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selectTags(item) { // 选择热门标签
|
||||
this.searchData = item;
|
||||
this.search();
|
||||
},
|
||||
research(){
|
||||
this.onlyStore = !this.onlyStore
|
||||
},
|
||||
search () { // 全平台搜索商品
|
||||
const url = this.$route.path;
|
||||
if(url == '/goodsList'){
|
||||
this.$emit('search', this.searchData)
|
||||
}else{
|
||||
const pushData = {
|
||||
path:'/goodsList',
|
||||
query: { keyword: this.searchData },
|
||||
}
|
||||
if(this.storeId && this.onlyStore) pushData.query.storeId = this.storeId
|
||||
defineOptions({ name: 'search' })
|
||||
|
||||
|
||||
this.$router.push(pushData);
|
||||
}
|
||||
},
|
||||
searchStore() { // 店铺搜索商品
|
||||
this.$emit('search', this.searchData)
|
||||
}
|
||||
const props = defineProps({
|
||||
showTag: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
computed: {
|
||||
promotionTags() {
|
||||
if (this.$store.state.hotWordsList) {
|
||||
return JSON.parse(this.$store.state.hotWordsList)
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
showLogo: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
created() {
|
||||
this.searchData = this.$route.query.keyword
|
||||
|
||||
if (!this.hover) { // 首页顶部固定搜索栏不调用热词接口
|
||||
// 搜索热词每5分钟请求一次
|
||||
const reloadTime = storage.getItem('hotWordsReloadTime')
|
||||
const time = new Date().getTime() - 5 * 60 * 1000
|
||||
if (!reloadTime) {
|
||||
hotWords({count: 5}).then(res => {
|
||||
if (res.success && res.result) storage.setItem('hotWordsList', res.result)
|
||||
})
|
||||
storage.setItem('hotWordsReloadTime', new Date().getTime())
|
||||
} else if (reloadTime && time > reloadTime) {
|
||||
hotWords({count: 5}).then(res => {
|
||||
if (res.success && res.result) storage.setItem('hotWordsList', res.result)
|
||||
})
|
||||
storage.setItem('hotWordsReloadTime', new Date().getTime())
|
||||
}
|
||||
}
|
||||
storeId: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
hover: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
useClass: {
|
||||
type: null,
|
||||
default: ''
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
const emit = defineEmits(['search'])
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const HISTORY_KEY = 'buyerSearchHistory'
|
||||
const HISTORY_LIMIT = 10
|
||||
|
||||
function readHistory() {
|
||||
try {
|
||||
const raw = localStorage.getItem(HISTORY_KEY)
|
||||
const list = raw ? JSON.parse(raw) : []
|
||||
return Array.isArray(list) ? list.filter((item) => typeof item === 'string' && item.trim()) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function parseHotWords(raw) {
|
||||
if (!raw) return []
|
||||
if (Array.isArray(raw)) return raw.filter(Boolean)
|
||||
if (typeof raw !== 'string') return []
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
if (Array.isArray(parsed)) return parsed.filter(Boolean)
|
||||
} catch {
|
||||
// 旧数据可能是逗号拼接
|
||||
}
|
||||
return raw.split(',').map((item) => item.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
const rootRef = ref(null)
|
||||
const searchData = ref('')
|
||||
const history = ref([])
|
||||
const panelVisible = ref(false)
|
||||
const suggestions = ref([])
|
||||
const suggesting = ref(false)
|
||||
let suggestTimer = null
|
||||
let suggestSeq = 0
|
||||
|
||||
const logoImg = computed(() => appStore.logoImg)
|
||||
|
||||
const trimmedKeyword = computed(() => {
|
||||
return (searchData.value || '').trim()
|
||||
})
|
||||
|
||||
const promotionTags = computed(() => {
|
||||
return parseHotWords(appStore.hotWordsList)
|
||||
})
|
||||
|
||||
const showPanel = computed(() => {
|
||||
if (!panelVisible.value) return false
|
||||
if (trimmedKeyword.value) return true
|
||||
return history.value.length > 0 || promotionTags.value.length > 0
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.query.keyword,
|
||||
(value) => {
|
||||
searchData.value = value || ''
|
||||
}
|
||||
)
|
||||
|
||||
function selectTags(item) {
|
||||
searchData.value = item
|
||||
search()
|
||||
}
|
||||
|
||||
function remember(keyword) {
|
||||
const next = [keyword, ...history.value.filter((item) => item !== keyword)].slice(0, HISTORY_LIMIT)
|
||||
history.value = next
|
||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(next))
|
||||
}
|
||||
|
||||
function clearHistory() {
|
||||
history.value = []
|
||||
localStorage.removeItem(HISTORY_KEY)
|
||||
}
|
||||
|
||||
function openPanel() {
|
||||
panelVisible.value = true
|
||||
if (trimmedKeyword.value) queueSuggest()
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
panelVisible.value = false
|
||||
}
|
||||
|
||||
function onDocumentClick(event) {
|
||||
if (rootRef.value && !rootRef.value.contains(event.target)) closePanel()
|
||||
}
|
||||
|
||||
function queueSuggest() {
|
||||
panelVisible.value = true
|
||||
clearTimeout(suggestTimer)
|
||||
const keyword = trimmedKeyword.value
|
||||
if (!keyword) {
|
||||
suggestions.value = []
|
||||
suggesting.value = false
|
||||
return
|
||||
}
|
||||
suggesting.value = true
|
||||
suggestTimer = setTimeout(() => fetchSuggest(keyword), 300)
|
||||
}
|
||||
|
||||
function fetchSuggest(keyword) {
|
||||
const seq = ++suggestSeq
|
||||
const params = { keyword, pageNumber: 1, pageSize: 8 }
|
||||
if (props.storeId) params.storeId = props.storeId
|
||||
goodsList(params).then((res) => {
|
||||
if (seq !== suggestSeq || keyword !== trimmedKeyword.value) return
|
||||
const records = res && res.success ? (res.result.records || []) : []
|
||||
const seen = new Set()
|
||||
const names = []
|
||||
promotionTags.value.forEach((word) => {
|
||||
if (word && String(word).toLowerCase().includes(keyword.toLowerCase()) && !seen.has(word)) {
|
||||
seen.add(word)
|
||||
names.push(word)
|
||||
}
|
||||
})
|
||||
records.forEach((item) => {
|
||||
const name = item && item.goodsName
|
||||
if (name && !seen.has(name)) {
|
||||
seen.add(name)
|
||||
names.push(name)
|
||||
}
|
||||
})
|
||||
suggestions.value = names.slice(0, 8)
|
||||
}).catch(() => {
|
||||
if (seq === suggestSeq) suggestions.value = []
|
||||
}).finally(() => {
|
||||
if (seq === suggestSeq) suggesting.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function search() {
|
||||
const keyword = trimmedKeyword.value
|
||||
searchData.value = keyword
|
||||
closePanel()
|
||||
if (keyword) remember(keyword)
|
||||
const url = route.path
|
||||
if (url == '/goodsList') {
|
||||
emit('search', keyword)
|
||||
} else {
|
||||
const pushData = {
|
||||
path: '/goodsList',
|
||||
query: { keyword },
|
||||
}
|
||||
if (props.storeId) pushData.query.storeId = props.storeId
|
||||
|
||||
router.push(pushData)
|
||||
}
|
||||
}
|
||||
|
||||
function searchStore() {
|
||||
emit('search', trimmedKeyword.value)
|
||||
}
|
||||
|
||||
function applyHotWords(result) {
|
||||
const list = parseHotWords(result)
|
||||
appStore.setHotWords(list)
|
||||
storage.setItem('hotWordsList', JSON.stringify(list))
|
||||
}
|
||||
|
||||
function loadHotWords() {
|
||||
hotWords({ count: 5 }).then((res) => {
|
||||
if (res.success && res.result) applyHotWords(res.result)
|
||||
}).catch(() => {
|
||||
// 热词为可选数据,失败时沿用缓存或默认热词
|
||||
})
|
||||
storage.setItem('hotWordsReloadTime', new Date().getTime())
|
||||
}
|
||||
|
||||
// 初始化(原 created 钩子逻辑)
|
||||
searchData.value = route.query.keyword || ''
|
||||
history.value = readHistory()
|
||||
|
||||
if (!props.hover) {
|
||||
const reloadTime = storage.getItem('hotWordsReloadTime')
|
||||
const time = new Date().getTime() - 5 * 60 * 1000
|
||||
if (!reloadTime || time > reloadTime) loadHotWords()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onDocumentClick)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(suggestTimer)
|
||||
document.removeEventListener('click', onDocumentClick)
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.only-store{
|
||||
text-align: right;
|
||||
color:$theme_color;
|
||||
cursor: pointer;
|
||||
}
|
||||
.navbar {
|
||||
height: 113px;
|
||||
background: #fff;
|
||||
}
|
||||
.search-icon{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.small-search-box{
|
||||
height: 60px;
|
||||
|
||||
margin: 0 !important;
|
||||
.navbar.small-search-box {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.search-embedded {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search {
|
||||
$btn-gap: 4px;
|
||||
$btn-width: 60px;
|
||||
$input-height: 37.8px;
|
||||
|
||||
position: relative;
|
||||
width: 778.4px;
|
||||
height: $input-height;
|
||||
margin: 10px 0px 5px 0;
|
||||
|
||||
border-radius: 18.9px;
|
||||
.search-input {
|
||||
width: 100%;
|
||||
|
||||
|
||||
/deep/ .ivu-input.ivu-input-large {
|
||||
border: 1.4px solid $theme_color;
|
||||
box-sizing: border-box;
|
||||
border-radius: 19.6px;
|
||||
position: relative;
|
||||
padding-left: 26px;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
height: 37.8px;
|
||||
color: #999;
|
||||
&:focus {
|
||||
:deep(.el-input__wrapper) {
|
||||
border: 1.4px solid $theme_color;
|
||||
box-sizing: border-box;
|
||||
border-radius: 19.6px;
|
||||
padding-left: 26px;
|
||||
padding-right: calc(#{$btn-gap * 2} + #{$btn-width});
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
height: $input-height;
|
||||
color: #999;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .ivu-input-group-append {
|
||||
border-radius: 19.6px !important;
|
||||
.search-btn {
|
||||
position: absolute;
|
||||
right: $btn-gap;
|
||||
top: $btn-gap;
|
||||
bottom: $btn-gap;
|
||||
width: $btn-width;
|
||||
border-radius: calc((#{$input-height} - #{$btn-gap * 2}) / 2);
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
border: 1.4px solid $theme_color;
|
||||
width: 67.2px;
|
||||
height: 37.8px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: 0;
|
||||
z-index: 99;
|
||||
z-index: 1;
|
||||
background-color: $theme_color;
|
||||
color: #ffffff;
|
||||
|
||||
button {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-embedded.search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-box{
|
||||
margin-left: 28px;
|
||||
}
|
||||
@@ -220,28 +389,7 @@ export default {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.store-search {
|
||||
width: 55.6px;
|
||||
padding: 0 9px;
|
||||
border-radius: 0;
|
||||
border-radius: 3px;
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 55px;
|
||||
margin-left: -2px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-div {
|
||||
position: relative;
|
||||
height: 0px;
|
||||
top: -38px;
|
||||
left: 352px;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
|
||||
margin-left: 28px;
|
||||
}
|
||||
|
||||
@@ -254,4 +402,18 @@ export default {
|
||||
color: #666666;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.suggest-panel {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: calc(100% + 8px);
|
||||
z-index: 30;
|
||||
padding: 8px 0;
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
150
buyer/src/components/SearchSuggest.vue
Normal file
150
buyer/src/components/SearchSuggest.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="keyword" class="suggest-section">
|
||||
<div v-if="suggesting && !suggestions.length" class="suggest-empty">正在查找...</div>
|
||||
<button
|
||||
v-for="item in suggestions"
|
||||
:key="item"
|
||||
type="button"
|
||||
class="suggest-item"
|
||||
@click="$emit('pick', item)"
|
||||
>
|
||||
<span v-for="(part, index) in parts(item)" :key="index" :class="{ hit: part.hit }">{{ part.text }}</span>
|
||||
</button>
|
||||
<div v-if="!suggesting && !suggestions.length" class="suggest-empty">没有匹配的商品,回车查看全部结果</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="history.length" class="suggest-section">
|
||||
<div class="suggest-head">
|
||||
<span>搜索历史</span>
|
||||
<button type="button" class="suggest-clear" @click="$emit('clear')">清空</button>
|
||||
</div>
|
||||
<div class="suggest-tags">
|
||||
<button
|
||||
v-for="item in history"
|
||||
:key="item"
|
||||
type="button"
|
||||
class="suggest-tag"
|
||||
@click="$emit('pick', item)"
|
||||
>{{ item }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hotWords.length" class="suggest-section">
|
||||
<div class="suggest-head"><span>热门搜索</span></div>
|
||||
<div class="suggest-tags">
|
||||
<button
|
||||
v-for="item in hotWords"
|
||||
:key="item"
|
||||
type="button"
|
||||
class="suggest-tag"
|
||||
@click="$emit('pick', item)"
|
||||
>{{ item }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({ name: "SearchSuggest" });
|
||||
|
||||
const props = defineProps({
|
||||
keyword: { type: String, default: "" },
|
||||
history: { type: Array, default: () => [] },
|
||||
hotWords: { type: Array, default: () => [] },
|
||||
suggestions: { type: Array, default: () => [] },
|
||||
suggesting: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
defineEmits(["pick", "clear"]);
|
||||
|
||||
function parts(text) {
|
||||
const value = String(text || "");
|
||||
const key = props.keyword;
|
||||
if (!key) return [{ text: value, hit: false }];
|
||||
const index = value.toLowerCase().indexOf(key.toLowerCase());
|
||||
if (index < 0) return [{ text: value, hit: false }];
|
||||
return [
|
||||
{ text: value.slice(0, index), hit: false },
|
||||
{ text: value.slice(index, index + key.length), hit: true },
|
||||
{ text: value.slice(index + key.length), hit: false },
|
||||
].filter((part) => part.text);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.suggest-section + .suggest-section {
|
||||
margin-top: 4px;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.suggest-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 16px;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.suggest-clear {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.suggest-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 0 16px 8px;
|
||||
}
|
||||
|
||||
.suggest-tag {
|
||||
max-width: 180px;
|
||||
padding: 4px 10px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.suggest-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.hit {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
.suggest-empty {
|
||||
padding: 10px 16px;
|
||||
color: #999;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,172 +1,177 @@
|
||||
<template>
|
||||
<div>
|
||||
<Modal v-model="showAddr" width="800" title="收件人地址">
|
||||
<Form
|
||||
<el-dialog v-model="showAddr" width="800" title="收件人地址">
|
||||
<el-form
|
||||
:model="formData"
|
||||
ref="form"
|
||||
ref="formRef"
|
||||
label-position="left"
|
||||
:label-width="100"
|
||||
label-width="100px"
|
||||
:rules="ruleInline"
|
||||
>
|
||||
<FormItem label="收件人" prop="name">
|
||||
<i-input v-model="formData.name" style="width: 600px"></i-input>
|
||||
</FormItem>
|
||||
<FormItem label="收件地区" prop="address">
|
||||
<el-form-item label="收件人" prop="name">
|
||||
<el-input v-model="formData.name" style="width: 600px"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="收件地区" prop="address">
|
||||
{{ formData.address || '暂无地址' }}
|
||||
<Button type="primary" style="margin-left: 10px;" size="small" @click="$refs.map.open()">选择</Button>
|
||||
</FormItem>
|
||||
<FormItem label="详细地址" prop="detail">
|
||||
<i-input v-model="formData.detail" style="width: 600px"></i-input>
|
||||
</FormItem>
|
||||
<FormItem label="手机号码" prop="mobile">
|
||||
<i-input v-model="formData.mobile" style="width: 600px"></i-input>
|
||||
</FormItem>
|
||||
<FormItem label="地址别名">
|
||||
<i-input
|
||||
<el-button type="primary" style="margin-left: 10px;" size="small" @click="mapRef.open()">选择</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="详细地址" prop="detail">
|
||||
<el-input v-model="formData.detail" style="width: 600px"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号码" prop="mobile">
|
||||
<el-input v-model="formData.mobile" style="width: 600px"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="地址别名">
|
||||
<el-input
|
||||
v-model="formData.alias"
|
||||
length
|
||||
:maxlength="4"
|
||||
:maxlength="5"
|
||||
show-word-limit
|
||||
placeholder="请输入地址别名,例如公司"
|
||||
style="width: 600px"
|
||||
></i-input>
|
||||
</FormItem>
|
||||
<FormItem label="默认地址">
|
||||
<i-switch v-model="formData.isDefault" />
|
||||
</FormItem>
|
||||
</Form>
|
||||
<div class="mt_20" slot="footer">
|
||||
<Button @click="hide">取消</Button>
|
||||
<Button type="primary" class="mr_10" :loading="loading" @click="save">保存收货地址</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<multipleMap ref="map" @callback="getAddress"></multipleMap>
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="默认地址">
|
||||
<el-switch v-model="formData.isDefault" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer><div class="mt_20">
|
||||
<el-button @click="hide">取消</el-button>
|
||||
<el-button type="primary" class="mr_10" :loading="loading" @click="save">保存收货地址</el-button>
|
||||
</div></template>
|
||||
</el-dialog>
|
||||
<multipleMap ref="mapRef" @callback="getAddress"></multipleMap>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { Message } from "@/utils/message";
|
||||
import multipleMap from "@/components/map/multiple-map";
|
||||
import {
|
||||
newMemberAddress,
|
||||
editMemberAddress,
|
||||
getAddrDetail
|
||||
} from '@/api/address';
|
||||
export default {
|
||||
name: 'addressManage',
|
||||
props: {
|
||||
id: { // 传入的地址id
|
||||
defalut: '',
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showAddr: false, // 控制模态框显隐
|
||||
formData: { // 表单数据
|
||||
isDefault: false
|
||||
},
|
||||
ruleInline: { // 验证规则
|
||||
name: [{ required: true, message: '请输入收件人姓名', trigger: 'blur' }],
|
||||
address: [{ required: true, message: '请输入地址', trigger: 'change' }],
|
||||
detail: [
|
||||
{ required: true, message: '请输入详细地址', trigger: 'blur' }
|
||||
],
|
||||
mobile: [
|
||||
{ required: true, message: '手机号不能为空', trigger: 'blur' },
|
||||
{
|
||||
type: 'string',
|
||||
pattern: /^1[3|4|5|6|7|8][0-9]{9}$/,
|
||||
message: '手机号格式出错',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
},
|
||||
loading: false, // 提交的加载状态
|
||||
mapMsg: {} // 地图信息
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
save () { // 保存地址
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (valid) {
|
||||
const params = JSON.parse(JSON.stringify(this.formData));
|
||||
params.consigneeAddressPath = params.address.replace(/\s/g, ',');
|
||||
delete params.address;
|
||||
this.loading = true;
|
||||
if (this.id) {
|
||||
editMemberAddress(params).then((res) => {
|
||||
this.loading = false;
|
||||
if (res.success) {
|
||||
this.$Message.success('编辑地址成功');
|
||||
this.$emit('change', true);
|
||||
this.hide();
|
||||
}
|
||||
}).catch(() => { this.loading = false; });
|
||||
} else {
|
||||
newMemberAddress(params).then((res) => {
|
||||
this.loading = false;
|
||||
if (res.success) {
|
||||
this.$Message.success('新增地址成功');
|
||||
this.$emit('change', true);
|
||||
this.hide();
|
||||
}
|
||||
}).catch(() => { this.loading = false; });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
getAddrById (id) {
|
||||
// 获取地址详情
|
||||
getAddrDetail(id).then((res) => {
|
||||
if (res.success) {
|
||||
console.log(res);
|
||||
const data = res.result;
|
||||
data.address = res.result.consigneeAddressPath.replace(/,/g, ' ');
|
||||
this.formData = data;
|
||||
}
|
||||
});
|
||||
},
|
||||
getAddress (val) {
|
||||
// 获取地图选择信息
|
||||
if(val.type === 'select'){
|
||||
const paths = val.data.map(item => item.name).join(',')
|
||||
const ids = val.data.map(item => item.id).join(',')
|
||||
this.$set(this.formData,'address',paths)
|
||||
this.$set(this.formData,'consigneeAddressIdPath',ids)
|
||||
const coord = val.data[val.data.length - 1].center.split(',')
|
||||
this.formData.lat = coord[1]
|
||||
this.formData.lon = coord[0]
|
||||
}else{
|
||||
this.$set(this.formData, "address", val.data.addr);
|
||||
this.$set(this.formData, "consigneeAddressIdPath", val.data.addrId);
|
||||
this.$set(this.formData, "detail", val.data.address);
|
||||
this.formData.lat = val.data.position.lat;
|
||||
this.formData.lon = val.data.position.lng;
|
||||
}
|
||||
|
||||
},
|
||||
show () { // 地址模态框显示
|
||||
this.showAddr = true;
|
||||
},
|
||||
hide () { // 地址模态框隐藏
|
||||
this.showAddr = false;
|
||||
defineOptions({ name: 'addressManage' });
|
||||
|
||||
const props = defineProps({
|
||||
id: { // 传入的地址id
|
||||
defalut: '',
|
||||
type: String
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const showAddr = ref(false); // 控制模态框显隐
|
||||
const formData = ref({ // 表单数据
|
||||
isDefault: false
|
||||
});
|
||||
const ruleInline = { // 验证规则
|
||||
name: [{ required: true, message: '请输入收件人姓名', trigger: 'blur' }],
|
||||
address: [{ required: true, message: '请输入地址', trigger: 'change' }],
|
||||
detail: [
|
||||
{ required: true, message: '请输入详细地址', trigger: 'blur' }
|
||||
],
|
||||
mobile: [
|
||||
{ required: true, message: '手机号不能为空', trigger: 'blur' },
|
||||
{
|
||||
type: 'string',
|
||||
pattern: /^1[3|4|5|6|7|8][0-9]{9}$/,
|
||||
message: '手机号格式出错',
|
||||
trigger: 'blur'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
id: { // 传入的地址id
|
||||
handler: function (v) {
|
||||
if (v) {
|
||||
this.getAddrById(v);
|
||||
} else {
|
||||
this.formData = {isDefault: false}
|
||||
this.$refs.form.resetFields();
|
||||
}
|
||||
]
|
||||
};
|
||||
const loading = ref(false); // 提交的加载状态
|
||||
const mapMsg = ref({}); // 地图信息
|
||||
|
||||
const formRef = ref(null);
|
||||
const mapRef = ref(null);
|
||||
|
||||
function save () { // 保存地址
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
const params = JSON.parse(JSON.stringify(formData.value));
|
||||
params.consigneeAddressPath = params.address.replace(/\s/g, ',');
|
||||
delete params.address;
|
||||
loading.value = true;
|
||||
if (props.id) {
|
||||
editMemberAddress(params).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
Message.success('编辑地址成功');
|
||||
emit('change', true);
|
||||
hide();
|
||||
}
|
||||
}).catch(() => { loading.value = false; });
|
||||
} else {
|
||||
newMemberAddress(params).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
Message.success('新增地址成功');
|
||||
emit('change', true);
|
||||
hide();
|
||||
}
|
||||
}).catch(() => { loading.value = false; });
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
multipleMap
|
||||
});
|
||||
}
|
||||
|
||||
function getAddrById (id) {
|
||||
// 获取地址详情
|
||||
getAddrDetail(id).then((res) => {
|
||||
if (res.success) {
|
||||
const data = res.result;
|
||||
data.address = res.result.consigneeAddressPath.replace(/,/g, ' ');
|
||||
formData.value = data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getAddress (val) {
|
||||
// 获取地图选择信息
|
||||
if(val.type === 'select'){
|
||||
const paths = val.data.map(item => item.name).join(',')
|
||||
const ids = val.data.map(item => item.id).join(',')
|
||||
formData.value['address'] = paths
|
||||
formData.value['consigneeAddressIdPath'] = ids
|
||||
const coord = val.data[val.data.length - 1].center.split(',')
|
||||
formData.value.lat = coord[1]
|
||||
formData.value.lon = coord[0]
|
||||
}else{
|
||||
formData.value["address"] = val.data.addr;
|
||||
formData.value["consigneeAddressIdPath"] = val.data.addrId;
|
||||
formData.value["detail"] = val.data.address;
|
||||
formData.value.lat = val.data.position.lat;
|
||||
formData.value.lon = val.data.position.lng;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function show () { // 地址模态框显示
|
||||
showAddr.value = true;
|
||||
}
|
||||
|
||||
function hide () { // 地址模态框隐藏
|
||||
showAddr.value = false;
|
||||
}
|
||||
|
||||
defineExpose({ show, hide });
|
||||
|
||||
watch(
|
||||
() => props.id, // 传入的地址id
|
||||
(v) => {
|
||||
if (v) {
|
||||
getAddrById(v);
|
||||
} else {
|
||||
formData.value = {isDefault: false}
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
<template>
|
||||
<!-- 头部广告 -->
|
||||
<div class="advertising" v-if="show" :style="{'background-color': data.bgColor}">
|
||||
<div class="advertising" v-if="show && data" :style="{'background-color': data.bgColor}">
|
||||
<img :src="data.img" class="hover-pointer" @click="linkTo(data.url)"/>
|
||||
<Icon type="md-close-circle" size="20" @click="show = false" />
|
||||
<el-icon :size="20" @click="show = false"><CircleClose /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
data: { // 传入的广告信息
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
show: true // 是否显示头部广告
|
||||
}
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { CircleClose } from '@element-plus/icons-vue';
|
||||
|
||||
defineProps({
|
||||
data: { // 传入的广告信息
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const show = ref(true); // 是否显示头部广告
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.advertising {
|
||||
|
||||
@@ -1,148 +1,159 @@
|
||||
<template>
|
||||
<Card class="_Card" :bordered="false" :dis-hover="true">
|
||||
<div slot="title" class="cardTitle">
|
||||
<span :style="{fontSize:`${_Size}px`}">{{_Title}}</span>
|
||||
|
||||
<div v-if="_Tabs" class="cardTabs">
|
||||
<div @click="tabsChange(index)" :class="{active:(isActive==index)}" class="cardTabsItem" :style="{fontSize:`${_Size-2}px`}" v-for="(item,index) in _Tabs"
|
||||
:key="index">
|
||||
{{item}}
|
||||
<el-card class="_Card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="cardTitle">
|
||||
<span :style="{ fontSize: `${_Size}px` }">{{ _Title }}</span>
|
||||
<div v-if="_Tabs" class="cardTabs">
|
||||
<div
|
||||
@click="tabsChange(index)"
|
||||
:class="{ active: isActive == index }"
|
||||
class="cardTabsItem"
|
||||
:style="{ fontSize: `${_Size - 2}px` }"
|
||||
v-for="(item, index) in _Tabs"
|
||||
:key="index"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cardExtra" v-if="_More" @click="callBack()">
|
||||
{{ _More }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="extra" class="cardExtra" v-if="_More" @click="callBack()">
|
||||
{{_More}}
|
||||
</div>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
<slot />
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'index',
|
||||
props:
|
||||
{
|
||||
_Tabs: { // 可点击的tab栏
|
||||
type: null,
|
||||
default: ''
|
||||
},
|
||||
// 头部
|
||||
_Title: { // 标题
|
||||
type: null,
|
||||
default: '卡片头部'
|
||||
},
|
||||
// 右侧更多
|
||||
_More: {
|
||||
type: null,
|
||||
default: false
|
||||
},
|
||||
_Size: { // 文字大小
|
||||
type: Number,
|
||||
default: 16
|
||||
},
|
||||
// 点击更多触发跳转
|
||||
_Src: {
|
||||
type: null,
|
||||
default: function (val) {
|
||||
if (this._More) {
|
||||
return val;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isActive: 0 // 已激活tab栏下标
|
||||
};
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
defineOptions({ name: "index" });
|
||||
|
||||
const props = defineProps({
|
||||
_Tabs: {
|
||||
type: null,
|
||||
default: "",
|
||||
},
|
||||
methods: {
|
||||
// 点击右侧的回调
|
||||
callBack () {
|
||||
let _this = this;
|
||||
if (this._Src !== '' || this._Src != null) {
|
||||
this.$router.push({
|
||||
path: _this._Src
|
||||
});
|
||||
}
|
||||
},
|
||||
// 点击tab的回调
|
||||
tabsChange (index) {
|
||||
// 处理并返回index
|
||||
this.isActive = index;
|
||||
this.$emit('_Change', index);
|
||||
}
|
||||
_Title: {
|
||||
type: null,
|
||||
default: "卡片头部",
|
||||
},
|
||||
_More: {
|
||||
type: null,
|
||||
default: false,
|
||||
},
|
||||
_Size: {
|
||||
type: Number,
|
||||
default: 16,
|
||||
},
|
||||
_Src: {
|
||||
type: null,
|
||||
default: null,
|
||||
},
|
||||
_ActiveTab: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["_Change"]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const isActive = ref(props._ActiveTab);
|
||||
|
||||
watch(
|
||||
() => props._ActiveTab,
|
||||
(val) => {
|
||||
isActive.value = val;
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
function callBack() {
|
||||
if (props._Src) {
|
||||
router.push({ path: props._Src });
|
||||
}
|
||||
}
|
||||
|
||||
function tabsChange(index) {
|
||||
isActive.value = index;
|
||||
emit("_Change", index);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cardTitle {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
.active{
|
||||
color: $theme_color;
|
||||
position: relative;
|
||||
&::before{
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: $theme_color;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.cardTitle {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
.active {
|
||||
color: $theme_color;
|
||||
position: relative;
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
.cardTabs {
|
||||
display: flex;
|
||||
.cardTabs {
|
||||
display: flex;
|
||||
padding: 0 12px;
|
||||
|
||||
> .cardTabsItem {
|
||||
padding: 0 12px;
|
||||
|
||||
> .cardTabsItem {
|
||||
padding: 0 12px;
|
||||
|
||||
}
|
||||
|
||||
> .cardTabsItem:hover {
|
||||
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .ivu-card, .ivu-card-head, ._Card {
|
||||
margin-bottom: 20px;
|
||||
@include white_background_color();
|
||||
}
|
||||
|
||||
/deep/ .ivu-card-head {
|
||||
position: relative;
|
||||
padding: 0 14px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 50%;
|
||||
top: 25%;
|
||||
background: $theme_color;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cardExtra {
|
||||
> .cardTabsItem:hover {
|
||||
color: $theme_color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .ivu-card-body {
|
||||
padding: 0 !important;
|
||||
display: none;
|
||||
:deep(._Card) {
|
||||
margin-bottom: 20px;
|
||||
@include white_background_color();
|
||||
}
|
||||
|
||||
:deep(._Card .el-card__header) {
|
||||
position: relative;
|
||||
padding: 0 14px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 3px;
|
||||
height: 50%;
|
||||
top: 25%;
|
||||
background: $theme_color;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cardExtra {
|
||||
color: $theme_color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(._Card .el-card__body) {
|
||||
padding: 0 !important;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(._Card .el-card__body:empty) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
<ul>
|
||||
<li v-for="(goods,goodsIndex) in cartList" :key="goodsIndex">
|
||||
<div>
|
||||
<img :src="goods.goodsSku.thumbnail" width="90" height="90" alt="">
|
||||
<img :src="goods.goodsSku.thumbnail" loading="lazy" decoding="async" width="90" height="90" alt="">
|
||||
</div>
|
||||
<div>
|
||||
<p class="hover-color" @click="linkTo(`/goodsDetail?skuId=${goods.goodsSku.id}&goodsId=${goods.goodsSku.goodsId}`)">{{goods.goodsSku.goodsName}}</p>
|
||||
<p class="price">{{goods.goodsSku.price | unitPrice('¥')}}<span> x{{goods.num}}</span></p>
|
||||
<p class="price">{{ $filters.unitPrice(goods.goodsSku.price, '¥') }}<span> x{{goods.num}}</span></p>
|
||||
</div>
|
||||
<span class="del hover-color" @click="delGoods(goods.goodsSku.id)">删除</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Button size="large" class="mt_10" type="primary" @click="linkTo('/cart')" long>去购物车结算</Button>
|
||||
<el-button size="large" class="mt_10" type="primary" @click="linkTo('/cart')" style="width:100%">去购物车结算</el-button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="title === '我的订单'" class="order-con">
|
||||
@@ -21,15 +21,15 @@
|
||||
<li v-for="(order,orderIndex) in orderList" :key="orderIndex">
|
||||
<div class="order-status"><span>{{filterOrderStatus(order.orderStatus)}}</span><span>{{order.createTime}}</span></div>
|
||||
<div class="goods-img">
|
||||
<img :src="img.image"
|
||||
<img :src="img.image" loading="lazy" decoding="async"
|
||||
@click="linkTo(`/goodsDetail?skuId=${img.skuId}&goodsId=${img.goodsId}`)"
|
||||
v-for="(img,imgIndex) in order.orderItems"
|
||||
:key="imgIndex" width="40" height="40" alt="">
|
||||
</div>
|
||||
<div class="order-handle"><span>{{ order.flowPrice | unitPrice("¥") }}</span><span class="hover-color" @click="linkTo(`home/OrderDetail?sn=${order.sn}`)">查看订单</span></div>
|
||||
<div class="order-handle"><span>{{ $filters.unitPrice(order.flowPrice, "¥") }}</span><span class="hover-color" @click="linkTo(`/home/OrderDetail?sn=${order.sn}`)">查看订单</span></div>
|
||||
</li>
|
||||
</ul>
|
||||
<Button type="primary" @click="linkTo('/home/MyOrder')" long>查看全部订单</Button>
|
||||
<el-button type="primary" @click="linkTo('/home/MyOrder')" style="width:100%">查看全部订单</el-button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="title === '优惠券'" class="coupon-con">
|
||||
@@ -37,7 +37,7 @@
|
||||
<li v-for="(coupon, index) in couponList" class="coupon-item" :key="index">
|
||||
<div class="c-left">
|
||||
<div>
|
||||
<span v-if="coupon.couponType === 'PRICE'" class="fontsize_12 global_color">¥<span class="price">{{coupon.price | unitPrice}}</span></span>
|
||||
<span v-if="coupon.couponType === 'PRICE'" class="fontsize_12 global_color">¥<span class="price">{{ $filters.unitPrice(coupon.price) }}</span></span>
|
||||
<span v-if="coupon.couponType === 'DISCOUNT'" class="fontsize_12 global_color"><span class="price">{{coupon.couponDiscount}}</span>折</span>
|
||||
<span class="describe">满{{coupon.consumeThreshold}}元可用</span>
|
||||
</div>
|
||||
@@ -54,9 +54,9 @@
|
||||
<div v-else-if="title === '我的足迹'" class="tracks-con">
|
||||
<ul>
|
||||
<li v-for="(track,trackIndex) in tracksList" :key="trackIndex">
|
||||
<img :src="track.thumbnail" :alt="track.thumbnail" @click="linkTo(`/goodsDetail?skuId=${track.id}&goodsId=${track.goodsId}`)" width="100" height="100">
|
||||
<img :src="track.thumbnail" loading="lazy" decoding="async" :alt="track.thumbnail" @click="linkTo(`/goodsDetail?skuId=${track.id}&goodsId=${track.goodsId}`)" width="100" height="100">
|
||||
<div @click="addToCart(track.id)">加入购物车</div>
|
||||
<p class="global_color">{{track.price | unitPrice('¥')}}</p>
|
||||
<p class="global_color">{{ $filters.unitPrice(track.price, '¥') }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="hover-color" style="text-align:center;" @click="linkTo('/home/MyTracks')">查看更多>></div>
|
||||
@@ -64,238 +64,242 @@
|
||||
<div v-else-if="title === '我的收藏'" class="collect-con">
|
||||
<ul>
|
||||
<li v-for="(collect,collectIndex) in collectList" :key="collectIndex">
|
||||
<img :src="collect.image" :alt="collect.image" @click="linkTo(`/goodsDetail?skuId=${collect.skuId}&goodsId=${collect.goodsId}`)" width="100" height="100">
|
||||
<img :src="collect.image" loading="lazy" decoding="async" :alt="collect.image" @click="linkTo(`/goodsDetail?skuId=${collect.skuId}&goodsId=${collect.goodsId}`)" width="100" height="100">
|
||||
<div @click="addToCart(collect.skuId)">加入购物车</div>
|
||||
<span class="del-icon" @click.stop="cancelCollect(collect.skuId)">
|
||||
<Icon type="md-trash" />
|
||||
<el-icon><Delete /></el-icon>
|
||||
</span>
|
||||
<p class="global_color">{{collect.price | unitPrice('¥')}}</p>
|
||||
<p class="global_color">{{ $filters.unitPrice(collect.price, '¥') }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="hover-color" style="text-align:center;" @click="linkTo('/home/Favorites')">查看更多>></div>
|
||||
</div>
|
||||
<Spin v-if="loading" fix></Spin>
|
||||
<el-skeleton v-if="loading" fix></el-skeleton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
import { Delete } from '@element-plus/icons-vue';
|
||||
import {cartGoodsAll, delCartGoods, addCartGoods, cartCount} from '@/api/cart.js'
|
||||
import { getOrderList } from '@/api/order';
|
||||
import {couponList, receiveCoupon, tracksList, collectList, cancelCollect} from '@/api/member.js'
|
||||
export default {
|
||||
name: 'Drawer',
|
||||
props: {
|
||||
title: {
|
||||
default: '',
|
||||
type: String
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
title (val) {
|
||||
switch (val) {
|
||||
case '购物车':
|
||||
this.getCartList()
|
||||
break;
|
||||
case '我的订单':
|
||||
this.getOrderList()
|
||||
break;
|
||||
case '我的足迹':
|
||||
this.getTracksList()
|
||||
break;
|
||||
case '优惠券':
|
||||
this.getCouponList()
|
||||
break;
|
||||
case '我的收藏':
|
||||
this.getCollectList()
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false, // 控制spin显隐
|
||||
cartList: [], // 购物车列表
|
||||
couponList: [], // 优惠券列表
|
||||
orderList: [], // 订单列表
|
||||
collectList: [], // 收藏列表
|
||||
tracksList: [], // 足迹列表
|
||||
orderStatusList: [ // 订单状态
|
||||
{
|
||||
name: '未付款',
|
||||
status: 'UNPAID'
|
||||
},
|
||||
{
|
||||
name: '已付款',
|
||||
status: 'PAID'
|
||||
},
|
||||
{
|
||||
name: '待发货',
|
||||
status: 'UNDELIVERED'
|
||||
},
|
||||
{
|
||||
name: '已发货',
|
||||
status: 'DELIVERED'
|
||||
},
|
||||
{
|
||||
name: '已完成',
|
||||
status: 'COMPLETED'
|
||||
},
|
||||
{
|
||||
name: '待核验',
|
||||
status: 'TAKE'
|
||||
},
|
||||
{
|
||||
name: '已取消',
|
||||
status: 'CANCELLED'
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getCartList () { // 获取购物车列表
|
||||
this.loading = true
|
||||
cartGoodsAll().then(res => {
|
||||
this.loading = false
|
||||
this.cartList = res.result.skuList
|
||||
})
|
||||
},
|
||||
// 删除商品
|
||||
delGoods (id) {
|
||||
delCartGoods({ skuIds: id }).then((res) => {
|
||||
if (res.success) {
|
||||
this.$Message.success('删除成功');
|
||||
this.getCartList();
|
||||
cartCount().then(res => {
|
||||
this.$store.commit('SET_CARTNUM', res.result)
|
||||
this.Cookies.setItem('cartNum', res.result)
|
||||
})
|
||||
} else {
|
||||
this.$Message.error(res.message);
|
||||
}
|
||||
});
|
||||
},
|
||||
filterOrderStatus (status) { // 获取订单状态中文
|
||||
const ob = this.orderStatusList.filter(e => { return e.status === status });
|
||||
return ob[0].name
|
||||
},
|
||||
receive (item) { // 领取优惠券
|
||||
receiveCoupon(item.id).then(res => {
|
||||
if (res.success) {
|
||||
this.$Modal.confirm({
|
||||
title: '领取优惠券',
|
||||
content: '<p>优惠券领取成功,可到我的优惠券页面查看</p>',
|
||||
okText: '我的优惠券',
|
||||
cancelText: '立即使用',
|
||||
onOk: () => {
|
||||
this.$router.push('/home/Coupons')
|
||||
},
|
||||
onCancel: () => {
|
||||
if (item.storeId !== '0') {
|
||||
this.$router.push({path: '/merchant', query: {id: item.storeId}})
|
||||
} else {
|
||||
if (item.scopeType === 'PORTION_GOODS_CATEGORY') {
|
||||
this.$router.push({path: '/goodsList', query: {categoryId: item.scopeId}})
|
||||
} else {
|
||||
this.$router.push({path: '/goodsList'})
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
useScope (type, storeName) { // 判断优惠券使用范围
|
||||
let shop = '平台';
|
||||
let goods = '全部商品'
|
||||
if (storeName !== 'platform') shop = storeName
|
||||
switch (type) {
|
||||
case 'ALL':
|
||||
goods = '全部商品'
|
||||
break;
|
||||
case 'PORTION_GOODS':
|
||||
goods = '部分商品'
|
||||
break;
|
||||
case 'PORTION_GOODS_CATEGORY':
|
||||
goods = '部分分类商品'
|
||||
break;
|
||||
}
|
||||
return `${shop}${goods}可用`
|
||||
},
|
||||
addToCart (id) { // 添加商品到购物车
|
||||
const params = {
|
||||
num: 1,
|
||||
skuId: id
|
||||
}
|
||||
this.loading = true;
|
||||
addCartGoods(params).then(res => {
|
||||
this.loading = false;
|
||||
if (res.success) {
|
||||
this.$Message.success('商品已成功添加到购物车')
|
||||
} else {
|
||||
this.$Message.warning(res.message);
|
||||
}
|
||||
}).catch(() => { this.loading = false });
|
||||
},
|
||||
getCouponList () { // 获取优惠券列表
|
||||
// this.loading = true;
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 10
|
||||
}
|
||||
couponList(params).then(res => {
|
||||
this.loading = false
|
||||
if (res.success) {
|
||||
this.couponList = res.result.records
|
||||
}
|
||||
}).catch(() => { this.loading = false })
|
||||
},
|
||||
getOrderList () { // 获取订单列表
|
||||
this.loading = true
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
tag: 'ALL'
|
||||
}
|
||||
getOrderList(params).then(res => {
|
||||
this.loading = false
|
||||
if (res.success) {
|
||||
this.orderList = res.result.records;
|
||||
}
|
||||
});
|
||||
},
|
||||
getCollectList () { // 获取收藏列表
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
type: 'GOODS'
|
||||
}
|
||||
this.loading = true
|
||||
collectList(params).then(res => {
|
||||
this.loading = false
|
||||
this.collectList = res.result.records
|
||||
})
|
||||
},
|
||||
cancelCollect (id) { // 取消商品收藏
|
||||
cancelCollect('GOODS', id).then(res => {
|
||||
if (res.success) {
|
||||
this.$Message.success('取消收藏成功')
|
||||
this.getCollectList();
|
||||
}
|
||||
})
|
||||
},
|
||||
getTracksList () { // 获取足迹列表
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
this.loading = true
|
||||
tracksList(params).then(res => {
|
||||
this.tracksList = res.result.records
|
||||
this.loading = false
|
||||
}).catch(() => { this.loading = false })
|
||||
}
|
||||
import { getOrderList as fetchOrderList } from '@/api/order';
|
||||
import {couponList as fetchCouponList, receiveCoupon, tracksList as fetchTracksList, collectList as fetchCollectList, cancelCollect as apiCancelCollect} from '@/api/member.js'
|
||||
import storage from '@/plugins/storage.js'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
defineOptions({ name: 'Drawer' })
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
default: '',
|
||||
type: String
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const loading = ref(false) // 控制spin显隐
|
||||
const cartList = ref([]) // 购物车列表
|
||||
const couponList = ref([]) // 优惠券列表
|
||||
const orderList = ref([]) // 订单列表
|
||||
const collectList = ref([]) // 收藏列表
|
||||
const tracksList = ref([]) // 足迹列表
|
||||
const orderStatusList = ref([ // 订单状态
|
||||
{
|
||||
name: '未付款',
|
||||
status: 'UNPAID'
|
||||
},
|
||||
{
|
||||
name: '已付款',
|
||||
status: 'PAID'
|
||||
},
|
||||
{
|
||||
name: '待发货',
|
||||
status: 'UNDELIVERED'
|
||||
},
|
||||
{
|
||||
name: '已发货',
|
||||
status: 'DELIVERED'
|
||||
},
|
||||
{
|
||||
name: '已完成',
|
||||
status: 'COMPLETED'
|
||||
},
|
||||
{
|
||||
name: '待核验',
|
||||
status: 'TAKE'
|
||||
},
|
||||
{
|
||||
name: '已取消',
|
||||
status: 'CANCELLED'
|
||||
}
|
||||
])
|
||||
|
||||
watch(() => props.title, (val) => {
|
||||
switch (val) {
|
||||
case '购物车':
|
||||
getCartList()
|
||||
break;
|
||||
case '我的订单':
|
||||
getOrderList()
|
||||
break;
|
||||
case '我的足迹':
|
||||
getTracksList()
|
||||
break;
|
||||
case '优惠券':
|
||||
getCouponList()
|
||||
break;
|
||||
case '我的收藏':
|
||||
getCollectList()
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
function getCartList () { // 获取购物车列表
|
||||
loading.value = true
|
||||
cartGoodsAll().then(res => {
|
||||
loading.value = false
|
||||
cartList.value = res.result.skuList
|
||||
})
|
||||
}
|
||||
// 删除商品
|
||||
function delGoods (id) {
|
||||
delCartGoods({ skuIds: id }).then((res) => {
|
||||
if (res.success) {
|
||||
Message.success('删除成功');
|
||||
getCartList();
|
||||
cartCount().then(res => {
|
||||
appStore.setCartNum(res.result)
|
||||
storage.setItem('cartNum', res.result)
|
||||
})
|
||||
} else {
|
||||
Message.error(res.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
function filterOrderStatus (status) { // 获取订单状态中文
|
||||
const ob = orderStatusList.value.filter(e => { return e.status === status });
|
||||
return ob[0].name
|
||||
}
|
||||
function receive (item) { // 领取优惠券
|
||||
receiveCoupon(item.id).then(res => {
|
||||
if (res.success) {
|
||||
Modal.confirm({
|
||||
title: '领取优惠券',
|
||||
content: '<p>优惠券领取成功,可到我的优惠券页面查看</p>',
|
||||
okText: '我的优惠券',
|
||||
cancelText: '立即使用',
|
||||
onOk: () => {
|
||||
router.push('/home/Coupons')
|
||||
},
|
||||
onCancel: () => {
|
||||
if (item.storeId !== '0') {
|
||||
router.push({path: '/merchant', query: {id: item.storeId}})
|
||||
} else {
|
||||
if (item.scopeType === 'PORTION_GOODS_CATEGORY') {
|
||||
router.push({path: '/goodsList', query: {categoryId: item.scopeId}})
|
||||
} else {
|
||||
router.push({path: '/goodsList'})
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
function useScope (type, storeName) { // 判断优惠券使用范围
|
||||
let shop = '平台';
|
||||
let goods = '全部商品'
|
||||
if (storeName !== 'platform') shop = storeName
|
||||
switch (type) {
|
||||
case 'ALL':
|
||||
goods = '全部商品'
|
||||
break;
|
||||
case 'PORTION_GOODS':
|
||||
goods = '部分商品'
|
||||
break;
|
||||
case 'PORTION_GOODS_CATEGORY':
|
||||
goods = '部分分类商品'
|
||||
break;
|
||||
}
|
||||
return `${shop}${goods}可用`
|
||||
}
|
||||
function addToCart (id) { // 添加商品到购物车
|
||||
const params = {
|
||||
num: 1,
|
||||
skuId: id
|
||||
}
|
||||
loading.value = true;
|
||||
addCartGoods(params).then(res => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
Message.success('商品已成功添加到购物车')
|
||||
} else {
|
||||
Message.warning(res.message);
|
||||
}
|
||||
}).catch(() => { loading.value = false });
|
||||
}
|
||||
function getCouponList () { // 获取优惠券列表
|
||||
// loading.value = true;
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 10
|
||||
}
|
||||
fetchCouponList(params).then(res => {
|
||||
loading.value = false
|
||||
if (res.success) {
|
||||
couponList.value = res.result.records
|
||||
}
|
||||
}).catch(() => { loading.value = false })
|
||||
}
|
||||
function getOrderList () { // 获取订单列表
|
||||
loading.value = true
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
tag: 'ALL'
|
||||
}
|
||||
fetchOrderList(params).then(res => {
|
||||
loading.value = false
|
||||
if (res.success) {
|
||||
orderList.value = res.result.records;
|
||||
}
|
||||
});
|
||||
}
|
||||
function getCollectList () { // 获取收藏列表
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
type: 'GOODS'
|
||||
}
|
||||
loading.value = true
|
||||
fetchCollectList(params).then(res => {
|
||||
loading.value = false
|
||||
collectList.value = res.result.records
|
||||
})
|
||||
}
|
||||
function cancelCollect (id) { // 取消商品收藏
|
||||
apiCancelCollect('GOODS', id).then(res => {
|
||||
if (res.success) {
|
||||
Message.success('取消收藏成功')
|
||||
getCollectList();
|
||||
}
|
||||
})
|
||||
}
|
||||
function getTracksList () { // 获取足迹列表
|
||||
const params = {
|
||||
pageNumber: 1,
|
||||
pageSize: 20
|
||||
}
|
||||
loading.value = true
|
||||
fetchTracksList(params).then(res => {
|
||||
tracksList.value = res.result.records
|
||||
loading.value = false
|
||||
}).catch(() => { loading.value = false })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -3,102 +3,99 @@
|
||||
<div class="wrapper" :style="{right:handleDrawer ? '300px' : '0px'}">
|
||||
|
||||
<div class="barItem" @mouseenter="showCartNum(item)" @click="clickBar(item)" v-for="(item,index) in resetConfig.menuList" :key="index">
|
||||
<Tooltip placement="left" :content="item.title">
|
||||
<Icon size="20" :type="item.icon"/>
|
||||
<el-tooltip placement="left" :content="item.title">
|
||||
<el-icon :size="20"><component :is="resolveIcon(item.icon)" /></el-icon>
|
||||
<p class="barTitle" v-if="item.titleShow"> {{item.title}}</p>
|
||||
<div class="circle" v-if="item.title === '购物车'">
|
||||
{{cartNum < 100 ? cartNum : 99}}
|
||||
{{ Number(cartNum) > 99 ? '99+' : cartNum }}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<Drawer width="300" class="popup" :title="drawerData.title" :mask="resetConfig.mask" :closable="resetConfig.closable"
|
||||
<el-drawer width="300" class="popup" :title="drawerData.title" :modal="resetConfig.mask" :show-close="resetConfig.closable"
|
||||
v-model="handleDrawer">
|
||||
<drawerPage :title="drawerData.title" />
|
||||
</Drawer>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Storage from '@/plugins/storage.js';
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Modal } from "@/utils/message";
|
||||
import storage from '@/plugins/storage.js';
|
||||
import Configuration from './config';
|
||||
import drawerPage from './Drawer'
|
||||
import {cartCount} from '@/api/cart.js'
|
||||
export default {
|
||||
name: 'Main',
|
||||
data () {
|
||||
return {
|
||||
resetConfig: Configuration, // 菜单项
|
||||
handleDrawer: false, // 是否可展开
|
||||
drawerData: '' // 菜单基础数据
|
||||
}
|
||||
},
|
||||
components: {drawerPage},
|
||||
computed: {
|
||||
// 用户信息
|
||||
userInfo () {
|
||||
return Storage.getItem('userInfo');
|
||||
},
|
||||
// 购物车商品数量
|
||||
cartNum () {
|
||||
return this.$store.state.cartNum
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showCartNum (item) { // 获取购物车数量
|
||||
if (this.userInfo && item.title === '购物车') {
|
||||
this.getCartList()
|
||||
}
|
||||
},
|
||||
clickBar (val) { // tabbar点击操作
|
||||
if (!this.userInfo) {
|
||||
this.$Modal.confirm({
|
||||
title: '请登录',
|
||||
content: '<p>请登录后执行此操作</p>',
|
||||
okText: '立即登录',
|
||||
cancelText: '继续浏览',
|
||||
onOk: () => {
|
||||
this.$router.push({
|
||||
path: '/login',
|
||||
query: {
|
||||
rePath: this.$router.history.current.path,
|
||||
query: JSON.stringify(this.$router.history.current.query)
|
||||
}
|
||||
});
|
||||
}
|
||||
import { resolveIcon } from '@/utils/iconMap'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
defineOptions({ name: 'Main' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const resetConfig = ref(Configuration) // 菜单项
|
||||
const handleDrawer = ref(false) // 是否可展开
|
||||
const drawerData = ref('') // 菜单基础数据
|
||||
|
||||
// 用户信息
|
||||
const userInfo = computed(() => {
|
||||
return storage.getItem('userInfo');
|
||||
})
|
||||
|
||||
// 购物车商品数量
|
||||
const cartNum = computed(() => appStore.cartNum)
|
||||
|
||||
function showCartNum (item) { // 获取购物车数量
|
||||
if (userInfo.value && item.title === '购物车') {
|
||||
getCartList()
|
||||
}
|
||||
}
|
||||
|
||||
function clickBar (val) { // tabbar点击操作
|
||||
if (!userInfo.value) {
|
||||
Modal.confirm({
|
||||
title: '请登录',
|
||||
content: '请登录后执行此操作',
|
||||
okText: '立即登录',
|
||||
cancelText: '继续浏览',
|
||||
onOk: () => {
|
||||
router.push({
|
||||
path: '/login',
|
||||
query: {
|
||||
rePath: route.path,
|
||||
query: JSON.stringify(route.query || {}),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (val.display) {
|
||||
this.handleDrawer = true
|
||||
this.drawerData = val
|
||||
} else {
|
||||
this.handleDrawer = false
|
||||
switch (val.title) {
|
||||
case '会员中心':
|
||||
this.openBlank('/home')
|
||||
break;
|
||||
case '我的资产':
|
||||
this.openBlank('/home/MoneyManagement')
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
openBlank (path) { // 新页面打开地址
|
||||
let routerUrl = this.$router.resolve({
|
||||
path: path
|
||||
})
|
||||
window.open(routerUrl.href, '_blank')
|
||||
},
|
||||
getCartList () { // 获取购物车列表
|
||||
cartCount().then(res => {
|
||||
this.$store.commit('SET_CARTNUM', res.result)
|
||||
this.Cookies.setItem('cartNum', res.result)
|
||||
})
|
||||
});
|
||||
} else {
|
||||
if (val.display) {
|
||||
handleDrawer.value = true
|
||||
drawerData.value = val
|
||||
} else {
|
||||
handleDrawer.value = false
|
||||
switch (val.title) {
|
||||
case '会员中心':
|
||||
router.push('/home')
|
||||
break;
|
||||
case '我的资产':
|
||||
router.push('/home/MoneyManagement')
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCartList () { // 获取购物车列表
|
||||
cartCount().then(res => {
|
||||
appStore.setCartNum(res.result)
|
||||
storage.setItem('cartNum', res.result)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -128,11 +125,15 @@ export default {
|
||||
}
|
||||
|
||||
.circle {
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
padding: 0 4px;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
background: $theme_color;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@@ -147,11 +148,11 @@ export default {
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
/deep/.popup .ivu-drawer-body{
|
||||
padding: 0!important;
|
||||
:deep(.popup .el-drawer__body) {
|
||||
padding: 0 !important;
|
||||
background-color: #eee;
|
||||
}
|
||||
/deep/.popup .ivu-drawer-wrap{
|
||||
:deep(.popup) {
|
||||
z-index: 3001;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,42 +1,30 @@
|
||||
# 右侧侧边栏组件
|
||||
> 本组件依赖于iview的组件基础上进行封装
|
||||
>项目结构
|
||||
>* Main -- 组件用于挂在右侧的横栏
|
||||
>* drawer -- 右侧横栏的内容
|
||||
>* config -- 用于设置大小
|
||||
|
||||
基于 Element Plus `el-drawer` 封装的右侧悬浮栏。
|
||||
|
||||
#### config设置
|
||||
```
|
||||
## 项目结构
|
||||
|
||||
- `Main.vue` — 挂载右侧横栏的容器
|
||||
- `Drawer.vue` — 横栏展开后的内容面板
|
||||
- `config.js` — 尺寸与菜单配置
|
||||
|
||||
## config 设置
|
||||
|
||||
```js
|
||||
/**
|
||||
menuList // 组件的menu
|
||||
display //是否显示此menu
|
||||
badge //显示徽标数
|
||||
titleShow //是否显示title
|
||||
*/
|
||||
|
||||
//实例代码
|
||||
width : 50, //bar的大小
|
||||
menuList:[
|
||||
{
|
||||
icon
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
* menuList 菜单项列表
|
||||
* display 是否显示该项
|
||||
* badge 徽标数字
|
||||
* titleShow 是否显示标题
|
||||
*/
|
||||
width: 50,
|
||||
menuList: [{ icon: '...' }]
|
||||
```
|
||||
|
||||
## 功能模块
|
||||
|
||||
账户信息、购物车、我的订单、优惠券、我的资产、我的足迹、我的收藏、邮箱订阅
|
||||
|
||||
> 账户信息
|
||||
> 购物车
|
||||
> 我的订单
|
||||
> 优惠券
|
||||
> 我的资产
|
||||
> 我的足迹
|
||||
> 我的收藏
|
||||
> 邮箱订阅
|
||||
## 使用
|
||||
|
||||
|
||||
## 如何使用
|
||||
1.
|
||||
在页面中注册全局组件 `<drawer />` 即可(已在 `global.js` 中注册)。
|
||||
|
||||
@@ -11,20 +11,19 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Main',
|
||||
props: {
|
||||
_Title: { // 描述内容
|
||||
type: null,
|
||||
default: '暂无更多'
|
||||
},
|
||||
_Size: { // 图片大小
|
||||
type: Number,
|
||||
default: 150
|
||||
}
|
||||
<script setup>
|
||||
defineOptions({ name: 'Main' })
|
||||
|
||||
defineProps({
|
||||
_Title: { // 描述内容
|
||||
type: null,
|
||||
default: '暂无更多'
|
||||
},
|
||||
_Size: { // 图片大小
|
||||
type: Number,
|
||||
default: 150
|
||||
}
|
||||
};
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,81 +1,111 @@
|
||||
<template>
|
||||
<div class="shadow-box">
|
||||
<div class="shadow-item flex flex-a-c flex-j-c" v-for="(item,index) in iconList" @click="handleClickIcon(item)" :key="index">
|
||||
<img class="icon" :src="require(`@/assets/iconfont/${item.icon}.png`)">
|
||||
<img class="icon" :src="getIconUrl(item.icon)">
|
||||
<div class="shadow-label">{{item.label}}</div>
|
||||
<div class="line" v-if="iconList.length-1 !=index"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "fixed-index",
|
||||
data(){
|
||||
return{
|
||||
iconList:[
|
||||
{
|
||||
icon:"user",
|
||||
label:"会员中心",
|
||||
path:"/home"
|
||||
},
|
||||
{
|
||||
icon:"carts",
|
||||
label:"购物车",
|
||||
path:"/cart"
|
||||
},
|
||||
{
|
||||
icon:"notification",
|
||||
label:"消息",
|
||||
path:"/home/MsgList"
|
||||
},
|
||||
{
|
||||
icon:"collage",
|
||||
label:"收藏",
|
||||
path:"/home/Favorites"
|
||||
},
|
||||
{
|
||||
icon:"back",
|
||||
label:"顶部",
|
||||
path:"back"
|
||||
},
|
||||
],
|
||||
isScrolling:false,
|
||||
timeoutId:"",
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getIconUrl } from "@/assets/iconfont/iconMap";
|
||||
import storage from "@/plugins/storage.js";
|
||||
|
||||
defineOptions({ name: "fixed-index" });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const iconList = ref([
|
||||
{
|
||||
icon:"user",
|
||||
label:"会员中心",
|
||||
path:"/home",
|
||||
requireLogin: true,
|
||||
},
|
||||
{
|
||||
icon:"carts",
|
||||
label:"购物车",
|
||||
path:"/cart",
|
||||
requireLogin: true,
|
||||
},
|
||||
{
|
||||
icon:"notification",
|
||||
label:"消息",
|
||||
path:"/home/MsgList",
|
||||
requireLogin: true,
|
||||
},
|
||||
{
|
||||
icon:"collage",
|
||||
label:"收藏",
|
||||
path:"/home/Favorites",
|
||||
requireLogin: true,
|
||||
},
|
||||
{
|
||||
icon:"back",
|
||||
label:"顶部",
|
||||
path:"back"
|
||||
},
|
||||
]);
|
||||
let isScrolling = false;
|
||||
let timeoutId = "";
|
||||
|
||||
onMounted(() => {
|
||||
// 添加滚轮事件监听器
|
||||
window.addEventListener('wheel', function() {
|
||||
if (isScrolling) {
|
||||
// 如果页面正在滚动,则取消定时器并设置 isScrolling 为 false
|
||||
window.cancelAnimationFrame(timeoutId);
|
||||
isScrolling = false;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const vm = this
|
||||
// 添加滚轮事件监听器
|
||||
window.addEventListener('wheel', function() {
|
||||
if (vm.isScrolling) {
|
||||
// 如果页面正在滚动,则取消定时器并设置 isScrolling 为 false
|
||||
window.cancelAnimationFrame(vm.timeoutId);
|
||||
vm.isScrolling = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
methods:{
|
||||
scrollToTop() {
|
||||
const c = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
if (c > 0) {
|
||||
this.timeoutId = window.requestAnimationFrame(this.scrollToTop);
|
||||
window.scrollTo(0, c - c / 4);
|
||||
this.isScrolling = true;
|
||||
}else {
|
||||
this.isScrolling = false;
|
||||
}
|
||||
},
|
||||
handleClickIcon(val){
|
||||
if(val.path === 'back'){
|
||||
this.scrollToTop()
|
||||
}else{
|
||||
this.$router.push(val.path)
|
||||
}
|
||||
});
|
||||
|
||||
function scrollToTop() {
|
||||
const c = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
if (c > 0) {
|
||||
timeoutId = window.requestAnimationFrame(scrollToTop);
|
||||
window.scrollTo(0, c - c / 4);
|
||||
isScrolling = true;
|
||||
}else {
|
||||
isScrolling = false;
|
||||
}
|
||||
}
|
||||
function isLoggedIn () {
|
||||
const rawUserInfo = storage.getItem('userInfo');
|
||||
if (rawUserInfo) {
|
||||
try {
|
||||
return !!JSON.parse(rawUserInfo).username;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !!storage.getItem('accessToken');
|
||||
}
|
||||
function goLogin (rePath) {
|
||||
router.push({
|
||||
path: '/login',
|
||||
query: {
|
||||
rePath,
|
||||
query: JSON.stringify(route.query || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
function handleClickIcon (val) {
|
||||
if (val.path === 'back') {
|
||||
scrollToTop();
|
||||
return;
|
||||
}
|
||||
if (val.requireLogin && !isLoggedIn()) {
|
||||
goLogin(val.path);
|
||||
return;
|
||||
}
|
||||
router.push(val.path);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -68,34 +68,29 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import config from '@/config'
|
||||
|
||||
export default {
|
||||
name: "Footer",
|
||||
data() {
|
||||
return {
|
||||
config:require('@/config'),
|
||||
guideArr: [
|
||||
// 导航链接
|
||||
["购物指南", "购物流程", "会员介绍", "生活旅行", "常见问题"],
|
||||
["配送方式", "上门自提", "配送查询", "收取标准", "物流规则"],
|
||||
["支付方式", "在线支付", "公司转账", "余额支付", "积分支付"],
|
||||
["售后服务", "售后政策", "退款说明", "返修/退货", "取消订单"],
|
||||
],
|
||||
moreLink: ["关于我们", "联系我们", "联系客服", "商家帮助", "隐私政策"], // 更多链接
|
||||
year: new Date().getFullYear(), // 当前年份
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
goArticle() {
|
||||
// 跳转文章页
|
||||
let routeUrl = this.$router.resolve({
|
||||
path: "/article",
|
||||
});
|
||||
window.open(routeUrl.href, "_blank");
|
||||
},
|
||||
},
|
||||
};
|
||||
defineOptions({ name: 'Footer' })
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const guideArr = ref([
|
||||
// 导航链接
|
||||
["购物指南", "购物流程", "会员介绍", "生活旅行", "常见问题"],
|
||||
["配送方式", "上门自提", "配送查询", "收取标准", "物流规则"],
|
||||
["支付方式", "在线支付", "公司转账", "余额支付", "积分支付"],
|
||||
["售后服务", "售后政策", "退款说明", "返修/退货", "取消订单"],
|
||||
])
|
||||
const moreLink = ref(["关于我们", "联系我们", "联系客服", "商家帮助", "隐私政策"]) // 更多链接
|
||||
const year = ref(new Date().getFullYear()) // 当前年份
|
||||
|
||||
function goArticle() {
|
||||
// 跳转文章页
|
||||
router.push("/article")
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,54 +1,21 @@
|
||||
// 全局组件配置
|
||||
import empty from "./empty/Main.vue";
|
||||
import drawer from "./drawer/Main.vue";
|
||||
import Header from "@/components/header/Header.vue";
|
||||
import FixedTopPage from "@/components/advertising/FixedTop.vue";
|
||||
import Footer from "@/components/footer/Footer.vue";
|
||||
import Search from "@/components/Search.vue";
|
||||
import card from "@/components/card/index.vue";
|
||||
import cateNav from "@/components/nav/CateNav.vue";
|
||||
import UserCenterLayout from "@/components/userCenter/Layout.vue";
|
||||
|
||||
import empty from './empty/Main' // 空状态组件
|
||||
import drawer from './drawer/Main' // 右侧bar
|
||||
import Header from '@/components/header/Header'; // 头部组件
|
||||
import FixedTopPage from '@/components/advertising/FixedTop'; // 顶部广告
|
||||
import Footer from '@/components/footer/Footer'; // 底部栏
|
||||
import Search from '@/components/Search' // 搜索框
|
||||
import card from '@/components/card' // 个人中心 卡片
|
||||
import cateNav from '@/components/nav/CateNav' // 个人中心 卡片
|
||||
|
||||
empty.install = function (Vue) {
|
||||
Vue.component('empty', empty);
|
||||
};
|
||||
|
||||
drawer.install = function (Vue) {
|
||||
Vue.component('drawer', drawer);
|
||||
};
|
||||
|
||||
Header.install = function (Vue) {
|
||||
Vue.component('BaseHeader', Header);
|
||||
};
|
||||
|
||||
FixedTopPage.install = function (Vue) {
|
||||
Vue.component('FixedTopPage', FixedTopPage);
|
||||
};
|
||||
|
||||
Footer.install = function (Vue) {
|
||||
Vue.component('BaseFooter', Footer);
|
||||
};
|
||||
|
||||
Search.install = function (Vue) {
|
||||
Vue.component('Search', Search);
|
||||
};
|
||||
|
||||
card.install = function (Vue) {
|
||||
Vue.component('card', card)
|
||||
}
|
||||
|
||||
cateNav.install = function (Vue) {
|
||||
Vue.component('cateNav', cateNav)
|
||||
}
|
||||
|
||||
// 引用本js中所有的组件
|
||||
export function InstallAll (Vue) {
|
||||
Vue.use(empty)
|
||||
Vue.use(drawer)
|
||||
Vue.use(Header)
|
||||
Vue.use(FixedTopPage)
|
||||
Vue.use(Footer)
|
||||
Vue.use(Search)
|
||||
Vue.use(card)
|
||||
Vue.use(cateNav)
|
||||
export function registerGlobalComponents(app) {
|
||||
app.component("empty", empty);
|
||||
app.component("drawer", drawer);
|
||||
app.component("BaseHeader", Header);
|
||||
app.component("FixedTopPage", FixedTopPage);
|
||||
app.component("BaseFooter", Footer);
|
||||
app.component("Search", Search);
|
||||
app.component("card", card);
|
||||
app.component("UserCenterLayout", UserCenterLayout);
|
||||
app.component("cateNav", cateNav);
|
||||
}
|
||||
|
||||
122
buyer/src/components/goods/GoodsThumb.vue
Normal file
122
buyer/src/components/goods/GoodsThumb.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="goods-thumb" ref="rootEl" :class="{ 'is-loaded': loaded, 'is-error': error }">
|
||||
<img v-if="currentSrc" :src="currentSrc" :alt="alt" @load="loaded = true" @error="onError" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
|
||||
defineOptions({ name: "GoodsThumb" });
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
alt: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const rootEl = ref(null);
|
||||
const currentSrc = ref("");
|
||||
const loaded = ref(false);
|
||||
const error = ref(false);
|
||||
let observer = null;
|
||||
|
||||
watch(
|
||||
() => props.src,
|
||||
() => {
|
||||
observe();
|
||||
}
|
||||
);
|
||||
|
||||
function disconnect() {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function observe() {
|
||||
disconnect();
|
||||
currentSrc.value = "";
|
||||
loaded.value = false;
|
||||
error.value = false;
|
||||
if (!props.src) {
|
||||
error.value = true;
|
||||
return;
|
||||
}
|
||||
if (typeof IntersectionObserver === "undefined") {
|
||||
currentSrc.value = props.src;
|
||||
return;
|
||||
}
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (!entries.some((entry) => entry.isIntersecting)) return;
|
||||
currentSrc.value = props.src;
|
||||
disconnect();
|
||||
},
|
||||
{ rootMargin: "160px 0px" }
|
||||
);
|
||||
observer.observe(rootEl.value);
|
||||
}
|
||||
|
||||
function onError() {
|
||||
error.value = true;
|
||||
loaded.value = false;
|
||||
currentSrc.value = "";
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
observe();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.goods-thumb {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
background: #f3f3f3;
|
||||
}
|
||||
|
||||
.goods-thumb::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, #f3f3f3 25%, #e8e8e8 37%, #f3f3f3 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: goods-thumb-loading 1.2s ease infinite;
|
||||
}
|
||||
|
||||
.goods-thumb.is-loaded::before,
|
||||
.goods-thumb.is-error::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.goods-thumb img {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes goods-thumb-loading {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="wrapper" v-if="type === 'goodsDetail'">
|
||||
<div class="wr-l"><Icon size="23" type="ios-alarm-outline" /> 秒杀活动</div>
|
||||
<div class="wr-l"><el-icon :size="23"><AlarmClock /></el-icon> 秒杀活动</div>
|
||||
<div class="count-down" v-if="end === ''">
|
||||
<p>倒计时:</p><span>{{ hours }}</span><span>{{ minutes }}</span><span>{{ seconds }}</span>
|
||||
<span class="count-down-label">倒计时:</span>
|
||||
<span class="count-down-num">{{ hours }}</span>
|
||||
<span class="count-down-colon">:</span>
|
||||
<span class="count-down-num">{{ minutes }}</span>
|
||||
<span class="count-down-colon">:</span>
|
||||
<span class="count-down-num">{{ seconds }}</span>
|
||||
</div>
|
||||
<div v-else>{{end}}</div>
|
||||
</div>
|
||||
@@ -13,58 +18,57 @@
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
time: { // 传入的初始时间
|
||||
default: 1718977559428
|
||||
},
|
||||
type: { // 区分是在详情还是购物车调用
|
||||
default: 'goodsDetail', // 设置两个值,goodsDetail和cart,样式不同
|
||||
type: String
|
||||
}
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { AlarmClock } from '@element-plus/icons-vue';
|
||||
|
||||
const props = defineProps({
|
||||
time: { // 传入的初始时间
|
||||
default: 1718977559428
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
end: '', // 结束状态
|
||||
hours: '', // 小时
|
||||
minutes: '', // 分钟
|
||||
seconds: '', // 秒
|
||||
interval: '' // 定时器
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.init()
|
||||
},
|
||||
methods: {
|
||||
countDown (val) { // 倒计时方法
|
||||
function addZero (i) {
|
||||
return i < 10 ? '0' + i : i + '';
|
||||
}
|
||||
var nowtime = new Date();
|
||||
var endtime = new Date(val);
|
||||
var lefttime = parseInt((endtime.getTime() - nowtime.getTime()) / 1000);
|
||||
var h = parseInt((lefttime / (60 * 60)) % 24);
|
||||
var m = parseInt((lefttime / 60) % 60);
|
||||
var s = parseInt(lefttime % 60);
|
||||
h = addZero(h);
|
||||
m = addZero(m);
|
||||
s = addZero(s);
|
||||
this.hours = h;
|
||||
this.minutes = m
|
||||
this.seconds = s;
|
||||
if (lefttime <= 0) {
|
||||
this.end = `活动已结束`;
|
||||
clearInterval(this.interval)
|
||||
}
|
||||
},
|
||||
init () { // 初始化
|
||||
this.interval = setInterval(() => {
|
||||
this.countDown(this.time);
|
||||
}, 1000);
|
||||
}
|
||||
type: { // 区分是在详情还是购物车调用
|
||||
default: 'goodsDetail', // 设置两个值,goodsDetail和cart,样式不同
|
||||
type: String
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const end = ref(''); // 结束状态
|
||||
const hours = ref(''); // 小时
|
||||
const minutes = ref(''); // 分钟
|
||||
const seconds = ref(''); // 秒
|
||||
let interval = null; // 定时器
|
||||
|
||||
function countDown (val) { // 倒计时方法
|
||||
function addZero (i) {
|
||||
return i < 10 ? '0' + i : i + '';
|
||||
}
|
||||
var nowtime = new Date();
|
||||
var endtime = new Date(val);
|
||||
var lefttime = parseInt((endtime.getTime() - nowtime.getTime()) / 1000);
|
||||
var h = parseInt((lefttime / (60 * 60)) % 24);
|
||||
var m = parseInt((lefttime / 60) % 60);
|
||||
var s = parseInt(lefttime % 60);
|
||||
h = addZero(h);
|
||||
m = addZero(m);
|
||||
s = addZero(s);
|
||||
hours.value = h;
|
||||
minutes.value = m
|
||||
seconds.value = s;
|
||||
if (lefttime <= 0) {
|
||||
end.value = `活动已结束`;
|
||||
clearInterval(interval)
|
||||
}
|
||||
}
|
||||
|
||||
function init () { // 初始化
|
||||
interval = setInterval(() => {
|
||||
countDown(props.time);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.cart-promotion{
|
||||
@@ -87,36 +91,35 @@ export default {
|
||||
font-size: 13px;
|
||||
}
|
||||
.count-down {
|
||||
margin-right: -20px;
|
||||
p{
|
||||
float: left;
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
|
||||
.count-down-label {
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
> span {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 20px;
|
||||
|
||||
.count-down-num {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
padding: 0 2px;
|
||||
background-color: #2f3430;
|
||||
margin-right: 20px;
|
||||
color: white;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
&::after {
|
||||
content: ":";
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
font-weight: bolder;
|
||||
font-size: 14px;
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
}
|
||||
line-height: 1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
> span:last-child::after {
|
||||
content: "";
|
||||
|
||||
.count-down-colon {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
495
buyer/src/components/goodsDetail/StoreSide.vue
Normal file
495
buyer/src/components/goodsDetail/StoreSide.vue
Normal file
@@ -0,0 +1,495 @@
|
||||
<template>
|
||||
<aside class="store-side" v-if="storeId">
|
||||
<div class="store-card">
|
||||
<div class="store-head" @click="goStore">
|
||||
<img
|
||||
v-if="store.storeLogo && !logoError"
|
||||
class="store-logo"
|
||||
:src="store.storeLogo"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
:alt="storeName"
|
||||
@error="logoError = true"
|
||||
/>
|
||||
<span v-else class="store-logo store-logo-fallback">{{ storeInitial }}</span>
|
||||
<div class="store-head-text">
|
||||
<p class="store-name">
|
||||
<span v-if="isSelfOperated" class="store-self">自营</span>
|
||||
<span class="store-name-text">{{ storeName }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="store-scores">
|
||||
<div class="score-item">
|
||||
<span class="score-label">描述</span>
|
||||
<span class="score-value">{{ formatScore(store.descriptionScore) }}</span>
|
||||
</div>
|
||||
<div class="score-item">
|
||||
<span class="score-label">服务</span>
|
||||
<span class="score-value">{{ formatScore(store.serviceScore) }}</span>
|
||||
</div>
|
||||
<div class="score-item">
|
||||
<span class="score-label">物流</span>
|
||||
<span class="score-value">{{ formatScore(store.deliveryScore) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="store-actions">
|
||||
<button type="button" class="store-btn" @click="contact">联系客服</button>
|
||||
<button type="button" class="store-btn" :class="{ 'is-on': collected }" @click="toggleCollect">
|
||||
{{ collected ? "已收藏" : "收藏店铺" }}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="enter-store" @click="goStore">进店逛逛</button>
|
||||
</div>
|
||||
|
||||
<div class="recommend-card" v-if="recommends.length">
|
||||
<p class="recommend-title">店铺推荐</p>
|
||||
<div
|
||||
class="recommend-item"
|
||||
v-for="item in recommends"
|
||||
:key="item.id"
|
||||
@click="goGoods(item)"
|
||||
>
|
||||
<img class="recommend-img" :src="item.thumbnail || item.small" loading="lazy" decoding="async" :alt="item.goodsName" />
|
||||
<div class="recommend-info">
|
||||
<p class="recommend-name">{{ item.goodsName }}</p>
|
||||
<div class="recommend-meta">
|
||||
<p class="recommend-price">{{ $filters.unitPrice(item.price, "¥") }}</p>
|
||||
<span class="recommend-sales">销量 {{ item.buyCount || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
import { goodsList } from "@/api/goods";
|
||||
import { getStoreById } from "@/api/shopentry";
|
||||
import { cancelStoreCollect, collectStore, isStoreCollection } from "@/api/member";
|
||||
import { getIMDetail } from "@/api/common";
|
||||
import { getMemberMsg } from "@/api/login";
|
||||
import storage from "@/plugins/storage.js";
|
||||
|
||||
defineOptions({ name: "StoreSide" });
|
||||
|
||||
const props = defineProps({
|
||||
storeId: {
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
},
|
||||
goodsId: {
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
},
|
||||
skuId: {
|
||||
type: [String, Number],
|
||||
default: "",
|
||||
},
|
||||
storeNameFallback: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const store = ref({});
|
||||
const logoError = ref(false);
|
||||
const collected = ref(false);
|
||||
const recommends = ref([]);
|
||||
// 原 talkIm mixin 的 IM 链接
|
||||
const IMLink = ref("");
|
||||
|
||||
const storeName = computed(() => {
|
||||
return store.value.storeName || props.storeNameFallback || "店铺";
|
||||
});
|
||||
const storeInitial = computed(() => {
|
||||
return storeName.value.slice(0, 1);
|
||||
});
|
||||
// 自营标记后端可能是 true、1 或 "1"
|
||||
const isSelfOperated = computed(() => {
|
||||
const flag = store.value.selfOperated;
|
||||
return flag === true || flag === 1 || flag === "1";
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.storeId,
|
||||
(id) => {
|
||||
if (!id) return;
|
||||
logoError.value = false;
|
||||
loadStore(id);
|
||||
loadRecommend(id);
|
||||
loadCollected(id);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 没有评分或小于等于 0 时按满分展示
|
||||
function formatScore(value) {
|
||||
const score = Number(value);
|
||||
if (!Number.isFinite(score) || score <= 0) return "5.00";
|
||||
return score.toFixed(2);
|
||||
}
|
||||
|
||||
function loadStore(id) {
|
||||
getStoreById(id).then((res) => {
|
||||
if (res.success && res.result) store.value = res.result;
|
||||
}).catch((err) => {
|
||||
console.warn('获取店铺信息失败', err);
|
||||
});
|
||||
}
|
||||
|
||||
// 按销量取店内商品,去掉当前商品,最多 4 个
|
||||
function loadRecommend(id) {
|
||||
goodsList({
|
||||
storeId: id,
|
||||
pageNumber: 1,
|
||||
pageSize: 8,
|
||||
sort: "buyCount",
|
||||
order: "desc",
|
||||
}).then((res) => {
|
||||
const records = (res.success && res.result && res.result.records) || [];
|
||||
recommends.value = records
|
||||
.filter((item) => String(item.goodsId) !== String(props.goodsId))
|
||||
.slice(0, 4);
|
||||
}).catch(() => {
|
||||
// 推荐商品为可选数据,加载失败时静默
|
||||
});
|
||||
}
|
||||
|
||||
// 同时有 token 和用户信息才查收藏,避免残留 userInfo 触发登录跳转
|
||||
function loadCollected(id) {
|
||||
if (!storage.getItem("accessToken") || !storage.getItem("userInfo")) return;
|
||||
isStoreCollection("STORE", id).then((res) => {
|
||||
if (res.success && res.result) collected.value = true;
|
||||
}).catch(() => {
|
||||
// 收藏状态为可选数据,失败时静默
|
||||
});
|
||||
}
|
||||
|
||||
function goStore() {
|
||||
router.push({ path: "/merchant", query: { id: props.storeId } });
|
||||
}
|
||||
|
||||
// 推荐商品新开详情页,店铺页仍在当前页打开
|
||||
function goGoods(item) {
|
||||
const href = router.resolve({
|
||||
path: "/goodsDetail",
|
||||
query: { skuId: item.id, goodsId: item.goodsId },
|
||||
}).href;
|
||||
window.open(href, "_blank");
|
||||
}
|
||||
|
||||
// 原 talkIm mixin 的登录提示
|
||||
function promptLogin() {
|
||||
Modal.confirm({
|
||||
title: "温馨提示",
|
||||
content: "请登录后执行此操作",
|
||||
okText: "立即登录",
|
||||
cancelText: "取消",
|
||||
onOk: () => {
|
||||
router.push({
|
||||
path: "/login",
|
||||
query: {
|
||||
rePath: route.path,
|
||||
query: JSON.stringify(route.query || {}),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 获取im信息(原 talkIm mixin)
|
||||
async function getIMDetailMethods() {
|
||||
let res = await getIMDetail();
|
||||
if (res.success) {
|
||||
IMLink.value = res.result;
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转im客服(原 talkIm mixin;本组件无 storeMsg,mixin 的 storeMsg?.storeId 兜底恒为 undefined,等价于直接用传入的 id)
|
||||
async function IMService(id, goodsId, skuId) {
|
||||
if (!storage.getItem("accessToken")) {
|
||||
promptLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await getIMDetailMethods();
|
||||
const userInfo = await getMemberMsg();
|
||||
if (!userInfo.success) {
|
||||
promptLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
const accessToken = storage.getItem("accessToken");
|
||||
const storeId = id;
|
||||
const baseUrl = `${IMLink.value}?token=${accessToken}&id=${storeId}`;
|
||||
|
||||
if (goodsId && skuId) {
|
||||
window.open(`${baseUrl}&goodsId=${goodsId}&skuId=${skuId}`);
|
||||
} else {
|
||||
window.open(baseUrl);
|
||||
}
|
||||
} catch {
|
||||
// token 过期等场景由 request 拦截器处理;未登录时避免未捕获异常
|
||||
if (!storage.getItem("accessToken")) {
|
||||
promptLogin();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function contact() {
|
||||
IMService(props.storeId, props.goodsId, props.skuId);
|
||||
}
|
||||
|
||||
// 未登录先去登录,并带回当前商品页
|
||||
async function toggleCollect() {
|
||||
if (!storage.getItem("accessToken") || !storage.getItem("userInfo")) {
|
||||
router.push({
|
||||
path: "/login",
|
||||
query: {
|
||||
rePath: route.path,
|
||||
query: JSON.stringify(route.query || {}),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (collected.value) {
|
||||
const cancel = await cancelStoreCollect("STORE", props.storeId);
|
||||
if (cancel.success) {
|
||||
collected.value = false;
|
||||
Message.success("已取消收藏");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const collect = await collectStore("STORE", props.storeId);
|
||||
if (collect.code === 200) {
|
||||
collected.value = true;
|
||||
Message.success("收藏店铺成功,可以前往个人中心我的收藏查看");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.store-side {
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.store-card,
|
||||
.recommend-card {
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.store-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.store-head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.store-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.store-logo-fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: $theme_color;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.store-head-text {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.store-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
color: #171A26;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.store-name-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.store-self {
|
||||
flex-shrink: 0;
|
||||
padding: 0 6px;
|
||||
border-radius: 2px;
|
||||
background: $theme_color;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.store-scores {
|
||||
display: flex;
|
||||
margin-top: 14px;
|
||||
padding: 10px 0;
|
||||
border-top: 1px solid #f5f5f5;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.score-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
display: block;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
color: $theme_color;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.store-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.store-btn,
|
||||
.enter-store {
|
||||
border: 1px solid #eee;
|
||||
background: #fff;
|
||||
color: #171A26;
|
||||
font-size: 13px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $theme_color;
|
||||
border-color: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
.store-btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.store-btn.is-on {
|
||||
color: $theme_color;
|
||||
border-color: $theme_color;
|
||||
background: #fff5f4;
|
||||
}
|
||||
|
||||
.enter-store {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.recommend-card {
|
||||
margin-top: 12px;
|
||||
padding: 12px 12px 4px;
|
||||
}
|
||||
|
||||
.recommend-title {
|
||||
margin: 0 0 8px;
|
||||
color: #171A26;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.recommend-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px 0 12px;
|
||||
cursor: pointer;
|
||||
|
||||
& + & {
|
||||
border-top: 1px solid #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.recommend-img {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.recommend-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recommend-name {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recommend-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.recommend-price {
|
||||
margin: 0;
|
||||
color: $theme_color;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.recommend-sales {
|
||||
flex-shrink: 0;
|
||||
color: #999;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
}
|
||||
</style>
|
||||
@@ -23,12 +23,13 @@
|
||||
<li v-show="!!userInfo.username">
|
||||
<div class="username-p">
|
||||
<div>
|
||||
<Avatar class="person-icon" :src="userInfo.face" icon="person" size="small"/>
|
||||
<span class="username">{{
|
||||
userInfo.nickName ? userInfo.nickName : userInfo.username | secrecyMobile
|
||||
}}</span>
|
||||
<el-avatar class="person-icon" :src="userInfo.face" :size="24" />
|
||||
<span class="username">{{ $filters.secrecyMobile(
|
||||
userInfo.nickName ? userInfo.nickName : userInfo.username ) }}</span>
|
||||
</div>
|
||||
<transition name='fade'>
|
||||
<!-- 下拉菜单通过父级 hover 的 CSS 控制显隐,transition 内无需 v-if/v-show -->
|
||||
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
|
||||
<ul class="drop-items">
|
||||
<li @click="goUserCenter('/home')">我的主页</li>
|
||||
<li @click="goUserCenter('/home/Coupons')">优惠券</li>
|
||||
@@ -40,46 +41,45 @@
|
||||
</li>
|
||||
<li @click="goUserCenter('/home/MyOrder')"><span class="nav-item">我的订单</span></li>
|
||||
<li @click="goUserCenter('/home/MyTracks')"><span class="nav-item">我的足迹</span></li>
|
||||
<li @click="goUserCenter('/home/MsgList')"><span class="nav-item">我的消息</span></li>
|
||||
<li v-if="$route.name !== 'Cart'" style="position:relative;">
|
||||
<Dropdown placement="bottom-start">
|
||||
<router-link to="/cart" target="_blank">
|
||||
<span class="nav-item" @mouseenter="getCartList">
|
||||
<Icon size="18" type="ios-cart-outline"></Icon>
|
||||
购物车({{ cartNum < 100 ? cartNum : '99' }})
|
||||
</span>
|
||||
</router-link>
|
||||
<DropdownMenu slot="list">
|
||||
<div class="shopping-cart-null" style="width:200px" v-show="shoppingCart.length <= 0">
|
||||
<Icon type="ios-cart-outline" class="cart-null-icon"></Icon>
|
||||
<li @click="goUserCenter('/home/MsgList')">
|
||||
<span class="nav-item">
|
||||
我的消息
|
||||
<i v-if="unreadMsg > 0" class="msg-badge">{{ unreadLabel }}</i>
|
||||
</span>
|
||||
</li>
|
||||
<li v-if="$route.name !== 'Cart'" class="cart-nav-item">
|
||||
<el-dropdown
|
||||
class="cart-nav-dropdown"
|
||||
placement="bottom-start"
|
||||
trigger="hover"
|
||||
@visible-change="handleCartDropdownVisible"
|
||||
>
|
||||
<span class="nav-item" @click="goToPay">
|
||||
<el-icon :size="18"><ShoppingCart /></el-icon>
|
||||
购物车({{ Number(cartNum) > 99 ? '99+' : cartNum }})
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<div class="shopping-cart-null" v-show="shoppingCart.length <= 0">
|
||||
<el-icon class="cart-null-icon"><ShoppingCart /></el-icon>
|
||||
<span>你的购物车没有宝贝哦</span>
|
||||
<span>赶快去添加商品吧~</span>
|
||||
</div>
|
||||
<div class="shopping-cart-list" v-show="shoppingCart.length > 0">
|
||||
<div class="shopping-cart-box" v-for="(item, index) in shoppingCart" @click="goToPay" :key="index">
|
||||
<div class="shopping-cart-img">
|
||||
<img :src="item.goodsSku.thumbnail" class="hover-pointer"/>
|
||||
<img :src="item.goodsSku.thumbnail" alt="" />
|
||||
</div>
|
||||
<div class="shopping-cart-info">
|
||||
<div class="shopping-cart-title ">
|
||||
<p class="hover-pointer goods-title ellipsis">{{ item.goodsSku.goodsName }}</p>
|
||||
</div>
|
||||
<p class="goods-title">{{ item.goodsSku.goodsName }}</p>
|
||||
<div class="shopping-cart-detail">
|
||||
<div>
|
||||
数量:
|
||||
<span class="shopping-cart-text">{{ item.num }}</span>
|
||||
|
||||
</div>
|
||||
<div class="shopping-price">
|
||||
{{ item.purchasePrice | unitPrice('¥') }}
|
||||
</div>
|
||||
<span>数量 {{ item.num }}</span>
|
||||
<span class="shopping-price">{{ $filters.unitPrice(item.purchasePrice, '¥') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</li>
|
||||
<li>
|
||||
<span class="nav-item" @click="shopEntry">店铺入驻</span>
|
||||
@@ -90,145 +90,243 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Modal } from "@/utils/message";
|
||||
import { ShoppingCart } from '@element-plus/icons-vue';
|
||||
import storage from '@/plugins/storage.js';
|
||||
import {cartGoodsAll} from '@/api/cart.js';
|
||||
import {logout} from '@/api/account.js';
|
||||
import config from '@/config';
|
||||
import { refreshUnreadMessage } from '@/utils/unreadMessage';
|
||||
import { useAppStore } from '@/stores/app';
|
||||
|
||||
defineOptions({ name: 'M-Header' });
|
||||
|
||||
export default {
|
||||
name: 'M-Header',
|
||||
created() {
|
||||
if (storage.getItem('userInfo')) {
|
||||
this.userInfo = JSON.parse(storage.getItem('userInfo'));
|
||||
}
|
||||
},
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
data() {
|
||||
return {
|
||||
config: require('@/config'),
|
||||
userInfo: {}, // 用户信息
|
||||
shoppingCart: [] // 购物车
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 购物车商品数量
|
||||
cartNum() {
|
||||
return this.$store.state.cartNum;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToPay() { // 跳转购物车
|
||||
let url = this.$router.resolve({
|
||||
path: '/cart'
|
||||
});
|
||||
window.open(url.href, '_blank');
|
||||
},
|
||||
myInfo() { // 跳转会员中心
|
||||
let url = this.$router.resolve({
|
||||
path: '/home'
|
||||
});
|
||||
window.open(url.href, '_blank');
|
||||
},
|
||||
signOutFun() { // 退出登录
|
||||
logout().then(res => {
|
||||
storage.removeItem('accessToken');
|
||||
storage.removeItem('refreshToken');
|
||||
storage.removeItem('userInfo');
|
||||
storage.removeItem('cartNum');
|
||||
this.$store.commit('SET_CARTNUM', 0)
|
||||
this.$router.push('/login');
|
||||
});
|
||||
},
|
||||
goUserCenter(path) {
|
||||
// 跳转我的订单,我的足迹、收藏等
|
||||
if (this.userInfo.username) {
|
||||
this.$router.push({path: path});
|
||||
} else {
|
||||
this.$Modal.confirm({
|
||||
title: '请登录',
|
||||
content: '<p>请登录后执行此操作</p>',
|
||||
okText: '立即登录',
|
||||
cancelText: '继续浏览',
|
||||
onOk: () => {
|
||||
this.$router.push({
|
||||
path: '/login',
|
||||
query: {
|
||||
rePath: this.$router.history.current.path,
|
||||
query: JSON.stringify(this.$router.history.current.query)
|
||||
}
|
||||
});
|
||||
}
|
||||
const userInfo = ref({}); // 用户信息
|
||||
const shoppingCart = ref([]); // 购物车
|
||||
const cartLoading = ref(false);
|
||||
const cartLoadedAt = ref(0);
|
||||
const cartCacheTime = ref(5000);
|
||||
|
||||
// 购物车商品数量
|
||||
const cartNum = computed(() => appStore.cartNum);
|
||||
const unreadMsg = computed(() => appStore.unreadMsg);
|
||||
const unreadLabel = computed(() => {
|
||||
return unreadMsg.value > 99 ? '99+' : String(unreadMsg.value);
|
||||
});
|
||||
|
||||
if (storage.getItem('userInfo')) {
|
||||
userInfo.value = JSON.parse(storage.getItem('userInfo'));
|
||||
refreshUnreadMessage();
|
||||
}
|
||||
|
||||
function goToPay() {
|
||||
if (!userInfo.value.username) {
|
||||
Modal.confirm({
|
||||
title: '温馨提示',
|
||||
content: '请登录后执行此操作',
|
||||
okText: '立即登录',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
router.push({
|
||||
path: '/login',
|
||||
query: {
|
||||
rePath: '/cart',
|
||||
query: JSON.stringify(route.query || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
shopEntry() {
|
||||
// 店铺入驻
|
||||
if (storage.getItem('accessToken')) {
|
||||
let routeUrl = this.$router.resolve({
|
||||
path: '/shopEntry',
|
||||
query: {id: 1}
|
||||
});
|
||||
window.open(routeUrl.href, '_blank');
|
||||
} else {
|
||||
this.$router.push('login');
|
||||
}
|
||||
},
|
||||
getCartList() {
|
||||
// 获取购物车列表
|
||||
if (this.userInfo.username) {
|
||||
cartGoodsAll().then((res) => {
|
||||
this.shoppingCart = res.result.skuList;
|
||||
this.$store.commit('SET_CARTNUM', this.shoppingCart.length);
|
||||
this.Cookies.setItem('cartNum', this.shoppingCart.length);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
router.push('/cart');
|
||||
}
|
||||
|
||||
function myInfo() { // 跳转会员中心
|
||||
router.push('/home');
|
||||
}
|
||||
|
||||
function signOutFun() { // 退出登录
|
||||
logout().then(res => {
|
||||
storage.removeItem('accessToken');
|
||||
storage.removeItem('refreshToken');
|
||||
storage.removeItem('userInfo');
|
||||
storage.removeItem('cartNum');
|
||||
appStore.setCartNum(0)
|
||||
appStore.setUnreadMsg(0)
|
||||
router.push('/login');
|
||||
});
|
||||
}
|
||||
|
||||
function goUserCenter(path) {
|
||||
// 跳转我的订单,我的足迹、收藏等
|
||||
if (userInfo.value.username) {
|
||||
router.push({path: path});
|
||||
} else {
|
||||
Modal.confirm({
|
||||
title: '请登录',
|
||||
content: '请登录后执行此操作',
|
||||
okText: '立即登录',
|
||||
cancelText: '继续浏览',
|
||||
onOk: () => {
|
||||
router.push({
|
||||
path: '/login',
|
||||
query: {
|
||||
rePath: route.path,
|
||||
query: JSON.stringify(route.query || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function shopEntry() {
|
||||
if (storage.getItem('accessToken')) {
|
||||
router.push({
|
||||
path: '/shopEntry',
|
||||
query: { id: 1 },
|
||||
});
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '温馨提示',
|
||||
content: '请登录后执行此操作',
|
||||
okText: '立即登录',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
router.push({
|
||||
path: '/login',
|
||||
query: {
|
||||
rePath: '/shopEntry',
|
||||
query: JSON.stringify({ id: 1 }),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleCartDropdownVisible(visible) {
|
||||
if (visible) {
|
||||
getCartList();
|
||||
}
|
||||
}
|
||||
|
||||
function getCartList() {
|
||||
// 获取购物车列表
|
||||
if (!userInfo.value.username || cartLoading.value) {
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (cartLoadedAt.value && now - cartLoadedAt.value < cartCacheTime.value) {
|
||||
return;
|
||||
}
|
||||
cartLoadedAt.value = now;
|
||||
cartLoading.value = true;
|
||||
cartGoodsAll().then((res) => {
|
||||
if (res.success) {
|
||||
shoppingCart.value = (res.result && res.result.skuList) || [];
|
||||
appStore.setCartNum(shoppingCart.value.length);
|
||||
storage.setItem('cartNum', shoppingCart.value.length);
|
||||
}
|
||||
}).finally(() => {
|
||||
cartLoading.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.box {
|
||||
width: 100%;
|
||||
font-size: 12px !important;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
height: 40px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav ul {
|
||||
.nav > ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav li {
|
||||
.nav > ul > li,
|
||||
.location > ul.flex > li {
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
line-height: 36px;
|
||||
float: none;
|
||||
line-height: 40px;
|
||||
margin-right: 15px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav a,
|
||||
.nav-item {
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding-left: 20px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.msg-badge {
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
border-radius: 8px;
|
||||
background: $theme_color;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cart-nav-item {
|
||||
:deep(.cart-nav-dropdown),
|
||||
:deep(.el-dropdown) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
:deep(.el-tooltip__trigger),
|
||||
:deep(.el-dropdown-selfdefine),
|
||||
:deep([aria-expanded="true"]) {
|
||||
outline: none !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.location a {
|
||||
@@ -261,30 +359,10 @@ export default {
|
||||
}
|
||||
|
||||
|
||||
.shopping-cart-img {
|
||||
margin-right: 15px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.shopping-cart-img img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.shopping-cart-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-content: space-between;
|
||||
width: 200px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
.shopping-cart-null {
|
||||
padding: 15px;
|
||||
width: 320px;
|
||||
padding: 28px 16px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -292,20 +370,102 @@ export default {
|
||||
}
|
||||
|
||||
.cart-null-icon {
|
||||
font-size: 38px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 36px;
|
||||
margin-bottom: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.shopping-cart-null span {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.shopping-cart-list {
|
||||
width: 320px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
padding: 4px 0;
|
||||
box-sizing: border-box;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.shopping-cart-box {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
& + .shopping-cart-box {
|
||||
border-top: 1px solid #f2f2f2;
|
||||
}
|
||||
}
|
||||
|
||||
.shopping-cart-img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.shopping-cart-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.shopping-cart-info .goods-title {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.shopping-cart-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.shopping-price {
|
||||
color: $theme_color;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.username-p {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
|
||||
div {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> span {
|
||||
margin-left: 5px;
|
||||
@@ -315,59 +475,71 @@ export default {
|
||||
.drop-items {
|
||||
position: absolute;
|
||||
display: none;
|
||||
top: 45px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
padding: 5px 10px;
|
||||
z-index: 20;
|
||||
height: 150px;
|
||||
flex-direction: column;
|
||||
top: calc(100% + 10px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
z-index: 1000;
|
||||
min-width: 100px;
|
||||
width: max-content;
|
||||
background-color: #fff;
|
||||
width: 80px;
|
||||
border: 1px solid #eee;
|
||||
box-shadow: 2px 2px 7px #999;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
color: rgb(107, 106, 106);
|
||||
display: block;
|
||||
float: none;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid rgb(207, 206, 206);
|
||||
margin: 0;
|
||||
padding: 8px 16px;
|
||||
line-height: 1.4;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: $theme_color;
|
||||
background-color: #fff7f7;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: 30px;
|
||||
top: -6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 10px solid #999;
|
||||
border-color: transparent transparent #fff transparent;
|
||||
border: 6px solid transparent;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
height: 20px;
|
||||
top: -20px;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
top: -10px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.drop-items {
|
||||
display: block;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,7 +576,7 @@ export default {
|
||||
}
|
||||
|
||||
.my-info p {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sign-out {
|
||||
@@ -424,7 +596,7 @@ export default {
|
||||
}
|
||||
|
||||
.sign-out p {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.goods-title:hover {
|
||||
@@ -446,34 +618,4 @@ export default {
|
||||
z-index: 3;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.shopping-cart-box {
|
||||
padding: 8px 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px #ccc dotted;
|
||||
}
|
||||
|
||||
.shopping-cart-list {
|
||||
padding: 10px 10px;
|
||||
box-sizing: border-box;
|
||||
max-height: 300px;
|
||||
overflow-y: scroll;
|
||||
color: #333;
|
||||
|
||||
}
|
||||
|
||||
.shopping-cart-detail {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
|
||||
}
|
||||
|
||||
.shopping-price {
|
||||
color: $theme_color;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<template>
|
||||
<div class="scroll-show">
|
||||
<div class="content clearfix">
|
||||
<cateNav class="cate" :hover="true" :showNavBar="false"
|
||||
useClass="fixed-show"></cateNav>
|
||||
<div class="content">
|
||||
<cateNav
|
||||
class="cate"
|
||||
:hover="true"
|
||||
:showNavBar="false"
|
||||
useClass="fixed-show"
|
||||
/>
|
||||
<Search
|
||||
class="search-con"
|
||||
:hover="true"
|
||||
@@ -10,108 +14,118 @@
|
||||
:showLogo="false"
|
||||
:showTag="false"
|
||||
useClass="fixed-show"
|
||||
></Search>
|
||||
<div class="flex flex-a-c cart">
|
||||
<Icon
|
||||
type="ios-cart-outline"
|
||||
@click="goCartList"
|
||||
class="cart-icon"
|
||||
@mouseenter.native="getCartList"
|
||||
/>
|
||||
<i class="cart-badge">{{ cartNum < 100 ? cartNum : "99" }}</i>
|
||||
/>
|
||||
<div class="cart">
|
||||
<span @mouseenter="getCartList">
|
||||
<el-icon class="cart-icon" :size="20" @click="goCartList"><ShoppingCart /></el-icon>
|
||||
</span>
|
||||
<i class="cart-badge">{{ Number(cartNum) > 99 ? "99+" : cartNum }}</i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { cartCount } from "@/api/cart.js";
|
||||
import storage from "@/plugins/storage.js";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userInfo: {}, // 用户信息
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
cartNum() {
|
||||
// 购物车商品数量
|
||||
return this.$store.state.cartNum;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
goCartList() {
|
||||
// 跳转购物车页面
|
||||
let routerUrl = this.$router.resolve({
|
||||
path: "/cart",
|
||||
});
|
||||
window.open(routerUrl.href, "_blank");
|
||||
},
|
||||
getCartList() {
|
||||
// 获取购物车列表
|
||||
if (storage.getItem("userInfo")) {
|
||||
cartCount().then((res) => {
|
||||
this.$store.commit("SET_CARTNUM", res.result);
|
||||
this.Cookies.setItem("cartNum", res.result);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Search from '@/components/Search.vue'
|
||||
import cateNav from '@/components/nav/CateNav.vue'
|
||||
import { cartCount } from '@/api/cart.js'
|
||||
import storage from '@/plugins/storage.js'
|
||||
import { ShoppingCart } from '@element-plus/icons-vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
if (storage.getItem("userInfo")) {
|
||||
this.userInfo = JSON.parse(storage.getItem("userInfo"));
|
||||
}
|
||||
},
|
||||
};
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const userInfo = ref({})
|
||||
const cartNum = computed(() => appStore.cartNum)
|
||||
|
||||
function goCartList() {
|
||||
router.push('/cart')
|
||||
}
|
||||
|
||||
function getCartList() {
|
||||
if (storage.getItem('userInfo')) {
|
||||
cartCount().then((res) => {
|
||||
appStore.setCartNum(res.result)
|
||||
storage.setItem('cartNum', res.result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (storage.getItem('userInfo')) {
|
||||
userInfo.value = JSON.parse(storage.getItem('userInfo'))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
$side-col-width: 263.2px;
|
||||
$col-gap: 10px;
|
||||
$cart-col-width: 40px;
|
||||
|
||||
.scroll-show {
|
||||
height: 60px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 1200px;
|
||||
height: 40px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
.cate {
|
||||
float: left;
|
||||
width: 200px !important;
|
||||
}
|
||||
.search-con {
|
||||
float: left;
|
||||
width: 800px;
|
||||
overflow: hidden;
|
||||
margin-top: -27px;
|
||||
}
|
||||
.cart{
|
||||
height: 60px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: $side-col-width 1fr $cart-col-width;
|
||||
column-gap: $col-gap;
|
||||
align-items: center;
|
||||
}
|
||||
.cart-icon {
|
||||
width: 30px;
|
||||
float: left;
|
||||
font-size: 25px;
|
||||
|
||||
color: $theme_color;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
.cate {
|
||||
min-width: 0;
|
||||
|
||||
:deep(.cate-nav) {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.nav-con) {
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-con {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cart {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 60px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cart-icon {
|
||||
font-size: 25px;
|
||||
color: $theme_color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cart-badge {
|
||||
position: absolute;
|
||||
font-style: normal;
|
||||
right: 165px;
|
||||
top: 12px;
|
||||
right: -2px;
|
||||
display: block;
|
||||
background-color: $theme_color;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
width: 17px;
|
||||
min-width: 17px;
|
||||
height: 17px;
|
||||
padding: 0 4px;
|
||||
border-radius: 10px;
|
||||
line-height: 17px;
|
||||
text-align: center;
|
||||
z-index: 5;
|
||||
top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,31 +1,47 @@
|
||||
<template>
|
||||
<div class="model-form">
|
||||
<div class="model-content">
|
||||
<template v-for="(element, index) in data.list">
|
||||
<model-form-item
|
||||
v-if="element && element.key"
|
||||
:key="element.key"
|
||||
:element="element"
|
||||
:index="index"
|
||||
:data="data"
|
||||
></model-form-item>
|
||||
</template>
|
||||
<model-form-item
|
||||
v-for="(element, index) in renderList"
|
||||
:key="element.key"
|
||||
:element="element"
|
||||
:index="index"
|
||||
:data="data"
|
||||
></model-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import ModelFormItem from './ModelFormItem.vue';
|
||||
export default {
|
||||
name: 'modelForm',
|
||||
components: {
|
||||
ModelFormItem
|
||||
},
|
||||
props: ['data']
|
||||
};
|
||||
|
||||
defineOptions({ name: 'modelForm' });
|
||||
|
||||
const props = defineProps(['data']);
|
||||
|
||||
const renderList = computed(() => {
|
||||
const list = props.data?.list || [];
|
||||
return list.filter(
|
||||
(el) => el && el.key && !isBuiltinModule(el.type)
|
||||
);
|
||||
});
|
||||
|
||||
function isBuiltinModule(type) {
|
||||
// 顶部广告、快捷导航由 Index 页单独渲染
|
||||
return type === 'topAdvert' || type === 'navBar';
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.model-form {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.model-content {
|
||||
width: 100%;
|
||||
min-height: 1200px;
|
||||
box-sizing: border-box;
|
||||
/* 勿设 overflow-x: hidden,会裁切 discountAdvert 的 margin-left: -100px 左溢背景 */
|
||||
overflow-x: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
<div class="model-item" v-if="element && element.key">
|
||||
<!-- 轮播图模块,包括个人信息,快捷导航模块 -->
|
||||
<template v-if="element.type == 'carousel'">
|
||||
<model-carousel
|
||||
:data="element"
|
||||
class="mb_20 width_1200_auto"
|
||||
></model-carousel>
|
||||
<div class="carousel-wrap mb_20">
|
||||
<model-carousel :key="element.key" :data="element"></model-carousel>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="element.type == 'carousel1'">
|
||||
<model-carousel1 :data="element" class="mb_20"></model-carousel1>
|
||||
@@ -18,62 +17,66 @@
|
||||
</template>
|
||||
<!-- 热门广告 -->
|
||||
<template v-if="element.type == 'hotAdvert'">
|
||||
<div class="mb_20 width_1200_auto">
|
||||
<img
|
||||
style="display: block"
|
||||
class="hover-pointer"
|
||||
:src="element.options.list[0].img"
|
||||
@click="linkTo(element.options.list[0].url)"
|
||||
width="1200"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<ul class="advert-list width_1200_auto">
|
||||
<template v-for="(item, index) in element.options.list">
|
||||
<li
|
||||
v-if="index !== 0"
|
||||
@click="linkTo(item.url)"
|
||||
<div class="hot-advert mb_20 width_1200_auto">
|
||||
<div class="hot-advert-banner">
|
||||
<img
|
||||
class="hover-pointer"
|
||||
:key="index"
|
||||
>
|
||||
<img :src="item.img" width="230" height="190" alt="" />
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
:src="element.options.list[0].img"
|
||||
@click="linkTo(element.options.list[0].url)"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<ul class="advert-list">
|
||||
<template v-for="(item, index) in element.options.list" :key="index">
|
||||
<li
|
||||
v-if="index !== 0"
|
||||
@click="linkTo(item.url)"
|
||||
class="hover-pointer"
|
||||
>
|
||||
<img :src="item.img" alt="" />
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 限时秒杀 待完善 -->
|
||||
<template v-if="element.type == 'seckill' && element.options.list.length">
|
||||
<template v-if="element.type == 'seckill' && element.options?.list?.length">
|
||||
<seckill :data="element" class="mb_20 width_1200_auto"></seckill>
|
||||
</template>
|
||||
<!-- 折扣广告 -->
|
||||
<template v-if="element.type == 'discountAdvert'">
|
||||
<div
|
||||
class="discountAdvert"
|
||||
:style="{
|
||||
backgroundImage:
|
||||
'url(' + require('@/assets/images/decorate.png') + ')',
|
||||
}"
|
||||
>
|
||||
<img
|
||||
@click="linkTo(item.url)"
|
||||
class="hover-pointer"
|
||||
v-for="(item, index) in element.options.classification"
|
||||
:key="index"
|
||||
:src="item.img"
|
||||
width="190"
|
||||
height="210"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
@click="linkTo(item.url)"
|
||||
class="hover-pointer"
|
||||
v-for="(item, index) in element.options.brandList"
|
||||
:key="'discount' + index"
|
||||
:src="item.img"
|
||||
width="240"
|
||||
height="105"
|
||||
alt=""
|
||||
/>
|
||||
<div class="discountAdvert-wrap width_1200_auto mb_20">
|
||||
<div
|
||||
class="discountAdvert"
|
||||
:style="{
|
||||
backgroundImage: 'url(' + decorateImg + ')',
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
@click="linkTo(item.url)"
|
||||
class="hover-pointer"
|
||||
v-for="(item, index) in element.options.classification"
|
||||
:key="index"
|
||||
:src="item.img"
|
||||
width="190"
|
||||
height="210"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
@click="linkTo(item.url)"
|
||||
class="hover-pointer"
|
||||
v-for="(item, index) in element.options.brandList"
|
||||
:key="'discount' + index"
|
||||
:src="item.img"
|
||||
width="240"
|
||||
height="105"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -117,15 +120,12 @@
|
||||
></first-page-advert>
|
||||
</template>
|
||||
<!-- 横幅广告 -->
|
||||
<template v-if="element.type == 'bannerAdvert'">
|
||||
<div style="width: 100%; text-align: center">
|
||||
<img
|
||||
width="1200"
|
||||
class="hover-pointer mb_20 bannerAd"
|
||||
@click="linkTo(element.options.url)"
|
||||
:src="element.options.img"
|
||||
alt=""
|
||||
/>
|
||||
<template v-if="element.type == 'bannerAdvert' && element.options.img">
|
||||
<div
|
||||
class="horizontal-advert width_1200_auto mb_20 hover-pointer"
|
||||
@click="linkTo(element.options.url)"
|
||||
>
|
||||
<img class="bannerAd" :src="element.options.img" alt="" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="element.type == 'notEnough'"
|
||||
@@ -138,7 +138,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import ModelCarousel from "./modelList/Carousel.vue";
|
||||
import ModelCarousel1 from "./modelList/Carousel1.vue";
|
||||
import ModelCarousel2 from "./modelList/Carousel2.vue";
|
||||
@@ -152,53 +153,79 @@ import goodsAndType from "./modelList/goodsAndType.vue";
|
||||
import onlyGoodsModel from "./modelList/onlyGoodsModel.vue";
|
||||
import mixModel from "./modelList/mixModel.vue";
|
||||
import forYour from "./modelList/forYour.vue";
|
||||
import decorateImgUrl from "@/assets/images/decorate.png";
|
||||
|
||||
defineOptions({ name: "modelFormItem" });
|
||||
|
||||
export default {
|
||||
name: "modelFormItem",
|
||||
props: ["element", "select", "index", "data"],
|
||||
components: {
|
||||
ModelCarousel,
|
||||
ModelCarousel1,
|
||||
ModelCarousel2,
|
||||
Recommend,
|
||||
NewGoodsSort,
|
||||
FirstPageAdvert,
|
||||
NotEnough,
|
||||
Seckill,
|
||||
oneRowThreeColumns,
|
||||
goodsAndType,
|
||||
onlyGoodsModel,
|
||||
mixModel,
|
||||
forYour
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showModal: false, // 控制模态框显隐
|
||||
selected: {}, // 已选数据
|
||||
};
|
||||
},
|
||||
};
|
||||
defineProps(["element", "select", "index", "data"]);
|
||||
|
||||
const decorateImg = ref(decorateImgUrl);
|
||||
const showModal = ref(false); // 控制模态框显隐
|
||||
const selected = ref({}); // 已选数据
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.model-item {
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.bannerAd{
|
||||
width: 1183px;
|
||||
height: 166.6px;
|
||||
.carousel-wrap {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
/** 横幅广告 */
|
||||
.horizontal-advert {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
line-height: 0;
|
||||
}
|
||||
.bannerAd {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 166px;
|
||||
object-fit: cover;
|
||||
}
|
||||
/** 热门广告 */
|
||||
.advert-list {
|
||||
background: $theme_color;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 3px 10px;
|
||||
> li {
|
||||
.hot-advert {
|
||||
width: 1200px;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.hot-advert-banner {
|
||||
line-height: 0;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
.advert-list {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
list-style: none;
|
||||
box-sizing: border-box;
|
||||
background: $theme_color;
|
||||
height: 210px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
> li {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 190px;
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
transition: all 150ms ease-in-out;
|
||||
@@ -211,25 +238,43 @@ export default {
|
||||
}
|
||||
|
||||
/** 折扣广告 */
|
||||
.discountAdvert-wrap {
|
||||
width: 1200px;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
.discountAdvert {
|
||||
width: 1300px;
|
||||
height: 566px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 20px;
|
||||
height: 585px;
|
||||
margin-left: -100px;
|
||||
box-sizing: border-box;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1300px 585px;
|
||||
background-position: left top;
|
||||
position: relative;
|
||||
left: -47px;
|
||||
padding-left: 295px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
img {
|
||||
margin-top: 10px;
|
||||
margin-right: 10px;
|
||||
transition: all 150ms ease-in-out;
|
||||
&:hover {
|
||||
box-shadow: 0 5px 12px 0 rgba(0, 0, 0, 0.4);
|
||||
transform: translateY(-2px);
|
||||
> div {
|
||||
padding-left: 295px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
box-sizing: border-box;
|
||||
&:nth-child(1) img {
|
||||
margin: 10px 10px 0 0;
|
||||
transition: all 150ms ease-in-out;
|
||||
&:hover {
|
||||
box-shadow: 0 5px 12px 0 rgba(0, 0, 0, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
&:nth-child(2) {
|
||||
margin-top: 10px;
|
||||
img {
|
||||
margin: 0 10px 0 0;
|
||||
transition: all 150ms ease-in-out;
|
||||
&:hover {
|
||||
box-shadow: 0 5px 12px 0 rgba(0, 0, 0, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
<template>
|
||||
<div class="model-carousel">
|
||||
<div class="nav-body clearfix">
|
||||
<!-- 侧边导航占位 -->
|
||||
<div class="nav-side"></div>
|
||||
<div class="nav-body">
|
||||
<!-- 侧边导航占位(与 CateNav 分类栏同宽,勿与 CateNav .nav-side 混用类名) -->
|
||||
<div class="carousel-side-spacer"></div>
|
||||
<div class="nav-content">
|
||||
<!-- 轮播图 -->
|
||||
<!-- autoplay autoplay-speed="10000"-->
|
||||
<Carousel :height="333.9" autoplay :autoplay-speed="5000">
|
||||
<CarouselItem v-for="(item, index) in data.options.list" :key="index">
|
||||
<div class="swiper-img" >
|
||||
<!-- 轮播图:无有效图片时不挂载 el-carousel;勿对 el-carousel 使用随图片变化的 key,否则 Vue3 patch 会报 emitsOptions 错误 -->
|
||||
<el-carousel
|
||||
v-if="displaySlides.length"
|
||||
height="334px"
|
||||
:interval="5000"
|
||||
:loop="displaySlides.length > 1"
|
||||
>
|
||||
<el-carousel-item
|
||||
v-for="(item, index) in displaySlides"
|
||||
:key="carouselItemKey(item, index)"
|
||||
>
|
||||
<div class="swiper-img">
|
||||
<img
|
||||
:src="item.img"
|
||||
@click="linkTo(item.url)"
|
||||
class="hover-pointer"
|
||||
/>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
</Carousel>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<div v-else class="swiper-img carousel-placeholder"></div>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="person-msg">
|
||||
@@ -24,7 +32,7 @@
|
||||
<img class="user-face" :src="userInfo.face || defaultAvatar" alt />
|
||||
<div class="welcome">
|
||||
Hi, {{
|
||||
userInfo.nickName || `欢迎来到${config.title}` | secrecyMobile
|
||||
$filters.secrecyMobile(userInfo.nickName || `欢迎来到${config.title}`)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,18 +51,14 @@
|
||||
</div> -->
|
||||
</div>
|
||||
<div v-else class="flex flex-a-c ">
|
||||
<div class="btns" @click="$router.push('login')" shape="circle"
|
||||
>登录</div
|
||||
>
|
||||
<div class="btns sign-up" @click="$router.push('signUp')" shape="circle"
|
||||
>注册</div
|
||||
>
|
||||
<div class="btns" @click="goLogin">登录</div>
|
||||
<div class="btns sign-up" @click="$router.push('signUp')">注册</div>
|
||||
</div>
|
||||
|
||||
<div class="gray-line"></div>
|
||||
<div class="icon-list flex flex-j-sb" >
|
||||
<div class="icon-item" @click="entryControl(item)" :key="index" v-for="(item, index) in entranceList">
|
||||
<img class="icon" :src="require(`@/assets/iconfont/${item.icon}.png`)">
|
||||
<img class="icon" :src="getIconUrl(item.icon)">
|
||||
<div>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
@@ -62,7 +66,7 @@
|
||||
</div>
|
||||
<div class="icon-list flex flex-j-sb" >
|
||||
<div class="icon-item" :key="index" @click="entryControl(item)" v-for="(item, index) in appendList">
|
||||
<img class="icon" :src="require(`@/assets/iconfont/${item.icon}.png`)">
|
||||
<img class="icon" :src="getIconUrl(item.icon)">
|
||||
<div>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
@@ -78,95 +82,153 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Modal } from "@/utils/message";
|
||||
import storage from "@/plugins/storage";
|
||||
export default {
|
||||
name: "modelCarousel",
|
||||
props: ["data"],
|
||||
data() {
|
||||
return {
|
||||
config: require("@/config"),
|
||||
defaultAvatar: require("@/assets/images/default.png"),
|
||||
userInfo: {}, // 用户信息
|
||||
entranceList: [
|
||||
{
|
||||
icon: "collage",
|
||||
label: "宝贝收藏",
|
||||
path: "/home/Favorites"
|
||||
},
|
||||
{
|
||||
icon: "shop",
|
||||
label: "收藏店铺",
|
||||
path: "/home/Favorites?type=STORE"
|
||||
},
|
||||
{
|
||||
icon: "carts",
|
||||
label: "购物车",
|
||||
path: "/cart"
|
||||
},
|
||||
{
|
||||
icon: "story",
|
||||
label: "我的足迹",
|
||||
path: "/home/MyTracks"
|
||||
},
|
||||
],
|
||||
appendList: [
|
||||
{
|
||||
icon: "support",
|
||||
label: "帮助中心",
|
||||
path: "/article"
|
||||
},
|
||||
{
|
||||
icon: "feedback",
|
||||
label: "我的评论",
|
||||
path: "/home/CommentList"
|
||||
},
|
||||
{
|
||||
icon: "notice",
|
||||
label: "收货地址",
|
||||
path: "/home/MyAddress"
|
||||
},
|
||||
{
|
||||
icon: "notification",
|
||||
label: "我的消息",
|
||||
path: "/home/MsgList"
|
||||
},
|
||||
],
|
||||
recentList: [
|
||||
{
|
||||
value: "0",
|
||||
label: "购物车"
|
||||
},
|
||||
{
|
||||
value: "0",
|
||||
label: "待收货"
|
||||
},
|
||||
{
|
||||
value: "0",
|
||||
label: "待发货"
|
||||
},
|
||||
{
|
||||
value: "0",
|
||||
label: "代付款"
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
// 快捷跳转中心
|
||||
entryControl(val) {
|
||||
console.log("val",val)
|
||||
let url = this.$router.resolve(val.path);
|
||||
window.open(url.href, '_blank');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (storage.getItem("userInfo"))
|
||||
this.userInfo = JSON.parse(storage.getItem("userInfo"));
|
||||
import config from "@/config";
|
||||
import defaultAvatar from "@/assets/images/default.png";
|
||||
import { getIconUrl } from "@/assets/iconfont/iconMap";
|
||||
|
||||
defineOptions({ name: "modelCarousel" });
|
||||
|
||||
const props = defineProps(["data"]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const carouselList = computed(() => {
|
||||
return props.data?.options?.list || [];
|
||||
});
|
||||
|
||||
const displaySlides = computed(() => {
|
||||
return carouselList.value.filter((item) => item && item.img);
|
||||
});
|
||||
|
||||
const userInfo = ref({}); // 用户信息
|
||||
const entranceList = ref([
|
||||
{
|
||||
icon: "collage",
|
||||
label: "宝贝收藏",
|
||||
path: "/home/Favorites"
|
||||
},
|
||||
};
|
||||
{
|
||||
icon: "shop",
|
||||
label: "收藏店铺",
|
||||
path: "/home/Favorites?type=STORE"
|
||||
},
|
||||
{
|
||||
icon: "carts",
|
||||
label: "购物车",
|
||||
path: "/cart"
|
||||
},
|
||||
{
|
||||
icon: "story",
|
||||
label: "我的足迹",
|
||||
path: "/home/MyTracks"
|
||||
},
|
||||
]);
|
||||
const appendList = ref([
|
||||
{
|
||||
icon: "support",
|
||||
label: "帮助中心",
|
||||
path: "/article"
|
||||
},
|
||||
{
|
||||
icon: "feedback",
|
||||
label: "我的评论",
|
||||
path: "/home/CommentList"
|
||||
},
|
||||
{
|
||||
icon: "notice",
|
||||
label: "收货地址",
|
||||
path: "/home/MyAddress"
|
||||
},
|
||||
{
|
||||
icon: "notification",
|
||||
label: "我的消息",
|
||||
path: "/home/MsgList"
|
||||
},
|
||||
]);
|
||||
const recentList = ref([
|
||||
{
|
||||
value: "0",
|
||||
label: "购物车"
|
||||
},
|
||||
{
|
||||
value: "0",
|
||||
label: "待收货"
|
||||
},
|
||||
{
|
||||
value: "0",
|
||||
label: "待发货"
|
||||
},
|
||||
{
|
||||
value: "0",
|
||||
label: "待付款"
|
||||
},
|
||||
]);
|
||||
|
||||
function carouselItemKey(item, index) {
|
||||
return item?.img || item?.url || `slide-${index}`;
|
||||
}
|
||||
|
||||
function resolveRoute(pathStr) {
|
||||
if (!pathStr) return { path: "/" };
|
||||
const [path, search] = pathStr.split("?");
|
||||
if (!search) return { path };
|
||||
return {
|
||||
path,
|
||||
query: Object.fromEntries(new URLSearchParams(search)),
|
||||
};
|
||||
}
|
||||
|
||||
function isMemberRoute(location) {
|
||||
const path = location.path && location.path.charAt(0) === "/" ? location.path : `/${location.path || ""}`;
|
||||
return path === "/home" || path.startsWith("/home/") || path === "/cart";
|
||||
}
|
||||
|
||||
function normalizeLocation(location) {
|
||||
if (!location.path || location.path.charAt(0) === "/") return location;
|
||||
return { ...location, path: `/${location.path}` };
|
||||
}
|
||||
|
||||
function promptLoginThenNavigate(location) {
|
||||
Modal.confirm({
|
||||
title: "温馨提示",
|
||||
content: "请登录后执行此操作",
|
||||
okText: "立即登录",
|
||||
cancelText: "取消",
|
||||
onOk: () => {
|
||||
router.push({
|
||||
path: "/login",
|
||||
query: {
|
||||
rePath: location.path,
|
||||
query: JSON.stringify(location.query || {}),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function goLogin() {
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
// 快捷跳转中心
|
||||
function entryControl(val) {
|
||||
const location = normalizeLocation(resolveRoute(val.path));
|
||||
if (isMemberRoute(location) && !storage.getItem("userInfo")) {
|
||||
promptLoginThenNavigate(location);
|
||||
return;
|
||||
}
|
||||
router.push(location);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (storage.getItem("userInfo"))
|
||||
userInfo.value = JSON.parse(storage.getItem("userInfo"));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -182,10 +244,15 @@ export default {
|
||||
|
||||
.swiper-img {
|
||||
overflow: hidden;
|
||||
width: 637px;
|
||||
width: 100%;
|
||||
height: 329.9px;
|
||||
}
|
||||
|
||||
.carousel-placeholder {
|
||||
border-radius: 10px;
|
||||
background: #ececec;
|
||||
}
|
||||
|
||||
.icon-list {
|
||||
width: 216px
|
||||
}
|
||||
@@ -265,21 +332,21 @@ export default {
|
||||
}
|
||||
|
||||
.model-carousel {
|
||||
width: 1200px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
height: 340px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.hover-pointer {
|
||||
//display: block;
|
||||
//width: 100%;
|
||||
//height: 100%;
|
||||
width: 637px;
|
||||
width: 100%;
|
||||
height: 329.9px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.welcome {
|
||||
@@ -334,44 +401,62 @@ export default {
|
||||
|
||||
}
|
||||
|
||||
/* 导航主体 */
|
||||
/* 导航主体:263.2 + 10 + 1fr + 10 + 263.2 = 1200px,与下方楼层右缘对齐 */
|
||||
.nav-body {
|
||||
width: 1200px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
height: 340px;
|
||||
margin: 0px auto;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 263.2px minmax(0, 1fr) 263.2px;
|
||||
column-gap: 10px;
|
||||
align-items: start;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav-side {
|
||||
height: 334px;
|
||||
.carousel-side-spacer {
|
||||
grid-column: 1;
|
||||
width: 263.2px;
|
||||
height: 334px;
|
||||
flex-shrink: 0;
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
float: left;
|
||||
padding: 0px;
|
||||
color: #fff;
|
||||
// background-color: #6e6568;
|
||||
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
/*导航内容*/
|
||||
.nav-content {
|
||||
width: 637px;
|
||||
margin-left: 10.8px;
|
||||
|
||||
grid-column: 2;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin-top: 10px;
|
||||
height: 333.9px;
|
||||
|
||||
float: left;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.el-carousel) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__container) {
|
||||
height: 334px;
|
||||
}
|
||||
|
||||
:deep(.el-carousel__item) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
float: left;
|
||||
margin-top: 10px;
|
||||
grid-column: 3;
|
||||
width: 263.2px;
|
||||
margin-left: 10px;
|
||||
min-width: 263.2px;
|
||||
max-width: 263.2px;
|
||||
margin-top: 10px;
|
||||
|
||||
border-radius: 10px;
|
||||
background: #FFFFFF;
|
||||
|
||||
@@ -5,40 +5,38 @@
|
||||
<div class="nav-side"></div>
|
||||
<div class="nav-content">
|
||||
<!-- 轮播图 -->
|
||||
<Carousel autoplay @on-change="autoChange">
|
||||
<CarouselItem v-for="(item, index) in data.options.list" :key="index" >
|
||||
<el-carousel height="470px" :interval="5000" @change="autoChange">
|
||||
<el-carousel-item v-for="(item, index) in data.options.list" :key="index">
|
||||
<div style="overflow: hidden">
|
||||
<img :src="item.img" width="1200" height="470" />
|
||||
</div>
|
||||
</CarouselItem>
|
||||
</Carousel>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'modelCarousel1',
|
||||
props: ['data'],
|
||||
data () {
|
||||
return {
|
||||
showModal: false, // modal显隐
|
||||
selected: null, // 已选数据
|
||||
picModelFlag: false, // 选择图片modal
|
||||
bgColor: '#fff' // 轮播背景色
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.bgColor = this.data.options.list[0].bgColor
|
||||
},
|
||||
methods: {
|
||||
// 自动切换时改变背景色
|
||||
autoChange (oVal, val) {
|
||||
this.bgColor = this.data.options.list[val].bgColor
|
||||
}
|
||||
}
|
||||
};
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
defineOptions({ name: 'modelCarousel1' })
|
||||
|
||||
const props = defineProps(['data'])
|
||||
|
||||
const showModal = ref(false) // modal显隐
|
||||
const selected = ref(null) // 已选数据
|
||||
const picModelFlag = ref(false) // 选择图片modal
|
||||
const bgColor = ref('#fff') // 轮播背景色
|
||||
|
||||
onMounted(() => {
|
||||
bgColor.value = props.data.options.list[0].bgColor
|
||||
})
|
||||
|
||||
// 自动切换时改变背景色
|
||||
function autoChange(index) {
|
||||
bgColor.value = props.data.options.list[index].bgColor
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -5,18 +5,17 @@
|
||||
<div class="nav-side"></div>
|
||||
<div class="nav-content">
|
||||
<!-- 轮播图 -->
|
||||
<Carousel autoplay>
|
||||
<CarouselItem v-for="(item, index) in data.options.list" :key="index">
|
||||
<el-carousel height="470px" :interval="5000">
|
||||
<el-carousel-item v-for="(item, index) in data.options.list" :key="index">
|
||||
<div style="overflow: hidden">
|
||||
<img :src="item.img" width="590" height="470" />
|
||||
</div>
|
||||
</CarouselItem>
|
||||
</Carousel>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
<div class="nav-content1">
|
||||
<!-- 轮播图 -->
|
||||
<Carousel autoplay :autoplay-speed="5000">
|
||||
<CarouselItem v-for="(item, index) in data.options.listRight" :key="index">
|
||||
<el-carousel height="470px" :interval="5000">
|
||||
<el-carousel-item v-for="(item, index) in data.options.listRight" :key="index">
|
||||
<div class="mb_10">
|
||||
<img :src="item[0].img" width="190" height="150" />
|
||||
</div>
|
||||
@@ -26,19 +25,19 @@
|
||||
<div>
|
||||
<img :src="item[2].img" width="190" height="150" />
|
||||
</div>
|
||||
</CarouselItem>
|
||||
</Carousel>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="person-msg">
|
||||
<img :src="userInfo.face" v-if="userInfo.face" alt />
|
||||
<Avatar icon="ios-person" class="mb_10" v-else size="80" />
|
||||
<div>Hi,{{ userInfo.nickName || `欢迎来到${config.title}` | secrecyMobile }}</div>
|
||||
<el-avatar v-else :size="80" class="mb_10"><el-icon><User /></el-icon></el-avatar>
|
||||
<div>Hi,{{ $filters.secrecyMobile(userInfo.nickName || `欢迎来到${config.title}`) }}</div>
|
||||
<div v-if="userInfo.id">
|
||||
<Button type="error" shape="circle">会员中心</Button>
|
||||
<el-button type="danger">会员中心</el-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Button type="error" shape="circle">请登录</Button>
|
||||
<el-button type="danger">请登录</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shop-msg">
|
||||
@@ -56,47 +55,48 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {articleList} from '@/api/common.js'
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { articleList as getArticleListApi } from '@/api/common.js'
|
||||
import storage from '@/plugins/storage';
|
||||
export default {
|
||||
name: 'modelCarousel2',
|
||||
props: ['data'],
|
||||
data () {
|
||||
return {
|
||||
config:require('@/config'),
|
||||
userInfo: {}, // 用户信息
|
||||
articleList: [], // 常见问题
|
||||
params: { // 请求常见问题参数
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
type: 'ANNOUNCEMENT',
|
||||
sort: 'sort'
|
||||
}
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getArticleList () { // 获取常见问题列表
|
||||
articleList(this.params).then(res => {
|
||||
if (res.success) {
|
||||
this.articleList = res.result.records
|
||||
}
|
||||
})
|
||||
},
|
||||
goArticle (id) { // 跳转文章详情
|
||||
let routeUrl = this.$router.resolve({
|
||||
path: '/article',
|
||||
query: {id}
|
||||
});
|
||||
window.open(routeUrl.href, '_blank');
|
||||
import { User } from '@element-plus/icons-vue';
|
||||
import config from '@/config';
|
||||
|
||||
defineOptions({ name: 'modelCarousel2' })
|
||||
|
||||
defineProps(['data'])
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const userInfo = ref({}) // 用户信息
|
||||
const articleList = ref([]) // 常见问题
|
||||
const params = reactive({ // 请求常见问题参数
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
type: 'ANNOUNCEMENT',
|
||||
sort: 'sort'
|
||||
})
|
||||
|
||||
function getArticleList () { // 获取常见问题列表
|
||||
getArticleListApi(params).then(res => {
|
||||
if (res.success) {
|
||||
articleList.value = res.result.records
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (storage.getItem('userInfo')) this.userInfo = JSON.parse(storage.getItem('userInfo'));
|
||||
this.getArticleList()
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
function goArticle (id) { // 跳转文章详情
|
||||
router.push({
|
||||
path: '/article',
|
||||
query: {id}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (storage.getItem('userInfo')) userInfo.value = JSON.parse(storage.getItem('userInfo'));
|
||||
getArticleList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -163,7 +163,7 @@ export default {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ivu-btn-default {
|
||||
.el-button--default {
|
||||
color: $theme_color;
|
||||
border-color: $theme_color;
|
||||
}
|
||||
|
||||
@@ -15,24 +15,21 @@
|
||||
<span class="line btm-line"></span>
|
||||
<p>{{ item.describe }}</p>
|
||||
</div>
|
||||
<img :src="item.img" width="170" height="170" alt="" />
|
||||
<img :src="item.img" loading="lazy" decoding="async" width="170" height="170" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
options: this.data.options // 装修数据
|
||||
};
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const options = ref(props.data.options); // 装修数据
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.first-page-advert {
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
<div class="top-header" :style="{ background: options.left.bgColor }">
|
||||
<span>{{ options.left.title }}</span>
|
||||
<span @click="linkTo(options.left.url)" class="hover-pointer"
|
||||
>{{ options.left.secondTitle }} ></span
|
||||
>
|
||||
>{{ options.left.secondTitle }} ></span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div
|
||||
@@ -18,7 +17,7 @@
|
||||
<p>{{ item.name }}</p>
|
||||
<p class="describe">{{ item.describe }}</p>
|
||||
</div>
|
||||
<img :src="item.img" alt="" />
|
||||
<img :src="item.img" loading="lazy" decoding="async" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,8 +26,7 @@
|
||||
<div class="top-header" :style="{ background: options.middle.bgColor }">
|
||||
<span>{{ options.middle.title }}</span>
|
||||
<span class="hover-pointer" @click="linkTo(options.middle.url)"
|
||||
>{{ options.middle.secondTitle }} ></span
|
||||
>
|
||||
>{{ options.middle.secondTitle }} ></span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div
|
||||
@@ -41,7 +39,7 @@
|
||||
<p>{{ item.name }}</p>
|
||||
<p class="describe">{{ item.describe }}</p>
|
||||
</div>
|
||||
<img :src="item.img" alt="" />
|
||||
<img :src="item.img" loading="lazy" decoding="async" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,8 +48,7 @@
|
||||
<div class="top-header" :style="{ background: options.right.bgColor }">
|
||||
<span>{{ options.right.title }}</span>
|
||||
<span @click="linkTo(options.right.url)" class="hover-pointer"
|
||||
>{{ options.right.secondTitle }} ></span
|
||||
>
|
||||
>{{ options.right.secondTitle }} ></span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div
|
||||
@@ -60,134 +57,236 @@
|
||||
class="hover-pointer"
|
||||
@click="linkTo(item.url)"
|
||||
>
|
||||
<img :src="item.img" alt="" />
|
||||
<p>{{ item.name }}</p>
|
||||
<p>{{ item.price | unitPrice("¥") }}</p>
|
||||
<div class="jiaobiao" :class="'jiaobiao' + (index + 1)">
|
||||
{{ index + 1 }}
|
||||
<div class="img-wrap">
|
||||
<img :src="item.img" loading="lazy" decoding="async" alt="" />
|
||||
<div class="jiaobiao" :class="'jiaobiao' + (index + 1)">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
<p>{{ item.name }}</p>
|
||||
<p>{{ $filters.unitPrice(item.price, "¥") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
options: this.data.options // 装修数据
|
||||
};
|
||||
},
|
||||
methods: {}
|
||||
};
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const options = ref(props.data.options) // 装修数据
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.new-goods {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
> div {
|
||||
width: 393px;
|
||||
height: 440px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.left > .content {
|
||||
> div:nth-child(1) {
|
||||
height: 240px;
|
||||
flex-direction: column;
|
||||
border: 1px solid #eee;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
justify-content: space-between;
|
||||
img {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
.describe {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
> div:nth-child(2) {
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
> div:nth-child(3),
|
||||
> div:nth-child(4) {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.middle > .content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
flex-direction: unset;
|
||||
overflow: hidden;
|
||||
> div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
padding: 6px 4px 4px 6px;
|
||||
border-style: solid;
|
||||
border-color: #eee;
|
||||
border-width: 0;
|
||||
border-bottom-width: 1px;
|
||||
border-right-width: 1px;
|
||||
img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
> div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.describe {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
> div:nth-child(1) {
|
||||
grid-row: span 2;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 8px 6px;
|
||||
img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
> div {
|
||||
flex: unset;
|
||||
width: 100%;
|
||||
p {
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.describe {
|
||||
margin-top: 6px;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
> div:nth-child(1),
|
||||
> div:nth-child(2),
|
||||
> div:nth-child(3) {
|
||||
> div:nth-child(3),
|
||||
> div:nth-child(5) {
|
||||
border-right-width: 0;
|
||||
}
|
||||
> div:nth-child(4),
|
||||
> div:nth-child(5) {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.middle > .content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
flex-direction: unset;
|
||||
> div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
border-style: solid;
|
||||
border-color: #eee;
|
||||
border-width: 0;
|
||||
border-bottom-width: 1px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
> div:nth-child(6),
|
||||
> div:nth-child(3) {
|
||||
> div:nth-child(2n) {
|
||||
border-right-width: 0;
|
||||
}
|
||||
> div:nth-child(n + 5) {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.right > .content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
flex-direction: unset;
|
||||
flex-wrap: unset;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
> div {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
padding: 5px 10px 0 10px;
|
||||
img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 4px 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #eee;
|
||||
:nth-child(2) {
|
||||
height: 38px;
|
||||
border-right: 1px solid #eee;
|
||||
.img-wrap {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
.jiaobiao {
|
||||
position: absolute;
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
display: block;
|
||||
background: url(../../../assets/images/festival_icon.png) no-repeat;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 23px;
|
||||
font-size: 12px;
|
||||
-webkit-line-clamp: unset;
|
||||
-webkit-box-orient: unset;
|
||||
}
|
||||
.jiaobiao1,
|
||||
.jiaobiao4 {
|
||||
background-position: -2px -30px;
|
||||
}
|
||||
.jiaobiao2,
|
||||
.jiaobiao5 {
|
||||
background-position: -31px -30px;
|
||||
}
|
||||
.jiaobiao3,
|
||||
.jiaobiao6 {
|
||||
background-position: -60px -30px;
|
||||
}
|
||||
}
|
||||
> p:nth-child(2) {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 32px;
|
||||
line-height: 16px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
:nth-child(3) {
|
||||
color: $theme_color;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.jiaobiao {
|
||||
position: absolute;
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
top: 10px;
|
||||
right: 16px;
|
||||
background: url(../../../assets/images/festival_icon.png);
|
||||
color: #fff;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-align: center;
|
||||
}
|
||||
.jiaobiao1,
|
||||
.jiaobiao4 {
|
||||
background-position: -2px -30px;
|
||||
}
|
||||
.jiaobiao2,
|
||||
.jiaobiao5 {
|
||||
background-position: -31px -30px;
|
||||
}
|
||||
.jiaobiao3,
|
||||
.jiaobiao6 {
|
||||
background-position: -60px -30px;
|
||||
> p:nth-child(3) {
|
||||
color: $theme_color;
|
||||
margin-top: 2px;
|
||||
line-height: 1.2;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
> div:nth-child(4),
|
||||
> div:nth-child(5),
|
||||
> div:nth-child(6) {
|
||||
border: none;
|
||||
> div:nth-child(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
> div:nth-child(n + 4) {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +311,8 @@ export default {
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
height: 370px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.con-item {
|
||||
width: 185px;
|
||||
@@ -225,6 +326,42 @@ export default {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
.middle > .content .con-item {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
padding: 6px 4px 4px 6px;
|
||||
img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
> div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.describe {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.left > .content .con-item {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.describe {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<template>
|
||||
<div class="not-enough" ref="obtain" id="demo">
|
||||
<Affix :offset-top="62" @on-change="change">
|
||||
<ul class="nav-bar" v-show="topSearchShow">
|
||||
<li
|
||||
v-for="(item, index) in conData.options.navList"
|
||||
:class="currentIndex === index ? 'curr' : ''"
|
||||
@click="changeCurr(index)"
|
||||
:key="index"
|
||||
>
|
||||
<p @click="gotoDemo">{{ item.title }}</p>
|
||||
<p @click="gotoDemo">{{ item.desc }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</Affix>
|
||||
<el-affix :offset-top="62" @change="change" class="nav-affix">
|
||||
<div class="nav-bar" v-show="topSearchShow">
|
||||
<ul class="nav-list">
|
||||
<li
|
||||
v-for="(item, index) in conData.options.navList"
|
||||
:class="currentIndex === index ? 'curr' : ''"
|
||||
@click="changeCurr(index)"
|
||||
:key="index"
|
||||
>
|
||||
<p @click="gotoDemo">{{ item.title }}</p>
|
||||
<p @click="gotoDemo">{{ item.desc }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-affix>
|
||||
<div class="content" v-if="showContent">
|
||||
<div
|
||||
v-for="(item, index) in conData.options.list[currentIndex]"
|
||||
@@ -20,93 +22,119 @@
|
||||
class="hover-pointer"
|
||||
@click="linkTo(item.url)"
|
||||
>
|
||||
<img :src="item.img" width="210" height="210" :alt="item.name" />
|
||||
<img :src="item.img" loading="lazy" decoding="async" width="210" height="210" :alt="item.name" />
|
||||
<p>{{ item.name }}</p>
|
||||
<p>
|
||||
<span>{{ Number(item.price) | unitPrice("¥") }}</span>
|
||||
<span>{{ $filters.unitPrice(Number(item.price), "¥") }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
mounted() {
|
||||
window.addEventListener('scroll',this.handleScrollx,true)
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
screenHeight:document.body.clientHeight,
|
||||
scrollHieght:0,
|
||||
topSearchShow: true, //展示 导航条
|
||||
topIndex: 0, // 当前滚动条位置 (取整)
|
||||
scrollTops: 0,
|
||||
open:'',
|
||||
currentIndex: 0, // 当前分类下标
|
||||
conData: this.data, // 装修数据
|
||||
showContent: true, // 是否展示内容
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
data: function (val) {
|
||||
this.conData = val;
|
||||
},
|
||||
conData: function (val) {
|
||||
this.$emit("content", val);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleScrollx(){
|
||||
// console.log('滚动高度',window.pageYOffset) // 获取滚动条的高度
|
||||
// console.log(this.$refs.obtain.getBoundingClientRect().top) //获取到距离顶部的距离
|
||||
// this.scrollHieght = Number(window.pageYOffset);//获取到距离顶部的距离
|
||||
// this.scrollTops = Number(this.$refs.obtain.getBoundingClientRect().top); // 获取到距离顶部的距离
|
||||
// this.topSearchShow = true; // 展示图钉
|
||||
// if(this.scrollTops < -660){ // 超过隐藏
|
||||
// this.topSearchShow = false;
|
||||
// }
|
||||
},
|
||||
toguid(path,id){
|
||||
var path =path;
|
||||
var Id = id;
|
||||
localStorage.setItem('toId',Id);
|
||||
this.$router.push(path);
|
||||
},
|
||||
change(status){ //获取是否获取到图钉
|
||||
this.open = status
|
||||
},
|
||||
gotoDemo(){ // 跳转到demo的位置
|
||||
if(this.open){ // 获取到图钉之后在跳转当前位置
|
||||
document.querySelector("#demo").scrollIntoView(true);
|
||||
}
|
||||
//scrollIntoView()可以在所有的HTML元素上调用,通过滚动浏览器窗口或某个容器元素
|
||||
},
|
||||
changeCurr(index) {
|
||||
// 选择分类
|
||||
this.currentIndex = index;
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const emit = defineEmits(['content']);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const obtain = ref(null);
|
||||
const screenHeight = ref(document.body.clientHeight);
|
||||
const scrollHieght = ref(0);
|
||||
const topSearchShow = ref(true); //展示 导航条
|
||||
const topIndex = ref(0); // 当前滚动条位置 (取整)
|
||||
const scrollTops = ref(0);
|
||||
const open = ref('');
|
||||
const currentIndex = ref(0); // 当前分类下标
|
||||
const conData = ref(props.data); // 装修数据
|
||||
const showContent = ref(true); // 是否展示内容
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(val) => {
|
||||
conData.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
watch(conData, (val) => {
|
||||
emit('content', val);
|
||||
});
|
||||
|
||||
function handleScrollx() {
|
||||
// console.log('滚动高度',window.pageYOffset) // 获取滚动条的高度
|
||||
// console.log(obtain.value.getBoundingClientRect().top) //获取到距离顶部的距离
|
||||
// scrollHieght.value = Number(window.pageYOffset);//获取到距离顶部的距离
|
||||
// scrollTops.value = Number(obtain.value.getBoundingClientRect().top); // 获取到距离顶部的距离
|
||||
// topSearchShow.value = true; // 展示图钉
|
||||
// if(scrollTops.value < -660){ // 超过隐藏
|
||||
// topSearchShow.value = false;
|
||||
// }
|
||||
}
|
||||
|
||||
function toguid(path, id) {
|
||||
var path = path;
|
||||
var Id = id;
|
||||
localStorage.setItem('toId', Id);
|
||||
router.push(path);
|
||||
}
|
||||
|
||||
function change(status) { //获取是否获取到图钉
|
||||
open.value = status
|
||||
}
|
||||
|
||||
function gotoDemo() { // 跳转到demo的位置
|
||||
if (open.value) { // 获取到图钉之后在跳转当前位置
|
||||
document.querySelector("#demo").scrollIntoView(true);
|
||||
}
|
||||
//scrollIntoView()可以在所有的HTML元素上调用,通过滚动浏览器窗口或某个容器元素
|
||||
}
|
||||
|
||||
function changeCurr(index) {
|
||||
// 选择分类
|
||||
currentIndex.value = index;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', handleScrollx, true)
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.not-enough {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:deep(.nav-affix.el-affix) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
background-color: #f8f8f8;
|
||||
height: 60px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
li {
|
||||
padding: 0 30px;
|
||||
text-align: center;
|
||||
@@ -114,11 +142,15 @@ export default {
|
||||
font-size: 16px;
|
||||
border-radius: 50px;
|
||||
padding: 0 7px;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
p:nth-child(2) {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin: 2px 0 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -129,6 +161,7 @@ export default {
|
||||
}
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
li:last-of-type {
|
||||
border: none;
|
||||
}
|
||||
@@ -136,7 +169,6 @@ export default {
|
||||
.curr {
|
||||
p:nth-child(1) {
|
||||
background-color: $theme_color;
|
||||
|
||||
color: #fff;
|
||||
}
|
||||
p:nth-child(2) {
|
||||
@@ -146,6 +178,8 @@ export default {
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
</div>
|
||||
<div class="content-left">
|
||||
<div>
|
||||
<img class="hover-pointer" @click="linkTo(msgLeft.list[0].url)" :src="msgLeft.list[0].img" width="160" height="160" alt="" />
|
||||
<img class="hover-pointer" @click="linkTo(msgLeft.list[0].url)" :src="msgLeft.list[0].img" loading="lazy" decoding="async" width="160" height="160" alt="" />
|
||||
<div class="margin-left">{{ msgLeft.list[0].name }}</div>
|
||||
<div class="margin-left">{{ msgLeft.list[0].describe }}</div>
|
||||
<Button
|
||||
<el-button
|
||||
size="small"
|
||||
:style="{ background: msgLeft.bgColor }"
|
||||
@click="linkTo(msgLeft.list[0].url)"
|
||||
class="fz_12 view-btn"
|
||||
>点击查看</Button
|
||||
>点击查看</el-button
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<template v-for="(item, index) in msgLeft.list">
|
||||
<div v-if="index != 0" :key="index" @click="linkTo(item.url)" class="hover-pointer">
|
||||
<img :src="item.img" width="80" height="80" alt="" />
|
||||
<img :src="item.img" loading="lazy" decoding="async" width="80" height="80" alt="" />
|
||||
<div>
|
||||
<div>{{ item.name }}</div>
|
||||
<div>{{ item.describe }}</div>
|
||||
@@ -36,8 +36,7 @@
|
||||
<div class="head-recommend" :style="{ background: msgRight.bgColor }">
|
||||
<span>{{ msgRight.title }}</span>
|
||||
<span @click="linkTo(msgRight.url)" class="hover-pointer"
|
||||
>{{ msgRight.secondTitle }}></span
|
||||
>
|
||||
>{{ msgRight.secondTitle }}></span>
|
||||
</div>
|
||||
<div class="content-right">
|
||||
<div
|
||||
@@ -45,13 +44,13 @@
|
||||
:key="index"
|
||||
@click="linkTo(item.url)" class="hover-pointer"
|
||||
>
|
||||
<div class="right-item" :style="{'border': index===2 || index===3 ?'none': ''}">
|
||||
<div class="right-item">
|
||||
<div>
|
||||
<span :style="{ background: msgRight.bgColor }">{{item.name}}</span>
|
||||
<span>{{ item.describe }}</span>
|
||||
</div>
|
||||
<div class="right-img">
|
||||
<img :src="item.img" alt="" />
|
||||
<img :src="item.img" loading="lazy" decoding="async" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,22 +58,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
msgLeft: this.data.options.contentLeft, // 左侧数据
|
||||
msgRight: this.data.options.contentRight // 右侧数据
|
||||
};
|
||||
},
|
||||
methods: {}
|
||||
};
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const msgLeft = ref(props.data.options.contentLeft); // 左侧数据
|
||||
const msgRight = ref(props.data.options.contentRight); // 右侧数据
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.recommend {
|
||||
@@ -84,12 +79,17 @@ export default {
|
||||
width: 595px;
|
||||
.content-left {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
padding-top: 10px;
|
||||
font-size: 12px;
|
||||
box-sizing: content-box;
|
||||
> div:nth-child(1) {
|
||||
width: 189px;
|
||||
border-right: 1px solid #eee;
|
||||
height: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
img {
|
||||
margin: 40px 0 0 15px;
|
||||
}
|
||||
@@ -107,15 +107,20 @@ export default {
|
||||
color: #999;
|
||||
}
|
||||
.view-btn {
|
||||
margin-left: 15px;
|
||||
margin-top: 10px;
|
||||
align-self: center;
|
||||
width: 145px;
|
||||
margin-top: auto;
|
||||
margin-bottom: 15px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
> div:nth-child(2) {
|
||||
width: 405px;
|
||||
height: 360px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
box-sizing: border-box;
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -136,65 +141,85 @@ export default {
|
||||
|
||||
.recommend-right {
|
||||
width: 595px;
|
||||
height: 360px;
|
||||
|
||||
.head-recommend {
|
||||
background: #a25684;
|
||||
}
|
||||
.content-right {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
height: 360px;
|
||||
padding-top: 10px;
|
||||
box-sizing: content-box;
|
||||
|
||||
> div {
|
||||
width: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
height: 180px;
|
||||
padding-top: 10px;
|
||||
padding: 0;
|
||||
border-right: 1px solid #eee;
|
||||
border-bottom: 1px solid #eee;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:nth-child(2n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&:nth-child(n + 3) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.right-item {
|
||||
border-bottom: 1px solid #eee;
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
margin-top: 30px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
height: 150px;
|
||||
padding: 0 10px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 10px 15px;
|
||||
font-size: 12px;
|
||||
>div:nth-child(1) {
|
||||
box-sizing: border-box;
|
||||
|
||||
margin-top: 30px;
|
||||
span:nth-child(1){
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 0 5px;
|
||||
background-color: #a25684;
|
||||
display: block;
|
||||
> div:nth-child(1) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-top: 0;
|
||||
text-align: left;
|
||||
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin: 0 10px 10px 0;
|
||||
span:nth-child(1) {
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 0 5px;
|
||||
background-color: #a25684;
|
||||
display: block;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.right-img {
|
||||
width: 100;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
img{
|
||||
max-height: 100px;
|
||||
max-width: 100px;
|
||||
margin: 0;
|
||||
|
||||
img {
|
||||
max-height: 100px;
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
> div:nth-child(n + 1) {
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="seckill" v-if="goodsList.length">
|
||||
<div class="seckill" v-if="goodsList && goodsList.length">
|
||||
<div class="aside hover-pointer" @click="goToSeckill">
|
||||
<div class="title">{{ actName }}</div>
|
||||
<div class="hour">
|
||||
@@ -12,156 +12,114 @@
|
||||
</div>
|
||||
<div class="act-status" v-else>未开始</div>
|
||||
</div>
|
||||
<swiper :options="swiperOption" ref="mySwiper">
|
||||
<swiper-slide v-for="(item,index) in goodsList" :key="index">
|
||||
<div class="content hover-pointer" @click.stop="goToSeckill">
|
||||
<img :src="item.goodsImage" width="140" height="140" :alt="item.goodsName">
|
||||
<div class="ellipsis">{{item.goodsName}}</div>
|
||||
<div>
|
||||
<span>{{ item.price | unitPrice('¥') }}</span>
|
||||
<span>{{ item.originalPrice | unitPrice('¥') }}</span>
|
||||
</div>
|
||||
<div class="goods-scroll">
|
||||
<div
|
||||
v-for="(item, index) in goodsList"
|
||||
:key="index"
|
||||
class="content hover-pointer"
|
||||
@click.stop="goToSeckill"
|
||||
>
|
||||
<img :src="item.goodsImage" loading="lazy" decoding="async" width="140" height="140" :alt="item.goodsName" />
|
||||
<div class="ellipsis">{{ item.goodsName }}</div>
|
||||
<div>
|
||||
<span>{{ $filters.unitPrice(item.price, "¥") }}</span>
|
||||
<span>{{ $filters.unitPrice(item.originalPrice, "¥") }}</span>
|
||||
</div>
|
||||
</swiper-slide>
|
||||
<div class="swiper-button-prev" slot="button-prev">
|
||||
<Icon type="ios-arrow-back" />
|
||||
</div>
|
||||
<div class="swiper-button-next" slot="button-next">
|
||||
<Icon type="ios-arrow-forward" />
|
||||
</div>
|
||||
</swiper>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
// 引入插件
|
||||
import { swiper, swiperSlide } from 'vue-awesome-swiper';
|
||||
import 'swiper/dist/css/swiper.css';
|
||||
export default {
|
||||
components: {
|
||||
swiper,
|
||||
swiperSlide
|
||||
},
|
||||
props: {
|
||||
data: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
list: [], // 秒杀时段列表
|
||||
goodsList: [], // 商品列表
|
||||
actStatus: 0, // 0 未开始 1 进行中
|
||||
actName: '限时秒杀', // 活动名称
|
||||
currIndex: 0, // 当前时间段的下标
|
||||
currHour: '00', // 当前秒杀场
|
||||
diffSeconds: 0, // 倒计时秒数
|
||||
hours: 0, // 小时
|
||||
minutes: 0, // 分钟
|
||||
seconds: 0, // 秒
|
||||
interval: null, // 定时器
|
||||
swiperOption: { // 轮播图参数
|
||||
<script setup>
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
slidesPerView: 5,
|
||||
// 设置点击箭头
|
||||
navigation: {
|
||||
nextEl: '.swiper-button-next',
|
||||
prevEl: '.swiper-button-prev'
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
currIndex (val) {
|
||||
clearInterval(this.interval)
|
||||
this.interval = null
|
||||
this.countDown(val)
|
||||
this.goodsList = this.list[val].seckillGoodsList
|
||||
},
|
||||
diffSeconds (val) {
|
||||
const hours = Math.floor(val / 3600);
|
||||
// 当前秒数 / 60,向下取整
|
||||
// 获取到所有分钟数 3600 / 60 = 60分钟
|
||||
// 对60取模,超过小时数的分钟数
|
||||
const minutes = Math.floor(val / 60) % 60;
|
||||
// 当前的秒数 % 60,获取到 超过小时数、分钟数的秒数(秒数)
|
||||
const seconds = val % 60;
|
||||
this.hours = filteTime(hours)
|
||||
this.minutes = filteTime(minutes)
|
||||
this.seconds = filteTime(seconds)
|
||||
if (val <= 0) {
|
||||
clearInterval(this.interval)
|
||||
this.interval = null
|
||||
}
|
||||
function filteTime (time) {
|
||||
if (time < 10) {
|
||||
return '0' + time
|
||||
} else {
|
||||
return time
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
swiper () { // 轮播组件
|
||||
return this.$refs.mySwiper.swiper;
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
// 销毁前清除定时器
|
||||
clearInterval(this.interval);
|
||||
},
|
||||
mounted () {
|
||||
this.getListByDay()
|
||||
},
|
||||
methods: {
|
||||
goToSeckill () { // 跳转秒杀页面
|
||||
let routeUrl = this.$router.resolve({
|
||||
path: '/seckill'
|
||||
});
|
||||
window.open(routeUrl.href, '_blank');
|
||||
},
|
||||
countDown (currIndex) { // 倒计时
|
||||
// 0点时间戳
|
||||
let zeroTime = new Date(new Date().toLocaleDateString()).getTime();
|
||||
let currTime = new Date().getTime()
|
||||
let actTime = 0;
|
||||
let nowHour = new Date().getHours(); // 当前小时数
|
||||
if (this.list[currIndex].timeLine > nowHour) { // 活动未开始
|
||||
this.actStatus = 0;
|
||||
actTime = zeroTime + this.list[currIndex].timeLine * 3600 * 1000
|
||||
} else if (this.list[currIndex].timeLine <= nowHour) { // 活动进行中
|
||||
this.actStatus = 1;
|
||||
if (currIndex === this.list.length - 1) { // 如果是最后一个活动,直到24点结束
|
||||
actTime = zeroTime + 24 * 3600 * 1000
|
||||
} else {
|
||||
actTime = zeroTime + this.list[currIndex + 1].timeLine * 3600 * 1000
|
||||
}
|
||||
}
|
||||
this.currHour = this.list[this.currIndex].timeLine
|
||||
this.diffSeconds = Math.floor((actTime - currTime) / 1000)
|
||||
this.interval = setInterval(() => {
|
||||
this.diffSeconds--
|
||||
}, 1000)
|
||||
},
|
||||
getListByDay () { // 当天秒杀活动
|
||||
// const list = [
|
||||
// {
|
||||
// timeLine: 18,
|
||||
// seckillGoodsList: [
|
||||
// {goodsImage: 'https://lilishop-oss.oss-cn-beijing.aliyuncs.com/a9593607de404546953055f279fd5d54.png', goodsName: 'dfsdgsdf', originalPrice: 39, price: 12},
|
||||
// {goodsImage: 'https://lilishop-oss.oss-cn-beijing.aliyuncs.com/a9593607de404546953055f279fd5d54.png', goodsName: 'dfsdgsdf', originalPrice: 39, price: 12},
|
||||
// {goodsImage: 'https://lilishop-oss.oss-cn-beijing.aliyuncs.com/a9593607de404546953055f279fd5d54.png', goodsName: 'dfsdgsdf', originalPrice: 39, price: 12},
|
||||
// {goodsImage: 'https://lilishop-oss.oss-cn-beijing.aliyuncs.com/a9593607de404546953055f279fd5d54.png', goodsName: 'dfsdgsdf', originalPrice: 39, price: 12},
|
||||
// {goodsImage: 'https://lilishop-oss.oss-cn-beijing.aliyuncs.com/a9593607de404546953055f279fd5d54.png', goodsName: 'dfsdgsdf', originalPrice: 39, price: 12},
|
||||
// {goodsImage: 'https://lilishop-oss.oss-cn-beijing.aliyuncs.com/a9593607de404546953055f279fd5d54.png', goodsName: 'dfsdgsdf', originalPrice: 39, price: 12}
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
this.list = this.data.options.list
|
||||
this.goodsList = this.list[0].seckillGoodsList
|
||||
console.log( this.goodsList)
|
||||
this.countDown(this.currIndex)
|
||||
const props = defineProps({
|
||||
data: Object,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const list = ref([]);
|
||||
const goodsList = ref([]);
|
||||
const actStatus = ref(0);
|
||||
const actName = ref("限时秒杀");
|
||||
const currIndex = ref(0);
|
||||
const currHour = ref("00");
|
||||
const diffSeconds = ref(0);
|
||||
const hours = ref(0);
|
||||
const minutes = ref(0);
|
||||
const seconds = ref(0);
|
||||
const interval = ref(null);
|
||||
|
||||
watch(currIndex, (val) => {
|
||||
clearInterval(interval.value);
|
||||
interval.value = null;
|
||||
countDown(val);
|
||||
goodsList.value = list.value[val]?.seckillGoodsList || [];
|
||||
});
|
||||
|
||||
watch(diffSeconds, (val) => {
|
||||
const h = Math.floor(val / 3600);
|
||||
const m = Math.floor(val / 60) % 60;
|
||||
const s = val % 60;
|
||||
hours.value = filteTime(h);
|
||||
minutes.value = filteTime(m);
|
||||
seconds.value = filteTime(s);
|
||||
if (val <= 0) {
|
||||
clearInterval(interval.value);
|
||||
interval.value = null;
|
||||
}
|
||||
function filteTime(time) {
|
||||
return time < 10 ? "0" + time : time;
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(interval.value);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getListByDay();
|
||||
});
|
||||
|
||||
function goToSeckill() {
|
||||
router.push("/seckill");
|
||||
}
|
||||
|
||||
function countDown(index) {
|
||||
const zeroTime = new Date(new Date().toLocaleDateString()).getTime();
|
||||
const currTime = new Date().getTime();
|
||||
let actTime = 0;
|
||||
const nowHour = new Date().getHours();
|
||||
if (list.value[index].timeLine > nowHour) {
|
||||
actStatus.value = 0;
|
||||
actTime = zeroTime + list.value[index].timeLine * 3600 * 1000;
|
||||
} else if (list.value[index].timeLine <= nowHour) {
|
||||
actStatus.value = 1;
|
||||
if (index === list.value.length - 1) {
|
||||
actTime = zeroTime + 24 * 3600 * 1000;
|
||||
} else {
|
||||
actTime = zeroTime + list.value[index + 1].timeLine * 3600 * 1000;
|
||||
}
|
||||
}
|
||||
};
|
||||
currHour.value = list.value[currIndex.value].timeLine;
|
||||
diffSeconds.value = Math.floor((actTime - currTime) / 1000);
|
||||
interval.value = setInterval(() => {
|
||||
diffSeconds.value--;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function getListByDay() {
|
||||
list.value = props.data?.options?.list || [];
|
||||
if (!list.value.length) {
|
||||
goodsList.value = [];
|
||||
return;
|
||||
}
|
||||
goodsList.value = list.value[0]?.seckillGoodsList || [];
|
||||
if (goodsList.value.length) {
|
||||
countDown(currIndex.value);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.seckill {
|
||||
@@ -174,15 +132,13 @@ export default {
|
||||
width: 190px;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
background-image: url("../../../assets/images/seckillBg.png");
|
||||
|
||||
background: linear-gradient(180deg, var(--theme-color, #F31947) 0%, var(--theme-light, #ff6b35) 100%);
|
||||
.title {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 28px;
|
||||
margin-top: 31px;
|
||||
}
|
||||
|
||||
.hour {
|
||||
margin-top: 90px;
|
||||
text-align: center;
|
||||
@@ -190,7 +146,6 @@ export default {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.count-down {
|
||||
margin: 10px 0 0 30px;
|
||||
> span {
|
||||
@@ -219,15 +174,21 @@ export default {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
.act-status {
|
||||
margin: 10px 0 0 65px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-scroll {
|
||||
height: 260px;
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.content {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -262,26 +223,12 @@ export default {
|
||||
line-height: 24px;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
|
||||
span:nth-child(1) {
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
width: 92px;
|
||||
background-color: $theme_color;
|
||||
position: relative;
|
||||
&::before {
|
||||
content: " ";
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent white transparent transparent;
|
||||
border-style: solid;
|
||||
border-width: 24px 8px 0 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 84px;
|
||||
}
|
||||
}
|
||||
|
||||
span:nth-child(2) {
|
||||
color: #999;
|
||||
width: 66px;
|
||||
@@ -290,35 +237,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
.swiper-container {
|
||||
height: 260px;
|
||||
width: 1000px;
|
||||
margin-left: 10px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.swiper-button-prev, .swiper-button-next {
|
||||
background: #ccc;
|
||||
width: 25px;
|
||||
height: 35px;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.swiper-button-prev:hover, .swiper-button-next:hover {
|
||||
background: #aaa;
|
||||
}
|
||||
.swiper-button-prev {
|
||||
left: 0;
|
||||
border-bottom-right-radius: 18px;
|
||||
border-top-right-radius: 18px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.swiper-button-next {
|
||||
right: 0;
|
||||
border-top-left-radius: 18px;
|
||||
border-bottom-left-radius: 18px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<div class="model" v-if="data">
|
||||
<div class="for-your">{{ data.options.title }}</div>
|
||||
<div class="flex card">
|
||||
<div class="left">
|
||||
<img :src="data.options.data.image.src" alt="" />
|
||||
<div class="left hover-pointer" @click="linkTo(data.options.data.image.url)">
|
||||
<img :src="data.options.data.image.src" loading="lazy" decoding="async" alt="" />
|
||||
</div>
|
||||
<div class="right flex">
|
||||
<!-- 商品区 -->
|
||||
<div class="goods-list">
|
||||
<div class="for-your-goods-list">
|
||||
<div
|
||||
class="goods-item"
|
||||
class="for-your-goods-item"
|
||||
:key="index"
|
||||
v-for="(item, index) in data.options.data.list"
|
||||
@click="linkTo(item.url)"
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="goods-name">{{ item.title }}</div>
|
||||
<div class="goods-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
<img class="goods-img" :src="item.img" alt="" />
|
||||
<img class="goods-img" :src="item.img" loading="lazy" decoding="async" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 热卖区 -->
|
||||
@@ -30,11 +30,11 @@
|
||||
:key="index"
|
||||
@click="linkTo(item.url)"
|
||||
>
|
||||
<img :src="item.img" alt="" />
|
||||
<img :src="item.img" loading="lazy" decoding="async" alt="" />
|
||||
<div class="hot-goods">
|
||||
<div class="hot-goods-title">{{ item.title }}</div>
|
||||
<div>
|
||||
<div class="hot-price">¥{{ item.price | unitPrice }}</div>
|
||||
<div class="hot-price">¥{{ $filters.unitPrice(item.price) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,35 +44,34 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "forYour",
|
||||
data() {
|
||||
return {
|
||||
current: "",
|
||||
};
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
mounted() {},
|
||||
methods: {},
|
||||
};
|
||||
defineOptions({ name: 'forYour' })
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
|
||||
const current = ref('')
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.goods-list {
|
||||
.for-your-goods-list {
|
||||
display: flex;
|
||||
flex: 0 0 558px;
|
||||
width: 558px;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 0;
|
||||
}
|
||||
.goods-item {
|
||||
.for-your-goods-item {
|
||||
flex: 0 0 278px;
|
||||
width: 278px;
|
||||
height: 277px;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
@@ -84,10 +83,8 @@ export default {
|
||||
border-top: 1.4px solid #e2e2e2;
|
||||
}
|
||||
&:hover {
|
||||
/deep/ .goods-name {
|
||||
color: $theme_color;
|
||||
}
|
||||
/deep/ .goods-desc {
|
||||
.goods-name,
|
||||
.goods-desc {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
@@ -105,10 +102,12 @@ export default {
|
||||
color: #333333;
|
||||
}
|
||||
.goods-img {
|
||||
display: block;
|
||||
width: 190px;
|
||||
height: 156px;
|
||||
margin-top: 19px;
|
||||
margin: 19px auto 0;
|
||||
border-radius: 10px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.goods-desc {
|
||||
font-size: 14px;
|
||||
@@ -137,6 +136,8 @@ export default {
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0px 1px 13px 0px #E5E5E5;
|
||||
|
||||
&:hover {
|
||||
@@ -159,20 +160,27 @@ export default {
|
||||
}
|
||||
}
|
||||
.left {
|
||||
flex: 0 0 346px;
|
||||
width: 346px;
|
||||
height: 554px;
|
||||
border-radius: 9.8px 0px 0px 9.8px;
|
||||
overflow: hidden;
|
||||
> img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
flex: 0 0 839px;
|
||||
width: 839px;
|
||||
height: 554px;
|
||||
border-radius: 0px 9.8px 9.8px 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hot-list {
|
||||
flex: 0 0 279px;
|
||||
width: 279px;
|
||||
}
|
||||
.hot-title {
|
||||
@@ -201,8 +209,11 @@ export default {
|
||||
}
|
||||
|
||||
> img {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
width: 76.3px;
|
||||
height: 77.7px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
.hot-goods {
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- 商品部分 -->
|
||||
<div class="only-goods-list flex-j-sb">
|
||||
<div class="only-goods-list flex-j-sb" v-if="paramData.options">
|
||||
<div
|
||||
v-if="paramData.options"
|
||||
class="only-goods-list-item "
|
||||
v-for="(item, index) in paramData.options.list.filter((client) => {
|
||||
return client.___index == current;
|
||||
@@ -26,14 +25,14 @@
|
||||
@click="linkTo(item.url)"
|
||||
>
|
||||
<div class="goods-img">
|
||||
<img :src="item.img" />
|
||||
<img :src="item.img" loading="lazy" decoding="async" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="goods-name wes-2">{{ item.title }}</div>
|
||||
<div class="goods-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
<div class="goods-price">
|
||||
{{ item.price | unitPrice("¥") }}
|
||||
{{ $filters.unitPrice(item.price, "¥") }}
|
||||
</div>
|
||||
<!-- 查看 -->
|
||||
<div class="white-shadow">
|
||||
@@ -44,31 +43,28 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "goods",
|
||||
props: {
|
||||
paramData: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
defineOptions({ name: "goods" });
|
||||
|
||||
const props = defineProps({
|
||||
paramData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
current: "",
|
||||
};
|
||||
},
|
||||
watch: {},
|
||||
mounted() {
|
||||
this.current = this.paramData.options.labels[0].___index;
|
||||
},
|
||||
methods: {
|
||||
// 筛选出当前分类下的商品
|
||||
handleClickLabel(val, index) {
|
||||
this.current = index;
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const current = ref("");
|
||||
|
||||
onMounted(() => {
|
||||
current.value = props.paramData.options.labels[0].___index;
|
||||
});
|
||||
|
||||
// 筛选出当前分类下的商品
|
||||
function handleClickLabel(val, index) {
|
||||
current.value = index;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -173,7 +169,7 @@ export default {
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
|
||||
color: #f31947;
|
||||
color: $theme_color;
|
||||
-webkit-text-stroke: #979797 0.7px; /* 浏览器可能不支持 */
|
||||
}
|
||||
.goods-type-line {
|
||||
@@ -190,7 +186,7 @@ export default {
|
||||
color: #333333;
|
||||
}
|
||||
.active {
|
||||
color: #f31947;
|
||||
color: $theme_color;
|
||||
}
|
||||
.goods-type-labels {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -11,29 +11,18 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import left from './mixs/mix-goods'
|
||||
import right from './mixs/mix-brand'
|
||||
export default {
|
||||
name: "mixModel",
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
left,
|
||||
right
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
defineOptions({ name: "mixModel" });
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -49,5 +38,6 @@ export default {
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 1px 13px 0px #E5E5E5;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<template>
|
||||
<div class="flex" v-if="data.options.right.model == 'brand'">
|
||||
<div class="left-side" @click="linkTo(data.options.right.data.image.url)">
|
||||
<img :src="data.options.right.data.image.src" alt="">
|
||||
<div class="mix-panel flex" v-if="data.options.right.model == 'brand'">
|
||||
<div class="left-side hover-pointer" @click="linkTo(data.options.right.data.image.url)">
|
||||
<img :src="data.options.right.data.image.src" loading="lazy" decoding="async" alt="" />
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<div class="badge-box flex">
|
||||
<div class="round">
|
||||
<Icon type="ios-arrow-forward" />
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="flex goods-list">
|
||||
<div class="goods-item flex" @click="linkTo(item.url)" :key="index" v-for="(item,index) in data.options.right.data.list">
|
||||
<div
|
||||
class="goods-item flex hover-pointer"
|
||||
@click="linkTo(item.url)"
|
||||
:key="index"
|
||||
v-for="(item, index) in data.options.right.data.list"
|
||||
>
|
||||
<div class="goods-thumbnail">
|
||||
<img :src="item.img" alt="">
|
||||
<img :src="item.img" loading="lazy" decoding="async" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -22,63 +25,59 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "mix-goods",
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
},
|
||||
components: {},
|
||||
mounted() {
|
||||
<script setup>
|
||||
import { ArrowRight } from "@element-plus/icons-vue";
|
||||
|
||||
defineOptions({ name: "mix-brand" });
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.right-side{
|
||||
width: 330px;
|
||||
.mix-panel {
|
||||
height: 100%;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
}
|
||||
.goods-detail-title{
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
color: #333333;
|
||||
}
|
||||
.goods-detail-price{
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 17px;
|
||||
letter-spacing: 0px;
|
||||
color: $theme_color;
|
||||
margin-top: 8px;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
.goods-thumbnail{
|
||||
margin-left: 7px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
>img{
|
||||
width:90px;
|
||||
height: 90px;
|
||||
|
||||
.left-side {
|
||||
flex-shrink: 0;
|
||||
width: 254px;
|
||||
height: 344px;
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 9.8px 0 0 9.8px;
|
||||
}
|
||||
}
|
||||
.badge-box{
|
||||
margin-top: 33px;
|
||||
justify-content:right;
|
||||
>.round{
|
||||
|
||||
.right-side {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 344px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.badge-box {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
|
||||
> .round {
|
||||
width: 17.5px;
|
||||
height: 17.5px;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 17.5px;
|
||||
@@ -86,47 +85,48 @@ export default {
|
||||
margin-right: 17.5px;
|
||||
}
|
||||
}
|
||||
.goods-list{
|
||||
width: 330px;
|
||||
|
||||
.goods-list {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
flex-wrap: wrap;
|
||||
justify-content:space-between;
|
||||
padding: 0 16px;
|
||||
justify-content: space-between;
|
||||
align-content: flex-start;
|
||||
padding: 0 16px 12px;
|
||||
gap: 8px 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.goods-item{
|
||||
|
||||
.goods-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width:50%;
|
||||
height: 85px;
|
||||
flex: 0 0 calc(50% - 4px);
|
||||
width: calc(50% - 4px);
|
||||
height: 82px;
|
||||
border-radius: 9.8px;
|
||||
opacity: 1;
|
||||
transition: .35s;
|
||||
background: #FFFFFF;
|
||||
margin-bottom:9px;
|
||||
|
||||
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
transition: 0.35s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.left-side {
|
||||
>img{
|
||||
width: 254px;
|
||||
height: 344px;
|
||||
border-radius: 9.8px 0px 0px 9.8px;
|
||||
opacity: 1;
|
||||
|
||||
.goods-thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> img {
|
||||
max-width: 80px;
|
||||
max-height: 76px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
.badge {
|
||||
width: 50px;
|
||||
height: 27px;
|
||||
line-height:27px;
|
||||
border-radius: 13.3px 0px 0px 13.3px;
|
||||
opacity: 1;
|
||||
background: #F31947;
|
||||
font-size: 12.6px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
color: #FFFFFF;
|
||||
margin-top: 26px;
|
||||
margin-bottom:17px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,127 +1,148 @@
|
||||
<template>
|
||||
<div class="flex" v-if="data.options.left.model == 'goods'">
|
||||
<div class="left-side" @click="linkTo(data.options.right.data.image.url)">
|
||||
<img :src="data.options.left.data.image.src" alt="">
|
||||
<div class="mix-panel flex" v-if="data.options.left.model == 'goods'">
|
||||
<div class="left-side hover-pointer" @click="linkTo(data.options.left.data.image.url)">
|
||||
<img :src="data.options.left.data.image.src" loading="lazy" decoding="async" alt="" />
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<div class="badge-box flex">
|
||||
<div class="badge hover-pointer" @click="linkTo(data.options.left.data.badge.url)">
|
||||
{{ data.options.left.data.badge.label }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<div class="badge-box flex">
|
||||
<div class="badge" @click="linkTo(data.options.right.data.image.url)">
|
||||
{{data.options.left.data.badge.label}}
|
||||
</div>
|
||||
<div class="flex goods-list">
|
||||
<div
|
||||
class="goods-item flex hover-pointer"
|
||||
@click="linkTo(item.url)"
|
||||
:key="index"
|
||||
v-for="(item, index) in data.options.left.data.list"
|
||||
>
|
||||
<div class="goods-thumbnail">
|
||||
<img :src="item.img" loading="lazy" decoding="async" alt="" />
|
||||
</div>
|
||||
<div class="flex goods-list">
|
||||
|
||||
<div class="goods-item flex" @click="linkTo(item.url)" :key="index" v-for="(item,index) in data.options.left.data.list">
|
||||
<div class="goods-thumbnail">
|
||||
<img :src="item.img" alt="">
|
||||
</div>
|
||||
<div class="goods-detail">
|
||||
<div class="goods-detail-title">{{item.title}}</div>
|
||||
<div class="goods-detail-price">{{item.price | unitPrice('¥')}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="goods-detail">
|
||||
<div class="goods-detail-title">{{ item.title }}</div>
|
||||
<div class="goods-detail-price">{{ $filters.unitPrice(item.price, '¥') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "mix-goods",
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
},
|
||||
components: {},
|
||||
mounted() {
|
||||
<script setup>
|
||||
defineOptions({ name: "mix-goods" });
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.right-side{
|
||||
width: 387px;
|
||||
.mix-panel {
|
||||
height: 100%;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
}
|
||||
.goods-detail-title{
|
||||
|
||||
.left-side {
|
||||
flex-shrink: 0;
|
||||
width: 196.7px;
|
||||
height: 344px;
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 10px 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-side {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 344px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-detail-title {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
color: #333333;
|
||||
}
|
||||
.goods-detail-price{
|
||||
|
||||
.goods-detail-price {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 17px;
|
||||
letter-spacing: 0px;
|
||||
color: #F31947;
|
||||
color: $theme_color;
|
||||
margin-top: 8px;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
.goods-thumbnail{
|
||||
|
||||
.goods-thumbnail {
|
||||
margin-left: 7px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
>img{
|
||||
width:90px;
|
||||
flex-shrink: 0;
|
||||
|
||||
> img {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
.badge-box{
|
||||
|
||||
justify-content:right;
|
||||
.badge-box {
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.goods-list{
|
||||
width: 387px;
|
||||
|
||||
.goods-list {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
flex-wrap: wrap;
|
||||
justify-content:space-between;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
gap: 9px 0;
|
||||
}
|
||||
.goods-item{
|
||||
|
||||
.goods-item {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 173.6px;
|
||||
flex: 0 0 calc(50% - 4px);
|
||||
width: calc(50% - 4px);
|
||||
height: 119px;
|
||||
border-radius: 9.8px;
|
||||
opacity: 1;
|
||||
transition: .35s;
|
||||
background: #FFFFFF;
|
||||
margin-bottom:9px;
|
||||
&:hover{
|
||||
box-shadow: 0px 1px 6px 0px #E5E5E5;
|
||||
}
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
transition: 0.35s;
|
||||
|
||||
}
|
||||
.left-side {
|
||||
>img{
|
||||
border-radius: 10px 0px 0px 10px;
|
||||
display: block;
|
||||
width: 196.7px;
|
||||
height: 343.7px;
|
||||
&:hover {
|
||||
box-shadow: 0 1px 6px 0 #e5e5e5;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
width: 50px;
|
||||
height: 27px;
|
||||
line-height:27px;
|
||||
border-radius: 13.3px 0px 0px 13.3px;
|
||||
opacity: 1;
|
||||
background: #F31947;
|
||||
line-height: 27px;
|
||||
border-radius: 13.3px 0 0 13.3px;
|
||||
background: $theme_color;
|
||||
font-size: 12.6px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
letter-spacing: 0px;
|
||||
color: #FFFFFF;
|
||||
color: #ffffff;
|
||||
margin-top: 26px;
|
||||
margin-bottom:17px;
|
||||
margin-bottom: 17px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,36 +1,40 @@
|
||||
<template>
|
||||
<div class="line flex flex-a-c flex-j-sb">
|
||||
<div class="column" v-for="(item,index) in data.options.list" :key="index">
|
||||
<img :src="item.img" class="three-column-img">
|
||||
<div
|
||||
class="column hover-pointer"
|
||||
v-for="(item, index) in data.options.list"
|
||||
:key="index"
|
||||
@click="linkTo(item.url)"
|
||||
>
|
||||
<img :src="item.img" loading="lazy" decoding="async" class="three-column-img" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "oneRowThreeColumns",
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
<script setup>
|
||||
defineOptions({ name: "oneRowThreeColumns" });
|
||||
|
||||
};
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.column{
|
||||
.line {
|
||||
width: 100%;
|
||||
}
|
||||
.column {
|
||||
width: 385px;
|
||||
height: 165px;
|
||||
>img{
|
||||
cursor: pointer;
|
||||
> img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="goods-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
<div class="goods-img">
|
||||
<img :src="item.img" />
|
||||
<img :src="item.img" loading="lazy" decoding="async" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,35 +21,32 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { openLink } from "@/utils/buyerLogin";
|
||||
|
||||
export default {
|
||||
name: "onlyGoodsList",
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
flag:false,
|
||||
tabIndex:0,
|
||||
current: 0,
|
||||
showModal: false,
|
||||
selected: {}, // 已选数据
|
||||
picModelFlag: false,
|
||||
};
|
||||
},
|
||||
mounted(){
|
||||
defineOptions({ name: "onlyGoodsList" });
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
methods: {
|
||||
handleClick(val){
|
||||
this.linkTo(val.url)
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const flag = ref(false);
|
||||
const tabIndex = ref(0);
|
||||
const current = ref(0);
|
||||
const showModal = ref(false);
|
||||
const selected = ref({}); // 已选数据
|
||||
const picModelFlag = ref(false);
|
||||
|
||||
function handleClick(val) {
|
||||
openLink(val.url, router);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
62
buyer/src/components/indexDecorate/modelList/setup-box.scss
Normal file
62
buyer/src/components/indexDecorate/modelList/setup-box.scss
Normal file
@@ -0,0 +1,62 @@
|
||||
.setup-content{
|
||||
position: relative;
|
||||
&:hover{
|
||||
.setup-box{
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setup-box{
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: rgba(0, 0, 0, .2);
|
||||
z-index: 99;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.el-button{
|
||||
float: right;
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-tab-bar{
|
||||
margin-left: 20px;
|
||||
table{
|
||||
margin-top: 10px;
|
||||
display:inline-block;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
max-height: 400px !important;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
thead{
|
||||
background-color: #eee;
|
||||
th{
|
||||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
td{
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
tbody>tr:hover{
|
||||
background-color: aliceblue;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-top-advert{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
>*{
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.el-input{
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
@@ -1,150 +1,296 @@
|
||||
<template>
|
||||
<div class="invoice-modal">
|
||||
<Modal v-model="invoiceAvailable" width="600" footer-hide>
|
||||
<p slot="header">
|
||||
<el-dialog v-model="invoiceAvailable" width="640">
|
||||
<template #header><p>
|
||||
<span>发票信息</span>
|
||||
</p>
|
||||
<!-- 普通发票 -->
|
||||
</p></template>
|
||||
<div class="nav-content">
|
||||
<Form :model="invoiceForm" ref="form" label-position="left" :rules="ruleInline" :label-width="110">
|
||||
<FormItem label="发票类型">
|
||||
<RadioGroup v-model="invoice" type="button" button-style="solid">
|
||||
<Radio @on-change="changeInvoice" :label="1">电子普通发票</Radio>
|
||||
<Radio :label="2" :disabled="true">增值税专用发票</Radio>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
<FormItem label="发票抬头">
|
||||
<RadioGroup v-model="type" @on-change="changeInvoice" type="button" button-style="solid">
|
||||
<Radio :label="1">个人</Radio>
|
||||
<Radio :label="2">单位</Radio>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
<FormItem label="个人名称" v-if="type === 1" prop="receiptTitle">
|
||||
<i-input v-model="invoiceForm.receiptTitle"></i-input>
|
||||
</FormItem>
|
||||
<FormItem label="单位名称" v-if="type === 2" prop="receiptTitle">
|
||||
<i-input v-model="invoiceForm.receiptTitle"></i-input>
|
||||
</FormItem>
|
||||
<FormItem label="纳税人识别号" v-if="type === 2" prop="taxpayerId">
|
||||
<i-input v-model="invoiceForm.taxpayerId"></i-input>
|
||||
</FormItem>
|
||||
<FormItem label="发票内容">
|
||||
<RadioGroup v-model="invoiceForm.receiptContent" type="button" button-style="solid">
|
||||
<Radio label="商品明细">商品明细</Radio>
|
||||
<Radio label="商品类别">商品类别</Radio>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<el-form :model="invoiceForm" ref="formRef" label-position="left" :rules="formRules" label-width="118px">
|
||||
<el-form-item label="发票类型">
|
||||
<el-radio-group v-model="invoice" type="button" button-style="solid" @change="onInvoiceKindChange">
|
||||
<el-radio :label="1">电子普通发票</el-radio>
|
||||
<el-radio :label="2">增值税专用发票</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 电子普通发票 -->
|
||||
<div v-if="invoice === 1" key="invoice-normal">
|
||||
<el-form-item label="发票抬头">
|
||||
<el-radio-group v-model="type" type="button" button-style="solid" @change="onHeaderTypeChange">
|
||||
<el-radio :label="1">个人</el-radio>
|
||||
<el-radio :label="2">单位</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="type === 1 ? '个人名称' : '单位名称'" :prop="type === 1 ? 'personalName' : 'companyName'">
|
||||
<el-input v-if="type === 1" v-model="invoiceForm.personalName"></el-input>
|
||||
<el-input v-else v-model="invoiceForm.companyName"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="纳税人识别号" v-if="type === 2" prop="taxpayerId">
|
||||
<el-input v-model="invoiceForm.taxpayerId"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="发票内容">
|
||||
<el-radio-group v-model="invoiceForm.receiptContent" type="button" button-style="solid">
|
||||
<el-radio label="商品明细">商品明细</el-radio>
|
||||
<el-radio label="商品类别">商品类别</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="收票人手机" prop="receiptPhone">
|
||||
<el-input v-model="invoiceForm.receiptPhone"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="收票人邮箱" prop="receiptEmail">
|
||||
<el-input v-model="invoiceForm.receiptEmail" placeholder="可选"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 增值税专用发票(固定为单位) -->
|
||||
<div v-else key="invoice-vat">
|
||||
<el-form-item label="发票抬头">
|
||||
<span class="inv-title-fixed">单位</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="单位名称" prop="companyName">
|
||||
<el-input v-model="invoiceForm.companyName" placeholder="与营业执照一致"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="纳税人识别号" prop="taxpayerId">
|
||||
<el-input v-model="invoiceForm.taxpayerId"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="单位地址" prop="companyAddress">
|
||||
<el-input v-model="invoiceForm.companyAddress" placeholder="注册地址"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="单位电话" prop="companyPhone">
|
||||
<el-input v-model="invoiceForm.companyPhone" placeholder="固话或手机"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="开户银行" prop="bankName">
|
||||
<el-input v-model="invoiceForm.bankName"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="银行账号" prop="bankAccount">
|
||||
<el-input v-model="invoiceForm.bankAccount"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="发票内容">
|
||||
<el-radio-group v-model="invoiceForm.receiptContent" type="button" button-style="solid">
|
||||
<el-radio label="商品明细">商品明细</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="收票人手机" prop="receiptPhone">
|
||||
<el-input v-model="invoiceForm.receiptPhone"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="收票人邮箱" prop="receiptEmail">
|
||||
<el-input v-model="invoiceForm.receiptEmail" placeholder="可选"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<div style="text-align: center">
|
||||
<Button type="primary" :loading="loading" @click="submit">保存发票信息</Button>
|
||||
<Button type="default" @click="invoiceAvailable = false">取消</Button>
|
||||
<el-button type="primary" :loading="loading" @click="submit">保存发票信息</el-button>
|
||||
<el-button type="default" @click="invoiceAvailable = false">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div></el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { receiptSelect } from '@/api/cart.js';
|
||||
import { TINumber } from '@/plugins/RegExp.js';
|
||||
export default {
|
||||
name: 'invoiceModal',
|
||||
data () {
|
||||
import { mobile, email } from '@/plugins/RegExp.js';
|
||||
|
||||
defineOptions({ name: 'invoiceModal' });
|
||||
|
||||
const props = defineProps(['invoiceData']);
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const invoice = ref(1); // 1 电子普通发票 2 增值税专用发票
|
||||
const invoiceAvailable = ref(false);
|
||||
const loading = ref(false);
|
||||
const invoiceForm = ref({
|
||||
receiptTitle: '',
|
||||
personalName: '',
|
||||
companyName: '',
|
||||
taxpayerId: '',
|
||||
receiptContent: '商品明细',
|
||||
receiptPhone: '',
|
||||
receiptEmail: '',
|
||||
companyAddress: '',
|
||||
companyPhone: '',
|
||||
bankName: '',
|
||||
bankAccount: ''
|
||||
});
|
||||
const type = ref(1);
|
||||
|
||||
const formRef = ref(null);
|
||||
|
||||
const formRules = computed(() => {
|
||||
const receiptPhoneRules = [
|
||||
{ required: true, message: '请填写收票人手机', trigger: 'blur' },
|
||||
{ pattern: mobile, message: '请填写正确的手机号码', trigger: 'blur' }
|
||||
];
|
||||
// 收票人邮箱可选:仅校验格式;提示文案须与手机区分,避免误用「请填写收票人手机」
|
||||
const receiptEmailRules = [
|
||||
{
|
||||
validator (rule, val, cb) {
|
||||
const s = val != null ? String(val).trim() : '';
|
||||
if (!s) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
if (!email.test(s)) {
|
||||
cb(new Error('请填写正确的收票人邮箱'));
|
||||
return;
|
||||
}
|
||||
cb();
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
];
|
||||
|
||||
if (invoice.value === 2) {
|
||||
return {
|
||||
invoice: 1, // 发票类型
|
||||
invoiceAvailable: false, // 模态框显隐
|
||||
loading: false, // 提交状态
|
||||
invoiceForm: {
|
||||
// 普票表单
|
||||
receiptTitle: '', // 发票抬头
|
||||
taxpayerId: '', // 纳税人识别号
|
||||
receiptContent: '商品明细' // 发票内容
|
||||
},
|
||||
type: 1, // 1 个人 2 单位
|
||||
ruleInline: {
|
||||
taxpayerId: [
|
||||
{ required: true, message: '请填写纳税人识别号' },
|
||||
{ pattern: TINumber, message: '请填写正确的纳税人识别号' }
|
||||
]
|
||||
}
|
||||
companyName: [{ required: true, message: '请填写单位名称', trigger: 'blur' }],
|
||||
taxpayerId: [{ required: true, message: '请填写纳税人识别号', trigger: 'blur' }],
|
||||
companyAddress: [{ required: true, message: '请填写单位地址', trigger: 'blur' }],
|
||||
companyPhone: [{ required: true, message: '请填写单位电话', trigger: 'blur' }],
|
||||
bankName: [{ required: true, message: '请填写开户银行', trigger: 'blur' }],
|
||||
bankAccount: [{ required: true, message: '请填写银行账号', trigger: 'blur' }],
|
||||
receiptPhone: receiptPhoneRules,
|
||||
receiptEmail: receiptEmailRules
|
||||
};
|
||||
},
|
||||
props: ['invoiceData'],
|
||||
watch: {
|
||||
// 回显的发票信息
|
||||
invoiceData: {
|
||||
handler (val) {
|
||||
this.invoiceForm = { ...val };
|
||||
|
||||
if (val.taxpayerId) {
|
||||
this.type = 2;
|
||||
} else {
|
||||
this.type = 1;
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
immeadite: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 选择发票抬头
|
||||
*/
|
||||
changeInvoice (val) {
|
||||
this.$nextTick(() => {
|
||||
this.type = val;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存判断
|
||||
*/
|
||||
save () {
|
||||
let flage = true;
|
||||
|
||||
// 保存分为两种类型,个人以及企业
|
||||
const { receiptTitle } = JSON.parse(
|
||||
JSON.stringify(this.invoiceForm)
|
||||
);
|
||||
// 判断是否填写发票抬头
|
||||
if (!receiptTitle) {
|
||||
this.$Message.error('请填写发票抬头!');
|
||||
flage = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.type === 2) {
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (!valid) {
|
||||
flage = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
delete this.invoiceForm.taxpayerId;
|
||||
}
|
||||
|
||||
return flage;
|
||||
},
|
||||
// 保存发票信息
|
||||
async submit () {
|
||||
if (this.save()) {
|
||||
this.loading = true;
|
||||
let submit = {
|
||||
way: this.$route.query.way,
|
||||
...this.invoiceForm
|
||||
};
|
||||
let receipt = await receiptSelect(submit);
|
||||
if (receipt.success) {
|
||||
this.$emit('change', true);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const rules = {
|
||||
receiptPhone: receiptPhoneRules,
|
||||
receiptEmail: receiptEmailRules
|
||||
};
|
||||
if (type.value === 1) {
|
||||
rules.personalName = [{ required: true, message: '请填写个人名称', trigger: 'blur' }];
|
||||
} else {
|
||||
rules.companyName = [{ required: true, message: '请填写单位名称', trigger: 'blur' }];
|
||||
rules.taxpayerId = [{ required: true, message: '请填写纳税人识别号', trigger: 'blur' }];
|
||||
}
|
||||
return rules;
|
||||
});
|
||||
|
||||
watch(() => props.invoiceData, (val) => {
|
||||
if (!val) return;
|
||||
invoiceForm.value = { ...invoiceForm.value, ...val };
|
||||
if (!invoiceForm.value.receiptPhone) invoiceForm.value.receiptPhone = '';
|
||||
if (!invoiceForm.value.receiptEmail) invoiceForm.value.receiptEmail = '';
|
||||
if (!invoiceForm.value.receiptContent) invoiceForm.value.receiptContent = '商品明细';
|
||||
if (!invoiceForm.value.companyAddress) invoiceForm.value.companyAddress = '';
|
||||
if (!invoiceForm.value.companyPhone) invoiceForm.value.companyPhone = '';
|
||||
if (!invoiceForm.value.bankName) invoiceForm.value.bankName = '';
|
||||
if (!invoiceForm.value.bankAccount) invoiceForm.value.bankAccount = '';
|
||||
if (!invoiceForm.value.personalName) invoiceForm.value.personalName = '';
|
||||
if (!invoiceForm.value.companyName) invoiceForm.value.companyName = '';
|
||||
|
||||
const rt = val.receiptType;
|
||||
if (rt === 2 || rt === '2' || val.invoiceKind === 'VAT_SPECIAL') {
|
||||
invoice.value = 2;
|
||||
type.value = 2;
|
||||
} else {
|
||||
invoice.value = 1;
|
||||
if (val.taxpayerId) type.value = 2;
|
||||
else type.value = 1;
|
||||
}
|
||||
// 兼容后端 receiptTitle 存「个人/单位」,实际名称在 personalName/companyName;旧数据名称可能只在 receiptTitle
|
||||
const titleAsName = (t) => (t && t !== '个人' && t !== '单位' ? t : '');
|
||||
if (invoice.value === 2) {
|
||||
invoiceForm.value.companyName = val.companyName || titleAsName(val.receiptTitle) || '';
|
||||
} else if (type.value === 2) {
|
||||
invoiceForm.value.companyName = val.companyName || titleAsName(val.receiptTitle) || '';
|
||||
invoiceForm.value.personalName = val.personalName || '';
|
||||
} else {
|
||||
invoiceForm.value.personalName = val.personalName || titleAsName(val.receiptTitle) || '';
|
||||
invoiceForm.value.companyName = val.companyName || '';
|
||||
}
|
||||
if (invoice.value === 1) {
|
||||
invoiceForm.value.companyAddress = '';
|
||||
invoiceForm.value.companyPhone = '';
|
||||
invoiceForm.value.bankName = '';
|
||||
invoiceForm.value.bankAccount = '';
|
||||
}
|
||||
if (invoice.value === 2) {
|
||||
invoiceForm.value.personalName = '';
|
||||
}
|
||||
}, { deep: true, immediate: true });
|
||||
|
||||
function onHeaderTypeChange (val) {
|
||||
if (invoice.value === 1 && val === 1 && invoiceForm.value.personalName === '个人') {
|
||||
invoiceForm.value.personalName = '';
|
||||
}
|
||||
nextTick(() => {
|
||||
if (formRef.value) formRef.value.clearValidate();
|
||||
});
|
||||
}
|
||||
|
||||
function onInvoiceKindChange (val) {
|
||||
type.value = val === 2 ? 2 : 1;
|
||||
invoiceForm.value = {
|
||||
receiptTitle: '',
|
||||
personalName: '',
|
||||
companyName: '',
|
||||
taxpayerId: '',
|
||||
receiptContent: '商品明细',
|
||||
receiptPhone: '',
|
||||
receiptEmail: '',
|
||||
companyAddress: '',
|
||||
companyPhone: '',
|
||||
bankName: '',
|
||||
bankAccount: ''
|
||||
};
|
||||
nextTick(() => {
|
||||
if (formRef.value) formRef.value.clearValidate();
|
||||
});
|
||||
}
|
||||
|
||||
async function save () {
|
||||
if (invoice.value === 2) {
|
||||
invoiceForm.value.receiptContent = '商品明细';
|
||||
}
|
||||
|
||||
return await new Promise(resolve => {
|
||||
formRef.value.validate(valid => resolve(valid));
|
||||
});
|
||||
}
|
||||
|
||||
async function submit () {
|
||||
if (!await save()) return;
|
||||
loading.value = true;
|
||||
const raw = JSON.parse(JSON.stringify(invoiceForm.value));
|
||||
if (invoice.value === 1) {
|
||||
delete raw.companyAddress;
|
||||
delete raw.companyPhone;
|
||||
delete raw.bankName;
|
||||
delete raw.bankAccount;
|
||||
delete raw.invoiceAddress;
|
||||
if (type.value === 1) {
|
||||
delete raw.taxpayerId;
|
||||
}
|
||||
} else {
|
||||
delete raw.personalName;
|
||||
}
|
||||
const isCompanyTitle = invoice.value === 2 || type.value === 2;
|
||||
const titleName = isCompanyTitle
|
||||
? (raw.companyName != null ? String(raw.companyName).trim() : '')
|
||||
: (raw.personalName != null ? String(raw.personalName).trim() : '');
|
||||
const submit = {
|
||||
way: route.query.way,
|
||||
...raw,
|
||||
receiptType: invoice.value,
|
||||
receiptTitle: isCompanyTitle ? '单位' : '个人',
|
||||
personalName: isCompanyTitle ? '' : titleName,
|
||||
companyName: isCompanyTitle ? titleName : ''
|
||||
};
|
||||
let receipt = await receiptSelect(submit);
|
||||
if (receipt.success) {
|
||||
emit('change', true);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
defineExpose({ invoiceAvailable });
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
/** 普票 */
|
||||
.inv-type {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -158,7 +304,12 @@ export default {
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
width: 500px;
|
||||
width: 520px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
.inv-title-fixed {
|
||||
line-height: 32px;
|
||||
color: #515a6e;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div id="map-container"></div>
|
||||
|
||||
<div class="search-con">
|
||||
<Input placeholder="输入关键字搜索" id="input-map" v-model="mapSearch" />
|
||||
<el-input placeholder="输入关键字搜索" id="input-map" v-model="mapSearch" />
|
||||
<ul>
|
||||
<li v-for="(tip, index) in tips" :key="index" @click="selectAddr(tip.location)">
|
||||
<p>{{ tip.name }}</p>
|
||||
@@ -13,139 +13,137 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div slot="footer" class="footer">
|
||||
|
||||
<Button type="primary" :loading="loading" @click="ok">确定</Button>
|
||||
<div class="footer">
|
||||
<el-button type="primary" :loading="loading" @click="ok">确定</el-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { Message } from "@/utils/message";
|
||||
import AMapLoader from "@amap/amap-jsapi-loader";
|
||||
import { handleRegion } from "@/api/address.js";
|
||||
import config from "@/config/index";
|
||||
|
||||
const config = require('@/config/index')
|
||||
export default {
|
||||
name: "map",
|
||||
data() {
|
||||
return {
|
||||
config,
|
||||
showMap: false, // 地图显隐
|
||||
mapSearch: "", // 地图搜索
|
||||
map: null, // 初始化地图
|
||||
autoComplete: null, // 初始化搜索方法
|
||||
geocoder: null, // 初始化地理、坐标转化
|
||||
positionPicker: null, // 地图拖拽选点
|
||||
tips: [], //搜索关键字列表
|
||||
addrContent: {}, // 回显地址信息
|
||||
loading: false, // 加载状态
|
||||
defineOptions({ name: "map" });
|
||||
|
||||
const emit = defineEmits(["getAddress"]);
|
||||
|
||||
const showMap = ref(false); // 地图显隐
|
||||
const mapSearch = ref(""); // 地图搜索
|
||||
let map = null; // 初始化地图
|
||||
let autoComplete = null; // 初始化搜索方法
|
||||
let geocoder = null; // 初始化地理、坐标转化
|
||||
let positionPicker = null; // 地图拖拽选点
|
||||
const tips = ref([]); //搜索关键字列表
|
||||
const addrContent = ref({}); // 回显地址信息
|
||||
const loading = ref(false); // 加载状态
|
||||
|
||||
watch(mapSearch, function (val) {
|
||||
searchOfMap(val);
|
||||
});
|
||||
|
||||
function ok() {
|
||||
|
||||
if (addrContent.value && addrContent.value.regeocode) {
|
||||
const params = {
|
||||
cityCode: addrContent.value.regeocode.addressComponent.citycode,
|
||||
townName: addrContent.value.regeocode.addressComponent.township,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
mapSearch: function (val) {
|
||||
this.searchOfMap(val);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
ok() {
|
||||
handleRegion(params).then((res) => {
|
||||
if (res.success) {
|
||||
addrContent.value.addr = res.result.name.replace(/,/g, " ");
|
||||
addrContent.value.addrId = res.result.id;
|
||||
loading.value = false;
|
||||
|
||||
if (this.addrContent && this.addrContent.regeocode) {
|
||||
const params = {
|
||||
cityCode: this.addrContent.regeocode.addressComponent.citycode,
|
||||
townName: this.addrContent.regeocode.addressComponent.township,
|
||||
};
|
||||
handleRegion(params).then((res) => {
|
||||
if (res.success) {
|
||||
this.addrContent.addr = res.result.name.replace(/,/g, " ");
|
||||
this.addrContent.addrId = res.result.id;
|
||||
this.loading = false;
|
||||
|
||||
this.$emit("getAddress", this.addrContent);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.$Message.error('未获取到坐标信息!请查看高德API配置是否正确')
|
||||
emit("getAddress", addrContent.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Message.error('未获取到坐标信息!请查看高德API配置是否正确')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function init() {
|
||||
AMapLoader.load({
|
||||
key: config.aMapKey, // 申请好的Web端开发者Key,首次调用 load 时必填
|
||||
version: "", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
|
||||
plugins: [
|
||||
"AMap.ToolBar",
|
||||
"AMap.Autocomplete",
|
||||
"AMap.PlaceSearch",
|
||||
"AMap.Geolocation",
|
||||
"AMap.Geocoder",
|
||||
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
|
||||
AMapUI: {
|
||||
// 是否加载 AMapUI,缺省不加载
|
||||
version: "1.1", // AMapUI 缺省 1.1
|
||||
plugins: ["misc/PositionPicker"], // 需要加载的 AMapUI ui插件
|
||||
},
|
||||
init() {
|
||||
AMapLoader.load({
|
||||
key: this.config.aMapKey, // 申请好的Web端开发者Key,首次调用 load 时必填
|
||||
version: "", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
|
||||
plugins: [
|
||||
"AMap.ToolBar",
|
||||
"AMap.Autocomplete",
|
||||
"AMap.PlaceSearch",
|
||||
"AMap.Geolocation",
|
||||
"AMap.Geocoder",
|
||||
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
|
||||
AMapUI: {
|
||||
// 是否加载 AMapUI,缺省不加载
|
||||
version: "1.1", // AMapUI 缺省 1.1
|
||||
plugins: ["misc/PositionPicker"], // 需要加载的 AMapUI ui插件
|
||||
},
|
||||
})
|
||||
.then((AMap) => {
|
||||
let that = this;
|
||||
this.map = new AMap.Map("map-container", {
|
||||
zoom: 12,
|
||||
});
|
||||
that.map.addControl(new AMap.ToolBar());
|
||||
that.map.addControl(new AMap.Autocomplete());
|
||||
that.map.addControl(new AMap.PlaceSearch());
|
||||
that.map.addControl(new AMap.Geocoder());
|
||||
|
||||
// 实例化Autocomplete
|
||||
let autoOptions = {
|
||||
city: "全国",
|
||||
};
|
||||
that.autoComplete = new AMap.Autocomplete(autoOptions); // 搜索
|
||||
that.geocoder = new AMap.Geocoder(autoOptions);
|
||||
|
||||
that.positionPicker = new AMapUI.PositionPicker({
|
||||
// 拖拽选点
|
||||
mode: "dragMap",
|
||||
map: that.map,
|
||||
});
|
||||
that.positionPicker.start();
|
||||
/**
|
||||
*
|
||||
* 所有回显数据,都在positionResult里面
|
||||
* 需要字段可以查找
|
||||
*
|
||||
*/
|
||||
that.positionPicker.on("success", function (positionResult) {
|
||||
that.addrContent = positionResult;
|
||||
});
|
||||
})
|
||||
.catch((e) => { });
|
||||
},
|
||||
searchOfMap(val) {
|
||||
// 地图搜索
|
||||
let that = this;
|
||||
this.autoComplete.search(val, function (status, result) {
|
||||
// 搜索成功时,result即是对应的匹配数据
|
||||
if (status == "complete" && result.info == "OK") {
|
||||
that.tips = result.tips;
|
||||
} else {
|
||||
that.tips = [];
|
||||
}
|
||||
})
|
||||
.then((AMap) => {
|
||||
map = new AMap.Map("map-container", {
|
||||
zoom: 12,
|
||||
});
|
||||
},
|
||||
selectAddr(location) {
|
||||
// 选择坐标
|
||||
if (!location) {
|
||||
this.$Message.warning("请选择正确点位");
|
||||
return false;
|
||||
}
|
||||
const lnglat = [location.lng, location.lat];
|
||||
this.positionPicker.start(lnglat);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
};
|
||||
map.addControl(new AMap.ToolBar());
|
||||
map.addControl(new AMap.Autocomplete());
|
||||
map.addControl(new AMap.PlaceSearch());
|
||||
map.addControl(new AMap.Geocoder());
|
||||
|
||||
// 实例化Autocomplete
|
||||
let autoOptions = {
|
||||
city: "全国",
|
||||
};
|
||||
autoComplete = new AMap.Autocomplete(autoOptions); // 搜索
|
||||
geocoder = new AMap.Geocoder(autoOptions);
|
||||
|
||||
positionPicker = new AMapUI.PositionPicker({
|
||||
// 拖拽选点
|
||||
mode: "dragMap",
|
||||
map: map,
|
||||
});
|
||||
positionPicker.start();
|
||||
/**
|
||||
*
|
||||
* 所有回显数据,都在positionResult里面
|
||||
* 需要字段可以查找
|
||||
*
|
||||
*/
|
||||
positionPicker.on("success", function (positionResult) {
|
||||
addrContent.value = positionResult;
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('地图组件加载失败', e);
|
||||
});
|
||||
}
|
||||
|
||||
function searchOfMap(val) {
|
||||
// 地图搜索
|
||||
autoComplete.search(val, function (status, result) {
|
||||
// 搜索成功时,result即是对应的匹配数据
|
||||
if (status == "complete" && result.info == "OK") {
|
||||
tips.value = result.tips;
|
||||
} else {
|
||||
tips.value = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function selectAddr(location) {
|
||||
// 选择坐标
|
||||
if (!location) {
|
||||
Message.warning("请选择正确点位");
|
||||
return false;
|
||||
}
|
||||
const lnglat = [location.lng, location.lat];
|
||||
positionPicker.start(lnglat);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
#map-container {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<Modal width="800" footer-hide v-model="enableMap">
|
||||
<RadioGroup @on-change="changeMap" v-model="mapDefault" type="button">
|
||||
<Radio label="select">级联选择</Radio>
|
||||
<Radio label="map" v-if="aMapSwitch">高德地图</Radio>
|
||||
</RadioGroup>
|
||||
<el-dialog width="800" v-model="enableMap">
|
||||
<el-radio-group v-if="aMapSwitch" @change="changeMap" v-model="mapDefault">
|
||||
<el-radio label="select">地区选择</el-radio>
|
||||
<el-radio label="map">高德地图</el-radio>
|
||||
</el-radio-group>
|
||||
<div>
|
||||
<div v-if="mapDefault === 'select'">
|
||||
<div class="selector">
|
||||
@@ -18,103 +18,98 @@
|
||||
|
||||
</div>
|
||||
<div class="footer">
|
||||
<Button type="primary" @click="finished">确定</Button>
|
||||
<el-button type="primary" @click="finished">确定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<mapping v-if="mapDefault === 'map'" ref="map" @getAddress="getAddress" />
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</Modal>
|
||||
</div></el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { Message } from "@/utils/message";
|
||||
import { aMapSwitch } from '@/config/index'
|
||||
import mapping from "@/components/map/index.vue";
|
||||
import * as API_Setup from "@/api/common.js";
|
||||
export default {
|
||||
components: { mapping },
|
||||
data() {
|
||||
return {
|
||||
aMapSwitch,
|
||||
enableMap: false,
|
||||
mapDefault: "select",
|
||||
data: {
|
||||
province: [], //省
|
||||
city: [], //市
|
||||
area: [], //区
|
||||
street: [], //街道
|
||||
},
|
||||
chiosend: [],
|
||||
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const emit = defineEmits(['callback'])
|
||||
|
||||
this.chiosend = new Array(4).fill("");
|
||||
const enableMap = ref(false)
|
||||
const mapDefault = ref("select")
|
||||
const data = reactive({
|
||||
province: [], //省
|
||||
city: [], //市
|
||||
area: [], //区
|
||||
street: [], //街道
|
||||
})
|
||||
const chiosend = ref([])
|
||||
|
||||
},
|
||||
methods: {
|
||||
open() {
|
||||
this.enableMap = true
|
||||
this.init({ id: 0 }, 'province');
|
||||
},
|
||||
changeMap(val) {
|
||||
this.mapDefault = val
|
||||
onMounted(() => {
|
||||
chiosend.value = new Array(4).fill("");
|
||||
})
|
||||
|
||||
function open() {
|
||||
enableMap.value = true
|
||||
init({ id: 0 }, 'province');
|
||||
}
|
||||
|
||||
},
|
||||
init(val, level = 'province', index) {
|
||||
if (level == 0) {
|
||||
// 说明选择到了街道,将街道id存入数组
|
||||
this.chiosend.splice(3, 1, val);
|
||||
defineExpose({ open })
|
||||
|
||||
function changeMap(val) {
|
||||
mapDefault.value = val
|
||||
}
|
||||
|
||||
function init(val, level = 'province', index) {
|
||||
if (level == 0) {
|
||||
// 说明选择到了街道,将街道id存入数组
|
||||
chiosend.value.splice(3, 1, val);
|
||||
}
|
||||
else {
|
||||
API_Setup.getRegion(val.id).then((res) => {
|
||||
if (res.result.length && val.id !== 0) {
|
||||
chiosend.value[index] = val
|
||||
}
|
||||
else {
|
||||
API_Setup.getRegion(val.id).then((res) => {
|
||||
if (res.result.length && val.id !== 0) {
|
||||
this.chiosend[index] = val
|
||||
}
|
||||
else if(!res.result.length){
|
||||
this.chiosend[index] = val
|
||||
}
|
||||
this.data[level] = res.result;
|
||||
if (level == 'city') {
|
||||
this.data.area = []
|
||||
this.data.street = []
|
||||
this.chiosend.splice(1, 3, "","","");
|
||||
}
|
||||
if (level == 'area') {
|
||||
this.data.street = []
|
||||
this.chiosend.splice(2, 2, "","");
|
||||
}
|
||||
if (level == 'street') {
|
||||
this.chiosend.splice(3, 1, "");
|
||||
}
|
||||
});
|
||||
else if (!res.result.length) {
|
||||
chiosend.value[index] = val
|
||||
}
|
||||
},
|
||||
getAddress(center) {
|
||||
this.$emit('callback', {
|
||||
type: this.mapDefault,
|
||||
data: center
|
||||
})
|
||||
this.enableMap = false;
|
||||
},
|
||||
// 选择完成
|
||||
finished() {
|
||||
if(!this.chiosend[0]){
|
||||
this.$Message.error("请选择地址")
|
||||
return
|
||||
data[level] = res.result;
|
||||
if (level == 'city') {
|
||||
data.area = []
|
||||
data.street = []
|
||||
chiosend.value.splice(1, 3, "", "", "");
|
||||
}
|
||||
const params = this.chiosend.filter((item) => item!=="" && item.value !== "");
|
||||
this.enableMap = false;
|
||||
this.$emit('callback', {
|
||||
type: this.mapDefault,
|
||||
data: params
|
||||
})
|
||||
},
|
||||
},
|
||||
if (level == 'area') {
|
||||
data.street = []
|
||||
chiosend.value.splice(2, 2, "", "");
|
||||
}
|
||||
if (level == 'street') {
|
||||
chiosend.value.splice(3, 1, "");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getAddress(center) {
|
||||
emit('callback', {
|
||||
type: mapDefault.value,
|
||||
data: center
|
||||
})
|
||||
enableMap.value = false;
|
||||
}
|
||||
|
||||
// 选择完成
|
||||
function finished() {
|
||||
if (!chiosend.value[0]) {
|
||||
Message.error("请选择地址")
|
||||
return
|
||||
}
|
||||
const params = chiosend.value.filter((item) => item !== "" && item.value !== "");
|
||||
enableMap.value = false;
|
||||
emit('callback', {
|
||||
type: mapDefault.value,
|
||||
data: params
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,151 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<Cascader
|
||||
<el-cascader
|
||||
:data="data"
|
||||
:load-data="loadData"
|
||||
v-model="addr"
|
||||
@on-change="change"
|
||||
@change="change"
|
||||
style="width: 300px"
|
||||
></Cascader>
|
||||
></el-cascader>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import {getRegion} from '@/api/common.js';
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
data: [], // 地区数据
|
||||
addr: [] // 已选数据
|
||||
};
|
||||
},
|
||||
props: ['addressId'],
|
||||
mounted () {},
|
||||
methods: {
|
||||
change (val, selectedData) { // 选择地区
|
||||
/**
|
||||
* @returns [regionId,region]
|
||||
*/
|
||||
this.$emit('selected', [
|
||||
val,
|
||||
selectedData[selectedData.length - 1].__label.split('/')
|
||||
]);
|
||||
},
|
||||
loadData (item, callback) { // 加载数据
|
||||
item.loading = true;
|
||||
getRegion(item.value).then((res) => {
|
||||
if (res.result.length <= 0) {
|
||||
item.loading = false;
|
||||
} else {
|
||||
res.result.forEach((child) => {
|
||||
item.loading = false;
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { getRegion } from '@/api/common.js';
|
||||
|
||||
let data = {
|
||||
value: child.id,
|
||||
label: child.name,
|
||||
loading: false,
|
||||
children: []
|
||||
};
|
||||
const props = defineProps(['addressId']);
|
||||
const emit = defineEmits(['selected']);
|
||||
|
||||
if (child.level === 'street' || item.label === '香港特别行政区') {
|
||||
item.children.push({
|
||||
value: child.id,
|
||||
label: child.name
|
||||
});
|
||||
} else {
|
||||
item.children.push(data);
|
||||
}
|
||||
});
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
async init () { // 初始化地图数据
|
||||
let data = await getRegion(0);
|
||||
let arr = [];
|
||||
data.result.forEach((item) => {
|
||||
let obj;
|
||||
// 台湾省做处理
|
||||
if (item.name === '台湾省') {
|
||||
obj = {
|
||||
value: item.id,
|
||||
label: item.name
|
||||
};
|
||||
} else {
|
||||
obj = {
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
loading: false,
|
||||
children: []
|
||||
};
|
||||
}
|
||||
arr.push(obj);
|
||||
});
|
||||
this.data = arr;
|
||||
console.warn('init');
|
||||
},
|
||||
async reviewData () {
|
||||
// 数据回显
|
||||
let addr = JSON.parse(JSON.stringify(this.addressId.split(',')));
|
||||
let length = addr.length;
|
||||
let data = await getRegion(0);
|
||||
let arr0 = [];
|
||||
let arr1 = [];
|
||||
let arr2 = [];
|
||||
// 第一级数据
|
||||
data.result.forEach((item) => {
|
||||
let obj;
|
||||
// 台湾省做处理
|
||||
if (item.name === '台湾省') {
|
||||
obj = {
|
||||
value: item.id,
|
||||
label: item.name
|
||||
};
|
||||
} else {
|
||||
obj = {
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
loading: false,
|
||||
children: []
|
||||
};
|
||||
}
|
||||
arr0.push(obj);
|
||||
});
|
||||
// 根据选择的数据来加载数据列表
|
||||
if (length > 0) {
|
||||
let children = await getRegion(addr[0]);
|
||||
children = this.handleData(children.result);
|
||||
arr0.forEach((e) => {
|
||||
if (e.value === addr[0]) {
|
||||
e.children = arr1 = children;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (length > 1) {
|
||||
let children = await getRegion(addr[1]);
|
||||
children = this.handleData(children.result);
|
||||
arr1.forEach((e) => {
|
||||
if (e.value === addr[1]) {
|
||||
e.children = arr2 = children;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (length > 2) {
|
||||
let children = await getRegion(addr[2]);
|
||||
children = this.handleData(children.result);
|
||||
arr2.forEach((e) => {
|
||||
if (e.value === addr[2]) {
|
||||
e.children = children;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.data = arr0;
|
||||
this.addr = addr;
|
||||
},
|
||||
handleData (data) {
|
||||
// 处理接口数据
|
||||
let item = [];
|
||||
data.forEach((child) => {
|
||||
let obj = {
|
||||
const data = ref([]); // 地区数据
|
||||
const addr = ref([]); // 已选数据
|
||||
|
||||
function change (val, selectedData) { // 选择地区
|
||||
/**
|
||||
* @returns [regionId,region]
|
||||
*/
|
||||
emit('selected', [
|
||||
val,
|
||||
selectedData[selectedData.length - 1].__label.split('/')
|
||||
]);
|
||||
}
|
||||
|
||||
function loadData (item, callback) { // 加载数据
|
||||
item.loading = true;
|
||||
getRegion(item.value).then((res) => {
|
||||
if (res.result.length <= 0) {
|
||||
item.loading = false;
|
||||
} else {
|
||||
res.result.forEach((child) => {
|
||||
item.loading = false;
|
||||
|
||||
let data = {
|
||||
value: child.id,
|
||||
label: child.name,
|
||||
loading: false,
|
||||
@@ -153,30 +46,133 @@ export default {
|
||||
};
|
||||
|
||||
if (child.level === 'street' || item.label === '香港特别行政区') {
|
||||
item.push({
|
||||
item.children.push({
|
||||
value: child.id,
|
||||
label: child.name
|
||||
});
|
||||
} else {
|
||||
item.push(obj);
|
||||
item.children.push(data);
|
||||
}
|
||||
});
|
||||
return item;
|
||||
callback();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
addressId: {
|
||||
handler: function (v) {
|
||||
if (v) {
|
||||
this.reviewData();
|
||||
} else {
|
||||
this.init();
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
});
|
||||
}
|
||||
|
||||
async function init () { // 初始化地图数据
|
||||
let res = await getRegion(0);
|
||||
let arr = [];
|
||||
res.result.forEach((item) => {
|
||||
let obj;
|
||||
// 台湾省做处理
|
||||
if (item.name === '台湾省') {
|
||||
obj = {
|
||||
value: item.id,
|
||||
label: item.name
|
||||
};
|
||||
} else {
|
||||
obj = {
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
loading: false,
|
||||
children: []
|
||||
};
|
||||
}
|
||||
arr.push(obj);
|
||||
});
|
||||
data.value = arr;
|
||||
console.warn('init');
|
||||
}
|
||||
|
||||
async function reviewData () {
|
||||
// 数据回显
|
||||
let addrArr = JSON.parse(JSON.stringify(props.addressId.split(',')));
|
||||
let length = addrArr.length;
|
||||
let res = await getRegion(0);
|
||||
let arr0 = [];
|
||||
let arr1 = [];
|
||||
let arr2 = [];
|
||||
// 第一级数据
|
||||
res.result.forEach((item) => {
|
||||
let obj;
|
||||
// 台湾省做处理
|
||||
if (item.name === '台湾省') {
|
||||
obj = {
|
||||
value: item.id,
|
||||
label: item.name
|
||||
};
|
||||
} else {
|
||||
obj = {
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
loading: false,
|
||||
children: []
|
||||
};
|
||||
}
|
||||
arr0.push(obj);
|
||||
});
|
||||
// 根据选择的数据来加载数据列表
|
||||
if (length > 0) {
|
||||
let children = await getRegion(addrArr[0]);
|
||||
children = handleData(children.result);
|
||||
arr0.forEach((e) => {
|
||||
if (e.value === addrArr[0]) {
|
||||
e.children = arr1 = children;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
if (length > 1) {
|
||||
let children = await getRegion(addrArr[1]);
|
||||
children = handleData(children.result);
|
||||
arr1.forEach((e) => {
|
||||
if (e.value === addrArr[1]) {
|
||||
e.children = arr2 = children;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (length > 2) {
|
||||
let children = await getRegion(addrArr[2]);
|
||||
children = handleData(children.result);
|
||||
arr2.forEach((e) => {
|
||||
if (e.value === addrArr[2]) {
|
||||
e.children = children;
|
||||
}
|
||||
});
|
||||
}
|
||||
data.value = arr0;
|
||||
addr.value = addrArr;
|
||||
}
|
||||
|
||||
function handleData (data) {
|
||||
// 处理接口数据
|
||||
let item = [];
|
||||
data.forEach((child) => {
|
||||
let obj = {
|
||||
value: child.id,
|
||||
label: child.name,
|
||||
loading: false,
|
||||
children: []
|
||||
};
|
||||
|
||||
if (child.level === 'street' || item.label === '香港特别行政区') {
|
||||
item.push({
|
||||
value: child.id,
|
||||
label: child.name
|
||||
});
|
||||
} else {
|
||||
item.push(obj);
|
||||
}
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
watch(() => props.addressId, (v) => {
|
||||
if (v) {
|
||||
reviewData();
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getIMDetail } from "@/api/common";
|
||||
import Storage from "@/plugins/storage";
|
||||
import { getMemberMsg } from "@/api/login";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
Storage,
|
||||
IMLink: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
// 跳转im客服
|
||||
async IMService(id, goodsId, skuId) {
|
||||
|
||||
// 获取访问Token
|
||||
let accessToken = Storage.getItem("accessToken");
|
||||
await this.getIMDetailMethods();
|
||||
const userInfo = await getMemberMsg();
|
||||
if (userInfo.success) {
|
||||
//携带商品Id,在IM可以发送商品信息
|
||||
if(goodsId && skuId){
|
||||
window.open(
|
||||
this.IMLink + "?token=" + accessToken + "&id=" + id || this.storeMsg.storeId + "&goodsId=" + goodsId + "&skuId=" + skuId
|
||||
);
|
||||
}else{
|
||||
window.open(
|
||||
this.IMLink + "?token=" + accessToken + "&id=" + id || this.storeMsg.storeId
|
||||
);
|
||||
}
|
||||
|
||||
} else {
|
||||
this.$Message.error("请登录后再联系客服");
|
||||
return;
|
||||
}
|
||||
},
|
||||
// 获取im信息
|
||||
async getIMDetailMethods() {
|
||||
let res = await getIMDetail();
|
||||
if (res.success) {
|
||||
this.IMLink = res.result;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="cate-nav width_1200_auto" :class="{'fixed-show':useClass == 'fixed-show'}">
|
||||
<div class="cate-nav" :class="{'fixed-show':useClass == 'fixed-show'}">
|
||||
<div class="nav-con" :class="{'background-white':useClass == 'background-white'}">
|
||||
<div
|
||||
class="all-categories hover-pointer"
|
||||
@@ -26,32 +26,22 @@
|
||||
:style="{'top':!showNavBar ?'60px':'46px'}"
|
||||
v-show="showAlways || showFirstList"
|
||||
@mouseenter="showFirstList = true"
|
||||
@mouseleave="showFirstList = false"
|
||||
@mouseleave="closePanel"
|
||||
>
|
||||
<!-- 第一级分类 -->
|
||||
<div
|
||||
class="nav-side"
|
||||
:class="{ 'large-nav': large, 'opacity-nav': opacity }"
|
||||
@mouseleave="panel = false"
|
||||
>
|
||||
<ul>
|
||||
<li
|
||||
v-for="(item, index) in cateList"
|
||||
:key="index"
|
||||
:class="{ 'is-active': panel && panelIndex === index }"
|
||||
@mouseenter="showDetail(index)"
|
||||
>
|
||||
<span class="nav-side-item" @click="goGoodsList(item.id)">{{
|
||||
item.name
|
||||
}}</span>
|
||||
<span v-for="(second, secIndex) in item.children" :key="secIndex">
|
||||
<span v-if="secIndex < 2"> / </span>
|
||||
<span
|
||||
@click="goGoodsList(second.id, second.parentId)"
|
||||
class="nav-side-item"
|
||||
v-if="secIndex < 2"
|
||||
>{{ second.name }}</span
|
||||
>
|
||||
</span>
|
||||
<span class="nav-side-item" @click="goGoodsList(item.id)">{{ item.name }}</span>
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -69,8 +59,7 @@
|
||||
@click="goGoodsList(item.id,item.parentId)"
|
||||
v-if="index < 8"
|
||||
:key="index"
|
||||
>{{ item.name }}<Icon type="ios-arrow-forward"
|
||||
/></span>
|
||||
>{{ item.name }}<el-icon><ArrowRight /></el-icon></span>
|
||||
</template>
|
||||
</div>
|
||||
<ul>
|
||||
@@ -83,7 +72,7 @@
|
||||
class="detail-item-title"
|
||||
@click="goGoodsList(items.id, items.parentId)"
|
||||
>
|
||||
{{ items.name }} <Icon type="ios-arrow-forward" />
|
||||
{{ items.name }} <el-icon><ArrowRight /></el-icon>
|
||||
<span class="glyphicon glyphicon-menu-right"></span>
|
||||
</span>
|
||||
<div>
|
||||
@@ -92,8 +81,7 @@
|
||||
@click="goGoodsList(item.id, items.id, items.parentId)"
|
||||
:key="subIndex"
|
||||
class="detail-item"
|
||||
>{{ item.name }}</span
|
||||
>
|
||||
>{{ item.name }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -102,129 +90,143 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getCategory } from "@/api/goods";
|
||||
import storage from "@/plugins/storage.js";
|
||||
export default {
|
||||
name: "GoodsListNav",
|
||||
props: {
|
||||
showAlways: {
|
||||
// 总是显示下拉分类
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
showNavBar: {
|
||||
// 显示全部商品分类右侧导航条
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
hover: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
large: {
|
||||
// 是否更高的高度
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
opacity: {
|
||||
// 是否背景透明
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
useClass:{
|
||||
type:null,
|
||||
default:''
|
||||
import { ArrowRight } from "@element-plus/icons-vue";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
defineOptions({ name: "GoodsListNav" });
|
||||
|
||||
const props = defineProps({
|
||||
showAlways: {
|
||||
// 总是显示下拉分类
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
showNavBar: {
|
||||
// 显示全部商品分类右侧导航条
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
hover: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
large: {
|
||||
// 是否更高的高度
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
opacity: {
|
||||
// 是否背景透明
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
useClass: {
|
||||
type: null,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const panel = ref(false); // 二级分类展示
|
||||
const panelIndex = ref(-1);
|
||||
const panelData = ref([]); // 二级分类数据
|
||||
const showFirstList = ref(false); // 始终展示一级列表
|
||||
const cateList = ref([]); // 商品分类
|
||||
|
||||
// 导航列表
|
||||
const navList = computed(() => {
|
||||
if (storage.getItem("navList")) {
|
||||
return JSON.parse(storage.getItem("navList"));
|
||||
}
|
||||
return { list: [] };
|
||||
});
|
||||
|
||||
function showFirstLists() {
|
||||
showFirstList.value = true;
|
||||
if (
|
||||
localStorage.getItem("category") &&
|
||||
localStorage.getItem("category_expiration_time")
|
||||
) {
|
||||
// this.getCate();
|
||||
// 如果缓存过期,则获取最新的信息
|
||||
if (new Date() > localStorage.getItem("category_expiration_time")) {
|
||||
getCate();
|
||||
return;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
panel: false, // 二级分类展示
|
||||
panelData: [], // 二级分类数据
|
||||
showFirstList: false, // 始终展示一级列表
|
||||
cateList: [], // 商品分类
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
navList() {
|
||||
// 导航列表
|
||||
if (storage.getItem("navList")) {
|
||||
return JSON.parse(storage.getItem("navList"));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showFirstLists(){
|
||||
this.showFirstList = true;
|
||||
if(
|
||||
localStorage.getItem("category") &&
|
||||
localStorage.getItem("category_expiration_time")
|
||||
) {
|
||||
// this.getCate();
|
||||
// 如果缓存过期,则获取最新的信息
|
||||
if (new Date() > localStorage.getItem("category_expiration_time")) {
|
||||
this.getCate();
|
||||
return;
|
||||
}
|
||||
this.cateList = JSON.parse(localStorage.getItem("category"));
|
||||
// this.$Message.info(cateList)
|
||||
}
|
||||
},
|
||||
getCate() {
|
||||
// 获取分类数据
|
||||
if (this.hover) return false;
|
||||
getCategory(0).then((res) => {
|
||||
if (res.success) {
|
||||
this.cateList = res.result;
|
||||
this.$store.commit("SET_CATEGORY", res.result);
|
||||
// 过期时间
|
||||
var expirationTime = new Date().setHours(new Date().getHours() + 1);
|
||||
// 存放过期时间
|
||||
localStorage.setItem("category_expiration_time", expirationTime);
|
||||
// 存放分类信息
|
||||
localStorage.setItem("category", JSON.stringify(res.result));
|
||||
}
|
||||
});
|
||||
},
|
||||
showDetail(index) {
|
||||
// 展示全部分类
|
||||
this.panel = true;
|
||||
this.panelData = this.cateList[index].children;
|
||||
},
|
||||
goGoodsList(id, secondId, firstId) {
|
||||
// 分类共有三级,传全部分类过去
|
||||
const arr = [firstId, secondId, id];
|
||||
if (!arr[1]) {
|
||||
arr.splice(0, 2);
|
||||
}
|
||||
if (!arr[0]) {
|
||||
arr.shift();
|
||||
}
|
||||
let routerUrl = this.$router.resolve({
|
||||
path: "/goodsList",
|
||||
query: { categoryId: arr.toString() },
|
||||
});
|
||||
window.open(routerUrl.href, "_blank");
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (
|
||||
localStorage.getItem("category") &&
|
||||
localStorage.getItem("category_expiration_time")
|
||||
) {
|
||||
// 如果缓存过期,则获取最新的信息
|
||||
if (new Date() > localStorage.getItem("category_expiration_time")) {
|
||||
this.getCate();
|
||||
return;
|
||||
}
|
||||
this.cateList = JSON.parse(localStorage.getItem("category"));
|
||||
} else {
|
||||
this.getCate();
|
||||
cateList.value = JSON.parse(localStorage.getItem("category"));
|
||||
// Message.info(cateList)
|
||||
}
|
||||
}
|
||||
|
||||
function getCate() {
|
||||
// 获取分类数据
|
||||
if (props.hover) return false;
|
||||
getCategory(0).then((res) => {
|
||||
if (res.success) {
|
||||
cateList.value = res.result;
|
||||
appStore.setCategory(res.result);
|
||||
// 过期时间
|
||||
var expirationTime = new Date().setHours(new Date().getHours() + 1);
|
||||
// 存放过期时间
|
||||
localStorage.setItem("category_expiration_time", expirationTime);
|
||||
// 存放分类信息
|
||||
localStorage.setItem("category", JSON.stringify(res.result));
|
||||
}
|
||||
},
|
||||
};
|
||||
}).catch((err) => {
|
||||
console.warn('获取分类失败', err);
|
||||
});
|
||||
}
|
||||
|
||||
function showDetail(index) {
|
||||
// 展示全部分类
|
||||
panel.value = true;
|
||||
panelIndex.value = index;
|
||||
panelData.value = cateList.value[index].children;
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
showFirstList.value = false;
|
||||
panel.value = false;
|
||||
panelIndex.value = -1;
|
||||
}
|
||||
|
||||
function goGoodsList(id, secondId, firstId) {
|
||||
// 分类共有三级,传全部分类过去
|
||||
const arr = [firstId, secondId, id];
|
||||
if (!arr[1]) {
|
||||
arr.splice(0, 2);
|
||||
}
|
||||
if (!arr[0]) {
|
||||
arr.shift();
|
||||
}
|
||||
router.push({
|
||||
path: "/goodsList",
|
||||
query: { categoryId: arr.toString() },
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (
|
||||
localStorage.getItem("category") &&
|
||||
localStorage.getItem("category_expiration_time")
|
||||
) {
|
||||
// 如果缓存过期,则获取最新的信息
|
||||
if (new Date() > localStorage.getItem("category_expiration_time")) {
|
||||
getCate();
|
||||
return;
|
||||
}
|
||||
cateList.value = JSON.parse(localStorage.getItem("category"));
|
||||
} else {
|
||||
getCate();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -243,8 +245,10 @@ export default {
|
||||
}
|
||||
.fixed-show{
|
||||
margin-top: 0 !important;
|
||||
width: 100%;
|
||||
>.nav-con{
|
||||
>.all-categories{
|
||||
width: 100%;
|
||||
align-items: center !important;
|
||||
height: 60px;
|
||||
display: inherit;
|
||||
@@ -262,43 +266,50 @@ export default {
|
||||
}
|
||||
.cate-nav {
|
||||
position: relative;
|
||||
margin: 14px auto 0 auto;
|
||||
z-index: 5;
|
||||
width: 100%;
|
||||
margin: 14px 0 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/** 商品分类 */
|
||||
.nav-con {
|
||||
height: 46px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
.all-categories {
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
padding-left: 37.4px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 46px;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
padding-left: 16px;
|
||||
background: #FFFFFF;
|
||||
width: 263.2px;
|
||||
padding-top: 15.4px;
|
||||
line-height: 20px;
|
||||
border-bottom: none;
|
||||
font-size: 16.8px;
|
||||
font-weight: normal;
|
||||
color: #333333;
|
||||
letter-spacing: 0px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #171A26;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.nav-item {
|
||||
width: 914px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 46px;
|
||||
border-radius: 10px;
|
||||
background: #FFFFFF;
|
||||
margin-left: 10px;
|
||||
margin: 0 0 0 10px;
|
||||
padding: 0;
|
||||
line-height: 46px;
|
||||
overflow: hidden;
|
||||
list-style: none;
|
||||
// background-color: #eee;
|
||||
display: flex;
|
||||
li {
|
||||
float: left;
|
||||
font-size: 17px;
|
||||
font-weight: normal;
|
||||
color: #333333;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: #171A26;
|
||||
|
||||
|
||||
&:hover {
|
||||
@@ -307,24 +318,23 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 分类列表
|
||||
// 分类列表(浮层叠在轮播左列之上)
|
||||
.cate-list {
|
||||
margin: 0 auto;
|
||||
position: absolute;
|
||||
|
||||
left: 0;
|
||||
top: 46px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.nav-side {
|
||||
width: 263.2px;
|
||||
color: #666;
|
||||
color: #171A26;
|
||||
float: left;
|
||||
overflow:hidden;
|
||||
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
overflow: hidden;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: #fff;
|
||||
height: 340px;
|
||||
height: 344px;
|
||||
////自动移滚动条样式
|
||||
//&::-webkit-scrollbar {
|
||||
// width: 1px;
|
||||
@@ -341,21 +351,53 @@ export default {
|
||||
}
|
||||
.large-nav {
|
||||
height: 470px;
|
||||
ul > li {
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
.opacity-nav {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.nav-side ul {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.nav-side li {
|
||||
padding: 0 0 16.2px 37.4px;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
padding: 0 12px 0 16px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
.el-icon {
|
||||
flex-shrink: 0;
|
||||
color: #ccc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.is-active {
|
||||
background: #fff5f4;
|
||||
color: $theme_color;
|
||||
|
||||
.el-icon {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
.nav-side-item {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nav-side-item:hover {
|
||||
cursor: pointer;
|
||||
@@ -364,63 +406,102 @@ export default {
|
||||
|
||||
/*显示商品详细信息*/
|
||||
.detail-item-panel {
|
||||
width: 1000px;
|
||||
width: 936px;
|
||||
min-height: 340px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px 0px 15px #ccc;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
border-radius: 0 4px 4px 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 200px;
|
||||
left: 263.2px;
|
||||
z-index: 1000;
|
||||
padding: 15px;
|
||||
padding: 8px 20px 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.nav-detail-item {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 20px;
|
||||
margin: 8px 0 4px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
color: #eee;
|
||||
}
|
||||
.nav-detail-item span {
|
||||
padding: 6px;
|
||||
padding-left: 12px;
|
||||
margin-right: 15px;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #171A26;
|
||||
|
||||
.el-icon {
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
.nav-detail-item span:hover {
|
||||
background-color: $theme_color;
|
||||
color: #fff;
|
||||
color: $theme_color;
|
||||
|
||||
.el-icon {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
.detail-item-panel li {
|
||||
line-height: 30px;
|
||||
line-height: 28px;
|
||||
}
|
||||
.detail-item-title {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
width: 108px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 28px;
|
||||
cursor: pointer;
|
||||
color: #555555;
|
||||
padding-right: 10px;
|
||||
width: 81px;
|
||||
text-align: right;
|
||||
color: #171A26;
|
||||
text-align: left;
|
||||
|
||||
.glyphicon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
.detail-item-title:hover {
|
||||
color: $theme_color;
|
||||
|
||||
.el-icon {
|
||||
color: $theme_color;
|
||||
}
|
||||
}
|
||||
.detail-item-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 6px 0;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
.detail-item {
|
||||
font-size: 12px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 28px;
|
||||
padding: 0 12px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid #ccc;
|
||||
border: none;
|
||||
|
||||
&:first-child {
|
||||
border: none;
|
||||
padding-left: 0;
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
.detail-item:hover {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
163
buyer/src/components/userCenter/Layout.vue
Normal file
163
buyer/src/components/userCenter/Layout.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="user-center-page">
|
||||
<div v-if="title" class="user-center-title">{{ title }}</div>
|
||||
<div class="user-center-panel">
|
||||
<div v-if="showToolbar" class="user-center-toolbar" :class="{ 'is-actions-only': !tabs.length }">
|
||||
<div class="user-center-tabs">
|
||||
<div
|
||||
v-for="(item, index) in tabs"
|
||||
:key="index"
|
||||
class="user-center-tab"
|
||||
:class="{ active: activeTab === index }"
|
||||
@click="$emit('tab-change', index)"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-center-extra">
|
||||
<slot name="extra">
|
||||
<el-button v-if="moreText" type="primary" @click="handleMore">{{ moreText }}</el-button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-center-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, useSlots } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
defineOptions({ name: "UserCenterLayout" });
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
activeTab: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
moreText: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
moreTo: {
|
||||
type: [String, Object],
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["tab-change", "more"]);
|
||||
|
||||
const router = useRouter();
|
||||
const slots = useSlots();
|
||||
|
||||
const showToolbar = computed(() => {
|
||||
return props.tabs.length || props.moreText || slots.extra;
|
||||
});
|
||||
|
||||
function handleMore() {
|
||||
emit("more");
|
||||
if (!props.moreTo) return;
|
||||
if (typeof props.moreTo === "string") {
|
||||
router.push(props.moreTo);
|
||||
} else {
|
||||
router.push(props.moreTo);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-center-page {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.user-center-title {
|
||||
height: 54px;
|
||||
line-height: 54px;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-center-panel {
|
||||
padding: 0 16px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.user-center-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 54px;
|
||||
border-bottom: 1px solid #eee;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.user-center-toolbar.is-actions-only {
|
||||
justify-content: flex-start;
|
||||
|
||||
.user-center-tabs {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.user-center-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 54px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-center-tab {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
height: 54px;
|
||||
line-height: 54px;
|
||||
margin-right: 28px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
color: $theme_color;
|
||||
}
|
||||
|
||||
&.active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 2px;
|
||||
background: $theme_color;
|
||||
}
|
||||
}
|
||||
|
||||
.user-center-extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-center-content {
|
||||
padding-top: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,122 +3,123 @@
|
||||
<div class="imgBox" :style="{width:data.originalWidth+'px',height:data.originalHeight + 'px'}">
|
||||
<img :src="data.backImage" style="width:100%;height:100%" alt="">
|
||||
<img class="slider" :src="data.slidingImage" :style="{left:distance+'px',top:data.randomY+'px'}" :width="data.sliderWidth" :height="data.sliderHeight" alt="">
|
||||
<Icon type="md-refresh" class="refresh" @click="init" />
|
||||
<el-icon class="refresh" @click="init"><Refresh /></el-icon>
|
||||
</div>
|
||||
<div class="handle" :style="{width:data.originalWidth+'px'}">
|
||||
<span class="bgcolor" :style="{width:distance + 'px',background:bgColor}"></span>
|
||||
<span class="swiper" :style="{left:distance + 'px'}" @mousedown="mouseDown">
|
||||
<Icon type="md-arrow-round-forward" />
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</span>
|
||||
<span class="text">{{verifyText}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { Message } from "@/utils/message";
|
||||
import { Refresh, ArrowRight } from '@element-plus/icons-vue';
|
||||
import { getVerifyImg, postVerifyImg } from './verify.js';
|
||||
export default {
|
||||
props: {
|
||||
// 传入数据,判断是登录、注册、修改密码
|
||||
verifyType: {
|
||||
defalut: 'LOGIN',
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
show: false, // 验证码显隐
|
||||
type: 'LOGIN', // 请求类型
|
||||
data: { // 验证码数据
|
||||
backImage: '',
|
||||
slidingImage: '',
|
||||
originalHeight: 150,
|
||||
originalWidth: 300,
|
||||
sliderWidth: 60,
|
||||
sliderHeight: 60
|
||||
},
|
||||
distance: 0, // 拼图移动距离
|
||||
flag: false, // 判断滑块是否按下
|
||||
downX: 0, // 鼠标按下位置
|
||||
bgColor: '#04ad11', // 滑动背景颜色
|
||||
verifyText: '拖动滑块解锁' // 文字提示
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
// 鼠标按下事件,开始拖动滑块
|
||||
mouseDown (e) {
|
||||
this.downX = e.clientX;
|
||||
this.flag = true;
|
||||
},
|
||||
// 鼠标移动事件,计算距离
|
||||
mouseMove (e) {
|
||||
if (this.flag) {
|
||||
let offset = e.clientX - this.downX;
|
||||
|
||||
if (offset > this.data.originalWidth - 43) {
|
||||
this.distance = this.data.originalWidth - 43;
|
||||
} else if (offset < 0) {
|
||||
this.distance = 0;
|
||||
} else {
|
||||
this.distance = offset;
|
||||
}
|
||||
}
|
||||
},
|
||||
// 鼠标抬起事件,验证是否正确
|
||||
mouseUp () {
|
||||
if (!this.flag) return false;
|
||||
this.flag = false;
|
||||
let params = {
|
||||
verificationEnums: this.type,
|
||||
xPos: this.distance
|
||||
};
|
||||
postVerifyImg(params).then(res => {
|
||||
if (res.success) {
|
||||
if (res.result) {
|
||||
this.bgColor = 'green';
|
||||
this.verifyText = '解锁成功';
|
||||
this.$emit('change', { status: true, distance: this.distance });
|
||||
} else {
|
||||
this.bgColor = 'red';
|
||||
this.verifyText = '解锁失败';
|
||||
let that = this;
|
||||
setTimeout(() => {
|
||||
that.init();
|
||||
}, 1000);
|
||||
this.$emit('change', { status: false, distance: this.distance });
|
||||
}
|
||||
} else {
|
||||
this.init()
|
||||
}
|
||||
|
||||
}).catch(()=>{
|
||||
this.init()
|
||||
});
|
||||
},
|
||||
init () { // 初始化数据
|
||||
this.flag = false;
|
||||
this.downX = 0;
|
||||
this.distance = 0;
|
||||
this.bgColor = '#04ad11';
|
||||
this.verifyText = '拖动滑块解锁';
|
||||
getVerifyImg(this.type).then(res => {
|
||||
if (res.result) {
|
||||
this.data = res.result;
|
||||
this.show = true;
|
||||
} else {
|
||||
this.$Message.warning('请求失败请重试!')
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
verifyType: {
|
||||
immediate: true,
|
||||
handler: function (v) {
|
||||
this.type = v;
|
||||
}
|
||||
const props = defineProps({
|
||||
// 传入数据,判断是登录、注册、修改密码
|
||||
verifyType: {
|
||||
defalut: 'LOGIN',
|
||||
type: String
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const show = ref(false); // 验证码显隐
|
||||
const type = ref('LOGIN'); // 请求类型
|
||||
const data = ref({ // 验证码数据
|
||||
backImage: '',
|
||||
slidingImage: '',
|
||||
originalHeight: 150,
|
||||
originalWidth: 300,
|
||||
sliderWidth: 60,
|
||||
sliderHeight: 60
|
||||
});
|
||||
const distance = ref(0); // 拼图移动距离
|
||||
const flag = ref(false); // 判断滑块是否按下
|
||||
const downX = ref(0); // 鼠标按下位置
|
||||
const bgColor = ref('#04ad11'); // 滑动背景颜色
|
||||
const verifyText = ref('拖动滑块解锁'); // 文字提示
|
||||
|
||||
// 鼠标按下事件,开始拖动滑块
|
||||
function mouseDown (e) {
|
||||
downX.value = e.clientX;
|
||||
flag.value = true;
|
||||
}
|
||||
// 鼠标移动事件,计算距离
|
||||
function mouseMove (e) {
|
||||
if (flag.value) {
|
||||
let offset = e.clientX - downX.value;
|
||||
|
||||
if (offset > data.value.originalWidth - 43) {
|
||||
distance.value = data.value.originalWidth - 43;
|
||||
} else if (offset < 0) {
|
||||
distance.value = 0;
|
||||
} else {
|
||||
distance.value = offset;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
// 鼠标抬起事件,验证是否正确
|
||||
function mouseUp () {
|
||||
if (!flag.value) return false;
|
||||
flag.value = false;
|
||||
let params = {
|
||||
verificationEnums: type.value,
|
||||
xPos: distance.value
|
||||
};
|
||||
postVerifyImg(params).then(res => {
|
||||
if (res.success) {
|
||||
if (res.result) {
|
||||
bgColor.value = 'green';
|
||||
verifyText.value = '解锁成功';
|
||||
emit('change', { status: true, distance: distance.value });
|
||||
} else {
|
||||
bgColor.value = 'red';
|
||||
verifyText.value = '解锁失败';
|
||||
setTimeout(() => {
|
||||
init();
|
||||
}, 1000);
|
||||
emit('change', { status: false, distance: distance.value });
|
||||
}
|
||||
} else {
|
||||
init()
|
||||
}
|
||||
|
||||
}).catch(()=>{
|
||||
init()
|
||||
});
|
||||
}
|
||||
function close () {
|
||||
show.value = false;
|
||||
}
|
||||
function init () { // 初始化数据
|
||||
flag.value = false;
|
||||
downX.value = 0;
|
||||
distance.value = 0;
|
||||
bgColor.value = '#04ad11';
|
||||
verifyText.value = '拖动滑块解锁';
|
||||
getVerifyImg(type.value).then(res => {
|
||||
if (res.result) {
|
||||
data.value = res.result;
|
||||
show.value = true;
|
||||
} else {
|
||||
Message.warning('请求失败请重试!')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 父组件通过 ref 调用 init/close
|
||||
defineExpose({ init, close });
|
||||
|
||||
watch(() => props.verifyType, (v) => {
|
||||
type.value = v;
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.verify-content{
|
||||
@@ -174,7 +175,7 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.ivu-icon {
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import request, {Method, commonUrl} from '@/plugins/request.js';
|
||||
import request, {Method, buyerUrl} from '@/plugins/request.js';
|
||||
import storage from '@/plugins/storage.js';
|
||||
|
||||
/**
|
||||
@@ -7,7 +7,7 @@ import storage from '@/plugins/storage.js';
|
||||
*/
|
||||
export function getVerifyImg (verificationEnums) {
|
||||
return request({
|
||||
url: `${commonUrl}/common/common/slider/${verificationEnums}`,
|
||||
url: `${buyerUrl}/buyer/common/slider/${verificationEnums}`,
|
||||
method: Method.GET,
|
||||
needToken: false,
|
||||
headers: {uuid: storage.getItem('uuid')}
|
||||
@@ -19,7 +19,7 @@ export function getVerifyImg (verificationEnums) {
|
||||
*/
|
||||
export function postVerifyImg (params) {
|
||||
return request({
|
||||
url: `${commonUrl}/common/common/slider/${params.verificationEnums}`,
|
||||
url: `${buyerUrl}/buyer/common/slider/${params.verificationEnums}`,
|
||||
method: Method.POST,
|
||||
needToken: false,
|
||||
params,
|
||||
|
||||
@@ -19,5 +19,9 @@ module.exports = {
|
||||
aMapSwitch:false, //是否开启高德定位
|
||||
enableCDN: true, //生产环境 是否启用cdn加载 vue等js
|
||||
port: 10000, //端口
|
||||
inputMaxLength:'140' //全局输入框默认最大输入长度字
|
||||
inputMaxLength:'140', //全局输入框默认最大输入长度字
|
||||
|
||||
|
||||
PC_DOMAIN: "https://pc-b2b2c.pickmall.cn", // PC端域名
|
||||
WAP_DOMAIN: "https://m-b2b2c.pickmall.cn", // WAP端域名
|
||||
};
|
||||
|
||||
208
buyer/src/constants/goodsType.js
Normal file
208
buyer/src/constants/goodsType.js
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 卡密商品(E_COUPON / 电子卡券)— 买家端公共常量与工具
|
||||
*
|
||||
* 与 VIRTUAL_GOODS(核销型虚拟)区分:E_COUPON 常规促销走 BUY_NOW,
|
||||
* 特殊活动走 PINTUAN / POINTS / KANJIA;禁止 CART / VIRTUAL。
|
||||
* 需求:lilishop/docs/requirements/card-key-goods-v4.md · M-02
|
||||
*
|
||||
* @author Mike
|
||||
* @date 2026-07-31
|
||||
*/
|
||||
export const E_COUPON_GOODS_TYPE = "E_COUPON";
|
||||
export const E_COUPON_ORDER_TYPE = "E_COUPON";
|
||||
|
||||
/** 单次购买数量上限(O-09 默认:min(quantity, 999)) */
|
||||
export const E_COUPON_MAX_BUY_NUM = 999;
|
||||
|
||||
/** 常规立即购买(秒杀/满减/券/限时/第N件等) */
|
||||
export const E_COUPON_CART_BUY_NOW = "BUY_NOW";
|
||||
export const E_COUPON_CART_PINTUAN = "PINTUAN";
|
||||
export const E_COUPON_CART_POINTS = "POINTS";
|
||||
export const E_COUPON_CART_KANJIA = "KANJIA";
|
||||
|
||||
export function isECoupon(goodsType) {
|
||||
return goodsType === E_COUPON_GOODS_TYPE;
|
||||
}
|
||||
|
||||
export function isECouponOrder(orderType) {
|
||||
return orderType === E_COUPON_ORDER_TYPE;
|
||||
}
|
||||
|
||||
/** 详情页提示:卡密交付后不支持买家售后(FR-B-04) */
|
||||
export const E_COUPON_AFTER_SALE_HINT =
|
||||
"卡密交付后不支持买家售后,请确认商品信息后再购买。";
|
||||
|
||||
export function eCouponAfterSaleHint() {
|
||||
return E_COUPON_AFTER_SALE_HINT;
|
||||
}
|
||||
|
||||
/** 可售库存:E_COUPON 读 SKU quantity(由卡池 syncSkuStock 同步) */
|
||||
export function getECouponStock(sku) {
|
||||
if (!sku) return 0;
|
||||
return Number(sku.quantity) || 0;
|
||||
}
|
||||
|
||||
export function getECouponMaxBuyNum(sku) {
|
||||
const stock = getECouponStock(sku);
|
||||
if (stock <= 0) return 1;
|
||||
return Math.min(stock, E_COUPON_MAX_BUY_NUM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 E_COUPON 结算 cartType / way
|
||||
* @param {object} opts
|
||||
* @param {string} [opts.routeWay] 路由 query.way
|
||||
* @param {'BUY_NOW'|'PINTUAN'|'POINTS'|'KANJIA'} [opts.action] 用户点击的购买入口
|
||||
*/
|
||||
export function resolveECouponCartType({ routeWay, action } = {}) {
|
||||
if (action === E_COUPON_CART_PINTUAN) return E_COUPON_CART_PINTUAN;
|
||||
if (action === E_COUPON_CART_POINTS) return E_COUPON_CART_POINTS;
|
||||
if (action === E_COUPON_CART_KANJIA) return E_COUPON_CART_KANJIA;
|
||||
if (action === E_COUPON_CART_BUY_NOW) return E_COUPON_CART_BUY_NOW;
|
||||
const way = routeWay ? String(routeWay).toUpperCase() : "";
|
||||
if (way === "KANJIA") return E_COUPON_CART_KANJIA;
|
||||
if (way === "PINTUAN") return E_COUPON_CART_PINTUAN;
|
||||
if (way === "POINT" || way === "POINTS") return E_COUPON_CART_POINTS;
|
||||
return E_COUPON_CART_BUY_NOW;
|
||||
}
|
||||
|
||||
/** 详情页 promotionMap 初始结构(切换 SKU 时完整重置) */
|
||||
export function createEmptyPromotionMap() {
|
||||
return {
|
||||
SECKILL: null,
|
||||
FULL_DISCOUNT: null,
|
||||
COUPON: [],
|
||||
DISCOUNT: null,
|
||||
NTH: null,
|
||||
PINTUAN: null,
|
||||
KANJIA: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 卡密履约状态(与后端 CardKeyFulfillStatusEnum 一致) */
|
||||
export const CARD_KEY_FULFILL_STATUS = {
|
||||
PENDING: "PENDING",
|
||||
DELIVERED: "DELIVERED",
|
||||
FAILED: "FAILED",
|
||||
NOT_APPLICABLE: "NOT_APPLICABLE",
|
||||
};
|
||||
|
||||
export const CARD_KEY_FULFILL_STATUS_TEXT = {
|
||||
PENDING: "待发放",
|
||||
DELIVERED: "已发放",
|
||||
FAILED: "发放失败",
|
||||
NOT_APPLICABLE: "不适用",
|
||||
};
|
||||
|
||||
export function cardKeyFulfillAlertType(status) {
|
||||
const map = {
|
||||
PENDING: "info",
|
||||
DELIVERED: "success",
|
||||
FAILED: "warning",
|
||||
};
|
||||
return map[status] || "info";
|
||||
}
|
||||
|
||||
export function formatCardKeyFulfillStatus(status) {
|
||||
return CARD_KEY_FULFILL_STATUS_TEXT[status] || status || "—";
|
||||
}
|
||||
|
||||
export function resolveCardKeyFulfillMessage(line) {
|
||||
if (line && line.message) return line.message;
|
||||
return formatCardKeyFulfillStatus(line?.status);
|
||||
}
|
||||
|
||||
export function isCardKeyFulfillApplicable(item) {
|
||||
if (!item) return false;
|
||||
const status = item.cardKeyFulfillStatus;
|
||||
if (status) return status !== CARD_KEY_FULFILL_STATUS.NOT_APPLICABLE;
|
||||
if (item.cardKeyDelivered != null || (item.cardKeys && item.cardKeys.length)) return true;
|
||||
return item.goodsType === E_COUPON_GOODS_TYPE;
|
||||
}
|
||||
|
||||
function normalizeCardKeyFulfillLine(source) {
|
||||
const status =
|
||||
source.cardKeyFulfillStatus ||
|
||||
(source.cardKeyDelivered
|
||||
? CARD_KEY_FULFILL_STATUS.DELIVERED
|
||||
: CARD_KEY_FULFILL_STATUS.PENDING);
|
||||
const cardKeys =
|
||||
status === CARD_KEY_FULFILL_STATUS.DELIVERED && source.cardKeyDelivered
|
||||
? source.cardKeys || []
|
||||
: [];
|
||||
return {
|
||||
key: source.sn || source.orderSn || source.id || source.skuId,
|
||||
goodsName: source.goodsName,
|
||||
status,
|
||||
message: source.cardKeyFulfillMessage,
|
||||
cardKeys,
|
||||
isGift: !!source.isGift,
|
||||
};
|
||||
}
|
||||
|
||||
/** 汇总当前订单 orderItems 中的卡密履约行(不含满赠子单;赠品卡密在赠品子单详情单独查看) */
|
||||
export function collectCardKeyFulfillLines(orderItems = []) {
|
||||
const lines = [];
|
||||
(orderItems || []).forEach((item) => {
|
||||
if (!isCardKeyFulfillApplicable(item)) return;
|
||||
lines.push(normalizeCardKeyFulfillLine(item));
|
||||
});
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function orderHasCardKeySection({ orderItems, isECouponOrder } = {}) {
|
||||
if (isECouponOrder) return true;
|
||||
if (collectCardKeyFulfillLines(orderItems).length > 0) return true;
|
||||
return (orderItems || []).some(
|
||||
(item) =>
|
||||
item.goodsType === E_COUPON_GOODS_TYPE ||
|
||||
(Array.isArray(item.cardKeys) && item.cardKeys.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
export function flattenCardKeyFulfillLines(lines, withGoodsName = false) {
|
||||
const rows = [];
|
||||
(lines || []).forEach((line) => {
|
||||
if (line.status !== CARD_KEY_FULFILL_STATUS.DELIVERED) return;
|
||||
(line.cardKeys || []).forEach((ck) => {
|
||||
rows.push(withGoodsName ? { ...ck, goodsName: line.goodsName } : { ...ck });
|
||||
});
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 当前订单是否含已交付卡密(仅 orderItems,不含满赠子单) */
|
||||
export function orderHasCardKeyContent(orderItems) {
|
||||
return flattenCardKeyFulfillLines(collectCardKeyFulfillLines(orderItems)).length > 0;
|
||||
}
|
||||
|
||||
/** 扁平化当前订单 orderItems 卡密 */
|
||||
export function flattenOrderCardKeys(orderItems, withGoodsName = false) {
|
||||
const lines = collectCardKeyFulfillLines(orderItems);
|
||||
if (lines.some((line) => line.status)) {
|
||||
return flattenCardKeyFulfillLines(lines, withGoodsName);
|
||||
}
|
||||
const rows = [];
|
||||
(orderItems || []).forEach((item) => {
|
||||
if (!item.cardKeyDelivered || !item.cardKeys || !item.cardKeys.length) return;
|
||||
item.cardKeys.forEach((ck) => {
|
||||
rows.push(withGoodsName ? { ...ck, goodsName: item.goodsName } : { ...ck });
|
||||
});
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** createTrade 后是否无需进入收银台(积分单 / 0 元 / 服务端已标记) */
|
||||
export function shouldSkipPaymentPage(tradeResult, way) {
|
||||
if (!tradeResult) return false;
|
||||
if (way === E_COUPON_CART_POINTS) return true;
|
||||
if (tradeResult.needPay === false || tradeResult.paid === true) return true;
|
||||
const price =
|
||||
tradeResult.price != null
|
||||
? Number(tradeResult.price)
|
||||
: tradeResult.flowPrice != null
|
||||
? Number(tradeResult.flowPrice)
|
||||
: null;
|
||||
if (price != null && !Number.isNaN(price) && price <= 0) return true;
|
||||
return false;
|
||||
}
|
||||
@@ -1,70 +1,54 @@
|
||||
import Vue from "vue";
|
||||
import App from "./App";
|
||||
import router from "./router";
|
||||
import ViewUI from "view-design";
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import "core-js/stable";
|
||||
import "./assets/styles/global-layout.scss";
|
||||
import "./assets/styles/theme.less";
|
||||
// import './assets/iconfont/iconfont.css';
|
||||
import App from "./App.vue";
|
||||
import { router } from "./router/index";
|
||||
import { setupElementPlus } from "@/plugins/element";
|
||||
import { setupLegacyMessage } from "@/utils/message";
|
||||
import { registerGlobalComponents } from "@/components/global.js";
|
||||
import * as filters from "./plugins/filters";
|
||||
import store from "@/vuex/store";
|
||||
import storage from "@/plugins/storage";
|
||||
// 全局引入封装组件
|
||||
import {InstallAll} from "@/components/global.js";
|
||||
import { fetchAndApplyTheme } from "@/utils/theme";
|
||||
import { getThemeSetting } from "@/api/common.js";
|
||||
import config from "@/config";
|
||||
import { openLink } from "@/utils/buyerLogin";
|
||||
|
||||
const {aMapSecurityJsCode, title, inputMaxLength} = require("@/config");
|
||||
const { aMapSecurityJsCode, inputMaxLength } = config;
|
||||
|
||||
const copyViewUi = {...ViewUI}
|
||||
copyViewUi.Input.props.maxlength.default = inputMaxLength // 挂载最大输入值
|
||||
Vue.use(copyViewUi);
|
||||
|
||||
|
||||
Vue.use(InstallAll);
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
Object.keys(filters).forEach((key) => {
|
||||
Vue.filter(key, filters[key]);
|
||||
});
|
||||
|
||||
// 高德安全密钥
|
||||
if (aMapSecurityJsCode) {
|
||||
window._AMapSecurityConfig = {
|
||||
securityJsCode: aMapSecurityJsCode,
|
||||
};
|
||||
}
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
ViewUI.LoadingBar.start();
|
||||
window.document.title = to.meta.title === undefined ? title : to.meta.title;
|
||||
next();
|
||||
});
|
||||
const app = createApp(App);
|
||||
|
||||
router.afterEach((route) => {
|
||||
ViewUI.LoadingBar.finish();
|
||||
});
|
||||
setupElementPlus(app);
|
||||
setupLegacyMessage(app);
|
||||
registerGlobalComponents(app);
|
||||
|
||||
Vue.prototype.linkTo = function (url) {
|
||||
if (url.substr(0, 1) === "/") {
|
||||
// 非外部链接,没有origin,只有路由地址
|
||||
if (router.mode === "hash") {
|
||||
window.open(location.origin + "/#" + url, "_blank");
|
||||
} else {
|
||||
window.open(location.origin + url, "_blank");
|
||||
}
|
||||
} else {
|
||||
// 外部链接,完整的url地址
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
app.use(router);
|
||||
app.use(createPinia());
|
||||
|
||||
app.config.globalProperties.$filters = filters;
|
||||
app.config.globalProperties.Cookies = storage;
|
||||
app.config.globalProperties.$inputMaxLength = inputMaxLength;
|
||||
|
||||
app.config.globalProperties.linkTo = function (url) {
|
||||
openLink(url, router);
|
||||
};
|
||||
// 联系客服
|
||||
Vue.prototype.connectCs = function (
|
||||
|
||||
app.config.globalProperties.connectCs = function (
|
||||
sign = "37ef9b97807d03c6741298ed4eb5b536d2d238e08a3c00fb01fe48f03a569974c99ad767e72c04b3165ef29aca2c488b505fe4ca"
|
||||
) {
|
||||
const url = "https://yzf.qq.com/xv/web/static/chat/index.html?sign=" + sign;
|
||||
const url =
|
||||
"https://yzf.qq.com/xv/web/static/chat/index.html?sign=" + sign;
|
||||
window.open(url, "_blank");
|
||||
};
|
||||
Vue.prototype.Cookies = storage;
|
||||
/* eslint-disable no-new */
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
render: (h) => h(App),
|
||||
}).$mount("#app");
|
||||
|
||||
router.isReady().then(async () => {
|
||||
await fetchAndApplyTheme(getThemeSetting);
|
||||
app.mount("#app");
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user