feat: init
commit
4bdd10ccec
|
@ -0,0 +1,7 @@
|
|||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.next
|
||||
.git
|
|
@ -0,0 +1,11 @@
|
|||
BASE_URL=http://localhost:3000
|
||||
MONGODB_URL=mongodb://localhost:27017/choiceshop
|
||||
|
||||
NEXT_PUBLIC_ACCESS_TOKEN_SECRET=<your token secret>
|
||||
|
||||
NEXT_PUBLIC_ALI_REGION=<your ali endpoint>
|
||||
NEXT_PUBLIC_ALI_BUCKET_NAME=<your ali bucket name>
|
||||
NEXT_PUBLIC_ALI_ACCESS_KEY=<your ali access key>
|
||||
NEXT_PUBLIC_ALI_SECRET_KEY=<your ali secret key>
|
||||
NEXT_PUBLIC_ALI_ACS_RAM_NAME=<your ali acs:ram name>
|
||||
NEXT_PUBLIC_ALI_FILES_PATH=<your ali files pathname>
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier" // Add "prettier" last. This will turn off eslint rules conflicting with prettier. This is not what will format our code.
|
||||
]
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx --no -- commitlint --edit ${1}
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
#npm run lint
|
||||
#npm run format
|
||||
git add .
|
|
@ -0,0 +1,5 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
|
@ -0,0 +1,6 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/exercise-full-next.iml" filepath="$PROJECT_DIR$/.idea/exercise-full-next.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
// Lint then format TypeScript and JavaScript files
|
||||
'/**/*.(ts|tsx|js)': filenames => [
|
||||
`eslint --fix ${filenames.join(' ')}`,
|
||||
`prettier --write ${filenames.join(' ')}`,
|
||||
],
|
||||
|
||||
// Format MarkDown and JSON
|
||||
'/**/*.(md|json)': filenames => `prettier --write ${filenames.join(' ')}`,
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
module.exports = {
|
||||
//前头函数只有一个參数的时候可以忽略括号
|
||||
arrowParens: 'avoid',
|
||||
//括号内部不要出现空格
|
||||
bracketSpacing: true,
|
||||
//行结東符使用Unix格式
|
||||
endOfLine: 'lf',
|
||||
//true:Put>onthelastLineinsteadofatanewline
|
||||
jsxBracketSameLine: false,
|
||||
//行宽
|
||||
printWidth: 100,
|
||||
//换行方式
|
||||
proseWrap: 'preserve',
|
||||
//分号
|
||||
semi: false,
|
||||
//使用单引号
|
||||
singleQuote: true,
|
||||
//缩进
|
||||
tabWidth: 2,
|
||||
//使甩tab缩进
|
||||
useTabs: false,
|
||||
//后夏迎号,多行对象、数组在最后一行增加逗号
|
||||
trailingComma: 'es5',
|
||||
parser: 'typescript',
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
FROM node:18-alpine as dependencies
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
|
||||
FROM node:18-alpine as builder
|
||||
WORKDIR /app
|
||||
COPY --from=dependencies /app/node_modules ./node_modules
|
||||
COPY ./ ./
|
||||
COPY ./.env ./
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM node:18-alpine as runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV production
|
||||
|
||||
RUN addgroup -g 1001 -S nodejs
|
||||
RUN adduser -S nextjs -u 1001
|
||||
|
||||
COPY --from=builder /app/next.config.js ./
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --chown=nextjs:nodejs --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/.env ./.env
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
USER nextjs
|
||||
|
||||
ENV MONGODB_URL "mongodb://db:27017/choiceshop"
|
||||
EXPOSE 3000
|
||||
|
||||
# RUN addgroup --system --gid 1001 nodejs
|
||||
# RUN adduser --system --uid 1001 nextjs
|
||||
# RUN mkdir -p .next/cache/fetch-cache
|
||||
# RUN chown -R nextjs:nodejs .next
|
||||
# USER nextjs
|
||||
# CMD echo 'Waiting for db service start...' && while ! nc -z db 27017; do sleep 1; done; echo 'Connected!' && npm run build && npm run start
|
||||
# CMD npm run start
|
||||
CMD ["node_modules/.bin/next", "start"]
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Jipeng Huang
|
||||
|
||||
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.
|
|
@ -0,0 +1,263 @@
|
|||
<p align="center">
|
||||
<img alt="logo" src="https://www.cheerspublishing.com/uploads/article/3ce26e55-1e14-4e51-aec1-1c18533f953c.png" width="300">
|
||||
</p>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">C-Shopping v1.0.0</h1>
|
||||
|
||||
## README.md
|
||||
- en [English](README.md)
|
||||
- zh_CN [Simplified Chinese](README.zh_CN.md)
|
||||
|
||||
Hello, everyone! Welcome to C-Shopping, a journey into the world of e-commerce unveiling the technological wonders. I am "Ji Xiaopeng," the open-source author of C-Shopping, and today, I will introduce you to an open-source e-commerce platform based on the latest technologies. Let's explore together!
|
||||
|
||||
**Project Live Demo Links:**
|
||||
|
||||
- Docker Deployment Address: [http://shop.huanghanlian.com/](http://shop.huanghanlian.com/)
|
||||
- Vercel Address: [https://c-shopping-three.vercel.app/](https://c-shopping-three.vercel.app/)
|
||||
|
||||
|
||||
Project gateway: [https://github.com/huanghanzhilian/c-shopping](https://github.com/huanghanzhilian/c-shopping).
|
||||
|
||||
|
||||
**React Native mobile app application:**
|
||||
|
||||
Project gateway: [https://github.com/huanghanzhilian/c-shopping-rn](https://github.com/huanghanzhilian/c-shopping-rn).
|
||||
|
||||
If you find this helpful, please give me a Star. It will be a great encouragement.
|
||||
|
||||
---
|
||||
|
||||
## Project Background
|
||||
|
||||

|
||||
|
||||
**Background:**
|
||||
- Traditional front-end UI frameworks have long been constrained by fixed forms (limited by traditional UI frameworks), leading to visual fatigue. When developing highly customized projects, there is often a sense of powerlessness.
|
||||
- Excellent web projects with multi-device adaptation are rare, with high learning and maintenance costs.
|
||||
- As projects become complex, dealing with multiple API calls in components can become complicated. For example, managing multiple loading and error states can lead to a declaration of numerous states. Issues like request cancellation and request race conditions are also prone to being overlooked.
|
||||
- As the project complexity grows, the development and maintenance of styles become extensive and cumbersome.
|
||||
|
||||
**Intent:**
|
||||
|
||||
Address the issues mentioned in the background.
|
||||
|
||||
**Objective:**
|
||||
|
||||
Build a complete, well-designed ecosystem suitable for the web.
|
||||
|
||||
---
|
||||
|
||||
Firstly, let's delve into the technology behind C-Shopping. I have adopted a series of cutting-edge technologies, including Next.js, Tailwind CSS, Headless UI, Redux-Toolkit-RTK Query, JWT, and Docker, among others. This ensures that this project is not only efficient but also highly scalable. We are committed to addressing some pain points of traditional e-commerce platforms: lack of aesthetics, inadequate adaptation to different devices, and a monotonous interface, among others. By adopting the latest technologies and design principles, C-Shopping creates a fully responsive technical development experience for users.
|
||||
|
||||
C-Shopping prioritizes user experience. Our interface is not only beautiful but also responsive, allowing users to enjoy shopping easily on any device. The personal center and order management functions also make your shopping experience more personalized and convenient.
|
||||
|
||||
---
|
||||
|
||||
## Project Highlights
|
||||
|
||||
One of the highlights of C-Shopping is the adoption of a series of advanced technologies, including Next.js, Tailwind CSS, Headless UI, Redux-Toolkit-RTK Query, etc., providing users with an ultimate performance and experience. We not only focus on aesthetics but also strive for excellence in technology.
|
||||
|
||||
**Next.js Driven Lightning-Fast Experience**
|
||||
|
||||
C-Shopping uses Next.js, meaning not only is the webpage loading speed incredibly fast, but it also supports server-side rendering, providing an unprecedented level of smoothness.
|
||||
|
||||
🎨 **Tailwind CSS Stylish Design**
|
||||
|
||||
By using Tailwind CSS, C-Shopping injects a sense of style. Each interface is exquisite, making shopping a visual feast.
|
||||
|
||||
🔧 **Headless UI Freedom and Flexibility**
|
||||
|
||||
C-Shopping opts for the Headless UI style, giving users more freedom during the shopping process. No longer confined to traditional UI frameworks, it opens more doors for customization.
|
||||
|
||||
🔐 **JWT Security Without Worries**
|
||||
|
||||
Security is paramount! JWT is used for user authentication, providing the strongest guarantee for your shopping behavior, allowing you to shop with confidence.
|
||||
|
||||
🐳 **Docker Perfect Deployment**
|
||||
|
||||
C-Shopping embraces Docker, making project deployment incredibly simple. Containerization allows the entire project to run seamlessly in different environments.
|
||||
|
||||
🔄 **Redux Toolkit and RTK Query State Management Art**
|
||||
|
||||
C-Shopping uses Redux Toolkit and RTK Query, making state management more relaxed and enjoyable. You can better track data flow in the application, ensuring the stability of the shopping experience.
|
||||
|
||||
---
|
||||
|
||||
## Feature Demo
|
||||
|
||||
Now, let's take a look at some basic features of C-Shopping. From clear navigation and product displays to convenient search and shopping cart features, every detail has been carefully designed to provide users with a pleasant shopping experience.
|
||||
|
||||
**User-side**
|
||||
|
||||
| Module| Desktop devices| Mobile devices|
|
||||
|--|------------|--|
|
||||
| Home| <img src="https://www.cheerspublishing.com/uploads/article/901edcbd-b143-4f33-9d35-74fda6dbcb0d.gif" /> | <img src="https://www.cheerspublishing.com/uploads/article/cb1e4f8f-aab4-4b83-8cf5-13558bb8f6dc.gif" /> |
|
||||
| Secondary Category | <img src="https://www.cheerspublishing.com/uploads/article/6b53db16-d55b-4c7b-8088-fb637aad3921.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/542c8bf9-344a-4c19-a9e3-27bc0ec92bd5.png" /> |
|
||||
| Third-level Category | <img src="https://www.cheerspublishing.com/uploads/article/94ca43fa-3381-45a5-a5bf-80499533f3d5.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/f90b95ba-4b43-48fa-bf70-7736fcc7f9c5.png" /> |
|
||||
| Product Details | <img src="https://www.cheerspublishing.com/uploads/article/183dd238-2f33-48b3-85f6-d917bf78ba01.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/eb4ae7db-c490-4af2-a99c-d0b10fd6c01e.png" /> |
|
||||
| Login | <img src="https://www.cheerspublishing.com/uploads/article/e9a0ce6a-f1e9-4b5d-a03e-236338243e48.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/3ec1a909-294c-40d5-98dd-8c890cd8eba2.png" /> |
|
||||
| Register | <img src="https://www.cheerspublishing.com/uploads/article/5070ac14-4ae8-4eae-9491-27dc33db693f.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/88d32659-0a3d-453c-8f2d-daa8e4ee1b14.png" /> |
|
||||
| Search | <img src="https://www.cheerspublishing.com/uploads/article/375b23ff-c493-498a-9ca5-e42d1e15e4c9.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/52518186-e141-4614-8da8-c38a31c7895b.png" /> |
|
||||
| Shopping Cart | <img src="https://www.cheerspublishing.com/uploads/article/233ee4fb-e1ca-4716-ba5f-17224bb252bf.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/17578ef8-1af0-4b03-9942-f5f805d9045b.png" /> |
|
||||
| Checkout | <img src="https://www.cheerspublishing.com/uploads/article/2cc56a0c-f2b1-4f4c-9bd0-1aea5d4a36a5.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/334c73aa-db17-4624-8e0f-c7ea44236974.png" /> |
|
||||
| User Profile | <img src="https://www.cheerspublishing.com/uploads/article/3d1db865-9b6b-4c4d-8803-9b94444def73.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/a671fef1-401c-4c3d-9a1d-6cd59ecb9d63.png" /> |
|
||||
| My Orders | <img src="https://www.cheerspublishing.com/uploads/article/aab4ff6f-50ea-48b8-a74b-dbb6fe178810.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/8114f995-495c-4044-8b6b-2ef2a746d125.png" /> |
|
||||
| My Reviews | <img src="https://www.cheerspublishing.com/uploads/article/dfa14b9e-2c19-4ea1-b4c9-45483bbc52fe.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/686c0dd9-d9a0-4ff3-9953-eef499349930.png" /> |
|
||||
| Address Management | <img src="https://www.cheerspublishing.com/uploads/article/1c214382-d281-43b8-87c6-159b9b10e965.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/d4448bfc-40b0-4b18-ae47-c3a9f9884918.png" /> |
|
||||
| Recent Visits | <img src="https://www.cheerspublishing.com/uploads/article/c375fe8d-fb49-45a3-bdfc-8a90de031b25.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/73a67a1d-a9ae-4ded-990a-4ef172671d34.png" /> |
|
||||
|
||||
**Admin-side**
|
||||
|
||||
| Module | Desktop devices | Mobile devices|
|
||||
|--|------------|--|
|
||||
| Login | <img src="https://www.cheerspublishing.com/uploads/article/10fc1ee3-44ec-4380-ba90-6b2d809fb625.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/d3995bbe-df4f-490a-b8df-998932840ab6.png" /> |
|
||||
| Admin Center | <img src="https://www.cheerspublishing.com/uploads/article/ae09d053-e2df-4176-8470-b063f556069e.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/633169d7-a616-40fc-8970-d79748734873.png" /> |
|
||||
| User Management | <img src="https://www.cheerspublishing.com/uploads/article/250ee952-3757-42db-8828-60d8142edd4a.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/ad6fa92c-2bda-4391-9c93-e59fdeff59c3.png" /> |
|
||||
| Category Management | <img src="https://www.cheerspublishing.com/uploads/article/f644d10f-bda4-4309-944c-587dbe3e8931.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/458eb6ab-2c88-4654-8262-81dffe0b3c66.png" /> |
|
||||
| Category Management Tree | <img src="https://www.cheerspublishing.com/uploads/article/8eef2702-c06b-4996-bd15-229a3ccb6e2d.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/27516b00-c0e0-4a12-aedc-a9f64f64db1b.png" /> |
|
||||
| Specification Management | <img src="https://www.cheerspublishing.com/uploads/article/50eb69ce-0545-4def-91e2-ceac09b1222d.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/b96bc0fe-ad45-4b1c-b4d9-945e675cc7b9.png" /> |
|
||||
| Product Management | <img src="https://www.cheerspublishing.com/uploads/article/893128e7-06e3-47b5-9fb8-8757faf28941.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/1d9b03aa-8673-4405-ad2f-2d61e413c114.png" /> |
|
||||
| Order Management | <img src="https://www.cheerspublishing.com/uploads/article/e5473ac2-859c-4774-8879-f31516da956a.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/7ac7850b-798c-4954-95ad-3fab562bf418.png" /> |
|
||||
| Review Management | <img src="https://www.cheerspublishing.com/uploads/article/3979c2fc-87ca-4604-8258-5be1e5af97b9.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/0df0021a-626f-452c-b4dc-d9b0c927d4e3.png" /> |
|
||||
| Slider Management | <img src="https://www.cheerspublishing.com/uploads/article/6419e018-3322-40f6-b796-105e125d7052.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/b695af32-cd0e-4009-a278-adb2a4f22b2f.png" /> |
|
||||
| Banner Management | <img src="https://www.cheerspublishing.com/uploads/article/c8fd0a19-f020-41b1-8590-8e88d7d4f659.png" /> | <img src="https://www.cheerspublishing.com/uploads/article/7bc682e2-60c2-45f3-80c3-e94ade1223b2.png" /> |
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Project Structure
|
||||
|
||||
🏗️ **C-Shopping Project Structure:**
|
||||
|
||||
**Key structure explanation:**
|
||||
|
||||
- 📁 **app**: Main code of the application
|
||||
- 📁 **main**: Main application components
|
||||
- 📁 **client-layout**: Common layout pages for the user side
|
||||
- 📁 **empty-layout**: Common blank layout pages
|
||||
- 📁 **admin**: Admin pages
|
||||
- 📄 **layout.js**: Main layout configuration
|
||||
- 📁 **profile**: User profile page
|
||||
- 📄 **StoreProvider.js**: Global state management provider
|
||||
- 📁 **api**: API request-related routes
|
||||
- 📁 **auth**: User authentication API
|
||||
- 📁 **banner**: Advertisement banner API
|
||||
- 📁 **category**: Product category API
|
||||
- ...
|
||||
|
||||
- 📁 **components**: Reusable React components
|
||||
- 📁 **helpers**: Helper functions and tools
|
||||
- 📁 **api**: API request-related helper functions
|
||||
- 📄 **auth.js**: Helper functions related to user authentication
|
||||
- ...
|
||||
|
||||
- 📁 **hooks**: Custom React hooks
|
||||
- 📁 **models**: Data model definitions
|
||||
- 📁 **public**: Static resources, such as images, fonts, etc.
|
||||
- 📁 **store**: Configuration related to Redux state management
|
||||
- 📁 **services**: RTK Query
|
||||
- 📁 **slices**: Redux Toolkit
|
||||
- 📁 **styles**: Style files
|
||||
- 📁 **utils**: General utilities
|
||||
- ...
|
||||
|
||||
This structure is designed to make the project organized, easy to maintain, and scalable. Each section is divided based on
|
||||
|
||||
functionality and responsibilities, making it easier for team members to understand and collaborate.
|
||||
|
||||
---
|
||||
|
||||
## Deployment and Usage
|
||||
|
||||
**Development Environment**
|
||||
|
||||
1. Clone or download the repository by running the following command in the terminal:
|
||||
|
||||
```
|
||||
git clone https://github.com/huanghanzhilian/c-shopping.git
|
||||
```
|
||||
|
||||
2. Install project dependencies using npm or yarn:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
yarn
|
||||
```
|
||||
|
||||
3. Please create a new `.env` file from `.env.example` file in the project root directory to define the required environment variables. This step is crucial (for image upload to OSS):
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_ACCESS_TOKEN_SECRET=<your token secret>
|
||||
NEXT_PUBLIC_ALI_REGION=<your ali endpoint>
|
||||
NEXT_PUBLIC_ALI_BUCKET_NAME=<your ali bucket name>
|
||||
NEXT_PUBLIC_ALI_ACCESS_KEY=<your ali access key>
|
||||
NEXT_PUBLIC_ALI_SECRET_KEY=<your ali secret key>
|
||||
NEXT_PUBLIC_ALI_ACS_RAM_NAME=<your ali acs:ram name>
|
||||
NEXT_PUBLIC_ALI_FILES_PATH=<your ali files pathname>
|
||||
```
|
||||
|
||||
4. Install MongoDB on your local machine.
|
||||
|
||||
5. Run the project:
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
6. Register an account:
|
||||
|
||||
```
|
||||
http://localhost:3000/register
|
||||
```
|
||||
|
||||
7. After creating an account, find your account in the database and modify the `root` field to true and the `role` field to admin. This grants you access to all admin dashboard features:
|
||||
```
|
||||
mongo
|
||||
```
|
||||
```
|
||||
use choiceshop
|
||||
```
|
||||
```
|
||||
db.users.update({name:"admin"},{$set:{role:"admin"}})
|
||||
db.users.update({name:"admin"},{$set:{root:true}})
|
||||
```
|
||||
|
||||
Administrator entrance: http://localhost:3000/admin
|
||||
|
||||
8. In MongoDB, create the root category:
|
||||
|
||||
```
|
||||
mongo
|
||||
```
|
||||
|
||||
```
|
||||
use choiceshop
|
||||
```
|
||||
|
||||
```
|
||||
db.categories.insert({
|
||||
"name" : "Featured Items",
|
||||
"slug" : "choiceshop",
|
||||
"image" : "http://huanghanzhilian-test.oss-cn-beijing.aliyuncs.com/shop/upload/image//icons/zHle_bmdM_dhu2K938MMM.webp",
|
||||
"colors" : {
|
||||
"start" : "#EF394E",
|
||||
"end" : "#EF3F55"
|
||||
},
|
||||
"level" : 0
|
||||
})
|
||||
```
|
||||
|
||||
**Docker Deployment**
|
||||
|
||||
The project root directory is already configured with Docker Compose. After installing Docker, simply run the deployment:
|
||||
|
||||
```
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -0,0 +1,287 @@
|
|||
<p align="center">
|
||||
<img alt="logo" src="https://www.cheerspublishing.com/uploads/article/3ce26e55-1e14-4e51-aec1-1c18533f953c.png" width="300">
|
||||
</p>
|
||||
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">C-Shopping v1.0.0</h1>
|
||||
<h4 align="center">基于Nextjs开发同时适配Desktop、Tablet、Phone多种设备的精美购物平台</h4>
|
||||
|
||||
## README.md
|
||||
- en [English](README.md)
|
||||
- zh_CN [简体中文](README.zh_CN.md)
|
||||
|
||||
## 前言
|
||||
|
||||
`c-shopping`是一个精美的web电商系统,支持响应式交互,界面优雅,功能丰富,小巧迅速,包含一个电商平台MVP完整功能,具备良好的审美风格与编码设计。
|
||||
|
||||
希望来的人,有所收获。故事不结束,青春不散场。
|
||||
|
||||
## 项目在线演示
|
||||
|
||||
**项目在线演示地址:**
|
||||
|
||||
- docker 部署地址:[http://shop.huanghanlian.com/](http://shop.huanghanlian.com/)
|
||||
- vercel 部署地址:[https://c-shopping-three.vercel.app/](https://c-shopping-three.vercel.app/)
|
||||
|
||||
项目传送门: [https://github.com/huanghanzhilian/c-shopping](https://github.com/huanghanzhilian/c-shopping)
|
||||
|
||||
**React Native 移动app应用:**
|
||||
|
||||
项目传送门: [https://github.com/huanghanzhilian/c-shopping-rn](https://github.com/huanghanzhilian/c-shopping-rn)
|
||||
|
||||
## 项目介绍
|
||||
|
||||
**背景:**
|
||||
- 一直以来前端UI框架被固定形式占据(受限于传统的UI框架),导致视觉疲劳,在开发一些高度自定义的项目时,往往力不从心;
|
||||
- 多设备适配的web优秀项目很少,学习和维护成本较高;
|
||||
- 当项目复杂后,在组件需要调用多个 api 时会变得复杂起来,比如需要管理多个 loading 和 error 状态,这会导致产生非常多的 state 声明,还有请求取消、请求竞态等可能存在的问题也容易被忽略;
|
||||
- 随着项目复杂,样式的开发与维护变得庞大且臃肿;
|
||||
|
||||
**意图:**
|
||||
|
||||
改进背景中提到的问题。
|
||||
|
||||
**目的:**
|
||||
|
||||
打造一个完整的,适合web端的良好生态。
|
||||
|
||||
|
||||
|
||||
|
||||
### 使用技术
|
||||
|
||||
- NextJs
|
||||
- TailwindCss
|
||||
- Headless UI
|
||||
- MongoDB
|
||||
- Redux - Toolkit - RTK Query
|
||||
- JWT
|
||||
- Docker
|
||||
|
||||
### 功能
|
||||
|
||||
用户端:
|
||||
- 登录 JWT认证
|
||||
- 注册
|
||||
- 首页(分类navBar、banner、slider、特价板块、hot板块、畅销板块)
|
||||
- 搜索
|
||||
- 二级分类页(分类navBar、banner、slider、特价板块、hot板块、畅销板块)
|
||||
- 三级分类页(排序、筛选)
|
||||
- 商品详情(购物车)
|
||||
- 购物车页
|
||||
- 支付页
|
||||
- 个人中心
|
||||
- 我的订单
|
||||
- 我的评论
|
||||
- 地址管理
|
||||
- 近期访问
|
||||
|
||||
管理端:
|
||||
- 登录 JWT认证
|
||||
- 注册
|
||||
- 用户管理
|
||||
- 分类管理
|
||||
- 规格管理
|
||||
- 商品管理
|
||||
- 订单管理
|
||||
- 评论管理
|
||||
- 滑块管理
|
||||
- banner管理
|
||||
|
||||
### 演示图
|
||||
|
||||
#### 用户端
|
||||
|
||||
|模块|Desktop devices|Mobile devices|
|
||||
|--|------------|--|
|
||||
|首页|<img src="https://www.cheerspublishing.com/uploads/article/901edcbd-b143-4f33-9d35-74fda6dbcb0d.gif" />|<img src="https://www.cheerspublishing.com/uploads/article/cb1e4f8f-aab4-4b83-8cf5-13558bb8f6dc.gif" />|
|
||||
|二级分类|<img src="https://www.cheerspublishing.com/uploads/article/6b53db16-d55b-4c7b-8088-fb637aad3921.png" />|<img src="https://www.cheerspublishing.com/uploads/article/542c8bf9-344a-4c19-a9e3-27bc0ec92bd5.png" />|
|
||||
|三级分类|<img src="https://www.cheerspublishing.com/uploads/article/94ca43fa-3381-45a5-a5bf-80499533f3d5.png" />|<img src="https://www.cheerspublishing.com/uploads/article/f90b95ba-4b43-48fa-bf70-7736fcc7f9c5.png" />|
|
||||
|商品详情|<img src="https://www.cheerspublishing.com/uploads/article/183dd238-2f33-48b3-85f6-d917bf78ba01.png" />|<img src="https://www.cheerspublishing.com/uploads/article/eb4ae7db-c490-4af2-a99c-d0b10fd6c01e.png" />|
|
||||
|登录|<img src="https://www.cheerspublishing.com/uploads/article/e9a0ce6a-f1e9-4b5d-a03e-236338243e48.png" />|<img src="https://www.cheerspublishing.com/uploads/article/3ec1a909-294c-40d5-98dd-8c890cd8eba2.png" />|
|
||||
|注册|<img src="https://www.cheerspublishing.com/uploads/article/5070ac14-4ae8-4eae-9491-27dc33db693f.png" />|<img src="https://www.cheerspublishing.com/uploads/article/88d32659-0a3d-453c-8f2d-daa8e4ee1b14.png" />|
|
||||
|搜索|<img src="https://www.cheerspublishing.com/uploads/article/375b23ff-c493-498a-9ca5-e42d1e15e4c9.png" />|<img src="https://www.cheerspublishing.com/uploads/article/52518186-e141-4614-8da8-c38a31c7895b.png" />|
|
||||
|购物车|<img src="https://www.cheerspublishing.com/uploads/article/233ee4fb-e1ca-4716-ba5f-17224bb252bf.png" />|<img src="https://www.cheerspublishing.com/uploads/article/17578ef8-1af0-4b03-9942-f5f805d9045b.png" />|
|
||||
|支付页|<img src="https://www.cheerspublishing.com/uploads/article/2cc56a0c-f2b1-4f4c-9bd0-1aea5d4a36a5.png" />|<img src="https://www.cheerspublishing.com/uploads/article/334c73aa-db17-4624-8e0f-c7ea44236974.png" />|
|
||||
|个人中心|<img src="https://www.cheerspublishing.com/uploads/article/3d1db865-9b6b-4c4d-8803-9b94444def73.png" />|<img src="https://www.cheerspublishing.com/uploads/article/a671fef1-401c-4c3d-9a1d-6cd59ecb9d63.png" />|
|
||||
|我的订单|<img src="https://www.cheerspublishing.com/uploads/article/aab4ff6f-50ea-48b8-a74b-dbb6fe178810.png" />|<img src="https://www.cheerspublishing.com/uploads/article/8114f995-495c-4044-8b6b-2ef2a746d125.png" />|
|
||||
|我的评论|<img src="https://www.cheerspublishing.com/uploads/article/dfa14b9e-2c19-4ea1-b4c9-45483bbc52fe.png" />|<img src="https://www.cheerspublishing.com/uploads/article/686c0dd9-d9a0-4ff3-9953-eef499349930.png" />|
|
||||
|地址管理|<img src="https://www.cheerspublishing.com/uploads/article/1c214382-d281-43b8-87c6-159b9b10e965.png" />|<img src="https://www.cheerspublishing.com/uploads/article/d4448bfc-40b0-4b18-ae47-c3a9f9884918.png" />|
|
||||
|近期访问|<img src="https://www.cheerspublishing.com/uploads/article/c375fe8d-fb49-45a3-bdfc-8a90de031b25.png" />|<img src="https://www.cheerspublishing.com/uploads/article/73a67a1d-a9ae-4ded-990a-4ef172671d34.png" />|
|
||||
|
||||
|
||||
#### 管理端
|
||||
|
||||
|模块|Desktop devices|Mobile devices|
|
||||
|--|------------|--|
|
||||
|登录|<img src="https://www.cheerspublishing.com/uploads/article/10fc1ee3-44ec-4380-ba90-6b2d809fb625.png" />|<img src="https://www.cheerspublishing.com/uploads/article/d3995bbe-df4f-490a-b8df-998932840ab6.png" />|
|
||||
|管理中心|<img src="https://www.cheerspublishing.com/uploads/article/ae09d053-e2df-4176-8470-b063f556069e.png" />|<img src="https://www.cheerspublishing.com/uploads/article/633169d7-a616-40fc-8970-d79748734873.png" />|
|
||||
|用户管理|<img src="https://www.cheerspublishing.com/uploads/article/250ee952-3757-42db-8828-60d8142edd4a.png" />|<img src="https://www.cheerspublishing.com/uploads/article/ad6fa92c-2bda-4391-9c93-e59fdeff59c3.png" />|
|
||||
|分类管理|<img src="https://www.cheerspublishing.com/uploads/article/f644d10f-bda4-4309-944c-587dbe3e8931.png" />|<img src="https://www.cheerspublishing.com/uploads/article/458eb6ab-2c88-4654-8262-81dffe0b3c66.png" />|
|
||||
|分类管理树状|<img src="https://www.cheerspublishing.com/uploads/article/8eef2702-c06b-4996-bd15-229a3ccb6e2d.png" />|<img src="https://www.cheerspublishing.com/uploads/article/27516b00-c0e0-4a12-aedc-a9f64f64db1b.png" />|
|
||||
|规格管理|<img src="https://www.cheerspublishing.com/uploads/article/50eb69ce-0545-4def-91e2-ceac09b1222d.png" />|<img src="https://www.cheerspublishing.com/uploads/article/b96bc0fe-ad45-4b1c-b4d9-945e675cc7b9.png" />|
|
||||
|商品管理|<img src="https://www.cheerspublishing.com/uploads/article/893128e7-06e3-47b5-9fb8-8757faf28941.png" />|<img src="https://www.cheerspublishing.com/uploads/article/1d9b03aa-8673-4405-ad2f-2d61e413c114.png" />|
|
||||
|订单管理|<img src="https://www.cheerspublishing.com/uploads/article/e5473ac2-859c-4774-8879-f31516da956a.png" />|<img src="https://www.cheerspublishing.com/uploads/article/7ac7850b-798c-4954-95ad-3fab562bf418.png" />|
|
||||
|评论管理|<img src="https://www.cheerspublishing.com/uploads/article/3979c2fc-87ca-4604-8258-5be1e5af97b9.png" />|<img src="https://www.cheerspublishing.com/uploads/article/0df0021a-626f-452c-b4dc-d9b0c927d4e3.png" />|
|
||||
|滑块管理|<img src="https://www.cheerspublishing.com/uploads/article/6419e018-3322-40f6-b796-105e125d7052.png" />|<img src="https://www.cheerspublishing.com/uploads/article/b695af32-cd0e-4009-a278-adb2a4f22b2f.png" />|
|
||||
|banner管理|<img src="https://www.cheerspublishing.com/uploads/article/c8fd0a19-f020-41b1-8590-8e88d7d4f659.png" />|<img src="https://www.cheerspublishing.com/uploads/article/7bc682e2-60c2-45f3-80c3-e94ade1223b2.png" />|
|
||||
|
||||
|
||||
### 项目结构
|
||||
|
||||
🏗️ **C-Shopping 项目结构:**
|
||||
|
||||
```
|
||||
📂 c-shopping
|
||||
├── 📁 app
|
||||
│ ├── 📁 main
|
||||
│ │ ├── 📁 client-layout
|
||||
│ │ ├── 📁 empty-layout
|
||||
│ │ ├── 📁 admin
|
||||
│ │ ├── 📄 layout.js
|
||||
│ │ └── 📁 profile
|
||||
│ ├── 📄 StoreProvider.js
|
||||
│ ├── 📁 api
|
||||
│ │ ├── 📁 auth
|
||||
│ │ ├── 📁 banner
|
||||
│ │ ├── 📁 category
|
||||
│ │ ├── 📁 details
|
||||
│ │ ├── 📁 order
|
||||
│ │ ├── 📁 products
|
||||
│ │ ├── 📁 reviews
|
||||
│ │ ├── 📁 slider
|
||||
│ │ ├── 📁 upload
|
||||
│ │ └── 📁 user
|
||||
│ ├── 📄 layout.js
|
||||
│ └── 📄 not-found.js
|
||||
├── 📄 commitlint.config.js
|
||||
├── 📁 components
|
||||
├── 📄 docker-compose.yml
|
||||
├── 📁 helpers
|
||||
│ ├── 📁 api
|
||||
│ ├── 📄 auth.js
|
||||
│ ├── 📁 db-repo
|
||||
│ ├── 📄 db.js
|
||||
│ ├── 📄 getQuery.js
|
||||
│ └── 📄 index.js
|
||||
├── 📁 hooks
|
||||
├── 📄 jsconfig.json
|
||||
├── 📁 models
|
||||
├── 📄 next.config.js
|
||||
├── 📄 package-lock.json
|
||||
├── 📄 package.json
|
||||
├── 📄 postcss.config.js
|
||||
├── 📂 public
|
||||
├── 📁 store
|
||||
├── 📁 styles
|
||||
├── 📄 tailwind.config.js
|
||||
└── 📁 utils
|
||||
```
|
||||
|
||||
**主要结构解释:**
|
||||
|
||||
- 📁 **app**: 应用程序的主要代码
|
||||
- 📁 **main**: 主要应用程序组件
|
||||
- 📁 **client-layout**: 用户端通用布局页面
|
||||
- 📁 **empty-layout**: 通用空白布局页面
|
||||
- 📁 **admin**: 管理端页面
|
||||
- 📄 **layout.js**: 主要布局配置
|
||||
- 📁 **profile**: 用户个人资料页面
|
||||
- 📄 **StoreProvider.js**: 全局状态管理提供者
|
||||
- 📁 **api**: API 请求相关路由
|
||||
- 📁 **auth**: 用户认证 API
|
||||
- 📁 **banner**: 广告横幅 API
|
||||
- 📁 **category**: 商品分类 API
|
||||
- ...
|
||||
|
||||
- 📁 **components**: 可复用的 React 组件
|
||||
- 📁 **helpers**: 辅助函数和工具
|
||||
- 📁 **api**: API 请求相关的辅助函数
|
||||
- 📄 **auth.js**: 用户认证相关的辅助函数
|
||||
- ...
|
||||
|
||||
- 📁 **hooks**: 自定义 React hooks
|
||||
- 📁 **models**: 数据模型定义
|
||||
- 📁 **public**: 静态资源,如图片、字体等
|
||||
- 📁 **store**: Redux 状态管理相关配置
|
||||
- 📁 **services**: RTK Query
|
||||
- 📁 **slices**: Redux Toolkit
|
||||
- 📁 **styles**: 样式文件
|
||||
- 📁 **utils**: 通用工具
|
||||
- ...
|
||||
|
||||
这个结构旨在使项目组织有序,易于维护和扩展。每个部分都按照功能和职责进行划分,使团队成员更容易理解和协作。
|
||||
|
||||
|
||||
## 环境搭建与部署
|
||||
|
||||
### 开发环境
|
||||
|
||||
1. 通过在终端运行以下命令克隆或下载存储库:
|
||||
|
||||
```
|
||||
git clone https://github.com/huanghanzhilian/c-shopping.git
|
||||
```
|
||||
2. 使用npm或yarn安装项目依赖项:
|
||||
```
|
||||
npm install
|
||||
```
|
||||
or
|
||||
```
|
||||
yarn
|
||||
```
|
||||
3. 查看`.env.example`内容,创建新的`.env`的文件,在项目根目录,定义所需的环境变量。这个步骤是重要的(图片上传OSS):
|
||||
```
|
||||
NEXT_PUBLIC_ACCESS_TOKEN_SECRET=<your token secret>
|
||||
NEXT_PUBLIC_ALI_REGION=<your ali endpoint>
|
||||
NEXT_PUBLIC_ALI_BUCKET_NAME=<your ali bucket name>
|
||||
NEXT_PUBLIC_ALI_ACCESS_KEY=<your ali access key>
|
||||
NEXT_PUBLIC_ALI_SECRET_KEY=<your ali secret key>
|
||||
NEXT_PUBLIC_ALI_ACS_RAM_NAME=<your ali acs:ram name>
|
||||
NEXT_PUBLIC_ALI_FILES_PATH=<your ali files pathname>
|
||||
```
|
||||
4. 在本地机器上安装MongoDB
|
||||
5. 运行项目
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
6. 注册一个账户
|
||||
```
|
||||
http://localhost:3000/register
|
||||
```
|
||||
7. 创建帐户后,在数据库中找到您的帐户,并将root字段修改为true。role字段修改为admin,这将授予您访问所有管理仪表板功能的权限
|
||||
```
|
||||
mongo
|
||||
```
|
||||
```
|
||||
use choiceshop
|
||||
```
|
||||
```
|
||||
db.users.update({name:"admin"},{$set:{role:"admin"}})
|
||||
db.users.update({name:"admin"},{$set:{root:true}})
|
||||
```
|
||||
管理员入口:http://localhost:3000/admin
|
||||
8. 操作MongoDB,创建根分类
|
||||
```
|
||||
mongo
|
||||
```
|
||||
```
|
||||
use choiceshop
|
||||
```
|
||||
```
|
||||
db.categories.insert({
|
||||
"name" : "精选好物",
|
||||
"slug" : "choiceshop",
|
||||
"image" : "http://huanghanzhilian-test.oss-cn-beijing.aliyuncs.com/shop/upload/image//icons/zHle_bmdM_dhu2K938MMM.webp",
|
||||
"colors" : {
|
||||
"start" : "#EF394E",
|
||||
"end" : "#EF3F55"
|
||||
},
|
||||
"level" : 0
|
||||
})
|
||||
```
|
||||
### docker 部署
|
||||
|
||||
项目根目录已经配置好docker compose,在安装docker环境后,直接运行部署
|
||||
|
||||
```
|
||||
docker compose up -d --build
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
import { siteTitle } from '@/utils'
|
||||
|
||||
export const metadata = {
|
||||
title: `购物车-${siteTitle}`,
|
||||
}
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return <>{children}</>
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
'use client'
|
||||
import { Fragment } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import { clearCart } from 'store'
|
||||
|
||||
import {
|
||||
Icons,
|
||||
FreeShipping,
|
||||
CartItem,
|
||||
CartInfo,
|
||||
Header,
|
||||
RedirectToLogin,
|
||||
Button,
|
||||
EmptyCart,
|
||||
} from 'components'
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
|
||||
import { formatNumber } from 'utils'
|
||||
|
||||
import { useUserInfo, useDisclosure, useAppSelector, useAppDispatch } from 'hooks'
|
||||
|
||||
const CartPage = () => {
|
||||
//? Assets
|
||||
const dispatch = useAppDispatch()
|
||||
const { push } = useRouter()
|
||||
|
||||
const [isShowRedirectModal, redirectModalHandlers] = useDisclosure()
|
||||
|
||||
//? Get User Data
|
||||
const { userInfo } = useUserInfo()
|
||||
|
||||
//? Store
|
||||
const { cartItems, totalItems, totalPrice, totalDiscount } = useAppSelector(state => state.cart)
|
||||
|
||||
//? Handlers
|
||||
const handleRoute = () => {
|
||||
if (!userInfo) return redirectModalHandlers.open()
|
||||
|
||||
push('/checkout/shipping')
|
||||
}
|
||||
|
||||
//? Local Components
|
||||
const DeleteAllDropDown = () => (
|
||||
<Menu as="div" className="dropdown">
|
||||
<Menu.Button className="dropdown__button">
|
||||
<Icons.More className="icon" />
|
||||
</Menu.Button>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="w-32 dropdown__items ">
|
||||
<Menu.Item>
|
||||
<button onClick={() => dispatch(clearCart())} className="px-4 py-3 flex-center gap-x-2">
|
||||
<Icons.Delete className="icon" />
|
||||
<span>删除全部</span>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
)
|
||||
|
||||
//? Render(s)
|
||||
if (cartItems.length === 0)
|
||||
return (
|
||||
<>
|
||||
<section className="py-2 mx-auto mb-20 space-y-3 xl:mt-36 lg:mb-0 container lg:px-5 lg:mt-6 lg:space-y-0 lg:py-4 lg:border lg:border-gray-200 lg:rounded-md">
|
||||
<div className="section-divide-y" />
|
||||
|
||||
<div className="py-20">
|
||||
<EmptyCart className="mx-auto h-52 w-52" />
|
||||
<p className="text-base font-bold text-center">您的购物车是空的!</p>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<RedirectToLogin
|
||||
title="您还没有登录"
|
||||
text=""
|
||||
onClose={redirectModalHandlers.close}
|
||||
isShow={isShowRedirectModal}
|
||||
/>
|
||||
<main className="container py-2 mx-auto mb-20 space-y-3 xl:mt-36 lg:py-0 lg:mb-0 b lg:px-5 lg:mt-6 lg:gap-x-3 lg:flex lg:flex-wrap lg:space-y-0">
|
||||
<div className="lg:py-4 lg:border lg:border-gray-200 lg:rounded-md lg:flex-1 h-fit">
|
||||
{/* title */}
|
||||
<section className="flex justify-between px-4">
|
||||
<div>
|
||||
<h3 className="mb-2 text-sm font-bold">您的购物车</h3>
|
||||
<span className="">{formatNumber(totalItems)} 件商品</span>
|
||||
</div>
|
||||
<DeleteAllDropDown />
|
||||
</section>
|
||||
|
||||
{/* carts */}
|
||||
<section className="divide-y">
|
||||
{cartItems.map(item => (
|
||||
<CartItem item={item} key={item.itemID} />
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="section-divide-y lg:hidden" />
|
||||
|
||||
{/* cart Info */}
|
||||
<section className="lg:sticky lg:top-6 lg:h-fit xl:top-36">
|
||||
<div className="lg:border lg:border-gray-200 lg:rounded-md">
|
||||
<CartInfo handleRoute={handleRoute} cart />
|
||||
</div>
|
||||
<FreeShipping />
|
||||
</section>
|
||||
|
||||
{/* to Shipping */}
|
||||
<section className="fixed bottom-0 left-0 right-0 z-10 flex items-center justify-between px-3 py-3 bg-white border-t border-gray-300 shadow-3xl lg:hidden">
|
||||
<div>
|
||||
<span className="font-light">总计购物车</span>
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm">{formatNumber(totalPrice - totalDiscount)}</span>
|
||||
<span className="ml-1">¥</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button className="w-1/2" onClick={handleRoute}>
|
||||
继续
|
||||
</Button>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CartPage
|
|
@ -0,0 +1,9 @@
|
|||
import { siteTitle } from '@/utils'
|
||||
|
||||
export const metadata = {
|
||||
title: `付款-${siteTitle}`,
|
||||
}
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return <>{children}</>
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import { clearCart, showAlert } from 'store'
|
||||
|
||||
import { useCreateOrderMutation } from '@/store/services'
|
||||
|
||||
import {
|
||||
Button,
|
||||
CartInfo,
|
||||
HandleResponse,
|
||||
Icons,
|
||||
ResponsiveImage,
|
||||
WithAddressModal,
|
||||
} from 'components'
|
||||
|
||||
import { formatNumber } from 'utils'
|
||||
|
||||
import { useAppDispatch, useAppSelector, useUserInfo } from 'hooks'
|
||||
|
||||
const ShippingPage = () => {
|
||||
//? Assets
|
||||
const router = useRouter()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
//? Get User Data
|
||||
const { userInfo } = useUserInfo()
|
||||
|
||||
//? States
|
||||
const [paymentMethod, setPaymentMethod] = useState('在线支付')
|
||||
|
||||
//? Store
|
||||
const { cartItems, totalItems, totalDiscount, totalPrice } = useAppSelector(state => state.cart)
|
||||
|
||||
//? Create Order Query
|
||||
const [postData, { data, isSuccess, isError, isLoading, error }] = useCreateOrderMutation()
|
||||
|
||||
//? Handlers
|
||||
const handleCreateOrder = () => {
|
||||
if (
|
||||
!userInfo?.address?.city &&
|
||||
!userInfo?.address?.province &&
|
||||
!userInfo?.address?.area &&
|
||||
!userInfo?.address?.street &&
|
||||
!userInfo?.address?.postalCode
|
||||
)
|
||||
return dispatch(
|
||||
showAlert({
|
||||
status: 'error',
|
||||
title: '请填写您的地址',
|
||||
})
|
||||
)
|
||||
else
|
||||
postData({
|
||||
body: {
|
||||
address: {
|
||||
city: userInfo.address.city.name,
|
||||
area: userInfo.address.area.name,
|
||||
postalCode: userInfo.address.postalCode,
|
||||
provinces: userInfo.address.province.name,
|
||||
street: userInfo.address.street,
|
||||
},
|
||||
mobile: userInfo.mobile,
|
||||
cart: cartItems,
|
||||
totalItems,
|
||||
totalPrice,
|
||||
totalDiscount,
|
||||
paymentMethod,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
//? Local Components
|
||||
const ChangeAddress = () => {
|
||||
const BasicChangeAddress = ({ addressModalProps }) => {
|
||||
const { openAddressModal } = addressModalProps || {}
|
||||
return (
|
||||
<button type="button" onClick={openAddressModal} className="flex items-center ml-auto">
|
||||
<span className="text-base text-sky-500">改变 | 编辑</span>
|
||||
<Icons.ArrowRight2 className="icon text-sky-500" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<WithAddressModal>
|
||||
<BasicChangeAddress />
|
||||
</WithAddressModal>
|
||||
)
|
||||
}
|
||||
|
||||
//? Render(s)
|
||||
return (
|
||||
<>
|
||||
{/* Handle Create Order Response */}
|
||||
{(isSuccess || isError) && (
|
||||
<HandleResponse
|
||||
isError={isError}
|
||||
isSuccess={isSuccess}
|
||||
error={error?.data?.message}
|
||||
message={data?.message}
|
||||
onSuccess={() => {
|
||||
dispatch(clearCart())
|
||||
router.push('/profile')
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main className="py-2 mx-auto space-y-3 xl:mt-28 container">
|
||||
{/* header */}
|
||||
<header className="lg:border lg:border-gray-200 lg:rounded-lg py-2">
|
||||
<div className="flex items-center justify-evenly">
|
||||
<Link href="/checkout/cart" className="flex flex-col items-center gap-y-2">
|
||||
<Icons.Cart className="text-red-300 icon" />
|
||||
<span className="font-normal text-red-300">购物车</span>
|
||||
</Link>
|
||||
|
||||
<div className="h-[1px] w-8 bg-red-300" />
|
||||
<div className="flex flex-col items-center gap-y-2">
|
||||
<Icons.Wallet className="w-6 h-6 text-red-500 icon" />
|
||||
<span className="text-base font-normal text-red-500">付款方式</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="section-divide-y lg:hidden" />
|
||||
|
||||
<div className="lg:flex lg:gap-x-3">
|
||||
<div className="lg:flex-1">
|
||||
{/* address */}
|
||||
<section className="flex items-center px-3 py-4 lg:border lg:border-gray-200 lg:rounded-lg gap-x-3">
|
||||
<Icons.Location2 className="text-black w-7 h-7" />
|
||||
<div className="space-y-2">
|
||||
<span className="">订单送货地址</span>
|
||||
<p className="text-base text-black">{userInfo?.address?.street}</p>
|
||||
<span className="text-sm">{userInfo?.name}</span>
|
||||
</div>
|
||||
<ChangeAddress />
|
||||
</section>
|
||||
|
||||
<div className="section-divide-y lg:hidden" />
|
||||
|
||||
{/* products */}
|
||||
<section className="px-2 py-4 mx-3 border border-gray-200 rounded-lg lg:mx-0 lg:mt-3 ">
|
||||
<div className="flex mb-5">
|
||||
<Image src="/icons/car.png" className="mr-4" width={40} height={40} alt="icon" />
|
||||
<div>
|
||||
<span className="text-base text-black">正常发货</span>
|
||||
<span className="block">有现货</span>
|
||||
</div>
|
||||
<span className="inline-block px-2 py-1 ml-3 bg-gray-100 rounded-lg h-fit">
|
||||
{formatNumber(totalItems)} 件商品
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-start gap-x-8 gap-y-5">
|
||||
{cartItems.map(item => (
|
||||
<article key={item.itemID}>
|
||||
<ResponsiveImage dimensions="w-28 h-28" src={item.img.url} alt={item.name} />
|
||||
|
||||
{item.color && (
|
||||
<div className="flex items-center gap-x-2 ml-3 mt-1.5">
|
||||
<span
|
||||
className="inline-block w-4 h-4 shadow rounded-xl"
|
||||
style={{ background: item.color.hashCode }}
|
||||
/>
|
||||
<span>{item.color.name}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.size && (
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Icons.Rule className="icon" />
|
||||
<span>{item.size.size}</span>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Link href="/checkout/cart" className="inline-block mt-6 text-sm text-sky-500">
|
||||
返回购物车
|
||||
</Link>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="section-divide-y lg:hidden" />
|
||||
|
||||
{/* cart info */}
|
||||
<section className="lg:border lg:border-gray-200 lg:rounded-md lg:h-fit">
|
||||
<CartInfo />
|
||||
<div className="px-3 py-2 space-y-3">
|
||||
<div className="flex items-center gap-x-2 ">
|
||||
<input
|
||||
type="radio"
|
||||
name="cash"
|
||||
id="cash"
|
||||
value="在线支付"
|
||||
checked={paymentMethod === '在线支付'}
|
||||
onChange={e => setPaymentMethod(e.target.value)}
|
||||
/>
|
||||
<label className="text-sm" htmlFor="cash">
|
||||
在线支付
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2 ">
|
||||
<input
|
||||
type="radio"
|
||||
name="zarinPal"
|
||||
id="zarinPal"
|
||||
value="银行卡"
|
||||
checked={paymentMethod === '银行卡'}
|
||||
onChange={e => setPaymentMethod(e.target.value)}
|
||||
/>
|
||||
<label className="text-sm" htmlFor="zarinPal">
|
||||
银行卡
|
||||
</label>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleCreateOrder}
|
||||
isLoading={isLoading}
|
||||
className="w-full max-w-5xl mx-auto"
|
||||
>
|
||||
完成购买
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShippingPage
|
|
@ -0,0 +1,33 @@
|
|||
'use client' // Error components must be Client Components
|
||||
|
||||
import { Button } from '@/components'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Error({ error, reset }) {
|
||||
useEffect(() => {
|
||||
// Log the error to an error reporting service
|
||||
console.error(error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="lg:px-3 container xl:mt-32">
|
||||
<div className="py-20 mx-auto space-y-3 text-center w-fit">
|
||||
<h5 className="text-xl">{error.name}</h5>
|
||||
<p className="text-lg text-red-500">出现异常,请检查您的地址是否有误,或者联系管理员</p>
|
||||
<Button
|
||||
className="mx-auto"
|
||||
onClick={
|
||||
// Attempt to recover by trying to re-render the segment
|
||||
() => {
|
||||
console.log('发送异常警报通知到OA系统', error.message)
|
||||
}
|
||||
}
|
||||
>
|
||||
通知我们
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { ClientLayout } from 'components'
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<ClientLayout>{children}</ClientLayout>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import { db } from '@/helpers'
|
||||
|
||||
import { Category, Banner, Slider } from 'models'
|
||||
|
||||
import {
|
||||
BannerOne,
|
||||
BannerTwo,
|
||||
BestSellsSlider,
|
||||
Categories,
|
||||
DiscountSlider,
|
||||
MostFavouraiteProducts,
|
||||
Slider as MainSlider,
|
||||
} from 'components'
|
||||
import { siteTitle } from '@/utils'
|
||||
|
||||
// export const revalidate = 20
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export const getData = async category => {
|
||||
await db.connect()
|
||||
|
||||
const currentCategory = await Category.findOne({
|
||||
slug: category,
|
||||
}).lean()
|
||||
|
||||
if (!currentCategory) return { notFound: true }
|
||||
|
||||
const sliders = await Slider.find({ category_id: currentCategory?._id }).lean()
|
||||
|
||||
const bannerOneType = await Banner.find({
|
||||
category_id: currentCategory?._id,
|
||||
type: 'one',
|
||||
}).lean()
|
||||
const bannerTwoType = await Banner.find({
|
||||
category_id: currentCategory?._id,
|
||||
type: 'two',
|
||||
}).lean()
|
||||
|
||||
const childCategories = await Category.find({
|
||||
parent: currentCategory?._id,
|
||||
}).lean()
|
||||
|
||||
await db.disconnect()
|
||||
return {
|
||||
currentCategory,
|
||||
sliders,
|
||||
bannerOneType,
|
||||
bannerTwoType,
|
||||
childCategories,
|
||||
}
|
||||
}
|
||||
|
||||
const MainCategory = async ({ params: { category } }) => {
|
||||
const { currentCategory, sliders, bannerOneType, bannerTwoType, childCategories } =
|
||||
await getData(category)
|
||||
|
||||
//? Render(s)
|
||||
return (
|
||||
<main className="container min-h-screen space-y-6 xl:mt-28">
|
||||
<div className="py-4 mx-auto space-y-12 xl:mt-28">
|
||||
<MainSlider data={sliders} />
|
||||
|
||||
<DiscountSlider currentCategory={currentCategory} />
|
||||
|
||||
<Categories
|
||||
childCategories={{ categories: childCategories, title: '所有分类' }}
|
||||
color={currentCategory.colors?.start}
|
||||
name={currentCategory.name}
|
||||
/>
|
||||
|
||||
<BannerOne data={bannerOneType} />
|
||||
|
||||
<BestSellsSlider categorySlug={currentCategory.slug} />
|
||||
|
||||
<BannerTwo data={bannerTwoType} />
|
||||
|
||||
<MostFavouraiteProducts categorySlug={currentCategory.slug} />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainCategory
|
||||
|
||||
export async function generateMetadata({ params: { category } }) {
|
||||
const { currentCategory, sliders, bannerOneType, bannerTwoType, childCategories } =
|
||||
await getData(category)
|
||||
|
||||
return {
|
||||
title: `${currentCategory.name} | ${siteTitle}`,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
// import { Metadata } from 'next'
|
||||
import { bannerRepo, categoryRepo, sliderRepo } from '@/helpers'
|
||||
import {
|
||||
BannerOne,
|
||||
BannerTwo,
|
||||
BestSellsSlider,
|
||||
Categories,
|
||||
DiscountSlider,
|
||||
Slider as MainSlider,
|
||||
MostFavouraiteProducts,
|
||||
} from '@/components'
|
||||
import { enSiteTitle, siteTitle } from '@/utils'
|
||||
|
||||
export const metadata = {
|
||||
title: `${siteTitle} | ${enSiteTitle}`,
|
||||
}
|
||||
|
||||
// export const revalidate = 20
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function Home({ searchParams }) {
|
||||
const currentCategory = await categoryRepo.getOne({
|
||||
parent: undefined,
|
||||
})
|
||||
const childCategories = await categoryRepo.getAll(
|
||||
{},
|
||||
{
|
||||
parent: currentCategory?._id,
|
||||
}
|
||||
)
|
||||
|
||||
const sliders = await sliderRepo.getAll({}, { category_id: currentCategory?._id })
|
||||
|
||||
const bannerOneType = await bannerRepo.getAll(
|
||||
{},
|
||||
{
|
||||
category_id: currentCategory?._id,
|
||||
type: 'one',
|
||||
}
|
||||
)
|
||||
const bannerTwoType = await bannerRepo.getAll(
|
||||
{},
|
||||
{
|
||||
category_id: currentCategory?._id,
|
||||
type: 'two',
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<main className="min-h-screen xl:mt-28 container space-y-24">
|
||||
<div className="py-4 mx-auto space-y-24 xl:mt-28">
|
||||
<MainSlider data={sliders} />
|
||||
<DiscountSlider currentCategory={currentCategory} />
|
||||
<Categories
|
||||
childCategories={{ categories: childCategories, title: '所有分类' }}
|
||||
color={currentCategory?.colors?.start}
|
||||
name={currentCategory?.name}
|
||||
homePage
|
||||
/>
|
||||
<BannerOne data={bannerOneType} />
|
||||
<BestSellsSlider categorySlug={currentCategory?.slug} />
|
||||
<BannerTwo data={bannerTwoType} />
|
||||
<MostFavouraiteProducts categorySlug={currentCategory?.slug} />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
import { Product } from 'models'
|
||||
|
||||
import {
|
||||
FreeShipping,
|
||||
Services,
|
||||
SmilarProductsSlider,
|
||||
ImageGallery,
|
||||
Description,
|
||||
Specification,
|
||||
Reviews,
|
||||
SelectColor,
|
||||
SelectSize,
|
||||
OutOfStock,
|
||||
AddToCart,
|
||||
Info,
|
||||
Breadcrumb,
|
||||
InitialStore,
|
||||
} from 'components'
|
||||
import { db } from '@/helpers'
|
||||
|
||||
export const getData = async params => {
|
||||
await db.connect()
|
||||
const product = await Product.findById({ _id: params?.id })
|
||||
.populate('category_levels.level_one')
|
||||
.populate('category_levels.level_two')
|
||||
.populate('category_levels.Level_three')
|
||||
.lean()
|
||||
|
||||
if (!product) return { notFound: true }
|
||||
|
||||
const productCategoryID = product.category.pop()
|
||||
|
||||
const smilarProducts = await Product.find({
|
||||
category: { $in: productCategoryID },
|
||||
inStock: { $gte: 1 },
|
||||
_id: { $ne: product._id },
|
||||
})
|
||||
.select(
|
||||
'-description -info -specification -category -category_levels -sizes -reviews -numReviews'
|
||||
)
|
||||
.limit(11)
|
||||
.lean()
|
||||
|
||||
await db.disconnect()
|
||||
return {
|
||||
product: JSON.parse(JSON.stringify(product)),
|
||||
smilarProducts: {
|
||||
title: '类似商品',
|
||||
products: JSON.parse(JSON.stringify(smilarProducts)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const SingleProduct = async ({ params }) => {
|
||||
const { product, smilarProducts } = await getData(params)
|
||||
|
||||
return (
|
||||
<main className="xl:mt-28 container mx-auto py-4 space-y-4">
|
||||
<InitialStore product={product} />
|
||||
<Breadcrumb categoryLevels={product.category_levels} />
|
||||
|
||||
<div className="h-fit lg:h-fit lg:grid lg:grid-cols-9 lg:px-4 lg:gap-x-2 lg:gap-y-4 lg:mb-10 xl:gap-x-7">
|
||||
<ImageGallery
|
||||
images={product.images}
|
||||
discount={product.discount}
|
||||
inStock={product.inStock}
|
||||
productName={product.title}
|
||||
/>
|
||||
<div className="lg:col-span-4 ">
|
||||
{/* title */}
|
||||
<h2 className="p-3 text-base font-semibold leading-8 tracking-wide text-black/80 ">
|
||||
{product.title}
|
||||
</h2>
|
||||
|
||||
<div className="section-divide-y" />
|
||||
|
||||
{product.inStock > 0 && product.colors.length > 0 && (
|
||||
<SelectColor colors={product.colors} />
|
||||
)}
|
||||
|
||||
{product.inStock > 0 && product.sizes.length > 0 && <SelectSize sizes={product.sizes} />}
|
||||
|
||||
{product.inStock === 0 && <OutOfStock />}
|
||||
|
||||
<Info infos={product?.info} />
|
||||
|
||||
<FreeShipping />
|
||||
</div>
|
||||
<div className="lg:col-span-2">
|
||||
{product.inStock > 0 && <AddToCart product={product} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Services />
|
||||
|
||||
{product.description.length > 0 && <Description description={product.description} />}
|
||||
|
||||
<SmilarProductsSlider smilarProducts={smilarProducts} />
|
||||
|
||||
<div className="section-divide-y" />
|
||||
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<Specification specification={product.specification} />
|
||||
|
||||
<div className="section-divide-y" />
|
||||
|
||||
<Reviews
|
||||
numReviews={product.numReviews}
|
||||
prdouctID={product._id}
|
||||
productTitle={product.title}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="hidden w-full px-3 lg:block lg:max-w-xs xl:max-w-sm">
|
||||
{product.inStock > 0 && <AddToCart product={product} second />}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default SingleProduct
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const { product, smilarProducts } = await getData(params)
|
||||
|
||||
return {
|
||||
title: `购买 ${product.title}`,
|
||||
description: `${product.title}`,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { siteTitle } from '@/utils'
|
||||
|
||||
export const metadata = {
|
||||
title: `分类 ${siteTitle}`,
|
||||
}
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return <>{children}</>
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
'use client'
|
||||
|
||||
import {
|
||||
ProductCard,
|
||||
Pagination,
|
||||
Sort,
|
||||
ProductsAside,
|
||||
SubCategories,
|
||||
Filter,
|
||||
ProductSkeleton,
|
||||
} from 'components'
|
||||
|
||||
import { useChangeRoute, useMediaQuery } from 'hooks'
|
||||
|
||||
import { useUrlQuery } from '@/hooks'
|
||||
import { useGetCategoriesQuery, useGetProductsQuery } from '@/store/services'
|
||||
|
||||
const ProductsHome = () => {
|
||||
//? Assets
|
||||
const query = useUrlQuery()
|
||||
|
||||
const category = query?.category?.toString() ?? ''
|
||||
const page_size = query?.page_size?.toString() ?? ''
|
||||
const page = query?.page?.toString() ?? ''
|
||||
const sort = query?.sort?.toString() ?? ''
|
||||
const search = query?.search?.toString() ?? ''
|
||||
const inStock = query?.inStock?.toString() ?? ''
|
||||
const discount = query?.discount?.toString() ?? ''
|
||||
const price = query?.price?.toString() ?? ''
|
||||
|
||||
const isDesktop = useMediaQuery('(min-width:1280px)')
|
||||
|
||||
//? Handlers
|
||||
const changeRoute = useChangeRoute()
|
||||
|
||||
const handleChangeRoute = newQueries => {
|
||||
changeRoute({
|
||||
...query,
|
||||
page: 1,
|
||||
...newQueries,
|
||||
})
|
||||
}
|
||||
|
||||
//? Querirs
|
||||
//* Get Products Data
|
||||
const { data, isFetching: isFetchingProduct } = useGetProductsQuery({
|
||||
category,
|
||||
page_size,
|
||||
page,
|
||||
sort,
|
||||
search,
|
||||
inStock,
|
||||
discount,
|
||||
price,
|
||||
})
|
||||
|
||||
//* Get childCategories Data
|
||||
const { isLoading: isLoadingCategories, childCategories } = useGetCategoriesQuery(undefined, {
|
||||
selectFromResult: ({ isLoading, data }) => {
|
||||
const currentCategory = data?.data?.categories.find(cat => cat.slug === query?.category)
|
||||
const childCategories = data?.data?.categories.filter(
|
||||
cat => cat.parent === currentCategory?._id
|
||||
)
|
||||
return { childCategories, isLoading }
|
||||
},
|
||||
})
|
||||
|
||||
//? Render(s)
|
||||
return (
|
||||
<>
|
||||
<main className="lg:px-3 container xl:mt-32">
|
||||
<SubCategories childCategories={childCategories} isLoading={isLoadingCategories} />
|
||||
|
||||
<div className="px-1 lg:flex lg:gap-x-0 xl:gap-x-3">
|
||||
<ProductsAside
|
||||
mainMaxPrice={data?.data?.mainMaxPrice}
|
||||
mainMinPrice={data?.data?.mainMinPrice}
|
||||
handleChangeRoute={handleChangeRoute}
|
||||
/>
|
||||
<div id="_products" className="w-full p-4 mt-3 ">
|
||||
{/* Filters & Sort */}
|
||||
<div className="divide-y-2 ">
|
||||
<div className="flex py-2 gap-x-3">
|
||||
{!isDesktop && (
|
||||
<Filter
|
||||
mainMaxPrice={data?.mainMaxPrice}
|
||||
mainMinPrice={data?.mainMinPrice}
|
||||
handleChangeRoute={handleChangeRoute}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Sort handleChangeRoute={handleChangeRoute} />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between py-2">
|
||||
<span>所有商品</span>
|
||||
<span className="">{data?.data?.productsLength} 件商品</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Products */}
|
||||
{isFetchingProduct ? (
|
||||
<ProductSkeleton />
|
||||
) : data && data?.data?.products.length > 0 ? (
|
||||
<section className="sm:grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
|
||||
{data?.data?.products.map(item => <ProductCard product={item} key={item._id} />)}
|
||||
</section>
|
||||
) : (
|
||||
<section className="text-center text-red-500 xl:border xl:border-gray-200 xl:rounded-md xl:py-4">
|
||||
没有找到商品
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data && data?.data?.productsLength > 10 && (
|
||||
<div className="py-4 mx-auto lg:max-w-5xl">
|
||||
<Pagination
|
||||
pagination={data?.data?.pagination}
|
||||
changeRoute={handleChangeRoute}
|
||||
section="_products"
|
||||
client
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductsHome
|
|
@ -0,0 +1,3 @@
|
|||
export default function Layout({ children }) {
|
||||
return <>{children}</>
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export const metadata = {
|
||||
title: '登录',
|
||||
}
|
||||
|
||||
export default function LoginLayout({ children }) {
|
||||
return <>{children}</>
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
'use client'
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { HandleResponse, LoginForm, Logo } from '@/components'
|
||||
|
||||
import { useLoginMutation } from '@/store/services'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { userLogin } from 'store'
|
||||
|
||||
export default function LoginPage() {
|
||||
//? Assets
|
||||
const dispatch = useDispatch()
|
||||
const { replace } = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const redirectTo = searchParams.get('redirectTo')
|
||||
|
||||
//? Login User
|
||||
const [login, { data, isSuccess, isError, isLoading, error }] = useLoginMutation()
|
||||
|
||||
//? Handlers
|
||||
const submitHander = async ({ email, password }) => {
|
||||
if (email && password) {
|
||||
await login({
|
||||
body: { email, password },
|
||||
})
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{/* Handle Login Response */}
|
||||
{(isSuccess || isError) && (
|
||||
<HandleResponse
|
||||
isError={isError}
|
||||
isSuccess={isSuccess}
|
||||
error={error?.data?.message}
|
||||
message={data?.message}
|
||||
onSuccess={() => {
|
||||
dispatch(userLogin(data.data.token))
|
||||
replace(redirectTo || '/')
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<main className="grid items-center min-h-screen">
|
||||
<section className="container max-w-md px-12 py-6 space-y-6 lg:border lg:border-gray-100 lg:rounded-lg lg:shadow">
|
||||
<Link passHref href="/">
|
||||
<Logo className="mx-auto w-48 h-24" />
|
||||
</Link>
|
||||
<h1>
|
||||
<font className="">
|
||||
<font>登录</font>
|
||||
</font>
|
||||
</h1>
|
||||
<LoginForm isLoading={isLoading} onSubmit={submitHander} />
|
||||
<div className="text-xs">
|
||||
<p className="inline mr-2 text-gray-800 text-xs">我还没有账户</p>
|
||||
<Link href="/register" className="text-blue-400 text-xs">
|
||||
去注册
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export const metadata = {
|
||||
title: '注册',
|
||||
}
|
||||
|
||||
export default function LoginLayout({ children }) {
|
||||
return <>{children}</>
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
'use client'
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import { registerSchema } from 'utils'
|
||||
|
||||
import { TextField, LoginBtn, HandleResponse, RedirectToLogin, Logo } from '@/components'
|
||||
|
||||
import { useCreateUserMutation } from '@/store/services'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { userLogin } from 'store'
|
||||
|
||||
import { useDisclosure } from '@/hooks'
|
||||
|
||||
export default function RegisterPage() {
|
||||
//? Assets
|
||||
const [isShowRedirectModal, redirectModalHandlers] = useDisclosure()
|
||||
const dispatch = useDispatch()
|
||||
const { replace } = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const redirectTo = searchParams.get('redirectTo')
|
||||
|
||||
//? Create User
|
||||
const [createUser, { data, isSuccess, isError, isLoading, error }] = useCreateUserMutation()
|
||||
|
||||
//? Form Hook
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { errors: formErrors },
|
||||
reset,
|
||||
setFocus,
|
||||
control,
|
||||
} = useForm({
|
||||
resolver: yupResolver(registerSchema),
|
||||
defaultValues: { name: '', email: '', password: '', confirmPassword: '' },
|
||||
})
|
||||
|
||||
//? Focus On Mount
|
||||
useEffect(() => {
|
||||
setFocus('name')
|
||||
}, [])
|
||||
|
||||
//? Handlers
|
||||
const submitHander = async ({ name, email, password }) => {
|
||||
if (name && email && password) {
|
||||
await createUser({
|
||||
body: { name, email, password },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onError = () => {
|
||||
if (error.status === 422) {
|
||||
redirectModalHandlers.open()
|
||||
}
|
||||
}
|
||||
|
||||
const onSuccess = () => {
|
||||
dispatch(userLogin(data.data.token))
|
||||
reset()
|
||||
replace(redirectTo || '/')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RedirectToLogin
|
||||
title="注册异常"
|
||||
text={error?.data?.message}
|
||||
onClose={redirectModalHandlers.close}
|
||||
isShow={isShowRedirectModal}
|
||||
/>
|
||||
{/* Handle Login Response */}
|
||||
{(isSuccess || isError) && (
|
||||
<HandleResponse
|
||||
isError={isError}
|
||||
isSuccess={isSuccess}
|
||||
error={error?.data?.message}
|
||||
message={data?.message}
|
||||
onSuccess={onSuccess}
|
||||
onError={onError}
|
||||
/>
|
||||
)}
|
||||
<main className="grid items-center min-h-screen">
|
||||
<section className="container max-w-md px-12 py-6 space-y-6 lg:border lg:border-gray-100 lg:rounded-lg lg:shadow">
|
||||
<Link passHref href="/">
|
||||
<Logo className="mx-auto w-48 h-24" />
|
||||
</Link>
|
||||
<h1>
|
||||
<font className="">
|
||||
<font>注册</font>
|
||||
</font>
|
||||
</h1>
|
||||
<form className="space-y-4" onSubmit={handleSubmit(submitHander)} autoComplete="off">
|
||||
<TextField
|
||||
errors={formErrors.name}
|
||||
placeholder="请输入您的账户名称"
|
||||
name="name"
|
||||
control={control}
|
||||
/>
|
||||
<TextField
|
||||
errors={formErrors.email}
|
||||
placeholder="请输入您的账户邮箱"
|
||||
name="email"
|
||||
control={control}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
errors={formErrors.password}
|
||||
type="password"
|
||||
placeholder="请输入您的账户密码"
|
||||
name="password"
|
||||
control={control}
|
||||
/>
|
||||
<TextField
|
||||
control={control}
|
||||
errors={formErrors.confirmPassword}
|
||||
type="password"
|
||||
placeholder="确认密码,请再次输入"
|
||||
name="confirmPassword"
|
||||
/>
|
||||
<LoginBtn isLoading={isLoading}>注册</LoginBtn>
|
||||
</form>
|
||||
<div className="text-xs">
|
||||
<p className="inline mr-2 text-gray-800 text-xs">我已经有账户了</p>
|
||||
<Link href="/login" className="text-blue-400 text-xs">
|
||||
去登录
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import { BannerForm, HandleResponse, PageContainer } from 'components'
|
||||
|
||||
import { SubmitHandler } from 'react-hook-form'
|
||||
|
||||
import { useCreateBannerMutation } from '@/store/services'
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
|
||||
const CreateBannerPage = () => {
|
||||
useTitle('新增banner')
|
||||
//? Assets
|
||||
const { back } = useRouter()
|
||||
const query = useUrlQuery()
|
||||
const categoryId = query?.category_id
|
||||
|
||||
//? Queries
|
||||
//* Create Banner
|
||||
const [createBanner, { data, isSuccess, isLoading, error, isError }] = useCreateBannerMutation()
|
||||
|
||||
//? Handlers
|
||||
const createHandler = data => {
|
||||
const { image, isPublic, title, type, uri } = data
|
||||
createBanner({
|
||||
body: { category_id: categoryId, image, isPublic, title, type, uri },
|
||||
})
|
||||
}
|
||||
|
||||
const onSuccess = () => back()
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isSuccess || isError) && (
|
||||
<HandleResponse
|
||||
isError={isError}
|
||||
isSuccess={isSuccess}
|
||||
error={error?.data?.message}
|
||||
message={data?.message}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main>
|
||||
<PageContainer title="新增banner">
|
||||
<BannerForm mode="create" isLoadingCreate={isLoading} createHandler={createHandler} />
|
||||
</PageContainer>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateBannerPage
|
|
@ -0,0 +1,181 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import {
|
||||
BannerForm,
|
||||
BigLoading,
|
||||
ConfirmDeleteModal,
|
||||
ConfirmUpdateModal,
|
||||
HandleResponse,
|
||||
PageContainer,
|
||||
} from 'components'
|
||||
|
||||
import { useDisclosure } from 'hooks'
|
||||
|
||||
import {
|
||||
useDeleteBannerMutation,
|
||||
useGetSingleBannerQuery,
|
||||
useUpdateBannerMutation,
|
||||
} from '@/store/services'
|
||||
|
||||
import { SubmitHandler } from 'react-hook-form'
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
|
||||
const EditBannerPage = ({ params: { id: bannerId } }) => {
|
||||
//? Assets
|
||||
const { back } = useRouter()
|
||||
const query = useUrlQuery()
|
||||
const bannerName = query?.banner_name
|
||||
|
||||
const initialUpdataInfo = {}
|
||||
|
||||
//? Modals
|
||||
const [isShowConfirmUpdateModal, confirmUpdateModalHandlers] = useDisclosure()
|
||||
const [isShowConfirmDeleteModal, confirmDeleteModalHandlers] = useDisclosure()
|
||||
|
||||
//? States
|
||||
const [updateInfo, setUpdateInfo] = useState(initialUpdataInfo)
|
||||
|
||||
//? Queries
|
||||
//* Get Banner
|
||||
const { data: selectedBanner, isLoading: isLoadingGetSelectedBanner } = useGetSingleBannerQuery({
|
||||
id: bannerId,
|
||||
})
|
||||
|
||||
//* Update Banner
|
||||
const [
|
||||
updateBanner,
|
||||
{
|
||||
data: dataUpdate,
|
||||
isSuccess: isSuccessUpdate,
|
||||
isError: isErrorUpdate,
|
||||
error: errorUpdate,
|
||||
isLoading: isLoadingUpdate,
|
||||
},
|
||||
] = useUpdateBannerMutation()
|
||||
|
||||
//* Delete Banner
|
||||
const [
|
||||
deleteBanner,
|
||||
{
|
||||
isSuccess: isSuccessDelete,
|
||||
isError: isErrorDelete,
|
||||
error: errorDelete,
|
||||
data: dataDelete,
|
||||
isLoading: isLoadingDelete,
|
||||
},
|
||||
] = useDeleteBannerMutation()
|
||||
|
||||
//? Handlers
|
||||
//* Update
|
||||
const updateHandler = data => {
|
||||
setUpdateInfo(prev => ({ ...prev, ...selectedBanner.data, ...data }))
|
||||
confirmUpdateModalHandlers.open()
|
||||
}
|
||||
|
||||
const onConfirmUpdate = () => {
|
||||
updateBanner({
|
||||
id: bannerId,
|
||||
body: updateInfo,
|
||||
})
|
||||
}
|
||||
|
||||
const onCancelUpdate = () => {
|
||||
setUpdateInfo(initialUpdataInfo)
|
||||
confirmUpdateModalHandlers.close()
|
||||
}
|
||||
|
||||
const onSuccessUpdate = () => {
|
||||
setUpdateInfo(initialUpdataInfo)
|
||||
confirmUpdateModalHandlers.close()
|
||||
back()
|
||||
}
|
||||
|
||||
const onErrorUpdate = () => {
|
||||
setUpdateInfo(initialUpdataInfo)
|
||||
confirmUpdateModalHandlers.close()
|
||||
}
|
||||
|
||||
//* Delete
|
||||
const deleteHandler = () => confirmDeleteModalHandlers.open()
|
||||
|
||||
const onConfirmDelete = () => deleteBanner({ id: bannerId })
|
||||
|
||||
const onCancelDelete = () => confirmDeleteModalHandlers.close()
|
||||
|
||||
const onSuccessDelete = () => {
|
||||
confirmDeleteModalHandlers.close()
|
||||
back()
|
||||
}
|
||||
|
||||
const onErrorDelete = () => confirmDeleteModalHandlers.close()
|
||||
|
||||
useTitle('编辑banner' + ' ' + bannerName)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmDeleteModal
|
||||
title="banner"
|
||||
isLoading={isLoadingDelete}
|
||||
isShow={isShowConfirmDeleteModal}
|
||||
onClose={confirmDeleteModalHandlers.close}
|
||||
onCancel={onCancelDelete}
|
||||
onConfirm={onConfirmDelete}
|
||||
/>
|
||||
|
||||
{(isSuccessDelete || isErrorDelete) && (
|
||||
<HandleResponse
|
||||
isError={isErrorDelete}
|
||||
isSuccess={isSuccessDelete}
|
||||
error={errorDelete?.data?.message}
|
||||
message={dataDelete?.message}
|
||||
onSuccess={onSuccessDelete}
|
||||
onError={onErrorDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmUpdateModal
|
||||
title="banner"
|
||||
isLoading={isLoadingUpdate}
|
||||
isShow={isShowConfirmUpdateModal}
|
||||
onClose={confirmUpdateModalHandlers.close}
|
||||
onCancel={onCancelUpdate}
|
||||
onConfirm={onConfirmUpdate}
|
||||
/>
|
||||
|
||||
{(isSuccessUpdate || isErrorUpdate) && (
|
||||
<HandleResponse
|
||||
isError={isErrorUpdate}
|
||||
isSuccess={isSuccessUpdate}
|
||||
error={errorUpdate?.data?.message}
|
||||
message={dataUpdate?.message}
|
||||
onSuccess={onSuccessUpdate}
|
||||
onError={onErrorUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main>
|
||||
<PageContainer title={'编辑banner' + ' ' + bannerName}>
|
||||
{isLoadingGetSelectedBanner ? (
|
||||
<div className="px-3 py-20">
|
||||
<BigLoading />
|
||||
</div>
|
||||
) : selectedBanner.data ? (
|
||||
<BannerForm
|
||||
mode="edit"
|
||||
selectedBanner={selectedBanner.data}
|
||||
updateHandler={updateHandler}
|
||||
isLoadingDelete={isLoadingDelete}
|
||||
isLoadingUpdate={isLoadingUpdate}
|
||||
deleteHandler={deleteHandler}
|
||||
/>
|
||||
) : null}
|
||||
</PageContainer>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditBannerPage
|
|
@ -0,0 +1,134 @@
|
|||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useGetBannersQuery, useGetCategoriesQuery } from '@/store/services'
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
import { ResponsiveImage, EmptyCustomList, PageContainer, TableSkeleton } from '@/components'
|
||||
|
||||
const BannersPage = () => {
|
||||
const query = useUrlQuery()
|
||||
|
||||
const category_id = query?.category_id
|
||||
const category_name = query?.category_name
|
||||
|
||||
//? Get Categories
|
||||
const { categories, isLoading: isLodingGetCategories } = useGetCategoriesQuery(undefined, {
|
||||
selectFromResult: ({ data, isLoading }) => ({
|
||||
categories: data?.data?.categories
|
||||
.filter(category => category.level < 2)
|
||||
.sort((a, b) => a.level - b.level),
|
||||
isLoading,
|
||||
}),
|
||||
skip: !!category_id,
|
||||
})
|
||||
|
||||
const { data: banners, isLoading: isLoading_get_banners } = useGetBannersQuery(
|
||||
{ category: category_id },
|
||||
{
|
||||
skip: !!!category_id,
|
||||
}
|
||||
)
|
||||
|
||||
//? Render(s)
|
||||
const title = category_name ? `banner管理 - ${category_name}` : 'banner管理'
|
||||
useTitle(title)
|
||||
const renderContent = () => {
|
||||
if (isLoading_get_banners || isLodingGetCategories) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan="4">
|
||||
<TableSkeleton />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
if (categories && !category_id) {
|
||||
return categories.map(category => (
|
||||
<tr
|
||||
className="text-xs text-center transition-colors border-b border-gray-100 md:text-sm hover:bg-gray-50/50"
|
||||
key={category._id}
|
||||
>
|
||||
<td className="w-3/4 px-2 py-4">{category.name}</td>
|
||||
<td className="px-2 py-4">
|
||||
<Link
|
||||
href={`/admin/banners?category_id=${category._id}&category_name=${category.name}`}
|
||||
className="bg-rose-50 text-rose-500 rounded-sm py-1 px-1.5 mx-1.5 inline-block"
|
||||
>
|
||||
子集
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
|
||||
if (banners?.data && banners?.data?.length > 0) {
|
||||
return banners?.data.map(banner => (
|
||||
<tr
|
||||
className="text-xs text-center transition-colors border-b border-gray-100 md:text-sm hover:bg-gray-50/50"
|
||||
key={banner._id}
|
||||
>
|
||||
<td className="px-2 py-4">
|
||||
<ResponsiveImage
|
||||
dimensions={`h-7 ${banner.type === 'one' ? 'w-16' : 'w-10'}`}
|
||||
className="mx-auto"
|
||||
src={banner.image?.url}
|
||||
alt=""
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-4">{banner.title}</td>
|
||||
<td className="px-2 py-4">{banner.type}</td>
|
||||
<td className="px-2 py-4">
|
||||
<Link
|
||||
href={`/admin/banners/edit/${banner._id}?banner_name=${banner.title}`}
|
||||
className="bg-rose-50 text-rose-500 rounded-sm py-1 px-1.5 mx-1.5 inline-block"
|
||||
>
|
||||
编辑
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
} else
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan="4">
|
||||
<EmptyCustomList />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<PageContainer title={title}>
|
||||
<section className="p-3 mx-auto mb-10 space-y-8">
|
||||
{category_id && (
|
||||
<Link
|
||||
href={`banners/create?category_id=${category_id}&category_name=${category_name}`}
|
||||
className="flex items-center px-3 py-2 text-red-600 border-2 border-red-600 rounded-lg max-w-max gap-x-3"
|
||||
>
|
||||
添加新banner
|
||||
</Link>
|
||||
)}
|
||||
<div className="mx-3 overflow-x-auto mt-7 lg:mx-5 xl:mx-10">
|
||||
<table className="w-full whitespace-nowrap">
|
||||
<thead className="h-9 bg-emerald-50">
|
||||
<tr className="text-emerald-500">
|
||||
{category_name && <th className="border-gray-100 border-x-2">图片</th>}
|
||||
<th className="px-2 border-gray-100 border-x-2">
|
||||
{category_name ? 'banner标题' : '分类名称'}
|
||||
</th>
|
||||
{category_name && <th className="border-gray-100 border-x-2">类型</th>}
|
||||
<th className="border-gray-100 border-x-2">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-600">{renderContent()}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</PageContainer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default BannersPage
|
|
@ -0,0 +1,77 @@
|
|||
'use client'
|
||||
|
||||
import { BigLoading, CategoryForm, HandleResponse, PageContainer } from '@/components'
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
import { useCreateCategoryMutation, useGetCategoriesQuery } from '@/store/services'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
export default function CategoriesCreatePage() {
|
||||
useTitle('创建分类')
|
||||
//? Assets
|
||||
const { push } = useRouter()
|
||||
const query = useUrlQuery()
|
||||
const parentId = query.parent_id
|
||||
const parentLvl = query.parent_lvl ? +query.parent_lvl : 0
|
||||
|
||||
//? Queries
|
||||
//* Get Categories
|
||||
const { isLoading: isLoading_get, parentCategory } = useGetCategoriesQuery(undefined, {
|
||||
selectFromResult: ({ data, isLoading }) => ({
|
||||
parentCategory: data?.data?.categories.find(category => category._id === parentId),
|
||||
isLoading,
|
||||
}),
|
||||
})
|
||||
//* Create Category
|
||||
const [createCtegory, { data, isSuccess, isLoading, error, isError }] =
|
||||
useCreateCategoryMutation()
|
||||
|
||||
//? Handlers
|
||||
const createHandler = data => {
|
||||
const { name, slug, image, colors } = data
|
||||
createCtegory({
|
||||
body: {
|
||||
name,
|
||||
parent: parentId || '',
|
||||
slug: slug.trim().split(' ').join('-'),
|
||||
image,
|
||||
colors,
|
||||
level: parentCategory ? parentCategory?.level + 1 : 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const onSuccess = () => {
|
||||
push(`/admin/categories${parentId ? `?parent_id=${parentId}` : ''}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isSuccess || isError) && (
|
||||
<HandleResponse
|
||||
isError={isError}
|
||||
isSuccess={isSuccess}
|
||||
error={error?.data?.message}
|
||||
message={data?.data?.message}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isLoading_get ? (
|
||||
<div className="px-3 py-20">
|
||||
<BigLoading />
|
||||
</div>
|
||||
) : (
|
||||
<main>
|
||||
<PageContainer title="创建分类">
|
||||
<CategoryForm
|
||||
mode="create"
|
||||
isLoading={isLoading}
|
||||
parentLvl={parentLvl}
|
||||
createHandler={createHandler}
|
||||
/>
|
||||
</PageContainer>
|
||||
</main>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
'use client'
|
||||
|
||||
import {
|
||||
CategoryForm,
|
||||
HandleResponse,
|
||||
PageContainer,
|
||||
ConfirmUpdateModal,
|
||||
BigLoading,
|
||||
} from '@/components'
|
||||
import { useDisclosure, useTitle, useUrlQuery } from '@/hooks'
|
||||
import { useGetCategoriesQuery, useUpdateCategoryMutation } from '@/store/services'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function CategoriesEditPage({ params: { id } }) {
|
||||
useTitle('编辑分类')
|
||||
//? Assets
|
||||
const { push } = useRouter()
|
||||
const query = useUrlQuery()
|
||||
const parentId = query.parent_id
|
||||
const parentLvl = query.parent_lvl ? +query.parent_lvl : 0
|
||||
|
||||
//? Modals
|
||||
const [isShowConfirmUpdateModal, confirmUpdateModalHandlers] = useDisclosure()
|
||||
|
||||
//? States
|
||||
const [updateInfo, setUpdateInfo] = useState({})
|
||||
|
||||
//? Queries
|
||||
//* Get Categories
|
||||
const { isLoading: isLoading_get, selectedCategory } = useGetCategoriesQuery(undefined, {
|
||||
selectFromResult: ({ data, isLoading }) => ({
|
||||
selectedCategory: data?.data?.categories.find(category => category._id === id),
|
||||
isLoading,
|
||||
}),
|
||||
})
|
||||
|
||||
//* Update Category
|
||||
const [
|
||||
updateCategory,
|
||||
{
|
||||
data: data_update,
|
||||
isSuccess: isSuccess_update,
|
||||
isError: isError_update,
|
||||
error: error_update,
|
||||
isLoading: isLoading_update,
|
||||
},
|
||||
] = useUpdateCategoryMutation()
|
||||
|
||||
//? Handlers
|
||||
const updateHandler = data => {
|
||||
setUpdateInfo(prev => ({ ...prev, ...selectedCategory, ...data }))
|
||||
confirmUpdateModalHandlers.open()
|
||||
}
|
||||
|
||||
const onConfirm = () => {
|
||||
updateCategory({
|
||||
id,
|
||||
body: updateInfo,
|
||||
})
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setUpdateInfo({})
|
||||
confirmUpdateModalHandlers.close()
|
||||
}
|
||||
|
||||
const onSuccess = () => {
|
||||
setUpdateInfo({})
|
||||
confirmUpdateModalHandlers.close()
|
||||
push(`/admin/categories${parentId ? `?parent_id=${parentId}` : ''}`)
|
||||
}
|
||||
|
||||
const onError = () => {
|
||||
setUpdateInfo({})
|
||||
confirmUpdateModalHandlers.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Handle Update Category Response */}
|
||||
{(isSuccess_update || isError_update) && (
|
||||
<HandleResponse
|
||||
isError={isError_update}
|
||||
isSuccess={isSuccess_update}
|
||||
error={error_update?.data?.message}
|
||||
message={data_update?.message}
|
||||
onSuccess={onSuccess}
|
||||
onError={onError}
|
||||
/>
|
||||
)}
|
||||
<ConfirmUpdateModal
|
||||
title="分类"
|
||||
isLoading={isLoading_update}
|
||||
isShow={isShowConfirmUpdateModal}
|
||||
onClose={confirmUpdateModalHandlers.close}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
<main>
|
||||
<PageContainer title="编辑分类">
|
||||
{isLoading_get ? (
|
||||
<div className="px-3 py-20">
|
||||
<BigLoading />
|
||||
</div>
|
||||
) : selectedCategory ? (
|
||||
<CategoryForm
|
||||
mode="edit"
|
||||
isLoading={isLoading_update}
|
||||
selectedCategory={selectedCategory}
|
||||
updateHandler={updateHandler}
|
||||
/>
|
||||
) : null}
|
||||
</PageContainer>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
'use client'
|
||||
|
||||
import { BigLoading, PageContainer } from '@/components'
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
import { useGetCategoriesQuery } from '@/store/services'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function CategoriesPage() {
|
||||
useTitle('分类管理')
|
||||
const query = useUrlQuery()
|
||||
const parentId = query.parent_id
|
||||
const parentLvl = query.parent_lvl
|
||||
//? Get Categories Data
|
||||
const { childCategories, isLoading } = useGetCategoriesQuery(undefined, {
|
||||
selectFromResult: ({ data, isLoading }) => {
|
||||
return {
|
||||
childCategories: data?.data?.categories?.filter(category => category.parent === parentId),
|
||||
isLoading,
|
||||
}
|
||||
},
|
||||
})
|
||||
//? Render(s)
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="px-3 py-20">
|
||||
<BigLoading />
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<PageContainer title="分类管理">
|
||||
<section className="p-3">
|
||||
<div className="space-y-8 text-white">
|
||||
<div className="flex justify-between">
|
||||
{childCategories && childCategories[0]?.level !== 0 ? (
|
||||
<Link
|
||||
href={`categories/create${parentId ? `?parent_id=${parentId}` : ''}&${
|
||||
parentLvl ? `parent_lvl=${parentLvl}` : ''
|
||||
}`}
|
||||
className="flex items-center px-3 py-2 text-red-600 border-2 border-red-600 rounded-lg max-w-max gap-x-3"
|
||||
>
|
||||
添加新文件夹
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<Link
|
||||
href="/admin/categories/tree"
|
||||
className="flex items-center px-3 py-2 text-red-600 border-2 border-red-600 rounded-lg max-w-max gap-x-3"
|
||||
>
|
||||
图表展示
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className=" overflow-x-auto mt-7 ">
|
||||
<table className="w-full whitespace-nowrap">
|
||||
<thead className="h-9 bg-emerald-50">
|
||||
<tr className="text-emerald-500">
|
||||
<th className="px-2 border-gray-100 border-x-2">分类名称</th>
|
||||
<th className="border-gray-100 border-x-2">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-600">
|
||||
{childCategories && childCategories.length > 0 ? (
|
||||
childCategories?.map(category => (
|
||||
<tr
|
||||
className="text-xs text-center transition-colors border-b border-gray-100 md:text-sm hover:bg-gray-50/50 "
|
||||
key={category._id}
|
||||
>
|
||||
<td className="w-2/4 px-2 py-4">{category.name}</td>
|
||||
<td className=" gap-3 px-2 py-4">
|
||||
{category.level !== 3 && (
|
||||
<Link
|
||||
href={`/admin/categories?parent_id=${category._id}&parent_lvl=${category.level}`}
|
||||
className="bg-green-50 text-green-500 rounded-sm py-1 px-1.5 max-w-min"
|
||||
>
|
||||
子类
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
href={`/admin/categories/edit/${category._id}?${
|
||||
parentId ? `parent_id=${parentId}` : ''
|
||||
}&${parentLvl ? `parent_lvl=${parentLvl}` : ''}`}
|
||||
className="bg-amber-50 text-amber-500 rounded-sm py-1 px-1.5 max-w-min"
|
||||
>
|
||||
编辑
|
||||
</Link>
|
||||
{category.level === 2 && (
|
||||
<Link
|
||||
href={`/admin/details/${category._id}?category_name=${category.name}`}
|
||||
className="bg-blue-50 text-blue-500 rounded-sm py-1 px-1.5 max-w-min"
|
||||
>
|
||||
规格和特点
|
||||
</Link>
|
||||
)}
|
||||
{category.level < 2 && (
|
||||
<>
|
||||
<Link
|
||||
href={`/admin/sliders?category_id=${category._id}&category_name=${category.name}`}
|
||||
className="bg-fuchsia-50 text-fuchsia-500 rounded-sm py-1 px-1.5 max-w-min"
|
||||
>
|
||||
滑块
|
||||
</Link>
|
||||
<Link
|
||||
href={`/admin/banners?category_id=${category._id}&category_name=${category.name}`}
|
||||
className="bg-rose-50 text-rose-500 rounded-sm py-1 px-1.5 max-w-min"
|
||||
>
|
||||
横幅
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td>
|
||||
<p className="py-4 text-sm text-center text-red-700">还没有分类</p>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PageContainer>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
'use client'
|
||||
|
||||
import { BigLoading, PageContainer } from 'components'
|
||||
|
||||
import { useGetCategoriesQuery } from '@/store/services'
|
||||
import { useTitle } from '@/hooks'
|
||||
|
||||
export default function CategoriesTreePage() {
|
||||
useTitle('分类图表')
|
||||
//? Get Categories Data
|
||||
const { categoriesList, isLoading } = useGetCategoriesQuery(undefined, {
|
||||
selectFromResult: ({ data, isLoading }) => ({
|
||||
categoriesList: data?.data?.categoriesList,
|
||||
isLoading,
|
||||
}),
|
||||
})
|
||||
|
||||
//? Render(s)
|
||||
|
||||
return (
|
||||
<main>
|
||||
{isLoading ? (
|
||||
<div className="px-3 py-20">
|
||||
<BigLoading />
|
||||
</div>
|
||||
) : (
|
||||
<PageContainer title="分类图表">
|
||||
<section className="p-3">
|
||||
<div className="space-y-8 text-white">
|
||||
<div className="flex text-gray-600 gap-x-3">
|
||||
<p className="flex items-center text-sm gap-x-1">
|
||||
<span className="inline-block w-6 h-6 bg-red-500 rounded-md" />
|
||||
一级
|
||||
</p>
|
||||
<p className="flex items-center text-sm gap-x-1">
|
||||
<span className="inline-block w-6 h-6 bg-green-500 rounded-md" />
|
||||
二级
|
||||
</p>
|
||||
<p className="flex items-center text-sm gap-x-1">
|
||||
<span className="inline-block w-6 h-6 bg-blue-500 rounded-md" />
|
||||
三级
|
||||
</p>
|
||||
</div>
|
||||
<ul className="space-y-8">
|
||||
{categoriesList &&
|
||||
categoriesList.children?.map(mainCategory => (
|
||||
<li
|
||||
key={mainCategory._id}
|
||||
className="p-2 border border-gray-100 rounded-md shadow"
|
||||
>
|
||||
<div className="p-2 text-center bg-red-500 rounded">{mainCategory.name}</div>
|
||||
<ul className="flex flex-wrap gap-x-4">
|
||||
{mainCategory.children.map(parentCategory => (
|
||||
<li key={parentCategory._id} className="flex-1">
|
||||
<div className="p-2 mt-2 text-center bg-green-500 rounded">
|
||||
{parentCategory.name}
|
||||
</div>
|
||||
<ul className="flex flex-wrap gap-x-4">
|
||||
{parentCategory.children.map(childCategory => (
|
||||
<li key={childCategory._id} className="flex-1">
|
||||
<div className="flex-1 p-2 mt-2 text-center bg-blue-500 rounded">
|
||||
{childCategory.name}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</PageContainer>
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,380 @@
|
|||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import { showAlert } from 'store'
|
||||
|
||||
import {
|
||||
useCreateDetailsMutation,
|
||||
useDeleteDetailsMutation,
|
||||
useGetDetailsQuery,
|
||||
useUpdateDetailsMutation,
|
||||
} from '@/store/services'
|
||||
|
||||
import {
|
||||
BigLoading,
|
||||
Button,
|
||||
ConfirmDeleteModal,
|
||||
ConfirmUpdateModal,
|
||||
DetailsList,
|
||||
HandleResponse,
|
||||
PageContainer,
|
||||
} from 'components'
|
||||
import { Tab } from '@headlessui/react'
|
||||
|
||||
import { useAppDispatch, useDisclosure } from 'hooks'
|
||||
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
|
||||
const tabListNames = [
|
||||
{ id: 0, name: '选择类型' },
|
||||
{ id: 1, name: '属性' },
|
||||
{ id: 2, name: '规格' },
|
||||
]
|
||||
|
||||
const DetailsContentPage = ({ params: { id } }) => {
|
||||
//? Assets
|
||||
const { back } = useRouter()
|
||||
const query = useUrlQuery()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const categoryId = id
|
||||
const categoryName = query.category_name
|
||||
|
||||
const initialUpdataInfo = {}
|
||||
|
||||
//? Modals
|
||||
const [isShowConfirmDeleteModal, confirmDeleteModalHandlers] = useDisclosure()
|
||||
const [isShowConfirmUpdateModal, confirmUpdateModalHandlers] = useDisclosure()
|
||||
|
||||
//? States
|
||||
const [updateInfo, setUpdateInfo] = useState(initialUpdataInfo)
|
||||
|
||||
const [mode, setMode] = useState('create')
|
||||
|
||||
//? Queries
|
||||
//* Get Details
|
||||
const { data: details, isLoading: isLoadingGet } = useGetDetailsQuery({
|
||||
id: categoryId,
|
||||
})
|
||||
|
||||
//* Update Details
|
||||
const [
|
||||
updateDetails,
|
||||
{
|
||||
data: dataUpdate,
|
||||
isSuccess: isSuccessUpdate,
|
||||
isError: isErrorUpdate,
|
||||
error: errorUpdate,
|
||||
isLoading: isLoadingUpdate,
|
||||
},
|
||||
] = useUpdateDetailsMutation()
|
||||
|
||||
//* Create Details
|
||||
const [
|
||||
createDetails,
|
||||
{
|
||||
data: dataCreate,
|
||||
isSuccess: isSuccessCreate,
|
||||
isError: isErrorCreate,
|
||||
isLoading: isLoadingCreate,
|
||||
error: errorCreate,
|
||||
},
|
||||
] = useCreateDetailsMutation()
|
||||
|
||||
//* Delete Details
|
||||
const [
|
||||
deleteDetails,
|
||||
{
|
||||
isSuccess: isSuccessDelete,
|
||||
isError: isErrorDelete,
|
||||
error: errorDelete,
|
||||
data: dataDelete,
|
||||
isLoading: isLoadingDelete,
|
||||
},
|
||||
] = useDeleteDetailsMutation()
|
||||
|
||||
//? Hook Form
|
||||
const { handleSubmit, register, reset, control } = useForm({
|
||||
defaultValues: {
|
||||
optionsType: 'none',
|
||||
info: [],
|
||||
specification: [],
|
||||
},
|
||||
})
|
||||
|
||||
//? Re-Renders
|
||||
useEffect(() => {
|
||||
if (details?.data) {
|
||||
setMode('edit')
|
||||
reset({
|
||||
optionsType: details?.data?.optionsType,
|
||||
info: details?.data?.info,
|
||||
specification: details?.data?.specification,
|
||||
})
|
||||
}
|
||||
}, [details])
|
||||
|
||||
//? Handlers
|
||||
//* Create
|
||||
const createHandler = async ({ info, specification, optionsType }) => {
|
||||
if (info.length !== 0 && specification.length !== 0) {
|
||||
await createDetails({
|
||||
body: {
|
||||
category_id: categoryId,
|
||||
info,
|
||||
specification,
|
||||
optionsType,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
dispatch(
|
||||
showAlert({
|
||||
status: 'error',
|
||||
title: '请输入详细信息和属性',
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//* Update
|
||||
const updateHandler = ({ info, specification, optionsType }) => {
|
||||
setUpdateInfo(prev => ({
|
||||
...prev,
|
||||
...details?.data,
|
||||
info,
|
||||
specification,
|
||||
optionsType,
|
||||
}))
|
||||
|
||||
confirmUpdateModalHandlers.open()
|
||||
}
|
||||
|
||||
const onConfirmUpdate = () => {
|
||||
updateDetails({
|
||||
id: details?.data?._id,
|
||||
body: updateInfo,
|
||||
})
|
||||
}
|
||||
|
||||
const onCancelUpdate = () => {
|
||||
setUpdateInfo(initialUpdataInfo)
|
||||
confirmUpdateModalHandlers.close()
|
||||
}
|
||||
|
||||
const onSuccessUpdate = () => {
|
||||
setUpdateInfo(initialUpdataInfo)
|
||||
confirmUpdateModalHandlers.close()
|
||||
}
|
||||
|
||||
const onErrorUpdate = () => {
|
||||
setUpdateInfo(initialUpdataInfo)
|
||||
confirmUpdateModalHandlers.close()
|
||||
}
|
||||
|
||||
//* Delete
|
||||
const deleteHandler = () => confirmDeleteModalHandlers.open()
|
||||
|
||||
const onConfirmDelete = () => deleteDetails({ id: details?.data?._id })
|
||||
|
||||
const onCancelDelete = () => confirmDeleteModalHandlers.close()
|
||||
|
||||
const onSuccessDelete = () => {
|
||||
confirmDeleteModalHandlers.close()
|
||||
reset({
|
||||
optionsType: 'none',
|
||||
info: [],
|
||||
specification: [],
|
||||
})
|
||||
back()
|
||||
}
|
||||
|
||||
const onErrorDelete = () => confirmDeleteModalHandlers.close()
|
||||
|
||||
useTitle(`品类规格及特点 - ${categoryName ? categoryName : ''}`)
|
||||
|
||||
//? Render(s)
|
||||
return (
|
||||
<>
|
||||
<ConfirmDeleteModal
|
||||
title={`${categoryName}-分类规格`}
|
||||
isLoading={isLoadingDelete}
|
||||
isShow={isShowConfirmDeleteModal}
|
||||
onClose={confirmDeleteModalHandlers.close}
|
||||
onCancel={onCancelDelete}
|
||||
onConfirm={onConfirmDelete}
|
||||
/>
|
||||
|
||||
{/* Handle Delete Response */}
|
||||
{(isSuccessDelete || isErrorDelete) && (
|
||||
<HandleResponse
|
||||
isError={isErrorDelete}
|
||||
isSuccess={isSuccessDelete}
|
||||
error={errorDelete?.data?.message}
|
||||
message={dataDelete?.message}
|
||||
onSuccess={onSuccessDelete}
|
||||
onError={onErrorDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmUpdateModal
|
||||
title={`${categoryName}-分类规格`}
|
||||
isLoading={isLoadingUpdate}
|
||||
isShow={isShowConfirmUpdateModal}
|
||||
onClose={confirmUpdateModalHandlers.close}
|
||||
onCancel={onCancelUpdate}
|
||||
onConfirm={onConfirmUpdate}
|
||||
/>
|
||||
|
||||
{/* Handle Update Response */}
|
||||
{(isSuccessUpdate || isErrorUpdate) && (
|
||||
<HandleResponse
|
||||
isError={isErrorUpdate}
|
||||
isSuccess={isSuccessUpdate}
|
||||
error={errorUpdate?.data?.message}
|
||||
message={dataUpdate?.message}
|
||||
onSuccess={onSuccessUpdate}
|
||||
onError={onErrorUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Handle Create Details Response */}
|
||||
{(isSuccessCreate || isErrorCreate) && (
|
||||
<HandleResponse
|
||||
isError={isErrorCreate}
|
||||
isSuccess={isSuccessCreate}
|
||||
error={errorCreate?.data?.message}
|
||||
message={dataCreate?.message}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main>
|
||||
{isLoadingGet ? (
|
||||
<div className="px-3 py-20">
|
||||
<BigLoading />
|
||||
</div>
|
||||
) : (
|
||||
<PageContainer title={`品类规格及特点 - ${categoryName ? categoryName : ''}`}>
|
||||
<form
|
||||
onSubmit={
|
||||
mode === 'create' ? handleSubmit(createHandler) : handleSubmit(updateHandler)
|
||||
}
|
||||
className="p-3 space-y-6"
|
||||
>
|
||||
<Tab.Group>
|
||||
<Tab.List className="flex space-x-1 rounded-xl bg-slate-200 p-1">
|
||||
{tabListNames.map(item => (
|
||||
<Tab
|
||||
key={item.id}
|
||||
className={({ selected }) =>
|
||||
`tab
|
||||
${
|
||||
selected
|
||||
? 'bg-white shadow'
|
||||
: 'text-blue-400 hover:bg-white/[0.12] hover:text-blue-600'
|
||||
}
|
||||
`
|
||||
}
|
||||
>
|
||||
{item.name}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<div className="space-y-3">
|
||||
<p className="mb-2">选择类型:</p>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<input
|
||||
type="radio"
|
||||
id="none"
|
||||
value="none"
|
||||
className="mr-1"
|
||||
{...register('optionsType')}
|
||||
/>
|
||||
<label htmlFor="none">默认</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<input
|
||||
type="radio"
|
||||
id="colors"
|
||||
value="colors"
|
||||
className="mr-1"
|
||||
{...register('optionsType')}
|
||||
/>
|
||||
<label htmlFor="colors">根据颜色</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<input
|
||||
type="radio"
|
||||
id="sizes"
|
||||
value="sizes"
|
||||
className="mr-1"
|
||||
{...register('optionsType')}
|
||||
/>
|
||||
<label htmlFor="sizes">根据尺寸</label>
|
||||
</div>
|
||||
</div>
|
||||
</Tab.Panel>
|
||||
|
||||
<Tab.Panel>
|
||||
<DetailsList
|
||||
name="info"
|
||||
control={control}
|
||||
register={register}
|
||||
categoryName={categoryName}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<DetailsList
|
||||
name="specification"
|
||||
control={control}
|
||||
register={register}
|
||||
categoryName={categoryName}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
<div className="flex justify-center gap-x-4">
|
||||
{mode === 'edit' ? (
|
||||
<>
|
||||
<Button
|
||||
className="bg-amber-500 "
|
||||
isRounded={true}
|
||||
type="submit"
|
||||
isLoading={isLoadingUpdate}
|
||||
>
|
||||
更新分类规格
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="rounded-3xl"
|
||||
isLoading={isLoadingDelete}
|
||||
onClick={deleteHandler}
|
||||
>
|
||||
删除分类规格
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
className="bg-green-500 "
|
||||
isRounded={true}
|
||||
type="submit"
|
||||
isLoading={isLoadingCreate}
|
||||
>
|
||||
建立分类规格
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</PageContainer>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DetailsContentPage
|
|
@ -0,0 +1,75 @@
|
|||
'use client'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { BigLoading, PageContainer } from 'components'
|
||||
|
||||
import { useGetCategoriesQuery } from '@/store/services'
|
||||
import { useTitle } from '@/hooks'
|
||||
import moment from 'moment-jalaali'
|
||||
|
||||
const DetailsPage = () => {
|
||||
useTitle('分类规格')
|
||||
//? Get Categories
|
||||
const { categories, isLoading } = useGetCategoriesQuery(undefined, {
|
||||
selectFromResult: ({ data, isLoading }) => ({
|
||||
categories: data?.data?.categories.filter(category => category.level === 2),
|
||||
isLoading,
|
||||
}),
|
||||
})
|
||||
|
||||
//? Render(s)
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="px-3 py-20">
|
||||
<BigLoading />
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<main>
|
||||
<PageContainer title="分类规格">
|
||||
<section className="p-3 mx-auto mb-10 space-y-8">
|
||||
<div className="mx-3 overflow-x-auto mt-7 lg:mx-5 xl:mx-10">
|
||||
<table className="w-full whitespace-nowrap">
|
||||
<thead className="h-9 bg-emerald-50">
|
||||
<tr className="text-emerald-500">
|
||||
<th className="px-2 border-gray-100 border-x-2">名称</th>
|
||||
<th className="px-2 border-gray-100 border-x-2">创建时间</th>
|
||||
<th className="px-2 border-gray-100 border-x-2">更新时间</th>
|
||||
<th className="border-gray-100 border-x-2">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-600">
|
||||
{categories &&
|
||||
categories.map(category => (
|
||||
<tr
|
||||
className="text-xs text-center transition-colors border-b border-gray-100 md:text-sm hover:bg-gray-50"
|
||||
key={category._id}
|
||||
>
|
||||
<td className="w-1/4 px-2 py-4">{category.name}</td>
|
||||
<td className="w-1/4 px-2 py-4">
|
||||
{moment(category.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</td>
|
||||
<td className="w-1/4 px-2 py-4">
|
||||
{moment(category.updatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</td>
|
||||
<td className="px-2 py-4">
|
||||
<Link
|
||||
href={`/admin/details/${category._id}?category_name=${category.name}`}
|
||||
className="bg-blue-50 text-blue-500 rounded-sm py-1 px-1.5 mx-1.5 inline-block"
|
||||
>
|
||||
编辑规格
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</PageContainer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default DetailsPage
|
|
@ -0,0 +1,29 @@
|
|||
'use client' // Error components must be Client Components
|
||||
|
||||
import { Button } from '@/components'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Error({ error, reset }) {
|
||||
useEffect(() => {
|
||||
// Log the error to an error reporting service
|
||||
console.error(error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="py-20 mx-auto space-y-3 text-center w-fit">
|
||||
<h5 className="text-xl">{error.name}</h5>
|
||||
<p className="text-lg text-red-500">{error.message}</p>
|
||||
<Button
|
||||
className="mx-auto"
|
||||
onClick={
|
||||
// Attempt to recover by trying to re-render the segment
|
||||
() => reset()
|
||||
}
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
'use client'
|
||||
|
||||
import { DashboardLayout } from 'components'
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<DashboardLayout>{children}</DashboardLayout>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { ArrowLink, ResponsiveImage, DashboardLayout } from '@/components'
|
||||
|
||||
export default function NotFound() {
|
||||
//? Render(s)
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<main className="flex flex-col items-center justify-center py-8 gap-y-6 xl:mt-28">
|
||||
<p className="text-base font-semibold text-black">404 Not Found!</p>
|
||||
<ArrowLink path="/admin">返回管理后台</ArrowLink>
|
||||
<ResponsiveImage
|
||||
dimensions="w-full max-w-lg h-72"
|
||||
src="/icons/page-not-found.png"
|
||||
layout="fill"
|
||||
alt="404"
|
||||
/>
|
||||
</main>
|
||||
</DashboardLayout>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
'use client'
|
||||
|
||||
import { useGetSingleOrderQuery } from '@/store/services'
|
||||
|
||||
import { BigLoading, DashboardLayout, OrderCard, PageContainer } from 'components'
|
||||
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
|
||||
const SingleOrder = ({ params }) => {
|
||||
useTitle('订单详情')
|
||||
//? Assets
|
||||
const query = useUrlQuery()
|
||||
|
||||
//? Get Order Data
|
||||
const { data, isLoading } = useGetSingleOrderQuery({
|
||||
id: params.id,
|
||||
})
|
||||
|
||||
//? Render(s)
|
||||
return (
|
||||
<main>
|
||||
<PageContainer title="订单详情">
|
||||
{isLoading ? (
|
||||
<div className="px-3 py-20">
|
||||
<BigLoading />
|
||||
</div>
|
||||
) : data ? (
|
||||
<section className="max-w-5xl px-3 py-3 mx-auto lg:px-8">
|
||||
<OrderCard singleOrder order={data?.data} />
|
||||
</section>
|
||||
) : null}
|
||||
</PageContainer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default SingleOrder
|
|
@ -0,0 +1,65 @@
|
|||
'use client'
|
||||
|
||||
import { useGetOrdersListQuery } from '@/store/services'
|
||||
|
||||
import {
|
||||
Pagination,
|
||||
ShowWrapper,
|
||||
EmptyOrdersList,
|
||||
PageContainer,
|
||||
OrdersTable,
|
||||
TableSkeleton,
|
||||
} from 'components'
|
||||
|
||||
import { useChangeRoute } from 'hooks'
|
||||
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
|
||||
const OrdersHome = () => {
|
||||
useTitle('订单管理')
|
||||
//? Assets
|
||||
const query = useUrlQuery()
|
||||
const page = query.page ? +query.page : 1
|
||||
|
||||
const changeRoute = useChangeRoute()
|
||||
|
||||
//? Get Orders Query
|
||||
const { data, isSuccess, isFetching, error, isError, refetch } = useGetOrdersListQuery({
|
||||
page,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
//? Render(s)
|
||||
return (
|
||||
<main id="_adminOrders">
|
||||
<PageContainer title="订单管理">
|
||||
<section className="p-3 md:px-3 xl:px-8 2xl:px-10" id="orders">
|
||||
<ShowWrapper
|
||||
error={error}
|
||||
isError={isError}
|
||||
refetch={refetch}
|
||||
isFetching={isFetching}
|
||||
isSuccess={isSuccess}
|
||||
dataLength={data?.data?.ordersLength ?? 0}
|
||||
emptyComponent={<EmptyOrdersList />}
|
||||
loadingComponent={<TableSkeleton />}
|
||||
>
|
||||
{data && <OrdersTable orders={data?.data?.orders} />}
|
||||
</ShowWrapper>
|
||||
|
||||
{data && data?.data?.ordersLength > 10 && (
|
||||
<div className="py-4 mx-auto lg:max-w-5xl">
|
||||
<Pagination
|
||||
pagination={data?.data?.pagination}
|
||||
changeRoute={changeRoute}
|
||||
section="_adminOrders"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</PageContainer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrdersHome
|
|
@ -0,0 +1,31 @@
|
|||
'use client'
|
||||
|
||||
import { DashboardAside } from '@/components'
|
||||
import { useTitle } from '@/hooks'
|
||||
import { siteTitle } from '@/utils'
|
||||
import Image from 'next/image'
|
||||
|
||||
const AdminPage = () => {
|
||||
useTitle(`${siteTitle}-管理中心`)
|
||||
return (
|
||||
<>
|
||||
<div className="lg:hidden">
|
||||
<DashboardAside />
|
||||
</div>
|
||||
<section className="hidden py-20 lg:block">
|
||||
<Image
|
||||
src="/icons/chart.png"
|
||||
alt="图表"
|
||||
width={208}
|
||||
height={208}
|
||||
className="mx-auto mb-8"
|
||||
/>
|
||||
|
||||
<p className="text-center">情况分析</p>
|
||||
<span className="block my-3 text-base text-center text-amber-500">(开发中)</span>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AdminPage
|
|
@ -0,0 +1,50 @@
|
|||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import { HandleResponse, PageContainer, ProductsForm } from 'components'
|
||||
|
||||
import { useCreateProductMutation } from '@/store/services'
|
||||
import { useTitle } from '@/hooks'
|
||||
|
||||
const CreateProductPage = () => {
|
||||
useTitle('商品新增')
|
||||
//? Assets
|
||||
const { push } = useRouter()
|
||||
|
||||
//? Queries
|
||||
//* Create Product
|
||||
const [createProduct, { data, isSuccess, isLoading, isError, error }] = useCreateProductMutation()
|
||||
|
||||
//? Handlers
|
||||
const createHandler = data => {
|
||||
console.log(data)
|
||||
createProduct({ body: data })
|
||||
}
|
||||
|
||||
const onSuccess = () => {
|
||||
push('/admin/products')
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isSuccess || isError) && (
|
||||
<HandleResponse
|
||||
isError={isError}
|
||||
isSuccess={isSuccess}
|
||||
error={error?.data?.message}
|
||||
message={data?.message}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main>
|
||||
<PageContainer title="商品新增">
|
||||
<ProductsForm mode="create" isLoadingCreate={isLoading} createHandler={createHandler} />
|
||||
</PageContainer>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateProductPage
|
|
@ -0,0 +1,122 @@
|
|||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
|
||||
import {
|
||||
BigLoading,
|
||||
ConfirmUpdateModal,
|
||||
HandleResponse,
|
||||
PageContainer,
|
||||
ProductsForm,
|
||||
} from 'components'
|
||||
|
||||
import { useDisclosure } from 'hooks'
|
||||
|
||||
import { SubmitHandler } from 'react-hook-form'
|
||||
|
||||
import { useGetSingleProductQuery, useUpdateProductMutation } from '@/store/services'
|
||||
import { useTitle } from '@/hooks'
|
||||
|
||||
const EditProductPage = ({ params: { id } }) => {
|
||||
useTitle('编辑商品')
|
||||
//? Assets
|
||||
const { back } = useRouter()
|
||||
|
||||
const initialUpdataInfo = {}
|
||||
|
||||
//? Modals
|
||||
const [isShowConfirmUpdateModal, confirmUpdateModalHandlers] = useDisclosure()
|
||||
|
||||
//? States
|
||||
const [updateInfo, setUpdateInfo] = useState(initialUpdataInfo)
|
||||
|
||||
//? Queries
|
||||
//* Get Product
|
||||
const { data: selectedProduct, isLoading: isLoadingGetSelectedProduct } =
|
||||
useGetSingleProductQuery({ id })
|
||||
|
||||
//* Update Product
|
||||
const [
|
||||
updateProduct,
|
||||
{
|
||||
data: dataUpdate,
|
||||
isSuccess: isSuccessUpdate,
|
||||
isError: isErrorUpdate,
|
||||
error: errorUpdate,
|
||||
isLoading: isLoadingUpdate,
|
||||
},
|
||||
] = useUpdateProductMutation()
|
||||
|
||||
//? Handlers
|
||||
const updateHandler = data => {
|
||||
setUpdateInfo(prev => ({ ...prev, ...selectedProduct.data, ...data }))
|
||||
confirmUpdateModalHandlers.open()
|
||||
}
|
||||
|
||||
const onConfirmUpdate = () => {
|
||||
updateProduct({
|
||||
id,
|
||||
body: updateInfo,
|
||||
})
|
||||
}
|
||||
|
||||
const onCancelUpdate = () => {
|
||||
setUpdateInfo(initialUpdataInfo)
|
||||
confirmUpdateModalHandlers.close()
|
||||
}
|
||||
|
||||
const onSuccessUpdate = () => {
|
||||
setUpdateInfo(initialUpdataInfo)
|
||||
confirmUpdateModalHandlers.close()
|
||||
back()
|
||||
}
|
||||
|
||||
const onErrorUpdate = () => {
|
||||
setUpdateInfo(initialUpdataInfo)
|
||||
confirmUpdateModalHandlers.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmUpdateModal
|
||||
title="商品"
|
||||
isLoading={isLoadingUpdate}
|
||||
isShow={isShowConfirmUpdateModal}
|
||||
onClose={confirmUpdateModalHandlers.close}
|
||||
onCancel={onCancelUpdate}
|
||||
onConfirm={onConfirmUpdate}
|
||||
/>
|
||||
|
||||
{(isSuccessUpdate || isErrorUpdate) && (
|
||||
<HandleResponse
|
||||
isError={isErrorUpdate}
|
||||
isSuccess={isSuccessUpdate}
|
||||
error={errorUpdate?.data?.message}
|
||||
message={dataUpdate?.message}
|
||||
onSuccess={onSuccessUpdate}
|
||||
onError={onErrorUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main>
|
||||
<PageContainer title="编辑商品">
|
||||
{isLoadingGetSelectedProduct ? (
|
||||
<div className="px-3 py-20">
|
||||
<BigLoading />
|
||||
</div>
|
||||
) : selectedProduct.data ? (
|
||||
<ProductsForm
|
||||
mode="edit"
|
||||
isLoadingUpdate={isLoadingUpdate}
|
||||
updateHandler={updateHandler}
|
||||
selectedProduct={selectedProduct.data}
|
||||
/>
|
||||
) : null}
|
||||
</PageContainer>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditProductPage
|
|
@ -0,0 +1,290 @@
|
|||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import {
|
||||
useDeleteProductMutation,
|
||||
useGetCategoriesQuery,
|
||||
useGetProductsQuery,
|
||||
} from '@/store/services'
|
||||
|
||||
import {
|
||||
ConfirmDeleteModal,
|
||||
DeleteIconBtn,
|
||||
EditIconBtn,
|
||||
HandleResponse,
|
||||
Icons,
|
||||
PageContainer,
|
||||
Pagination,
|
||||
SelectCategories,
|
||||
ShowWrapper,
|
||||
TableSkeleton,
|
||||
} from 'components'
|
||||
|
||||
import { useDisclosure, useChangeRoute } from 'hooks'
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
|
||||
const Products = () => {
|
||||
useTitle('商品管理')
|
||||
//? Assets
|
||||
const { push } = useRouter()
|
||||
const query = useUrlQuery()
|
||||
const page = query.page ? +query.page : 1
|
||||
const category = query.category ?? ''
|
||||
|
||||
const changeRoute = useChangeRoute()
|
||||
|
||||
const initialSelectedCategories = {
|
||||
levelOne: {},
|
||||
levelTwo: {},
|
||||
levelThree: {},
|
||||
}
|
||||
|
||||
//? Get Categories Query
|
||||
const { categories } = useGetCategoriesQuery(undefined, {
|
||||
selectFromResult: ({ data }) => ({
|
||||
categories: data?.data?.categories,
|
||||
}),
|
||||
})
|
||||
|
||||
//? Modals
|
||||
const [isShowConfirmDeleteModal, confirmDeleteModalHandlers] = useDisclosure()
|
||||
|
||||
//? State
|
||||
const [deleteInfo, setDeleteInfo] = useState({
|
||||
id: '',
|
||||
})
|
||||
const [search, setSearch] = useState('')
|
||||
const [selectedCategories, setSelectedCategories] = useState(initialSelectedCategories)
|
||||
|
||||
//? Querirs
|
||||
//* Get Products Data
|
||||
const { data, isFetching, error, isError, refetch, isSuccess } = useGetProductsQuery({
|
||||
page,
|
||||
category,
|
||||
search: query?.search,
|
||||
})
|
||||
|
||||
//* Delete Product
|
||||
const [
|
||||
deleteProduct,
|
||||
{
|
||||
isSuccess: isSuccessDelete,
|
||||
isError: isErrorDelete,
|
||||
error: errorDelete,
|
||||
data: dataDelete,
|
||||
isLoading: isLoadingDelete,
|
||||
},
|
||||
] = useDeleteProductMutation()
|
||||
|
||||
//? Handlers
|
||||
const handleSearchChange = e => setSearch(e.target.value)
|
||||
|
||||
const handleSubmit = e => {
|
||||
e.preventDefault()
|
||||
|
||||
const queryParams = {
|
||||
page: 1,
|
||||
}
|
||||
|
||||
if (selectedCategories?.levelThree?._id) {
|
||||
queryParams.category = selectedCategories.levelThree.slug
|
||||
queryParams.levelOne = selectedCategories?.levelOne?._id
|
||||
queryParams.levelTwo = selectedCategories?.levelTwo?._id
|
||||
queryParams.levelThree = selectedCategories.levelThree._id
|
||||
} else if (selectedCategories?.levelTwo?._id) {
|
||||
queryParams.category = selectedCategories?.levelTwo.slug
|
||||
queryParams.levelOne = selectedCategories?.levelOne?._id
|
||||
queryParams.levelTwo = selectedCategories?.levelTwo._id
|
||||
} else if (selectedCategories?.levelOne?._id) {
|
||||
queryParams.category = selectedCategories?.levelOne.slug
|
||||
queryParams.levelOne = selectedCategories?.levelOne._id
|
||||
}
|
||||
|
||||
if (search.trim()) {
|
||||
queryParams.search = search
|
||||
}
|
||||
|
||||
changeRoute(queryParams)
|
||||
}
|
||||
|
||||
const handleRemoveSearch = () => {
|
||||
setSearch('')
|
||||
setSelectedCategories(initialSelectedCategories)
|
||||
refetch()
|
||||
push('/admin/products')
|
||||
}
|
||||
|
||||
const findCategory = id => categories?.find(cat => cat._id === id)
|
||||
|
||||
//* Delete Handlers
|
||||
const handleDelete = id => {
|
||||
setDeleteInfo({ id })
|
||||
confirmDeleteModalHandlers.open()
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setDeleteInfo({ id: '' })
|
||||
confirmDeleteModalHandlers.close()
|
||||
}
|
||||
|
||||
const onConfirm = () => {
|
||||
deleteProduct({ id: deleteInfo.id })
|
||||
}
|
||||
|
||||
const onSuccess = () => {
|
||||
confirmDeleteModalHandlers.close()
|
||||
setDeleteInfo({ id: '' })
|
||||
}
|
||||
const onError = () => {
|
||||
confirmDeleteModalHandlers.close()
|
||||
setDeleteInfo({ id: '' })
|
||||
}
|
||||
|
||||
//? Re-Render
|
||||
useEffect(() => {
|
||||
if (categories) {
|
||||
if (query?.levelThree)
|
||||
setSelectedCategories({
|
||||
levelOne: findCategory(query.levelOne),
|
||||
levelThree: findCategory(query.levelThree),
|
||||
levelTwo: findCategory(query.levelTwo),
|
||||
})
|
||||
else if (query?.levelTwo)
|
||||
setSelectedCategories({
|
||||
...selectedCategories,
|
||||
levelOne: findCategory(query.levelOne),
|
||||
levelTwo: findCategory(query.levelTwo),
|
||||
})
|
||||
else if (query?.levelOne)
|
||||
setSelectedCategories({
|
||||
...selectedCategories,
|
||||
levelOne: findCategory(query.levelOne),
|
||||
})
|
||||
}
|
||||
}, [categories])
|
||||
|
||||
useEffect(() => {
|
||||
if (query?.search) setSearch(query.search)
|
||||
}, [query?.search])
|
||||
|
||||
//? Render(s)
|
||||
return (
|
||||
<>
|
||||
<ConfirmDeleteModal
|
||||
title="该商品"
|
||||
isLoading={isLoadingDelete}
|
||||
isShow={isShowConfirmDeleteModal}
|
||||
onClose={confirmDeleteModalHandlers.close}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
|
||||
{/* Handle Delete Response */}
|
||||
{(isSuccessDelete || isErrorDelete) && (
|
||||
<HandleResponse
|
||||
isError={isErrorDelete}
|
||||
isSuccess={isSuccessDelete}
|
||||
error={errorDelete?.data?.message}
|
||||
message={dataDelete?.message}
|
||||
onSuccess={onSuccess}
|
||||
onError={onError}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main>
|
||||
<PageContainer title="商品管理">
|
||||
<section className="p-3 space-y-7" id="_adminProducts">
|
||||
<form className="max-w-4xl mx-auto space-y-5" onSubmit={handleSubmit}>
|
||||
<SelectCategories
|
||||
setSelectedCategories={setSelectedCategories}
|
||||
selectedCategories={selectedCategories}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row-reverse rounded-md gap-x-2 ">
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 text-white border flex-center gap-x-2 min-w-max"
|
||||
onClick={handleRemoveSearch}
|
||||
>
|
||||
<span>重制筛选</span>
|
||||
<Icons.Close className="icon" />
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="商品名称..."
|
||||
className="flex-grow p-2 text-left input"
|
||||
value={search}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
<button type="submit" className="p-2 border flex-center gap-x-2 min-w-max">
|
||||
<span>过滤筛选</span>
|
||||
<Icons.Search className="icon" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ShowWrapper
|
||||
error={error}
|
||||
isError={isError}
|
||||
refetch={refetch}
|
||||
isFetching={isFetching}
|
||||
isSuccess={isSuccess}
|
||||
dataLength={data?.data ? data?.data?.productsLength : 0}
|
||||
loadingComponent={<TableSkeleton count={10} />}
|
||||
>
|
||||
<div className="overflow-x mt-7">
|
||||
<table className="w-full overflow-scroll table-auto">
|
||||
<thead className="h-9 bg-emerald-50">
|
||||
<tr className="text-emerald-500">
|
||||
<th className="border-gray-50 border-x-2">ID</th>
|
||||
<th className="border-gray-100 border-x-2">名称</th>
|
||||
<th className="border-gray-100 border-x-2">价格</th>
|
||||
<th className="border-gray-100 border-x-2">销量</th>
|
||||
<th className="border-gray-100 border-x-2">库存</th>
|
||||
<th className="border-r-2 border-gray-100">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-600">
|
||||
{data?.data?.products.length > 0 &&
|
||||
data?.data?.products.map(item => (
|
||||
<tr
|
||||
className="text-xs text-center transition-colors border-b border-gray-100 md:text-sm hover:bg-gray-50"
|
||||
key={item._id}
|
||||
>
|
||||
<td className="px-2 py-4">{item._id}</td>
|
||||
<td className="px-2 py-4">{item.title}</td>
|
||||
<td className="px-2 py-4">{item.price}</td>
|
||||
<td className="px-2 py-4">{item.sold}</td>
|
||||
<td className="px-2 py-4">{item.inStock}</td>
|
||||
<td className="px-2 py-4">
|
||||
<DeleteIconBtn onClick={() => handleDelete(item._id)} />
|
||||
<Link href={`/admin/products/edit/${item._id}`}>
|
||||
<EditIconBtn />
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ShowWrapper>
|
||||
|
||||
{data && data?.data?.productsLength > 10 && (
|
||||
<Pagination
|
||||
pagination={data?.data?.pagination}
|
||||
changeRoute={changeRoute}
|
||||
section="_adminProducts"
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</PageContainer>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Products
|
|
@ -0,0 +1,33 @@
|
|||
'use client'
|
||||
|
||||
import { useTitle } from '@/hooks'
|
||||
import { useGetSingleReviewQuery } from '@/store/services'
|
||||
|
||||
import { BigLoading, PageContainer, ReveiwCard } from 'components'
|
||||
|
||||
const SingleCommentPage = ({ params: { id } }) => {
|
||||
useTitle('评价详情')
|
||||
//? Get Single Review Data
|
||||
const { data, isLoading } = useGetSingleReviewQuery({
|
||||
id,
|
||||
})
|
||||
|
||||
//? Render(s)
|
||||
return (
|
||||
<main>
|
||||
<PageContainer title="评价详情">
|
||||
{isLoading ? (
|
||||
<div className="px-3 py-20">
|
||||
<BigLoading />
|
||||
</div>
|
||||
) : data ? (
|
||||
<section className="px-3 py-3 mx-auto lg:px-8">
|
||||
<ReveiwCard singleComment item={data.data} />
|
||||
</section>
|
||||
) : null}
|
||||
</PageContainer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default SingleCommentPage
|
|
@ -0,0 +1,60 @@
|
|||
'use client'
|
||||
|
||||
import { useGetReviewsListQuery } from '@/store/services'
|
||||
|
||||
import {
|
||||
Pagination,
|
||||
ShowWrapper,
|
||||
EmptyCommentsList,
|
||||
PageContainer,
|
||||
ReviewsTable,
|
||||
TableSkeleton,
|
||||
} from 'components'
|
||||
|
||||
import { useChangeRoute } from 'hooks'
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
|
||||
const ReviewsPage = () => {
|
||||
useTitle('评价管理')
|
||||
//? Assets
|
||||
const query = useUrlQuery()
|
||||
const page = query.page ? +query.page : 1
|
||||
|
||||
const changeRoute = useChangeRoute()
|
||||
|
||||
//? Get Review Data
|
||||
const { data, isError, error, isFetching, refetch, isSuccess } = useGetReviewsListQuery({
|
||||
page,
|
||||
})
|
||||
|
||||
//? Render
|
||||
return (
|
||||
<main id="_adminReviews">
|
||||
<PageContainer title="评价管理">
|
||||
<ShowWrapper
|
||||
error={error}
|
||||
isError={isError}
|
||||
refetch={refetch}
|
||||
isFetching={isFetching}
|
||||
isSuccess={isSuccess}
|
||||
dataLength={data?.data?.reviewsLength ?? 0}
|
||||
emptyComponent={<EmptyCommentsList />}
|
||||
loadingComponent={<TableSkeleton />}
|
||||
>
|
||||
{data && data?.data && <ReviewsTable reviews={data?.data?.reviews} />}
|
||||
</ShowWrapper>
|
||||
{data && data?.data && data?.data?.reviewsLength > 10 && (
|
||||
<div className="py-4 mx-auto lg:max-w-5xl">
|
||||
<Pagination
|
||||
pagination={data?.data?.pagination}
|
||||
changeRoute={changeRoute}
|
||||
section="_adminReviews"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PageContainer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReviewsPage
|
|
@ -0,0 +1,55 @@
|
|||
'use client'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import { HandleResponse, PageContainer, SliderForm } from 'components'
|
||||
|
||||
import { SubmitHandler } from 'react-hook-form'
|
||||
|
||||
import { useCreateSliderMutation } from '@/store/services'
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
|
||||
const CreateSliderPage = () => {
|
||||
//? Assets
|
||||
const { back } = useRouter()
|
||||
const query = useUrlQuery()
|
||||
const categoryName = query?.category_name
|
||||
const categoryId = query?.category_id
|
||||
|
||||
//? Queries
|
||||
//* Create Slider
|
||||
const [createSlider, { data, isSuccess, isLoading, error, isError }] = useCreateSliderMutation()
|
||||
|
||||
//? Handlers
|
||||
const createHandler = data => {
|
||||
const { image, isPublic, title, uri } = data
|
||||
createSlider({
|
||||
body: { category_id: categoryId, image, isPublic, title, uri },
|
||||
})
|
||||
}
|
||||
|
||||
const onSuccess = () => back()
|
||||
|
||||
useTitle('新增类别滑块' + ' ' + categoryName)
|
||||
|
||||
return (
|
||||
<>
|
||||
{(isSuccess || isError) && (
|
||||
<HandleResponse
|
||||
isError={isError}
|
||||
isSuccess={isSuccess}
|
||||
error={error?.data?.message}
|
||||
message={data?.message}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main>
|
||||
<PageContainer title={'新增类别滑块' + ' ' + categoryName}>
|
||||
<SliderForm mode="create" isLoadingCreate={isLoading} createHandler={createHandler} />
|
||||
</PageContainer>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateSliderPage
|
|
@ -0,0 +1,183 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
import {
|
||||
BigLoading,
|
||||
ConfirmDeleteModal,
|
||||
ConfirmUpdateModal,
|
||||
HandleResponse,
|
||||
PageContainer,
|
||||
SliderForm,
|
||||
} from 'components'
|
||||
|
||||
import { useDisclosure } from 'hooks'
|
||||
|
||||
import { SubmitHandler } from 'react-hook-form'
|
||||
|
||||
import {
|
||||
useDeleteSliderMutation,
|
||||
useGetSingleSliderQuery,
|
||||
useUpdateSliderMutation,
|
||||
} from '@/store/services'
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
|
||||
const EditSliderPage = ({ params: { id: sliderId } }) => {
|
||||
//? Assets
|
||||
const { back } = useRouter()
|
||||
const query = useUrlQuery()
|
||||
const sliderName = query?.slider_name
|
||||
|
||||
const initialUpdataInfo = {}
|
||||
|
||||
//? States
|
||||
const [updateInfo, setUpdateInfo] = useState(initialUpdataInfo)
|
||||
|
||||
//? Modals
|
||||
const [isShowConfirmDeleteModal, confirmDeleteModalHandlers] = useDisclosure()
|
||||
const [isShowConfirmUpdateModal, confirmUpdateModalHandlers] = useDisclosure()
|
||||
|
||||
//? Queries
|
||||
//* Get Slider
|
||||
const { data: selectedSlider, isLoading: isLoadingGetSelectedSlider } = useGetSingleSliderQuery({
|
||||
id: sliderId,
|
||||
})
|
||||
|
||||
//* Update Slider
|
||||
const [
|
||||
updateSlider,
|
||||
{
|
||||
data: dataUpdate,
|
||||
isSuccess: isSuccessUpdate,
|
||||
isError: isErrorUpdate,
|
||||
error: errorUpdate,
|
||||
isLoading: isLoadingUpdate,
|
||||
},
|
||||
] = useUpdateSliderMutation()
|
||||
|
||||
//* Delete Slider
|
||||
const [
|
||||
deleteSlider,
|
||||
{
|
||||
isSuccess: isSuccessDelete,
|
||||
isError: isErrorDelete,
|
||||
error: errorDelete,
|
||||
data: dataDelete,
|
||||
isLoading: isLoadingDelete,
|
||||
},
|
||||
] = useDeleteSliderMutation()
|
||||
|
||||
//? Handlers
|
||||
//* Update
|
||||
const updateHandler = data => {
|
||||
setUpdateInfo(prev => ({ ...prev, ...selectedSlider.data, ...data }))
|
||||
confirmUpdateModalHandlers.open()
|
||||
}
|
||||
|
||||
const onConfirmUpdate = () => {
|
||||
updateSlider({
|
||||
id: sliderId,
|
||||
body: updateInfo,
|
||||
})
|
||||
}
|
||||
|
||||
const onCancelUpdate = () => {
|
||||
setUpdateInfo(initialUpdataInfo)
|
||||
confirmUpdateModalHandlers.close()
|
||||
}
|
||||
|
||||
const onSuccessUpdate = () => {
|
||||
setUpdateInfo(initialUpdataInfo)
|
||||
confirmUpdateModalHandlers.close()
|
||||
console.log('isSuccessUpdate', isSuccessUpdate)
|
||||
console.log('isErrorUpdate', isErrorUpdate)
|
||||
back()
|
||||
}
|
||||
|
||||
const onErrorUpdate = () => {
|
||||
setUpdateInfo(initialUpdataInfo)
|
||||
confirmUpdateModalHandlers.close()
|
||||
}
|
||||
|
||||
//* Delete
|
||||
const deleteHandler = () => confirmDeleteModalHandlers.open()
|
||||
|
||||
const onConfirmDelete = () => deleteSlider({ id: sliderId })
|
||||
|
||||
const onCancelDelete = () => confirmDeleteModalHandlers.close()
|
||||
|
||||
const onSuccessDelete = () => {
|
||||
confirmDeleteModalHandlers.close()
|
||||
back()
|
||||
}
|
||||
|
||||
const onErrorDelete = () => confirmDeleteModalHandlers.close()
|
||||
|
||||
useTitle('编辑滑块' + ' ' + sliderName)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmDeleteModal
|
||||
title="滑块"
|
||||
isLoading={isLoadingDelete}
|
||||
isShow={isShowConfirmDeleteModal}
|
||||
onClose={confirmDeleteModalHandlers.close}
|
||||
onCancel={onCancelDelete}
|
||||
onConfirm={onConfirmDelete}
|
||||
/>
|
||||
|
||||
{(isSuccessDelete || isErrorDelete) && (
|
||||
<HandleResponse
|
||||
isError={isErrorDelete}
|
||||
isSuccess={isSuccessDelete}
|
||||
error={errorDelete?.data?.message}
|
||||
message={dataDelete?.message}
|
||||
onSuccess={onSuccessDelete}
|
||||
onError={onErrorDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmUpdateModal
|
||||
title="滑块"
|
||||
isLoading={isLoadingUpdate}
|
||||
isShow={isShowConfirmUpdateModal}
|
||||
onClose={confirmUpdateModalHandlers.close}
|
||||
onCancel={onCancelUpdate}
|
||||
onConfirm={onConfirmUpdate}
|
||||
/>
|
||||
|
||||
{(isSuccessUpdate || isErrorUpdate) && (
|
||||
<HandleResponse
|
||||
isError={isErrorUpdate}
|
||||
isSuccess={isSuccessUpdate}
|
||||
error={errorUpdate?.data?.message}
|
||||
message={dataUpdate?.message}
|
||||
onSuccess={onSuccessUpdate}
|
||||
onError={onErrorUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main>
|
||||
<PageContainer title={'编辑滑块' + ' ' + sliderName}>
|
||||
{isLoadingGetSelectedSlider ? (
|
||||
<div className="px-3 py-20">
|
||||
<BigLoading />
|
||||
</div>
|
||||
) : selectedSlider ? (
|
||||
<SliderForm
|
||||
mode="edit"
|
||||
selectedSlider={selectedSlider.data}
|
||||
updateHandler={updateHandler}
|
||||
isLoadingDelete={isLoadingDelete}
|
||||
isLoadingUpdate={isLoadingUpdate}
|
||||
deleteHandler={deleteHandler}
|
||||
/>
|
||||
) : null}
|
||||
</PageContainer>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditSliderPage
|
|
@ -0,0 +1,133 @@
|
|||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useGetCategoriesQuery, useGetSlidersQuery } from '@/store/services'
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
import { ResponsiveImage, EmptyCustomList, PageContainer, TableSkeleton } from '@/components'
|
||||
|
||||
const SlidersPage = () => {
|
||||
const query = useUrlQuery()
|
||||
const category_id = query?.category_id
|
||||
const category_name = query?.category_name
|
||||
|
||||
//? Queries
|
||||
//* Get Categories
|
||||
const { categories, isLoading: isLoading_get_categories } = useGetCategoriesQuery(undefined, {
|
||||
selectFromResult: ({ data, isLoading }) => ({
|
||||
categories: data?.data?.categories
|
||||
.filter(category => category.level < 2)
|
||||
.sort((a, b) => a.level - b.level),
|
||||
isLoading,
|
||||
skip: !!category_id,
|
||||
}),
|
||||
})
|
||||
|
||||
//* Get Sliders
|
||||
const { data: sliders, isLoading: isLoadingGetSliders } = useGetSlidersQuery(
|
||||
{ category: category_id },
|
||||
{ skip: !!!category_id }
|
||||
)
|
||||
|
||||
//? Render(s)
|
||||
const title = category_name ? `分类滑块管理 - ${category_name}` : '滑块管理'
|
||||
|
||||
useTitle(title)
|
||||
|
||||
const renderContent = () => {
|
||||
if (isLoading_get_categories || isLoadingGetSliders) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan="4">
|
||||
<TableSkeleton />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
if (categories && !category_id) {
|
||||
return categories.map(category => (
|
||||
<tr
|
||||
className="text-xs text-center transition-colors border-b border-gray-100 md:text-sm hover:bg-gray-50/50"
|
||||
key={category._id}
|
||||
>
|
||||
<td className="w-3/4 px-2 py-4">{category.name}</td>
|
||||
<td className="px-2 py-4">
|
||||
<Link
|
||||
href={`/admin/sliders?category_id=${category._id}&category_name=${category.name}`}
|
||||
className="bg-rose-50 text-rose-500 rounded-sm py-1 px-1.5 mx-1.5 inline-block"
|
||||
>
|
||||
子集
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
|
||||
if (sliders && sliders.data && sliders.data.length > 0) {
|
||||
return sliders.data.map(slider => (
|
||||
<tr
|
||||
className="text-xs text-center transition-colors border-b border-gray-100 md:text-sm hover:bg-gray-50/50"
|
||||
key={slider._id}
|
||||
>
|
||||
<td className="px-2 py-4">
|
||||
<ResponsiveImage
|
||||
dimensions="h-7 w-32"
|
||||
className="mx-auto"
|
||||
src={slider.image?.url}
|
||||
alt=""
|
||||
/>
|
||||
</td>
|
||||
<td className="w-2/4 px-2 py-4">{slider.title}</td>
|
||||
<td className="px-2 py-4">
|
||||
<Link
|
||||
href={`/admin/sliders/edit/${slider._id}?slider_name=${slider.title}`}
|
||||
className="bg-rose-50 text-rose-500 rounded-sm py-1 px-1.5 mx-1.5 inline-block"
|
||||
>
|
||||
编辑
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
} else
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan="4">
|
||||
<EmptyCustomList />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<PageContainer title={title}>
|
||||
<section className="p-3 mx-auto mb-10 space-y-8">
|
||||
{category_id && (
|
||||
<Link
|
||||
href={`sliders/create?category_id=${category_id}&category_name=${category_name}`}
|
||||
className="flex items-center px-3 py-2 text-red-600 border-2 border-red-600 rounded-lg max-w-max gap-x-3"
|
||||
>
|
||||
添加新滑块
|
||||
</Link>
|
||||
)}
|
||||
<div className="mx-3 overflow-x-auto mt-7 lg:mx-5 xl:mx-10">
|
||||
<table className="w-full whitespace-nowrap">
|
||||
<thead className="h-9 bg-emerald-50">
|
||||
<tr className="text-emerald-500">
|
||||
{category_name && <th className="border-gray-100 border-x-2">图片</th>}
|
||||
<th className="px-2 border-gray-100 border-x-2">
|
||||
{category_name ? '标题' : '分类名称'}
|
||||
</th>
|
||||
<th className="border-gray-100 border-x-2">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-600">{renderContent()}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</PageContainer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default SlidersPage
|
|
@ -0,0 +1,117 @@
|
|||
'use client'
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
|
||||
import {
|
||||
PageContainer,
|
||||
ShowWrapper,
|
||||
EmptyUsersList,
|
||||
TableSkeleton,
|
||||
UsersTable,
|
||||
ConfirmDeleteModal,
|
||||
HandleResponse,
|
||||
Pagination,
|
||||
} from '@/components'
|
||||
import { useGetUsersQuery, useDeleteUserMutation } from '@/store/services'
|
||||
import { useDisclosure, useChangeRoute, useTitle } from '@/hooks'
|
||||
|
||||
export default function UsersPage() {
|
||||
useTitle('用户管理')
|
||||
//? Assets
|
||||
const { replace } = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const page = searchParams.get('page')
|
||||
|
||||
const changeRoute = useChangeRoute()
|
||||
|
||||
//? Modals
|
||||
const [isShowConfirmDeleteModal, confirmDeleteModalHandlers] = useDisclosure()
|
||||
|
||||
//? State
|
||||
const [deleteInfo, setDeleteInfo] = useState({
|
||||
id: '',
|
||||
})
|
||||
|
||||
//? Get User Data
|
||||
const { data, isSuccess, isFetching, error, isError, refetch } = useGetUsersQuery({
|
||||
page: page || 1,
|
||||
})
|
||||
|
||||
//? Delete User Query
|
||||
const [
|
||||
deleteUser,
|
||||
{
|
||||
isSuccess: isSuccess_delete,
|
||||
isLoading: isLoading_delete,
|
||||
isError: isError_delete,
|
||||
error: error_delete,
|
||||
data: data_delete,
|
||||
},
|
||||
] = useDeleteUserMutation()
|
||||
|
||||
//? Handlers
|
||||
const deleteUserHandler = id => {
|
||||
setDeleteInfo({ id })
|
||||
confirmDeleteModalHandlers.open()
|
||||
}
|
||||
const onConfirmUserDelete = () => deleteUser({ id: deleteInfo.id })
|
||||
|
||||
const onCancelUserDelete = () => {
|
||||
confirmDeleteModalHandlers.close()
|
||||
setDeleteInfo({ id: '' })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmDeleteModal
|
||||
title="用户"
|
||||
isLoading={isLoading_delete}
|
||||
isShow={isShowConfirmDeleteModal}
|
||||
onClose={onCancelUserDelete}
|
||||
onCancel={onCancelUserDelete}
|
||||
onConfirm={onConfirmUserDelete}
|
||||
/>
|
||||
{/* Handle Delete Response */}
|
||||
{(isSuccess_delete || isError_delete) && (
|
||||
<HandleResponse
|
||||
isError={isError_delete}
|
||||
isSuccess={isSuccess_delete}
|
||||
error={error_delete?.data?.message}
|
||||
message={data_delete?.message}
|
||||
onSuccess={() => {
|
||||
onCancelUserDelete()
|
||||
}}
|
||||
onError={() => {
|
||||
onCancelUserDelete()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<main id="_adminUsers">
|
||||
<PageContainer title="用户管理">
|
||||
<ShowWrapper
|
||||
error={error}
|
||||
isError={isError}
|
||||
refetch={refetch}
|
||||
isFetching={isFetching}
|
||||
isSuccess={isSuccess}
|
||||
dataLength={data && data.data ? data.data.usersLength : 0}
|
||||
emptyComponent={<EmptyUsersList />}
|
||||
loadingComponent={<TableSkeleton />}
|
||||
>
|
||||
<UsersTable deleteUserHandler={deleteUserHandler} users={data?.data?.users} />
|
||||
</ShowWrapper>
|
||||
{data?.data?.usersLength > 5 && (
|
||||
<div className="py-4 mx-auto lg:max-w-5xl">
|
||||
<Pagination
|
||||
pagination={data?.data?.pagination}
|
||||
changeRoute={changeRoute}
|
||||
section="_adminUsers"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PageContainer>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
'use client'
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { HandleResponse, LoginForm, Logo } from '@/components'
|
||||
|
||||
import { useLoginMutation } from '@/store/services'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useEffect } from 'react'
|
||||
import { userLogin, showAlert } from '@/store'
|
||||
import { useTitle } from '@/hooks'
|
||||
|
||||
export default function LoginPage() {
|
||||
useTitle('管理员登录')
|
||||
//? Assets
|
||||
const dispatch = useDispatch()
|
||||
const { push } = useRouter()
|
||||
|
||||
//? Login User
|
||||
const [login, { data, isSuccess, isError, isLoading, error }] = useLoginMutation()
|
||||
|
||||
//? Handlers
|
||||
const submitHander = async ({ email, password }) => {
|
||||
if (email && password) {
|
||||
await login({
|
||||
body: { email, password },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//? Handle Login User Response
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
if (data?.data?.user.root || data?.data?.user.role === 'admin') {
|
||||
dispatch(userLogin(data?.data.token))
|
||||
|
||||
dispatch(
|
||||
showAlert({
|
||||
status: 'success',
|
||||
title: data.message,
|
||||
})
|
||||
)
|
||||
push('/admin')
|
||||
} else {
|
||||
dispatch(
|
||||
showAlert({
|
||||
status: 'error',
|
||||
title: '您无权访问管理面板',
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [isSuccess])
|
||||
|
||||
useEffect(() => {
|
||||
if (isError && error)
|
||||
dispatch(
|
||||
showAlert({
|
||||
status: 'error',
|
||||
title: error?.data?.message,
|
||||
})
|
||||
)
|
||||
}, [isError])
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="grid items-center min-h-screen">
|
||||
<section className="container max-w-md px-12 py-6 space-y-6 lg:border lg:border-gray-100 lg:rounded-lg lg:shadow">
|
||||
<Link passHref href="/">
|
||||
<Logo className="mx-auto w-48 h-24" />
|
||||
</Link>
|
||||
<h1>
|
||||
<font className="">
|
||||
<font>登录</font>
|
||||
</font>
|
||||
</h1>
|
||||
<LoginForm isLoading={isLoading} onSubmit={submitHander} />
|
||||
</section>
|
||||
|
||||
<div className="fixed max-w-xs px-2 py-3 bg-white border rounded-lg shadow-lg top-5 right-5">
|
||||
<h5 className="mb-2 text-amber-600">
|
||||
您可以使用下面的电子邮件地址和密码来查看管理仪表板。
|
||||
</h5>
|
||||
<div className="text-left">
|
||||
<span className="text-sm text-zinc-500">Email: admin@gmail.com</span>
|
||||
<br />
|
||||
<span className="text-sm text-zinc-500">Password: 123456</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// ? Store
|
||||
import StoreProvider from 'app/StoreProvider'
|
||||
|
||||
// ? Conponents
|
||||
import { PageLoading, Alert } from '@/components'
|
||||
|
||||
export default function Layout({ children }) {
|
||||
//? Fix Hydration failed
|
||||
const [showChild, setShowChild] = useState(false)
|
||||
useEffect(() => {
|
||||
setShowChild(true)
|
||||
}, [])
|
||||
|
||||
if (!showChild) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<StoreProvider>
|
||||
{children}
|
||||
<Alert />
|
||||
<PageLoading />
|
||||
</StoreProvider>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
'use client'
|
||||
|
||||
import { useTitle } from '@/hooks'
|
||||
import { Address, Icons, PageContainer, Skeleton, WithAddressModal } from 'components'
|
||||
|
||||
import { useUserInfo } from 'hooks'
|
||||
|
||||
const BasicAddresses = ({ addressModalProps }) => {
|
||||
useTitle('地址管理')
|
||||
const { isAddress, address, isLoading, openAddressModal } = addressModalProps || {}
|
||||
|
||||
//? Get User Data
|
||||
const { userInfo } = useUserInfo()
|
||||
|
||||
//? Render(s)
|
||||
return (
|
||||
<main>
|
||||
<PageContainer title="地址">
|
||||
{isLoading ? (
|
||||
<section className="flex-1 px-5 ">
|
||||
<div className="flex justify-between py-4 border-b border-gray-200">
|
||||
<Skeleton.Item animated="background" height="h-5" width="w-52" />
|
||||
</div>
|
||||
<div className="my-2 space-y-3 text-gray-500">
|
||||
<div className="flex items-center gap-x-2 ">
|
||||
<Icons.UserLocation className="text-gray-500 icon" />
|
||||
<Skeleton.Item animated="background" height="h-5" width="w-40" />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2 ">
|
||||
<Icons.Post className="text-gray-500 icon" />
|
||||
<Skeleton.Item animated="background" height="h-5" width="w-40" />
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2 ">
|
||||
<Icons.Phone className="text-gray-500 icon" />
|
||||
<Skeleton.Item animated="background" height="h-5" width="w-40" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-x-2 ">
|
||||
<Icons.User className="text-gray-500 icon" />
|
||||
<Skeleton.Item animated="background" height="h-5" width="w-40" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : isAddress ? (
|
||||
<section className="flex-1 px-5 ">
|
||||
<div className="flex justify-between py-4 border-b border-gray-200">
|
||||
<p className="text-sm">{address?.street}</p>
|
||||
<button onClick={openAddressModal}>
|
||||
<Icons.Edit className="cursor-pointer icon" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="my-2 space-y-3 text-gray-500">
|
||||
<div className="flex items-center gap-x-2 ">
|
||||
<Icons.UserLocation className="text-gray-500 icon" />
|
||||
<span className="text-xs md:text-sm">
|
||||
{address?.province.name}, {address?.city.name}, {address?.area.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2 ">
|
||||
<Icons.Post className="text-gray-500 icon" />
|
||||
<span className="text-xs md:text-sm">{address?.postalCode}</span>
|
||||
</div>
|
||||
{userInfo?.mobile && (
|
||||
<div className="flex items-center gap-x-2 ">
|
||||
<Icons.Phone className="text-gray-500 icon" />
|
||||
<span className="text-xs md:text-sm">{userInfo?.mobile}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-x-2 ">
|
||||
<Icons.User className="text-gray-500 icon" />
|
||||
<span className="text-xs md:text-sm">{userInfo?.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<section className="flex flex-col items-center py-20 gap-y-4">
|
||||
<Address className="h-52 w-52" />
|
||||
<p>您尚未填写地址</p>
|
||||
<button
|
||||
className="flex items-center px-3 py-2 text-red-600 border-2 border-red-600 rounded-lg gap-x-3"
|
||||
onClick={openAddressModal}
|
||||
>
|
||||
<Icons.Location className="text-red-600 icon" />
|
||||
<span>地址登记</span>
|
||||
</button>
|
||||
</section>
|
||||
)}
|
||||
</PageContainer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
const Addresses = () => (
|
||||
<WithAddressModal>
|
||||
<BasicAddresses />
|
||||
</WithAddressModal>
|
||||
)
|
||||
|
||||
export default Addresses
|
|
@ -0,0 +1,9 @@
|
|||
import { ProfileLayout } from 'components'
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<>
|
||||
<ProfileLayout>{children}</ProfileLayout>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
'use client'
|
||||
|
||||
import { FavoritesListEmpty, PageContainer, ProfileLayout } from 'components'
|
||||
import { useTitle } from '@/hooks'
|
||||
|
||||
const Lists = () => {
|
||||
useTitle('我的收藏')
|
||||
//? Render(s)
|
||||
return (
|
||||
<main>
|
||||
<PageContainer title="我的收藏">
|
||||
<section className="py-20">
|
||||
<FavoritesListEmpty className="mx-auto h-52 w-52" />
|
||||
<p className="text-center">您的收藏夹列表为空</p>
|
||||
<span className="block my-3 text-base text-center text-amber-500">(即将上线)</span>
|
||||
</section>
|
||||
</PageContainer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Lists
|
|
@ -0,0 +1,62 @@
|
|||
'use client'
|
||||
import { useChangeRoute } from 'hooks'
|
||||
|
||||
import {
|
||||
OrderCard,
|
||||
Pagination,
|
||||
ShowWrapper,
|
||||
EmptyOrdersList,
|
||||
PageContainer,
|
||||
OrderSkeleton,
|
||||
} from 'components'
|
||||
|
||||
import { useGetOrdersQuery } from '@/store/services'
|
||||
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
|
||||
const Orders = () => {
|
||||
useTitle('订单管理')
|
||||
//? Assets
|
||||
const query = useUrlQuery()
|
||||
const changeRoute = useChangeRoute()
|
||||
|
||||
//? Get Orders Data
|
||||
const { data, isSuccess, isFetching, error, isError, refetch } = useGetOrdersQuery({
|
||||
pageSize: 5,
|
||||
page: query.page ? +query.page : 1,
|
||||
})
|
||||
|
||||
//? Render
|
||||
return (
|
||||
<main id="profileOrders">
|
||||
<PageContainer title="订单历史">
|
||||
<ShowWrapper
|
||||
error={error}
|
||||
isError={isError}
|
||||
refetch={refetch}
|
||||
isFetching={isFetching}
|
||||
isSuccess={isSuccess}
|
||||
dataLength={data ? data?.data?.ordersLength : 0}
|
||||
emptyComponent={<EmptyOrdersList />}
|
||||
loadingComponent={<OrderSkeleton />}
|
||||
>
|
||||
<div className="px-4 py-3 space-y-3">
|
||||
{data?.data?.orders.map(item => <OrderCard key={item._id} order={item} />)}
|
||||
</div>
|
||||
</ShowWrapper>
|
||||
|
||||
{data && data?.data?.ordersLength > 5 && (
|
||||
<div className="py-4 mx-auto lg:max-w-5xl">
|
||||
<Pagination
|
||||
pagination={data?.data?.pagination}
|
||||
changeRoute={changeRoute}
|
||||
section="profileOrders"
|
||||
client
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PageContainer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
export default Orders
|
|
@ -0,0 +1,22 @@
|
|||
'use client'
|
||||
|
||||
import { Orders, ProfileAside } from '@/components'
|
||||
import { useTitle } from '@/hooks'
|
||||
import { siteTitle } from '@/utils'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
export default function ProfilePage() {
|
||||
useTitle(`${siteTitle}-用户中心`)
|
||||
const { user } = useSelector(state => state.user)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="lg:hidden">
|
||||
<ProfileAside />
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
<Orders />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
'use client'
|
||||
|
||||
import { useTitle } from '@/hooks'
|
||||
import { Icons, UserMobileModal, UserNameModal, PageContainer, Skeleton } from 'components'
|
||||
|
||||
import { useUserInfo, useDisclosure } from 'hooks'
|
||||
|
||||
const PersonalInfo = () => {
|
||||
useTitle('账户信息')
|
||||
//? Assets
|
||||
const [isShowNameModal, nameModalHandlers] = useDisclosure()
|
||||
const [isShowPhoneModal, phoneModalHandlers] = useDisclosure()
|
||||
|
||||
//? Get User Data
|
||||
const { userInfo, isLoading } = useUserInfo()
|
||||
|
||||
//? Local Component
|
||||
const InfoField = ({ label, info, editHandler, isLoading }) => (
|
||||
<div className="flex-1 px-5">
|
||||
<div className="flex items-center justify-between py-4 border-b border-gray-200">
|
||||
<div>
|
||||
<span className="text-xs text-gray-700">{label}</span>
|
||||
{isLoading ? (
|
||||
<Skeleton.Item animated="background" height="h-5" width="w-44" />
|
||||
) : (
|
||||
<p className="h-5 text-sm">{info}</p>
|
||||
)}
|
||||
</div>
|
||||
{isLoading ? null : info ? (
|
||||
<Icons.Edit className="cursor-pointer icon" onClick={editHandler} />
|
||||
) : (
|
||||
<Icons.Plus className="cursor-pointer icon" onClick={editHandler} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
//? Render(s)
|
||||
return (
|
||||
<>
|
||||
{!isLoading && userInfo && (
|
||||
<>
|
||||
<UserNameModal
|
||||
isShow={isShowNameModal}
|
||||
onClose={nameModalHandlers.close}
|
||||
editedData={userInfo.name}
|
||||
/>
|
||||
<UserMobileModal
|
||||
isShow={isShowPhoneModal}
|
||||
onClose={phoneModalHandlers.close}
|
||||
editedData={userInfo.mobile}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<main>
|
||||
<PageContainer title="帐户信息">
|
||||
<section className="lg:flex">
|
||||
<InfoField
|
||||
label="名字和姓氏"
|
||||
info={userInfo?.name}
|
||||
editHandler={nameModalHandlers.open}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<InfoField
|
||||
label="电话号码"
|
||||
info={userInfo?.mobile}
|
||||
editHandler={phoneModalHandlers.open}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</section>
|
||||
</PageContainer>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PersonalInfo
|
|
@ -0,0 +1,141 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useDeleteReviewMutation, useGetReviewsQuery } from '@/store/services'
|
||||
|
||||
import {
|
||||
Pagination,
|
||||
ReveiwCard,
|
||||
ShowWrapper,
|
||||
EmptyCommentsList,
|
||||
ConfirmDeleteModal,
|
||||
PageContainer,
|
||||
HandleResponse,
|
||||
ReveiwSkeleton,
|
||||
} from 'components'
|
||||
|
||||
import { useDisclosure, useChangeRoute } from 'hooks'
|
||||
|
||||
import { useTitle, useUrlQuery } from '@/hooks'
|
||||
|
||||
const Reviews = () => {
|
||||
useTitle('我的评价')
|
||||
//? Assets
|
||||
const query = useUrlQuery()
|
||||
const changeRoute = useChangeRoute()
|
||||
|
||||
//? Modals
|
||||
const [isShowConfirmDeleteModal, confirmDeleteModalHandlers] = useDisclosure()
|
||||
|
||||
//? States
|
||||
const [deleteInfo, setDeleteInfo] = useState({
|
||||
id: '',
|
||||
})
|
||||
|
||||
//? Queries
|
||||
//* Delete Review
|
||||
const [
|
||||
deleteReview,
|
||||
{
|
||||
isSuccess: isSuccessDelete,
|
||||
isError: isErrorDelete,
|
||||
error: errorDelete,
|
||||
data: dataDelete,
|
||||
isLoading: isLoadingDelete,
|
||||
},
|
||||
] = useDeleteReviewMutation()
|
||||
|
||||
//* Get Reviews
|
||||
const { data, isSuccess, isFetching, error, isError, refetch } = useGetReviewsQuery({
|
||||
page: query.page ? +query.page : 1,
|
||||
})
|
||||
|
||||
//? Handlers
|
||||
const deleteReviewHandler = id => {
|
||||
setDeleteInfo({ id })
|
||||
confirmDeleteModalHandlers.open()
|
||||
}
|
||||
|
||||
const onConfirmDelete = () => deleteReview({ id: deleteInfo.id })
|
||||
|
||||
const onCancelDelete = () => {
|
||||
setDeleteInfo({ id: '' })
|
||||
confirmDeleteModalHandlers.close()
|
||||
}
|
||||
|
||||
const onSuccessDelete = () => {
|
||||
confirmDeleteModalHandlers.close()
|
||||
setDeleteInfo({ id: '' })
|
||||
}
|
||||
|
||||
const onErrorDelete = () => {
|
||||
confirmDeleteModalHandlers.close()
|
||||
setDeleteInfo({ id: '' })
|
||||
}
|
||||
|
||||
//? Render(s)
|
||||
return (
|
||||
<>
|
||||
<ConfirmDeleteModal
|
||||
title="评价"
|
||||
isLoading={isLoadingDelete}
|
||||
isShow={isShowConfirmDeleteModal}
|
||||
onClose={confirmDeleteModalHandlers.close}
|
||||
onCancel={onCancelDelete}
|
||||
onConfirm={onConfirmDelete}
|
||||
/>
|
||||
|
||||
{/* Handle Delete Response */}
|
||||
{(isSuccessDelete || isErrorDelete) && (
|
||||
<HandleResponse
|
||||
isError={isErrorDelete}
|
||||
isSuccess={isSuccessDelete}
|
||||
error={errorDelete?.data?.message}
|
||||
message={dataDelete?.message}
|
||||
onSuccess={onSuccessDelete}
|
||||
onError={onErrorDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main id="profileReviews">
|
||||
<PageContainer title="我的评价">
|
||||
<ShowWrapper
|
||||
error={error}
|
||||
isError={isError}
|
||||
refetch={refetch}
|
||||
isFetching={isFetching}
|
||||
isSuccess={isSuccess}
|
||||
dataLength={data ? data?.data?.reviewsLength : 0}
|
||||
emptyComponent={<EmptyCommentsList />}
|
||||
loadingComponent={<ReveiwSkeleton />}
|
||||
>
|
||||
<div className="px-4 py-3 space-y-3 ">
|
||||
{data &&
|
||||
data.data.reviews.map(item => (
|
||||
<ReveiwCard
|
||||
deleteReviewHandler={deleteReviewHandler}
|
||||
key={item._id}
|
||||
item={item}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</ShowWrapper>
|
||||
|
||||
{data && data?.data?.reviewsLength > 5 && (
|
||||
<div className="py-4 mx-auto lg:max-w-5xl">
|
||||
<Pagination
|
||||
pagination={data?.data?.pagination}
|
||||
changeRoute={changeRoute}
|
||||
section="profileReviews"
|
||||
client
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PageContainer>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Reviews
|
|
@ -0,0 +1,57 @@
|
|||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useAppSelector } from 'hooks'
|
||||
|
||||
import { truncate } from 'utils'
|
||||
|
||||
import { EmptyCart, PageContainer, ResponsiveImage } from 'components'
|
||||
import { useTitle } from '@/hooks'
|
||||
|
||||
const UserHistory = () => {
|
||||
useTitle('最近访问')
|
||||
//? Store
|
||||
const { lastSeen } = useAppSelector(state => state.user)
|
||||
|
||||
//? selector
|
||||
return (
|
||||
<main>
|
||||
<PageContainer title="最近访问">
|
||||
{lastSeen.length > 0 ? (
|
||||
<div className="px-3 space-y-4 md:py-4 md:space-y-0 md:grid md:grid-cols-2 md:gap-x-2 lg:grid-cols-3 md:gap-y-3">
|
||||
{lastSeen.map(item => (
|
||||
<article
|
||||
className="border-b md:hover:shadow-3xl md:h-64 md:border-0 "
|
||||
key={item.productID}
|
||||
>
|
||||
<Link
|
||||
href={`/products/${item.productID}`}
|
||||
className="flex items-center gap-4 py-4 md:items-start md:flex-col"
|
||||
>
|
||||
<ResponsiveImage
|
||||
dimensions="w-36 h-36"
|
||||
className="md:mx-auto"
|
||||
src={item.image.url}
|
||||
alt={item.title}
|
||||
/>
|
||||
|
||||
<h5 className="flex-1 px-3 text-left text-gray-800 leadiri-6 md:h-32">
|
||||
{truncate(item.title, 80)}
|
||||
</h5>
|
||||
</Link>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<section className="py-20">
|
||||
<EmptyCart className="mx-auto h-52 w-52" />
|
||||
<p className="text-center">您的最近访问列表为空</p>
|
||||
</section>
|
||||
)}
|
||||
</PageContainer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserHistory
|
|
@ -0,0 +1,15 @@
|
|||
'use client'
|
||||
|
||||
import { useRef } from 'react'
|
||||
import { Provider } from 'react-redux'
|
||||
import { store } from 'store'
|
||||
|
||||
export default function StoreProvider({ children }) {
|
||||
const storeRef = useRef()
|
||||
if (!storeRef.current) {
|
||||
// Create the store instance the first time this renders
|
||||
storeRef.current = store
|
||||
}
|
||||
|
||||
return <Provider store={storeRef.current}>{children}</Provider>
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { usersRepo } from 'helpers'
|
||||
import { apiHandler, setJson } from 'helpers/api'
|
||||
|
||||
const login = apiHandler(
|
||||
async req => {
|
||||
const body = await req.json()
|
||||
const result = await usersRepo.authenticate(body)
|
||||
return setJson({
|
||||
data: result,
|
||||
message: '登录成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
schema: joi.object({
|
||||
email: joi.string().required(),
|
||||
password: joi.string().min(6).required(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
export const POST = login
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,24 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { usersRepo } from 'helpers'
|
||||
import { apiHandler, setJson } from 'helpers/api'
|
||||
|
||||
const register = apiHandler(
|
||||
async req => {
|
||||
const body = await req.json()
|
||||
const result = await usersRepo.create(body)
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
},
|
||||
{
|
||||
schema: joi.object({
|
||||
name: joi.string().required(),
|
||||
email: joi.string().required(),
|
||||
password: joi.string().min(6).required(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
export const POST = register
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,28 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { usersRepo } from 'helpers'
|
||||
import { apiHandler } from 'helpers/api'
|
||||
import { setJson } from '@/helpers/api'
|
||||
|
||||
const getUertInfo = apiHandler(
|
||||
async req => {
|
||||
const userId = req.headers.get('userId')
|
||||
const user = await usersRepo.getById(userId)
|
||||
return setJson({
|
||||
data: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
root: user.root,
|
||||
address: user.address,
|
||||
mobile: user.mobile,
|
||||
},
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getUertInfo
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,54 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { bannerRepo } from '@/helpers'
|
||||
|
||||
const getDetail = apiHandler(async (req, { params }) => {
|
||||
const { id } = params
|
||||
const result = await bannerRepo.getById(id)
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
})
|
||||
|
||||
const update = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
const body = await req.json()
|
||||
await bannerRepo.update(id, body)
|
||||
return setJson({
|
||||
message: '更新成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'root',
|
||||
schema: joi.object({
|
||||
category_id: joi.string().required(),
|
||||
image: joi.object().required(),
|
||||
isPublic: joi.boolean().required(),
|
||||
title: joi.string().required(),
|
||||
type: joi.string().required(),
|
||||
uri: joi.string().required(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
const _delete = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
await bannerRepo.delete(id)
|
||||
return setJson({
|
||||
message: '删除成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'root',
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getDetail
|
||||
export const PUT = update
|
||||
export const DELETE = _delete
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,50 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { bannerRepo, getQuery } from '@/helpers'
|
||||
|
||||
const getAll = apiHandler(
|
||||
async req => {
|
||||
const query = getQuery(req)
|
||||
const category = query?.category
|
||||
const result = await bannerRepo.getAll(
|
||||
{},
|
||||
{
|
||||
category_id: category,
|
||||
}
|
||||
)
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
}
|
||||
)
|
||||
|
||||
const create = apiHandler(
|
||||
async req => {
|
||||
const body = await req.json()
|
||||
await bannerRepo.create(body)
|
||||
return setJson({
|
||||
message: '新增成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'root',
|
||||
schema: joi.object({
|
||||
category_id: joi.string().required(),
|
||||
image: joi.object().required(),
|
||||
isPublic: joi.boolean().required(),
|
||||
title: joi.string().required(),
|
||||
type: joi.string().required(),
|
||||
uri: joi.string().required(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getAll
|
||||
export const POST = create
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,45 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { categoryRepo } from '@/helpers'
|
||||
|
||||
const deleteCategory = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
await categoryRepo.delete(id)
|
||||
return setJson({
|
||||
message: '删除成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'root',
|
||||
}
|
||||
)
|
||||
|
||||
const updateCategory = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
const body = await req.json()
|
||||
await categoryRepo.update(id, body)
|
||||
return setJson({
|
||||
message: '更新成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
schema: joi.object({
|
||||
name: joi.string().required(),
|
||||
slug: joi.string().required(),
|
||||
image: joi.string().required(),
|
||||
colors: joi.object(),
|
||||
level: joi.number().required(),
|
||||
parent: joi.string(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
export const DELETE = deleteCategory
|
||||
export const PUT = updateCategory
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,62 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { categoryRepo } from '@/helpers'
|
||||
|
||||
const getCategory = apiHandler(async req => {
|
||||
const result = await categoryRepo.getAll()
|
||||
async function getCategoriesWithChildren() {
|
||||
const allCategories = await categoryRepo.getAll()
|
||||
|
||||
function findChildren(category) {
|
||||
const children = allCategories.filter(c => c.parent && c.parent === category._id)
|
||||
if (children.length > 0) {
|
||||
category.children = children.map(child => {
|
||||
return findChildren(child)
|
||||
})
|
||||
}
|
||||
return category
|
||||
}
|
||||
|
||||
const rootCategories = allCategories.filter(c => !c.parent)
|
||||
const categoriesWithChildren = rootCategories.map(category => {
|
||||
return findChildren(category)
|
||||
})
|
||||
|
||||
return categoriesWithChildren
|
||||
}
|
||||
const categoriesList = await getCategoriesWithChildren()
|
||||
return setJson({
|
||||
data: {
|
||||
categories: result,
|
||||
categoriesList: categoriesList[0],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const createCategory = apiHandler(
|
||||
async req => {
|
||||
const body = await req.json()
|
||||
await categoryRepo.create(body)
|
||||
|
||||
return setJson({
|
||||
message: '创建分类成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
schema: joi.object({
|
||||
name: joi.string().required(),
|
||||
slug: joi.string().required(),
|
||||
image: joi.string().required(),
|
||||
colors: joi.object().required(),
|
||||
level: joi.number().required(),
|
||||
parent: joi.string(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getCategory
|
||||
export const POST = createCategory
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,52 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { detailsRepo } from '@/helpers'
|
||||
|
||||
const getDetails = apiHandler(async (req, { params }) => {
|
||||
const { id } = params
|
||||
const result = await detailsRepo.getById(id)
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
})
|
||||
|
||||
const updateDetails = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
const body = await req.json()
|
||||
await detailsRepo.update(id, body)
|
||||
return setJson({
|
||||
message: '商品更新成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
schema: joi.object({
|
||||
category_id: joi.string().required(),
|
||||
info: joi.array().required(),
|
||||
optionsType: joi.string().required(),
|
||||
specification: joi.array().required(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
const deleteDetails = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
await detailsRepo.delete(id)
|
||||
return setJson({
|
||||
message: '商品已成功删除',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'root',
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getDetails
|
||||
export const PUT = updateDetails
|
||||
export const DELETE = deleteDetails
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,41 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { detailsRepo } from '@/helpers'
|
||||
|
||||
const getAllDetails = apiHandler(
|
||||
async req => {
|
||||
const result = await detailsRepo.getAll()
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
}
|
||||
)
|
||||
|
||||
const createDetails = apiHandler(
|
||||
async req => {
|
||||
const body = await req.json()
|
||||
await detailsRepo.create(body)
|
||||
return setJson({
|
||||
message: '新增商品成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
schema: joi.object({
|
||||
category_id: joi.string().required(),
|
||||
info: joi.array().required(),
|
||||
optionsType: joi.string().required(),
|
||||
specification: joi.array().required(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getAllDetails
|
||||
export const POST = createDetails
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,48 @@
|
|||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { bannerRepo, categoryRepo, sliderRepo } from '@/helpers'
|
||||
|
||||
const getFeed = apiHandler(
|
||||
async req => {
|
||||
const currentCategory = await categoryRepo.getOne({
|
||||
parent: undefined,
|
||||
})
|
||||
const childCategories = await categoryRepo.getAll(
|
||||
{},
|
||||
{
|
||||
parent: currentCategory?._id,
|
||||
}
|
||||
)
|
||||
|
||||
const sliders = await sliderRepo.getAll({}, { category_id: currentCategory?._id })
|
||||
|
||||
const bannerOneType = await bannerRepo.getAll(
|
||||
{},
|
||||
{
|
||||
category_id: currentCategory?._id,
|
||||
type: 'one',
|
||||
}
|
||||
)
|
||||
const bannerTwoType = await bannerRepo.getAll(
|
||||
{},
|
||||
{
|
||||
category_id: currentCategory?._id,
|
||||
type: 'two',
|
||||
}
|
||||
)
|
||||
return setJson({
|
||||
data: {
|
||||
currentCategory,
|
||||
childCategories,
|
||||
sliders,
|
||||
bannerOneType,
|
||||
bannerTwoType,
|
||||
},
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: false,
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getFeed
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,29 @@
|
|||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { orderRepo } from '@/helpers'
|
||||
|
||||
const getOrder = apiHandler(async (req, { params }) => {
|
||||
const { id } = params
|
||||
const result = await orderRepo.getById(id)
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
})
|
||||
|
||||
const updateOrder = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
const body = await req.json()
|
||||
await orderRepo.update(id, body)
|
||||
return setJson({
|
||||
message: '已经通过确认',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
}
|
||||
)
|
||||
|
||||
export const PATCH = updateOrder
|
||||
export const GET = getOrder
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,24 @@
|
|||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { getQuery, orderRepo } from '@/helpers'
|
||||
|
||||
const getOrders = apiHandler(
|
||||
async req => {
|
||||
const query = getQuery(req)
|
||||
const page = query.page ? +query.page : 1
|
||||
const page_size = query.page_size ? +query.page_size : 10
|
||||
const result = await orderRepo.getAll({
|
||||
page,
|
||||
page_size,
|
||||
})
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getOrders
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,52 @@
|
|||
import joi from 'joi'
|
||||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { getQuery, orderRepo } from '@/helpers'
|
||||
|
||||
const getOrders = apiHandler(
|
||||
async req => {
|
||||
const query = getQuery(req)
|
||||
const page = query.page ? +query.page : 1
|
||||
const page_size = query.page_size ? +query.page_size : 10
|
||||
const userId = req.headers.get('userId')
|
||||
const result = await orderRepo.getAll(
|
||||
{
|
||||
page,
|
||||
page_size,
|
||||
},
|
||||
{ user: userId }
|
||||
)
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
}
|
||||
)
|
||||
|
||||
const createOrder = apiHandler(
|
||||
async req => {
|
||||
const userId = req.headers.get('userId')
|
||||
const body = await req.json()
|
||||
await orderRepo.create(userId, body)
|
||||
return setJson({
|
||||
message: '创建订单成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
schema: joi.object({
|
||||
address: joi.object().required(),
|
||||
mobile: joi.string(),
|
||||
cart: joi.array().required(),
|
||||
totalItems: joi.number().required(),
|
||||
totalPrice: joi.number().required(),
|
||||
totalDiscount: joi.number().required(),
|
||||
paymentMethod: joi.string().required(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getOrders
|
||||
export const POST = createOrder
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,60 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { productRepo } from '@/helpers'
|
||||
|
||||
const getProduct = apiHandler(async (req, { params }) => {
|
||||
const { id } = params
|
||||
const result = await productRepo.getById(id)
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
})
|
||||
|
||||
const updateProduct = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
const body = await req.json()
|
||||
await productRepo.update(id, body)
|
||||
return setJson({
|
||||
message: '商品更新成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
schema: joi.object({
|
||||
title: joi.string().required(),
|
||||
price: joi.number().required(),
|
||||
category: joi.array().required(),
|
||||
images: joi.array().required(),
|
||||
info: joi.array().required(),
|
||||
specification: joi.array().required(),
|
||||
inStock: joi.number(),
|
||||
description: joi.string().allow(''),
|
||||
discount: joi.number(),
|
||||
sizes: joi.array(),
|
||||
colors: joi.array(),
|
||||
category_levels: joi.object(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
const deleteProduct = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
await productRepo.delete(id)
|
||||
return setJson({
|
||||
message: '商品已成功删除',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'root',
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getProduct
|
||||
export const PUT = updateProduct
|
||||
export const DELETE = deleteProduct
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,23 @@
|
|||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { getQuery, productRepo } from '@/helpers'
|
||||
|
||||
const itemDetail = apiHandler(async req => {
|
||||
const { id } = getQuery(req)
|
||||
const result = await productRepo.getItemDetail(id)
|
||||
|
||||
// return new Promise((resolve, reject) => {
|
||||
// setTimeout(() => {
|
||||
// resolve(
|
||||
// setJson({
|
||||
// data: result,
|
||||
// })
|
||||
// )
|
||||
// }, 3000)
|
||||
// })
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
})
|
||||
|
||||
export const GET = itemDetail
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,114 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { db, getQuery, productRepo } from '@/helpers'
|
||||
import Category from '@/models/Category'
|
||||
|
||||
const getAllProduct = apiHandler(async req => {
|
||||
const query = getQuery(req)
|
||||
|
||||
const page = query.page ? +query.page : 1
|
||||
const page_size = query.page_size ? +query.page_size : 10
|
||||
const sort = query.sort ? +query.sort : 1
|
||||
|
||||
const { category, search, inStock, discount, price } = query
|
||||
|
||||
//? Filters
|
||||
await db.connect()
|
||||
const currentCategory = await Category.findOne({ slug: category })
|
||||
await db.disconnect()
|
||||
|
||||
const categoryFilter = currentCategory
|
||||
? {
|
||||
category: { $in: currentCategory._id.toString() },
|
||||
}
|
||||
: {}
|
||||
|
||||
const searchFilter = search
|
||||
? {
|
||||
title: {
|
||||
$regex: search,
|
||||
$options: 'i',
|
||||
},
|
||||
}
|
||||
: {}
|
||||
|
||||
const inStockFilter = inStock === 'true' ? { inStock: { $gte: 1 } } : {}
|
||||
|
||||
const discountFilter = discount === 'true' ? { discount: { $gte: 1 }, inStock: { $gte: 1 } } : {}
|
||||
|
||||
const priceFilter = price
|
||||
? {
|
||||
price: {
|
||||
$gte: +price.split('-')[0],
|
||||
$lte: +price.split('-')[1],
|
||||
},
|
||||
}
|
||||
: {}
|
||||
|
||||
//? Sort
|
||||
const order =
|
||||
sort === 3
|
||||
? { price: 1 }
|
||||
: sort === 4
|
||||
? { price: -1 }
|
||||
: sort === 2
|
||||
? { sold: -1 }
|
||||
: sort === 1
|
||||
? { createdAt: -1 }
|
||||
: sort === 5
|
||||
? { rating: -1 }
|
||||
: sort === 6
|
||||
? { discount: -1 }
|
||||
: { _id: -1 }
|
||||
|
||||
const result = await productRepo.getAll(
|
||||
{
|
||||
page,
|
||||
page_size,
|
||||
},
|
||||
{
|
||||
...categoryFilter,
|
||||
...inStockFilter,
|
||||
...discountFilter,
|
||||
...priceFilter,
|
||||
...searchFilter,
|
||||
},
|
||||
order
|
||||
)
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
})
|
||||
|
||||
const createProduct = apiHandler(
|
||||
async req => {
|
||||
const body = await req.json()
|
||||
await productRepo.create(body)
|
||||
return setJson({
|
||||
message: '新增商品成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
schema: joi.object({
|
||||
title: joi.string().required(),
|
||||
price: joi.number().required(),
|
||||
category: joi.array().required(),
|
||||
images: joi.array().required(),
|
||||
info: joi.array().required(),
|
||||
specification: joi.array().required(),
|
||||
inStock: joi.number(),
|
||||
description: joi.string().allow(''),
|
||||
discount: joi.number(),
|
||||
sizes: joi.array(),
|
||||
colors: joi.array(),
|
||||
category_levels: joi.object(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getAllProduct
|
||||
export const POST = createProduct
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,74 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { productRepo, reviewRepo } from '@/helpers'
|
||||
|
||||
const getDetail = apiHandler(async (req, { params }) => {
|
||||
const { id } = params
|
||||
const result = await reviewRepo.getById(id)
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
})
|
||||
|
||||
const update = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
const body = await req.json()
|
||||
const review = await reviewRepo.update(id, body)
|
||||
const product = await productRepo.getById(review.product)
|
||||
const reviews = await reviewRepo.getAll(
|
||||
{ page: 0, page_size: 0 },
|
||||
{
|
||||
product: product?._id,
|
||||
}
|
||||
)
|
||||
|
||||
if (product && reviews.reviews.length) {
|
||||
let { totalRating, totalReviews } = reviews.reviews.reduce(
|
||||
(total, item) => {
|
||||
if (item.status === 2) {
|
||||
total.totalRating += item.rating
|
||||
total.totalReviews += 1
|
||||
}
|
||||
return total
|
||||
},
|
||||
{ totalRating: 0, totalReviews: 0 }
|
||||
)
|
||||
await productRepo.update(review.product, {
|
||||
numReviews: totalReviews,
|
||||
rating: totalReviews ? totalRating / totalReviews : 0,
|
||||
})
|
||||
}
|
||||
|
||||
return setJson({
|
||||
message: '更新成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
schema: joi.object({
|
||||
status: joi.number().required(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
const _delete = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
await reviewRepo.delete(id)
|
||||
return setJson({
|
||||
message: '删除成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'root',
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getDetail
|
||||
export const PATCH = update
|
||||
export const DELETE = _delete
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,27 @@
|
|||
import { getQuery, reviewRepo } from '@/helpers'
|
||||
import { apiHandler, setJson } from '@/helpers/api'
|
||||
|
||||
const getAll = apiHandler(
|
||||
async req => {
|
||||
const query = getQuery(req)
|
||||
|
||||
const page = query.page ? +query.page : 1
|
||||
const page_size = query.page_size ? +query.page_size : 10
|
||||
|
||||
const result = await reviewRepo.getAll({
|
||||
page,
|
||||
page_size,
|
||||
})
|
||||
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getAll
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,33 @@
|
|||
import { getQuery, reviewRepo } from '@/helpers'
|
||||
const { apiHandler, setJson } = require('@/helpers/api')
|
||||
|
||||
const getReviews = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
const query = getQuery(req)
|
||||
|
||||
const page = query.page ? +query.page : 1
|
||||
const page_size = query.page_size ? +query.page_size : 5
|
||||
|
||||
const result = await reviewRepo.getAll(
|
||||
{
|
||||
page,
|
||||
page_size,
|
||||
},
|
||||
{
|
||||
product: id,
|
||||
status: 2,
|
||||
}
|
||||
)
|
||||
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: false,
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getReviews
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,57 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { getQuery, reviewRepo } from '@/helpers'
|
||||
|
||||
const getAll = apiHandler(
|
||||
async req => {
|
||||
const userId = req.headers.get('userId')
|
||||
const query = getQuery(req)
|
||||
|
||||
const page = query.page ? +query.page : 1
|
||||
const page_size = query.page_size ? +query.page_size : 10
|
||||
|
||||
const result = await reviewRepo.getAll(
|
||||
{
|
||||
page,
|
||||
page_size,
|
||||
},
|
||||
{
|
||||
user: userId,
|
||||
}
|
||||
)
|
||||
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
}
|
||||
)
|
||||
|
||||
const create = apiHandler(
|
||||
async req => {
|
||||
const userId = req.headers.get('userId')
|
||||
const body = await req.json()
|
||||
await reviewRepo.create(userId, body)
|
||||
return setJson({
|
||||
message: '新增成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
schema: joi.object({
|
||||
product: joi.string().required(),
|
||||
title: joi.string().required(),
|
||||
rating: joi.number().required(),
|
||||
comment: joi.string().required(),
|
||||
negativePoints: joi.array(),
|
||||
positivePoints: joi.array(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getAll
|
||||
export const POST = create
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,53 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { sliderRepo } from '@/helpers'
|
||||
|
||||
const getDetail = apiHandler(async (req, { params }) => {
|
||||
const { id } = params
|
||||
const result = await sliderRepo.getById(id)
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
})
|
||||
|
||||
const update = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
const body = await req.json()
|
||||
await sliderRepo.update(id, body)
|
||||
return setJson({
|
||||
message: '更新成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
schema: joi.object({
|
||||
category_id: joi.string().required(),
|
||||
image: joi.object().required(),
|
||||
isPublic: joi.boolean().required(),
|
||||
title: joi.string().required(),
|
||||
uri: joi.string().required(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
const _delete = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
await sliderRepo.delete(id)
|
||||
return setJson({
|
||||
message: '删除成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'root',
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getDetail
|
||||
export const PUT = update
|
||||
export const DELETE = _delete
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,49 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { setJson, apiHandler } from '@/helpers/api'
|
||||
import { sliderRepo, getQuery } from '@/helpers'
|
||||
|
||||
const getAll = apiHandler(
|
||||
async req => {
|
||||
const query = getQuery(req)
|
||||
const category = query?.category
|
||||
const result = await sliderRepo.getAll(
|
||||
{},
|
||||
{
|
||||
category_id: category,
|
||||
}
|
||||
)
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
}
|
||||
)
|
||||
|
||||
const create = apiHandler(
|
||||
async req => {
|
||||
const body = await req.json()
|
||||
await sliderRepo.create(body)
|
||||
return setJson({
|
||||
message: '新增成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
schema: joi.object({
|
||||
category_id: joi.string().required(),
|
||||
image: joi.object().required(),
|
||||
isPublic: joi.boolean().required(),
|
||||
title: joi.string().required(),
|
||||
uri: joi.string().required(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getAll
|
||||
export const POST = create
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,28 @@
|
|||
import { apiHandler, setJson } from '@/helpers/api'
|
||||
import { STS } from 'ali-oss'
|
||||
|
||||
const storeSTS = new STS({
|
||||
accessKeyId: process.env.NEXT_PUBLIC_ALI_ACCESS_KEY,
|
||||
accessKeySecret: process.env.NEXT_PUBLIC_ALI_SECRET_KEY,
|
||||
})
|
||||
|
||||
const getToken = apiHandler(
|
||||
async req => {
|
||||
const result = await storeSTS.assumeRole(
|
||||
process.env.NEXT_PUBLIC_ALI_ACS_RAM_NAME,
|
||||
'',
|
||||
'3000',
|
||||
'sessiontest'
|
||||
)
|
||||
return setJson({
|
||||
data: { ...result.credentials },
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getToken
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,42 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { usersRepo } from 'helpers'
|
||||
import { apiHandler } from 'helpers/api'
|
||||
import { setJson } from '@/helpers/api'
|
||||
|
||||
const updateRole = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
const body = await req.json()
|
||||
await usersRepo.update(id, body)
|
||||
|
||||
return setJson({
|
||||
message: '更新成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
schema: joi.object({
|
||||
role: joi.string().required().valid('user', 'admin'),
|
||||
}),
|
||||
identity: 'root',
|
||||
}
|
||||
)
|
||||
|
||||
const deleteUser = apiHandler(
|
||||
async (req, { params }) => {
|
||||
const { id } = params
|
||||
await usersRepo.delete(id)
|
||||
return setJson({
|
||||
message: '用户信息已经删除',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'root',
|
||||
}
|
||||
)
|
||||
|
||||
export const PATCH = updateRole
|
||||
export const DELETE = deleteUser
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,26 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { usersRepo } from 'helpers'
|
||||
import { apiHandler } from 'helpers/api'
|
||||
import { setJson } from '@/helpers/api'
|
||||
|
||||
const resetPassword = apiHandler(
|
||||
async req => {
|
||||
const userId = req.headers.get('userId')
|
||||
const { password } = await req.json()
|
||||
await usersRepo.resetPassword(userId, password)
|
||||
|
||||
return setJson({
|
||||
message: '密码更新成功',
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
schema: joi.object({
|
||||
password: joi.string().min(6).required(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
export const PATCH = resetPassword
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,48 @@
|
|||
import joi from 'joi'
|
||||
|
||||
import { usersRepo } from 'helpers'
|
||||
import { apiHandler } from 'helpers/api'
|
||||
import { setJson } from '@/helpers/api'
|
||||
|
||||
const getUsers = apiHandler(
|
||||
async req => {
|
||||
const searchParams = req.nextUrl.searchParams
|
||||
const page = +searchParams.get('page') || 1
|
||||
const page_size = +searchParams.get('page_size') || 5
|
||||
|
||||
const result = await usersRepo.getAll({
|
||||
page,
|
||||
page_size,
|
||||
})
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
identity: 'admin',
|
||||
}
|
||||
)
|
||||
|
||||
const uploadInfo = apiHandler(
|
||||
async req => {
|
||||
const userId = req.headers.get('userId')
|
||||
const body = await req.json()
|
||||
const result = await usersRepo.update(userId, body)
|
||||
return setJson({
|
||||
data: result,
|
||||
})
|
||||
},
|
||||
{
|
||||
isJwt: true,
|
||||
schema: joi.object({
|
||||
name: joi.string(),
|
||||
address: joi.object(),
|
||||
mobile: joi.string(),
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
export const GET = getUsers
|
||||
export const PATCH = uploadInfo
|
||||
export const dynamic = 'force-dynamic'
|
|
@ -0,0 +1,21 @@
|
|||
import '/styles/main.css'
|
||||
import '/styles/browser-styles.css'
|
||||
import '/styles/swiper.css'
|
||||
|
||||
import { enSiteTitle, siteDescription, siteTitle } from '@/utils'
|
||||
|
||||
export const metadata = {
|
||||
title: `${siteTitle} | ${enSiteTitle}`,
|
||||
description: `${siteDescription}`,
|
||||
icons: {
|
||||
icon: '/favicon.ico',
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
export const metadata = {
|
||||
title: '404 Not Found!',
|
||||
}
|
||||
|
||||
import StoreProvider from 'app/StoreProvider'
|
||||
import { ArrowLink, ResponsiveImage, ClientLayout } from '@/components'
|
||||
|
||||
export default function NotFoundPage() {
|
||||
//? Render(s)
|
||||
return (
|
||||
<StoreProvider>
|
||||
<ClientLayout>
|
||||
<main className="flex flex-col items-center justify-center py-8 gap-y-6 xl:mt-28">
|
||||
<p className="text-base font-semibold text-black">404 Not Found!</p>
|
||||
<ArrowLink path="/">返回首页</ArrowLink>
|
||||
<ResponsiveImage
|
||||
dimensions="w-full max-w-lg h-72"
|
||||
src="/icons/page-not-found.png"
|
||||
layout="fill"
|
||||
alt="404"
|
||||
/>
|
||||
</main>
|
||||
</ClientLayout>
|
||||
</StoreProvider>
|
||||
)
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
module.exports = { extends: ['@commitlint/config-conventional'] }
|
|
@ -0,0 +1,48 @@
|
|||
import { Icons, Skeleton, WithAddressModal } from 'components'
|
||||
|
||||
const BasicAddressBar = ({ addressModalProps }) => {
|
||||
//? Props
|
||||
const { address, isLoading, isVerify, openAddressModal, isAddress } = addressModalProps || {}
|
||||
|
||||
//? Render(s)
|
||||
if (!isVerify) {
|
||||
return null
|
||||
} else if (isLoading) {
|
||||
return <Skeleton.Item animated="background" height="h-5 lg:h-6" width="w-3/4 lg:w-1/4" />
|
||||
} else if (!isAddress) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openAddressModal}
|
||||
className="flex items-center w-full gap-x-1 lg:w-fit"
|
||||
>
|
||||
<Icons.Location2 className="icon" />
|
||||
<span>请选择您所在的城市</span>
|
||||
|
||||
<Icons.ArrowRight2 className="mr-auto icon" />
|
||||
</button>
|
||||
)
|
||||
} else if (isAddress) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openAddressModal}
|
||||
className="flex items-center w-full gap-x-1 lg:w-fit"
|
||||
>
|
||||
<Icons.Location2 className="icon" />
|
||||
<span>
|
||||
发送{address?.province.name}, {address?.city.name}, {address?.area.name}
|
||||
</span>
|
||||
<Icons.ArrowRight2 className="mr-auto icon" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default function AddressBar() {
|
||||
return (
|
||||
<WithAddressModal>
|
||||
<BasicAddressBar />
|
||||
</WithAddressModal>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
'use client'
|
||||
import { useEffect } from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { removeAlert } from 'store'
|
||||
|
||||
export default function Alert() {
|
||||
//? Store
|
||||
const { isShow, status, title } = useSelector(state => state.alert)
|
||||
|
||||
//? Assets
|
||||
const dispatch = useDispatch()
|
||||
let IconSrc
|
||||
switch (status) {
|
||||
case 'error':
|
||||
IconSrc = '/icons/error.svg'
|
||||
break
|
||||
case 'success':
|
||||
IconSrc = '/icons/success.svg'
|
||||
break
|
||||
case 'exclamation':
|
||||
IconSrc = '/icons/exclamation.svg'
|
||||
break
|
||||
case 'question':
|
||||
IconSrc = '/icons/question.svg'
|
||||
break
|
||||
default:
|
||||
IconSrc = '/icons/nothing.svg'
|
||||
break
|
||||
}
|
||||
|
||||
//? Re-Renders
|
||||
useEffect(() => {
|
||||
if (isShow) {
|
||||
const timeout = setTimeout(() => {
|
||||
dispatch(removeAlert())
|
||||
}, 2000)
|
||||
return () => clearTimeout(timeout)
|
||||
}
|
||||
}, [isShow])
|
||||
|
||||
//? Render(s)
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
isShow ? 'opacity-100 visible' : 'opacity-0 invisible '
|
||||
} transition-all duration-500 fixed inset-0 z-40`}
|
||||
>
|
||||
<div className="w-full h-full bg-gray-400/20" onClick={() => dispatch(removeAlert())} />
|
||||
<div
|
||||
className={`${
|
||||
isShow ? 'top-40' : '-top-full'
|
||||
} max-w-md fixed transition-all duration-700 left-0 right-0 mx-auto z-40`}
|
||||
>
|
||||
<div className="p-3 mx-2 text-center bg-white rounded-md shadow h-fit">
|
||||
<Image className="mx-auto" src={IconSrc} width="80" height="80" alt={status} />
|
||||
<p className="mt-2">{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue