first commit

dev
王文杰 10 months ago
commit a33a46e8cd

@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

@ -0,0 +1,42 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier',
],
// 小程序全局变量
globals: {
uni: true,
wx: true,
WechatMiniprogram: true,
getCurrentPages: true,
UniApp: true,
UniHelper: true,
Page: true,
AnyObject: true,
},
parserOptions: {
ecmaVersion: 'latest',
},
rules: {
'prettier/prettier': [
'warn',
{
singleQuote: true,
semi: false,
printWidth: 100,
trailingComma: 'all',
endOfLine: 'auto',
},
],
'vue/multi-word-component-names': ['off'],
'vue/no-setup-props-destructure': ['off'],
'vue/no-deprecated-html-element-is': ['off'],
'@typescript-eslint/no-unused-vars': ['off'],
},
}

@ -0,0 +1,66 @@
# name: deploy
# on:
# push:
# branches: [master]
# pull_request:
# branches: [master]
# jobs:
# build:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# with:
# node-version: '18.4.0'
# - uses: actions/setup-node@v3
# with:
# node-version: '18.4.0'
# - run: yarn install
# - run: yarn run build:h5
# - uses: actions/upload-artifact@v3
# with:
# path: ./dist/build/h5
# deploy:
# needs: build
# runs-on: ubuntu-latest
# if: github.ref=='refs/heads/master'
# steps:
# - uses: actions/download-artifact@v2
# with:
# node-version: 'v16.13.0'
# path: ./dist/build/h5
# - uses: peaceiris/actions-gh-pages@v3
# with:
# github_token: ${{ secrets.GITHUB_TOKEN }}
# publish_dir: ./dist/build/h5/artifact
# gitee:
# needs: deploy
# runs-on: ubuntu-latest
# steps:
# - name: Sync To Gitee
# uses: wearerequired/git-mirror-action@master
# env:
# # 注意在 Settings->Secrets 配置 GITEE_RSA_PRIVATE_KEY
# SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }}
# with:
# # 注意替换为你的 GitHub 源仓库地址
# source-repo: git@github.com:erabbit-dev/erabbit-uni-app.git
# # 注意替换为你的 Gitee 目标仓库地址
# destination-repo: git@gitee.com:Megasu/uniapp-shop-vue3-ts.git
# # dry-run: true
# - name: Build Gitee Pages
# uses: yanglbme/gitee-pages-action@main
# with:
# # 注意替换为你的 Gitee 用户名
# gitee-username: megasu@qq.com
# # 注意在 Settings->Secrets 配置 GITEE_PASSWORD
# gitee-password: ${{ secrets.GITEE_PASSWORD }}
# # 注意替换为你的 Gitee 仓库,仓库名严格区分大小写,请准确填写,否则会出错
# gitee-repo: Megasu/uniapp-shop-vue3-ts
# # 要部署的分支,默认是 master若是其他分支则需要指定指定的分支必须存在
# branch: gh-pages
# # directory: docs/.vitepress/dist

13
.gitignore vendored

@ -0,0 +1,13 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
.DS_Store
*.local

@ -0,0 +1,30 @@
{
// launch.json configurations app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtypelocalremote, localremote
"version": "0.0",
"configurations": [
{
"app-plus" :
{
"launchtype" : "local"
},
"default" :
{
"launchtype" : "local"
},
"h5" :
{
"launchtype" : "local"
},
"mp-weixin" :
{
"launchtype" : "remote"
},
"type" : "uniCloud"
},
{
"playground": "standard",
"type": "uni-app:app-android"
}
]
}

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint-staged

@ -0,0 +1,7 @@
{
"singleQuote": true,
"semi": false,
"printWidth": 100,
"trailingComma": "all",
"endOfLine": "auto"
}

@ -0,0 +1,13 @@
{
//
"recommendations": [
"mrmaoddxxaa.create-uniapp-view", // uni-app
"uni-helper.uni-helper-vscode", // uni-app
"evils.uniapp-vscode", // uni-app
"vue.volar", // vue3
"vue.vscode-typescript-vue-plugin", // vue3 ts
"editorconfig.editorconfig", // editorconfig
"dbaeumer.vscode-eslint", // eslint
"esbenp.prettier-vscode" // prettier
]
}

@ -0,0 +1,15 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"program": "${file}"
}
]
}

@ -0,0 +1,13 @@
{
//
"editor.formatOnSave": true,
//
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
//
"files.associations": {
"pages.json": "jsonc", // pages.json
"manifest.json": "jsonc" // manifest.json
}
}

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -0,0 +1,39 @@
### 技术栈
- 前端框架:[uni-app](https://uniapp.dcloud.net.cn/) (Vue3 + TS)
- 状态管理:[pinia](https://pinia.vuejs.org/zh/)
- 组件库:[uni-ui](https://uniapp.dcloud.net.cn/component/uniui/uni-ui.html)
### 开发环境
- 开发工具: VS Code 、 HbuilderX 、 微信开发者工具
- Node 版本: v16.15.0
- pnpm 版本v8.6.10
### 运行程序
1. 安装依赖
```shell
# npm
npm i --registry=https://registry.npmmirror.com
# pnpm
pnpm i --registry=https://registry.npmmirror.com
```
2. 运行程序
```shell
# 微信小程序端
npm run dev:mp-weixin
# H5端
npm run dev:h5
# App端
需 HbuilderX 工具,运行 - 运行到手机或模拟器
```
3. 微信开发者工具导入 `/dist/dev/mp-weixin` 目录

@ -0,0 +1,32 @@
#!/usr/bin/env sh
# 确保脚本抛出遇到的错误
set -e
# 1. 先提交 master
git push git@github.com:erabbit-dev/erabbit-uni-app.git master:master
# 2. 打包生成静态文件
# npm run build:h5
# 注意:项目使用了 uniClound需在 HbuilderX 打包 H5 网站,否则无法使用云功能
cli publish --platform h5 --project erabbit-uni-app --spaceId mp-2fac390c-2ea7-4d12-9726-f5b9a3bb6c37
# 3. 生成流水线.yml
mkdir -p "./dist/build/h5/.github/workflows"
cp "./deploy.yml" "./dist/build/h5/.github/workflows/deploy.yml"
# 4. 进入生成的文件夹,提交到主仓库 gh-pages 分支
# 如果是发布到自定义域名
# echo 'www.example.com' > CNAME
cd dist/build/h5
git init -b master
git add -A
git commit -m 'deploy: gh-pages'
# 如果发布到 https://<USERNAME>.github.io/<REPO>
# git push -f git@gitee.com:<USERNAME>/<REPO>.git master:gh-pages
git push -f git@github.com:erabbit-dev/erabbit-uni-app.git master:gh-pages
cd -

@ -0,0 +1,36 @@
name: deploy
on:
push:
branches: [gh-pages]
pull_request:
branches: [gh-pages]
jobs:
gitee:
runs-on: ubuntu-latest
steps:
- name: Sync To Gitee
uses: wearerequired/git-mirror-action@master
env:
# 注意在 Settings->Secrets 配置 GITEE_RSA_PRIVATE_KEY
SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }}
with:
# 注意替换为你的 GitHub 源仓库地址
source-repo: git@github.com:erabbit-dev/erabbit-uni-app.git
# 注意替换为你的 Gitee 目标仓库地址
destination-repo: git@gitee.com:Megasu/uniapp-shop-vue3-ts.git
# dry-run: true
- name: Build Gitee Pages
uses: yanglbme/gitee-pages-action@main
with:
# 注意替换为你的 Gitee 用户名
gitee-username: megasu@qq.com
# 注意在 Settings->Secrets 配置 GITEE_PASSWORD
gitee-password: ${{ secrets.GITEE_PASSWORD }}
# 注意替换为你的 Gitee 仓库,仓库名严格区分大小写,请准确填写,否则会出错
gitee-repo: Megasu/uniapp-shop-vue3-ts
# 要部署的分支,默认是 master若是其他分支则需要指定指定的分支必须存在
branch: gh-pages
# directory: docs/.vitepress/dist

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<script>
var coverSupport =
'CSS' in window &&
typeof CSS.supports === 'function' &&
(CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') +
'" />',
)
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app">
<!--app-html-->
</div>
<script type="module" src="/src/main.ts"></script>
// <script>
// var _hmt = _hmt || []
// ;(function () {
// var hm = document.createElement('script')
// hm.src = 'https://hm.baidu.com/hm.js?47837f06b6f8cf8ba80165ca9a80711'
// var s = document.getElementsByTagName('script')[0]
// s.parentNode.insertBefore(hm, s)
// })()
// </script>
</body>
</html>

13893
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,94 @@
{
"name": "uni-app-chanko",
"version": "0.0.0",
"scripts": {
"dev:app": "uni -p app",
"dev:app-android": "uni -p app-android",
"dev:app-ios": "uni -p app-ios",
"dev:custom": "uni -p",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:app": "uni build -p app",
"build:app-android": "uni build -p app-android",
"build:app-ios": "uni build -p app-ios",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"tsc": "vue-tsc --noEmit --skipLibCheck",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"prepare": "husky install",
"lint-staged": "lint-staged"
},
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix"
]
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-3090620231104002",
"@dcloudio/uni-app-plus": "3.0.0-3090620231104002",
"@dcloudio/uni-components": "3.0.0-3090620231104002",
"@dcloudio/uni-h5": "3.0.0-3090620231104002",
"@dcloudio/uni-mp-alipay": "3.0.0-3090620231104002",
"@dcloudio/uni-mp-baidu": "3.0.0-3090620231104002",
"@dcloudio/uni-mp-jd": "3.0.0-3090620231104002",
"@dcloudio/uni-mp-kuaishou": "3.0.0-3090620231104002",
"@dcloudio/uni-mp-lark": "3.0.0-3090620231104002",
"@dcloudio/uni-mp-qq": "3.0.0-3090620231104002",
"@dcloudio/uni-mp-toutiao": "3.0.0-3090620231104002",
"@dcloudio/uni-mp-weixin": "3.0.0-3090620231104002",
"@dcloudio/uni-mp-xhs": "3.0.0-3090620231104002",
"@dcloudio/uni-quickapp-webview": "3.0.0-3090620231104002",
"@dcloudio/uni-ui": "^1.4.28",
"pinia": "2.0.27",
"pinia-plugin-persistedstate": "^3.2.0",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2"
},
"devDependencies": {
"@dcloudio/types": "^3.4.3",
"@dcloudio/uni-automator": "3.0.0-3090620231104002",
"@dcloudio/uni-cli-shared": "3.0.0-3090620231104002",
"@dcloudio/uni-stacktracey": "3.0.0-3090620231104002",
"@dcloudio/uni-vue-devtools": "3.0.0-alpha-3080220230511001",
"@dcloudio/vite-plugin-uni": "3.0.0-3090620231104002",
"@rushstack/eslint-patch": "^1.1.4",
"@types/node": "^18.11.9",
"@uni-helper/uni-app-types": "^0.5.12",
"@uni-helper/uni-ui-types": "^0.5.11",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/runtime-core": "^3.2.45",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.22.0",
"eslint-plugin-vue": "^9.3.0",
"husky": "^8.0.0",
"lint-staged": "^13.0.3",
"miniprogram-api-typings": "^3.12.0",
"prettier": "^2.7.1",
"sass": "^1.56.1",
"typescript": "^5.1.6",
"vite": "^4.0.3",
"vue-tsc": "^1.8.8"
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,28 @@
{
"appid": "wx8481e8716cf8d9e9",
"compileType": "miniprogram",
"libVersion": "3.3.0",
"packOptions": {
"ignore": [],
"include": []
},
"setting": {
"coverView": true,
"es6": true,
"postcss": true,
"minified": true,
"enhance": true,
"showShadowRootInWxmlPanel": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
}
},
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 4
}
}

@ -0,0 +1,7 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "uniapp-shop-vue3-ts",
"setting": {
"compileHotReLoad": true
}
}

@ -0,0 +1,19 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
onLaunch(() => {
console.log('App Launch')
})
onShow(() => {
console.log('App Show')
})
onHide(() => {
console.log('App Hide')
})
</script>
<style lang="scss">
//
@import '@/styles/fonts.scss';
@import '@/styles/base.scss';
</style>

@ -0,0 +1,152 @@
<script setup lang="ts">
import { getHomeGoodsGuessLikeAPI } from '@/services/home'
import type { PageParams } from '@/types/global'
import type { GuessItem } from '@/types/home'
import { onMounted, ref } from 'vue'
//
const pageParams: Required<PageParams> = {
page: 1,
pageSize: 10,
}
//
const guessList = ref<GuessItem[]>([])
//
const finish = ref(false)
//
const getHomeGoodsGuessLikeData = async () => {
// 退
if (finish.value === true) {
return uni.showToast({ icon: 'none', title: '没有更多数据~' })
}
const res = await getHomeGoodsGuessLikeAPI(pageParams)
// guessList.value = res.result.items
//
guessList.value.push(...res.result.items)
//
if (pageParams.page < res.result.pages) {
//
pageParams.page++
} else {
finish.value = true
}
}
//
const resetData = () => {
pageParams.page = 1
guessList.value = []
finish.value = false
}
//
onMounted(() => {
getHomeGoodsGuessLikeData()
})
//
defineExpose({
resetData,
getMore: getHomeGoodsGuessLikeData,
})
</script>
<template>
<!-- 猜你喜欢 -->
<view class="caption">
<text class="text">猜你喜欢</text>
</view>
<view class="guess">
<navigator
class="guess-item"
v-for="item in guessList"
:key="item.id"
:url="`/pages/goods/goods?id=${item.id}`"
>
<image class="image" mode="aspectFill" :src="item.picture"></image>
<view class="name"> {{ item.name }} </view>
<view class="price">
<text class="small">¥</text>
<text>{{ item.price }}</text>
</view>
</navigator>
</view>
<view class="loading-text">
{{ finish ? '没有更多数据~' : '正在加载...' }}
</view>
</template>
<style lang="scss">
:host {
display: block;
}
/* 分类标题 */
.caption {
display: flex;
justify-content: center;
line-height: 1;
padding: 36rpx 0 40rpx;
font-size: 32rpx;
color: #262626;
.text {
display: flex;
justify-content: center;
align-items: center;
padding: 0 28rpx 0 30rpx;
&::before,
&::after {
content: '';
width: 20rpx;
height: 20rpx;
background-image: url(@/static/images/bubble.png);
background-size: contain;
margin: 0 10rpx;
}
}
}
/* 猜你喜欢 */
.guess {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 20rpx;
.guess-item {
width: 345rpx;
padding: 24rpx 20rpx 20rpx;
margin-bottom: 20rpx;
border-radius: 10rpx;
overflow: hidden;
background-color: #fff;
}
.image {
width: 304rpx;
height: 304rpx;
}
.name {
height: 75rpx;
margin: 10rpx 0;
font-size: 26rpx;
color: #262626;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.price {
line-height: 1;
padding-top: 4rpx;
color: #cf4444;
font-size: 26rpx;
}
.small {
font-size: 80%;
}
}
//
.loading-text {
text-align: center;
font-size: 28rpx;
color: #666;
padding: 20rpx 0;
}
</style>

@ -0,0 +1,40 @@
<script setup lang="ts">
import type { BannerItem } from '@/types/home'
import { ref } from 'vue'
const activeIndex = ref(0)
// swiper
const onChange: UniHelper.SwiperOnChange = (ev) => {
activeIndex.value = ev.detail.current
}
// props
defineProps<{
list: BannerItem[]
}>()
</script>
<template>
<view class="carousel">
<swiper :circular="true" :autoplay="false" :interval="3000" @change="onChange">
<swiper-item v-for="item in list" :key="item.id">
<navigator url="/pages/index/index" hover-class="none" class="navigator">
<image mode="aspectFill" class="image" :src="item.imgUrl"></image>
</navigator>
</swiper-item>
</swiper>
<!-- 指示点 -->
<view class="indicator">
<text
v-for="(item, index) in list"
:key="item.id"
class="dot"
:class="{ active: index === activeIndex }"
></text>
</view>
</view>
</template>
<style lang="scss">
@import './styles/XtxSwiper.scss';
</style>

@ -0,0 +1,31 @@
/* 轮播图 */
.carousel {
height: 280rpx;
position: relative;
overflow: hidden;
transform: translateY(0);
background-color: #efefef;
.indicator {
position: absolute;
left: 0;
right: 0;
bottom: 16rpx;
display: flex;
justify-content: center;
.dot {
width: 30rpx;
height: 6rpx;
margin: 0 8rpx;
border-radius: 6rpx;
background-color: rgba(255, 255, 255, 0.4);
}
.active {
background-color: #fff;
}
}
.navigator,
.image {
width: 100%;
height: 100%;
}
}

@ -0,0 +1,145 @@
import { Component } from '@uni-helper/uni-app-types'
/** SKU 弹出层 */
export type SkuPopup = Component<SkuPopupProps>
/** SKU 弹出层实例 */
export type SkuPopupInstance = InstanceType<SkuPopup>
/** SKU 弹出层属性 */
export type SkuPopupProps = {
/** 双向绑定true 为打开组件false 为关闭组件 */
modelValue: boolean
/** 商品信息本地数据源 */
localdata: SkuPopupLocaldata
/** 按钮模式 1:都显示 2:只显示购物车 3:只显示立即购买 */
mode?: 1 | 2 | 3
/** 该商品已抢完时的按钮文字 */
noStockText?: string
/** 库存文字 */
stockText?: string
/** 点击遮罩是否关闭组件 */
maskCloseAble?: boolean
/** 顶部圆角值 */
borderRadius?: string | number
/** 最小购买数量 */
minBuyNum?: number
/** 最大购买数量 */
maxBuyNum?: number
/** 每次点击后的数量 */
stepBuyNum?: number
/** 是否只能输入 step 的倍数 */
stepStrictly?: boolean
/** 是否隐藏库存的显示 */
hideStock?: false
/** 主题风格 */
theme?: 'default' | 'red-black' | 'black-white' | 'coffee' | 'green'
/** 默认金额会除以100即100=1元若设置为0则不会除以100即1=1元 */
amountType?: 1 | 0
/** 自定义获取商品信息的函数已知支付宝不支持支付宝请改用localdata属性 */
customAction?: () => void
/** 是否显示右上角关闭按钮 */
showClose?: boolean
/** 关闭按钮的图片地址 */
closeImage?: string
/** 价格的字体颜色 */
priceColor?: string
/** 立即购买 - 按钮的文字 */
buyNowText?: string
/** 立即购买 - 按钮的字体颜色 */
buyNowColor?: string
/** 立即购买 - 按钮的背景颜色 */
buyNowBackgroundColor?: string
/** 加入购物车 - 按钮的文字 */
addCartText?: string
/** 加入购物车 - 按钮的字体颜色 */
addCartColor?: string
/** 加入购物车 - 按钮的背景颜色 */
addCartBackgroundColor?: string
/** 商品缩略图背景颜色 */
goodsThumbBackgroundColor?: string
/** 样式 - 不可点击时,按钮的样式 */
disableStyle?: object
/** 样式 - 按钮点击时的样式 */
activedStyle?: object
/** 样式 - 按钮常态的样式 */
btnStyle?: object
/** 字段名 - 商品表id的字段名 */
goodsIdName?: string
/** 字段名 - sku表id的字段名 */
skuIdName?: string
/** 字段名 - 商品对应的sku列表的字段名 */
skuListName?: string
/** 字段名 - 商品规格名称的字段名 */
specListName?: string
/** 字段名 - sku库存的字段名 */
stockName?: string
/** 字段名 - sku组合路径的字段名 */
skuArrName?: string
/** 字段名 - 商品缩略图字段名(未选择sku时) */
goodsThumbName?: string
/** 被选中的值 */
selectArr?: string[]
/** 打开弹出层 */
onOpen: () => void
/** 关闭弹出层 */
onClose: () => void
/** 点击加入购物车时需选择完SKU才会触发*/
onAddCart: (event: SkuPopupEvent) => void
/** 点击立即购买时需选择完SKU才会触发*/
onBuyNow: (event: SkuPopupEvent) => void
}
/** 商品信息本地数据源 */
export type SkuPopupLocaldata = {
/** 商品 ID */
_id: string
/** 商品名称 */
name: string
/** 商品图片 */
goods_thumb: string
/** 商品规格列表 */
spec_list: SkuPopupSpecItem[]
/** 商品SKU列表 */
sku_list: SkuPopupSkuItem[]
}
/** 商品规格名称的集合 */
export type SkuPopupSpecItem = {
/** 规格名称 */
name: string
/** 规格集合 */
list: { name: string }[]
}
/** 商品SKU列表 */
export type SkuPopupSkuItem = {
/** SKU ID */
_id: string
/** 商品 ID */
goods_id: string
/** 商品名称 */
goods_name: string
/** 商品图片 */
image: string
/** SKU 价格 * 100, 注意:需要乘以 100 */
price: number
/** SKU 规格组成, 注意:需要与 spec_list 数组顺序对应 */
sku_name_arr: string[]
/** SKU 库存 */
stock: number
}
/** 当前选择的sku数据 */
export type SkuPopupEvent = SkuPopupSkuItem & {
/** 商品购买数量 */
buy_num: number
}
/** 全局组件类型声明 */
declare module '@vue/runtime-core' {
export interface GlobalComponents {
'vk-data-goods-sku-popup': SkuPopup
}
}

@ -0,0 +1,52 @@
import { Component } from '@uni-helper/uni-app-types'
/** 步进器 */
export type InputNumberBox = Component<InputNumberBoxProps>
/** 步进器实例 */
export type InputNumberBoxInstance = InstanceType<InputNumberBox>
/** 步进器属性 */
export type InputNumberBoxProps = {
/** 输入框初始值默认1 */
modelValue: number
/** 用户可输入的最小值默认0 */
min: number
/** 用户可输入的最大值默认99999 */
max: number
/** 步长每次加或减的值默认1 */
step: number
/** 是否禁用操作,包括输入框,加减按钮 */
disabled: boolean
/** 输入框宽度单位rpx默认80 */
inputWidth: string | number
/** 输入框和按钮的高度单位rpx默认50 */
inputHeight: string | number
/** 输入框和按钮的背景颜色(默认#F2F3F5 */
bgColor: string
/** 步进器标识符 */
index: string
/** 输入框内容发生变化时触发 */
onChange: (event: InputNumberBoxEvent) => void
/** 输入框失去焦点时触发 */
onBlur: (event: InputNumberBoxEvent) => void
/** 点击增加按钮时触发 */
onPlus: (event: InputNumberBoxEvent) => void
/** 点击减少按钮时触发 */
onMinus: (event: InputNumberBoxEvent) => void
}
/** 步进器事件对象 */
export type InputNumberBoxEvent = {
/** 输入框当前值 */
value: number
/** 步进器标识符 */
index: string
}
/** 全局组件类型声明 */
declare module '@vue/runtime-core' {
export interface GlobalComponents {
'vk-data-input-number-box': InputNumberBox
}
}

@ -0,0 +1,458 @@
<!-- 步进器 -->
<template>
<view class="vk-data-input-number-box">
<view
class="u-icon-minus"
:class="{ 'u-icon-disabled': disabled || inputVal <= min }"
:style="{
background: bgColor,
height: inputHeight + 'rpx',
color: color,
fontSize: size + 'rpx',
minHeight: '1.4em',
}"
@touchstart.prevent="btnTouchStart('minus')"
@touchend.stop.prevent="clearTimer"
>
<view :style="'font-size:' + (Number(size) + 10) + 'rpx'" class="num-btn"></view>
</view>
<input
v-model="inputVal"
:disabled="disabledInput || disabled"
:cursor-spacing="getCursorSpacing"
:class="{ 'u-input-disabled': disabled }"
class="u-number-input"
type="number"
:style="{
color: color,
fontSize: size + 'rpx',
background: bgColor,
height: inputHeight + 'rpx',
width: inputWidth + 'rpx',
}"
@blur="onBlur"
/>
<view
class="u-icon-plus"
:class="{ 'u-icon-disabled': disabled || inputVal >= max }"
:style="{
background: bgColor,
height: inputHeight + 'rpx',
color: color,
fontSize: size + 'rpx',
minHeight: '1.4em',
}"
@touchstart.prevent="btnTouchStart('plus')"
@touchend.stop.prevent="clearTimer"
>
<view :style="'font-size:' + (Number(size) + 10) + 'rpx'" class="num-btn"></view>
</view>
</view>
</template>
<script>
/* eslint-disable */
/**
* numberBox 步进器此为uview组件改造
* @description 该组件一般用于商城购物选择物品数量的场景注意该输入框只能输入大于或等于0的整数不支持小数输入
* @tutorial https://www.uviewui.com/components/numberBox.html
* @property {Number} value 输入框初始值默认1
* @property {String} bg-color 输入框和按钮的背景颜色默认#F2F3F5
* @property {Number} min 用户可输入的最小值默认0
* @property {Number} max 用户可输入的最大值默认99999
* @property {Number} step 步长每次加或减的值默认1
* @property {Number} stepFirst 步进值首次增加或最后减的值(默认step值和一致
* @property {Boolean} disabled 是否禁用操作禁用后无法加减或手动修改输入框的值默认false
* @property {Boolean} disabled-input 是否禁止输入框手动输入值默认false
* @property {Boolean} positive-integer 是否只能输入正整数默认true
* @property {String | Number} size 输入框文字和按钮字体大小单位rpx默认26
* @property {String} color 输入框文字和加减按钮图标的颜色默认#323233
* @property {String | Number} input-width 输入框宽度单位rpx默认80
* @property {String | Number} input-height 输入框和按钮的高度单位rpx默认50
* @property {String | Number} index 事件回调时用以区分当前发生变化的是哪个输入框
* @property {Boolean} long-press 是否开启长按连续递增或递减(默认true)
* @property {String | Number} press-time 开启长按触发后每触发一次需要多久单位ms(默认250)
* @property {String | Number} cursor-spacing 指定光标于键盘的距离避免键盘遮挡输入框单位rpx默认200
* @event {Function} change 输入框内容发生变化时触发对象形式
* @event {Function} blur 输入框失去焦点时触发对象形式
* @event {Function} minus 点击减少按钮时触发(按钮可点击情况下)对象形式
* @event {Function} plus 点击增加按钮时触发(按钮可点击情况下)对象形式
* @example <vk-data-input-number-box :min="1" :max="100"></vk-data-input-number-box>
*/
export default {
name: 'vk-data-input-number-box',
emits: ['update:modelValue', 'input', 'change', 'blur', 'plus', 'minus'],
props: {
//
value: {
type: Number,
default: 1,
},
modelValue: {
type: Number,
default: 1,
},
//
bgColor: {
type: String,
default: '#F2F3F5',
},
//
min: {
type: Number,
default: 0,
},
//
max: {
type: Number,
default: 99999,
},
//
step: {
type: Number,
default: 1,
},
//
stepFirst: {
type: Number,
default: 0,
},
// step
stepStrictly: {
type: Boolean,
default: false,
},
//
disabled: {
type: Boolean,
default: false,
},
// inputrpx
size: {
type: [Number, String],
default: 26,
},
//
color: {
type: String,
default: '#323233',
},
// inputrpx
inputWidth: {
type: [Number, String],
default: 80,
},
// inputrpx
inputHeight: {
type: [Number, String],
default: 50,
},
// index使numberbox使forindex
index: {
type: [Number, String],
default: '',
},
// disabledOR
// disabledfalsedisabledInputtrue
disabledInput: {
type: Boolean,
default: false,
},
//
cursorSpacing: {
type: [Number, String],
default: 100,
},
//
longPress: {
type: Boolean,
default: true,
},
//
pressTime: {
type: [Number, String],
default: 250,
},
// 0()
positiveInteger: {
type: Boolean,
default: true,
},
},
watch: {
valueCom(v1, v2) {
// valueinputVal
if (!this.changeFromInner) {
this.inputVal = v1
// inputValthis.handleChange()changeFromInnertrue
// this.$nextTick
// changeFromInnerfalse
this.$nextTick(function () {
this.changeFromInner = false
})
}
},
inputVal(v1, v2) {
//
if (v1 == '') return
let value = 0
// minmax使
let tmp = this.isNumber(v1)
if (tmp && v1 >= this.min && v1 <= this.max) value = v1
else value = v2
// 0
if (this.positiveInteger) {
// 0
if (v1 < 0 || String(v1).indexOf('.') !== -1) {
value = v2
// input使$nextTick
this.$nextTick(() => {
this.inputVal = v2
})
}
}
// change
this.handleChange(value, 'change')
},
min(v1) {
if (v1 !== undefined && v1 != '' && this.valueCom < v1) {
this.$emit('input', v1)
this.$emit('update:modelValue', v1)
}
},
max(v1) {
if (v1 !== undefined && v1 != '' && this.valueCom > v1) {
this.$emit('input', v1)
this.$emit('update:modelValue', v1)
}
},
},
data() {
return {
inputVal: 1, // 使propsvalueprops
timer: null, //
changeFromInner: false, //
innerChangeTimer: null, //
}
},
created() {
this.inputVal = Number(this.valueCom)
},
computed: {
valueCom() {
// #ifndef VUE3
return this.value
// #endif
// #ifdef VUE3
return this.modelValue
// #endif
},
getCursorSpacing() {
// px
return Number(uni.upx2px(this.cursorSpacing))
},
},
methods: {
// 退
btnTouchStart(callback) {
// clearTimer
this[callback]()
//
if (!this.longPress) return
clearInterval(this.timer) //
this.timer = null
this.timer = setInterval(() => {
//
this[callback]()
}, this.pressTime)
},
clearTimer() {
this.$nextTick(() => {
clearInterval(this.timer)
this.timer = null
})
},
minus() {
this.computeVal('minus')
},
plus() {
this.computeVal('plus')
},
//
calcPlus(num1, num2) {
let baseNum, baseNum1, baseNum2
try {
baseNum1 = num1.toString().split('.')[1].length
} catch (e) {
baseNum1 = 0
}
try {
baseNum2 = num2.toString().split('.')[1].length
} catch (e) {
baseNum2 = 0
}
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2))
let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2 //
return ((num1 * baseNum + num2 * baseNum) / baseNum).toFixed(precision)
},
//
calcMinus(num1, num2) {
let baseNum, baseNum1, baseNum2
try {
baseNum1 = num1.toString().split('.')[1].length
} catch (e) {
baseNum1 = 0
}
try {
baseNum2 = num2.toString().split('.')[1].length
} catch (e) {
baseNum2 = 0
}
baseNum = Math.pow(10, Math.max(baseNum1, baseNum2))
let precision = baseNum1 >= baseNum2 ? baseNum1 : baseNum2
return ((num1 * baseNum - num2 * baseNum) / baseNum).toFixed(precision)
},
computeVal(type) {
uni.hideKeyboard()
if (this.disabled) return
let value = 0
// stepFirst
//
if (type === 'minus') {
if (this.stepFirst > 0 && this.inputVal == this.stepFirst) {
value = this.min
} else {
value = this.calcMinus(this.inputVal, this.step)
}
} else if (type === 'plus') {
if (this.stepFirst > 0 && this.inputVal < this.stepFirst) {
value = this.stepFirst
} else {
value = this.calcPlus(this.inputVal, this.step)
}
}
if (this.stepStrictly) {
let strictly = value % this.step
if (strictly > 0) {
value -= strictly
}
}
if (value > this.max) {
value = this.max
} else if (value < this.min) {
value = this.min
}
// stepFirst
this.inputVal = value
this.handleChange(value, type)
},
//
onBlur(event) {
let val = 0
let value = event.detail.value
// 0-90min
// props min0
if (!/(^\d+$)/.test(value) || value[0] == 0) val = this.min
val = +value
// stepFirst
if (this.stepFirst > 0 && this.inputVal < this.stepFirst && this.inputVal > 0) {
val = this.stepFirst
}
// stepFirst
if (this.stepStrictly) {
let strictly = val % this.step
if (strictly > 0) {
val -= strictly
}
}
if (val > this.max) {
val = this.max
} else if (val < this.min) {
val = this.min
}
this.$nextTick(() => {
this.inputVal = val
})
this.handleChange(val, 'blur')
},
handleChange(value, type) {
if (this.disabled) return
//
if (this.innerChangeTimer) {
clearTimeout(this.innerChangeTimer)
this.innerChangeTimer = null
}
// inputv-model
this.changeFromInner = true
// changeFromInner
// value
this.innerChangeTimer = setTimeout(() => {
this.changeFromInner = false
}, 150)
this.$emit('input', Number(value))
this.$emit('update:modelValue', Number(value))
this.$emit(type, {
// Number
value: Number(value),
index: this.index,
})
},
/**
* 验证十进制数字
*/
isNumber(value) {
return /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value)
},
},
}
</script>
<style lang="scss" scoped>
.vk-data-input-number-box {
display: inline-flex;
align-items: center;
}
.u-number-input {
position: relative;
text-align: center;
padding: 0;
margin: 0 6rpx;
display: flex;
align-items: center;
justify-content: center;
}
.u-icon-plus,
.u-icon-minus {
width: 60rpx;
display: flex;
justify-content: center;
align-items: center;
}
.u-icon-plus {
border-radius: 0 8rpx 8rpx 0;
}
.u-icon-minus {
border-radius: 8rpx 0 0 8rpx;
}
.u-icon-disabled {
color: #c8c9cc !important;
background: #f7f8fa !important;
}
.u-input-disabled {
color: #c8c9cc !important;
background-color: #f2f3f5 !important;
}
.num-btn {
font-weight: 550;
position: relative;
top: -4rpx;
}
</style>

@ -0,0 +1,21 @@
import type { XtxGuessInstance } from '@/types/components'
import { ref } from 'vue'
/**
*
*/
export const useGuessList = () => {
// 获取猜你喜欢组件实例
const guessRef = ref<XtxGuessInstance>()
// 滚动触底事件
const onScrolltolower = () => {
guessRef.value?.getMore()
}
// 返回 ref 和事件处理函数
return {
guessRef,
onScrolltolower,
}
}

8
src/env.d.ts vendored

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}

@ -0,0 +1,17 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
// 导入 pinia 实例
import pinia from './stores'
export function createApp() {
// 创建 vue 实例
const app = createSSRApp(App)
// 使用 pinia
app.use(pinia)
return {
app,
}
}

@ -0,0 +1,122 @@
{
"name": "",
"appid": "__UNI__265C604",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
/* 5+App */
"app-plus": {
//
"compatible": {
"ignoreVersion": true
},
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
/* */
"modules": {},
/* */
"distribute": {
/* android */
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios": {
"dSYMs": false
},
/* SDK */
"sdkConfigs": {
"ad": {}
},
"icons": {
"android": {
"hdpi": "unpackage/res/icons/72x72.png",
"xhdpi": "unpackage/res/icons/96x96.png",
"xxhdpi": "unpackage/res/icons/144x144.png",
"xxxhdpi": "unpackage/res/icons/192x192.png"
},
"ios": {
"appstore": "unpackage/res/icons/1024x1024.png",
"ipad": {
"app": "unpackage/res/icons/76x76.png",
"app@2x": "unpackage/res/icons/152x152.png",
"notification": "unpackage/res/icons/20x20.png",
"notification@2x": "unpackage/res/icons/40x40.png",
"proapp@2x": "unpackage/res/icons/167x167.png",
"settings": "unpackage/res/icons/29x29.png",
"settings@2x": "unpackage/res/icons/58x58.png",
"spotlight": "unpackage/res/icons/40x40.png",
"spotlight@2x": "unpackage/res/icons/80x80.png"
},
"iphone": {
"app@2x": "unpackage/res/icons/120x120.png",
"app@3x": "unpackage/res/icons/180x180.png",
"notification@2x": "unpackage/res/icons/40x40.png",
"notification@3x": "unpackage/res/icons/60x60.png",
"settings@2x": "unpackage/res/icons/58x58.png",
"settings@3x": "unpackage/res/icons/87x87.png",
"spotlight@2x": "unpackage/res/icons/80x80.png",
"spotlight@3x": "unpackage/res/icons/120x120.png"
}
}
}
}
},
/* */
"quickapp": {},
/* */
"h5": {
"router": {
// /
"base": "./"
}
},
/* */
"mp-weixin": {
"appid": "wx26729f20b9efae3a",
"setting": {
"minified": true,
"urlCheck": true
},
//
"lazyCodeLoading": "requiredComponents",
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3"
}

@ -0,0 +1,195 @@
{
//
"easycom": {
//
"autoscan": true,
//
"custom": {
// uni-ui
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
// Xtx components
"^Xtx(.*)": "@/components/Xtx$1.vue"
}
},
"pages": [
//pageshttps://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom",
"navigationBarTextStyle": "white",
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/category/category",
"style": {
"navigationBarTitleText": "商品分类"
}
},
{
"path": "pages/cart/cart",
"style": {
"navigationBarTitleText": "购物车"
}
},
{
"path": "pages/cart/cart2",
"style": {
"navigationBarTitleText": "购物车"
}
},
{
"path": "pages/my/my",
"style": {
"navigationStyle": "custom",
"navigationBarTextStyle": "white",
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/login/protocal",
"style": {
"navigationBarTitleText": "用户服务协议"
}
},
{
"path": "pages/hot/hot",
"style": {
"navigationBarTitleText": "热门推荐"
}
},
{
"path": "pages/goods/goods",
"style": {
"navigationBarTitleText": "商品详情"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
// TabBar
"tabBar": {
"color": "#333",
"selectedColor": "#27ba9b",
"backgroundColor": "#fff",
"borderStyle": "white",
"list": [
{
"text": "首页",
"pagePath": "pages/index/index",
"iconPath": "static/tabs/home_default.png",
"selectedIconPath": "static/tabs/home_selected.png"
},
{
"text": "服务站",
"pagePath": "pages/category/category",
"iconPath": "static/tabs/category_default.png",
"selectedIconPath": "static/tabs/category_selected.png"
},
{
"text": "一键预约",
"pagePath": "pages/cart/cart",
"iconPath": "static/tabs/cart_default.png",
"selectedIconPath": "static/tabs/cart_selected.png"
},
// {
// "text": "积分商城",
// "pagePath": "pages/cart/cart",
// "iconPath": "static/tabs/cart_default.png",
// "selectedIconPath": "static/tabs/cart_selected.png"
// },
{
"text": "我的",
"pagePath": "pages/my/my",
"iconPath": "static/tabs/user_default.png",
"selectedIconPath": "static/tabs/user_selected.png"
}
]
},
//
"subPackages": [
{
//
"root": "pagesMember",
//
"pages": [
{
"path": "settings/settings",
"style": {
"navigationBarTitleText": "设置"
}
},
{
"path": "profile/profile",
"style": {
"navigationStyle": "custom",
"navigationBarTextStyle": "white",
"navigationBarTitleText": "个人信息"
}
},
{
"path": "address/address",
"style": {
"navigationBarTitleText": "地址管理"
}
},
{
"path": "address-form/address-form",
"style": {
"navigationBarTitleText": ""
}
}
]
},
{
"root": "pagesOrder",
"pages": [
{
"path": "create/create",
"style": {
"navigationBarTitleText": "填写订单"
}
},
{
"path": "detail/detail",
"style": {
"navigationBarTitleText": "订单详情",
"navigationStyle": "custom"
}
},
{
"path": "payment/payment",
"style": {
"navigationBarTitleText": "支付结果"
}
},
{
"path": "list/list",
"style": {
"navigationBarTitleText": "订单列表"
}
}
]
}
],
//
"preloadRule": {
"pages/my/my": {
"network": "all",
"packages": [
"pagesMember"
]
}
}
}

@ -0,0 +1,13 @@
<script setup lang="ts">
import CartMain from './components/CartMain.vue'
</script>
<template>
<CartMain />
</template>
<style lang="scss">
page {
height: 100%;
}
</style>

@ -0,0 +1,14 @@
<script setup lang="ts">
import CartMain from './components/CartMain.vue'
</script>
<template>
<!-- 适配底部安全区 -->
<CartMain safe-area-inset-bottom />
</template>
<style lang="scss">
page {
height: 100%;
}
</style>

@ -0,0 +1,509 @@
<script setup lang="ts">
import type { InputNumberBoxEvent } from '@/components/vk-data-input-number-box/vk-data-input-number-box'
import { useGuessList } from '@/composables'
import {
deleteMemberCartAPI,
getMemberCartAPI,
putMemberCartBySkuIdAPI,
putMemberCartSelectedAPI,
} from '@/services/cart'
import { useMemberStore } from '@/stores'
import type { CartItem } from '@/types/cart'
import { onShow } from '@dcloudio/uni-app'
import { computed, ref } from 'vue'
//
defineProps<{
safeAreaInsetBottom?: boolean
}>()
//
const { safeAreaInsets } = uni.getSystemInfoSync()
// Store
const memberStore = useMemberStore()
//
const cartList = ref<CartItem[]>([])
//
const showCartList = ref(true)
const getMemberCartData = async () => {
const res = await getMemberCartAPI()
cartList.value = res.result
showCartList.value = res.result.length > 0
}
// :
onShow(() => {
if (memberStore.profile) {
getMemberCartData()
}
})
//
const onDeleteCart = (skuId: string) => {
//
uni.showModal({
content: '是否删除',
confirmColor: '#27BA9B',
success: async (res) => {
if (res.confirm) {
//
await deleteMemberCartAPI({ ids: [skuId] })
//
getMemberCartData()
}
},
})
}
//
const onChangeCount = (ev: InputNumberBoxEvent) => {
putMemberCartBySkuIdAPI(ev.index, { count: ev.value })
}
// -
const onChangeSelected = (item: CartItem) => {
// -
item.selected = !item.selected
//
putMemberCartBySkuIdAPI(item.skuId, { selected: item.selected })
}
//
const isSelectedAll = computed(() => {
return cartList.value.length && cartList.value.every((v) => v.selected)
})
// -
const onChangeSelectedAll = () => {
//
const _isSelectedAll = !isSelectedAll.value
//
cartList.value.forEach((item) => {
item.selected = _isSelectedAll
})
//
putMemberCartSelectedAPI({ selected: _isSelectedAll })
}
//
const selectedCartList = computed(() => {
return cartList.value.filter((v) => v.selected)
})
//
const selectedCartListCount = computed(() => {
return selectedCartList.value.reduce((sum, item) => sum + item.count, 0)
})
//
const selectedCartListMoney = computed(() => {
return selectedCartList.value
.reduce((sum, item) => sum + item.count * item.nowPrice, 0)
.toFixed(2)
})
//
const gotoPayment = () => {
if (selectedCartListCount.value === 0) {
return uni.showToast({
icon: 'none',
title: '请选择商品',
})
}
//
uni.navigateTo({ url: '/pagesOrder/create/create' })
}
//
const { guessRef, onScrolltolower } = useGuessList()
</script>
<template>
<scroll-view enable-back-to-top scroll-y class="scroll-view" @scrolltolower="onScrolltolower">
<!-- 已登录: 显示购物车 -->
<template v-if="memberStore.profile">
<!-- 购物车列表 -->
<view class="cart-list" v-if="showCartList">
<!-- 优惠提示 -->
<view class="tips">
<text class="label">满减</text>
<text class="desc">满1件, 即可享受9折优惠</text>
</view>
<!-- 滑动操作分区 -->
<uni-swipe-action>
<!-- 滑动操作项 -->
<uni-swipe-action-item v-for="item in cartList" :key="item.skuId" class="cart-swipe">
<!-- 商品信息 -->
<view class="goods">
<!-- 选中状态 -->
<text
@tap="onChangeSelected(item)"
class="checkbox"
:class="{ checked: item.selected }"
></text>
<navigator
:url="`/pages/goods/goods?id=${item.id}`"
hover-class="none"
class="navigator"
>
<image mode="aspectFill" class="picture" :src="item.picture"></image>
<view class="meta">
<view class="name ellipsis">{{ item.name }}</view>
<view class="attrsText ellipsis">{{ item.attrsText }}</view>
<view class="price">{{ item.nowPrice }}</view>
</view>
</navigator>
<!-- 商品数量 -->
<view class="count">
<vk-data-input-number-box
v-model="item.count"
:min="1"
:max="item.stock"
:index="item.skuId"
@change="onChangeCount"
/>
</view>
</view>
<!-- 右侧删除按钮 -->
<template #right>
<view class="cart-swipe-right">
<button @tap="onDeleteCart(item.skuId)" class="button delete-button">删除</button>
</view>
</template>
</uni-swipe-action-item>
</uni-swipe-action>
</view>
<!-- 购物车空状态 -->
<view class="cart-blank" v-else>
<image src="/static/images/blank_cart.png" class="image" />
<text class="text">购物车还是空的快来挑选好货吧</text>
<navigator url="/pages/index/index" hover-class="none">
<button class="button">去首页看看</button>
</navigator>
</view>
<!-- 吸底工具栏 -->
<view
v-if="showCartList"
class="toolbar"
:style="{ paddingBottom: safeAreaInsetBottom ? safeAreaInsets?.bottom + 'px' : 0 }"
>
<text @tap="onChangeSelectedAll" class="all" :class="{ checked: isSelectedAll }">全选</text>
<text class="text">合计:</text>
<text class="amount">{{ selectedCartListMoney }}</text>
<view class="button-grounp">
<view
@tap="gotoPayment"
class="button payment-button"
:class="{ disabled: selectedCartListCount === 0 }"
>
去结算({{ selectedCartListCount }})
</view>
</view>
</view>
</template>
<!-- 未登录: 提示登录 -->
<view class="login-blank" v-else>
<text class="text">登录后可查看购物车中的商品</text>
<navigator url="/pages/login/login" hover-class="none">
<button class="button">去登录</button>
</navigator>
</view>
<!-- 猜你喜欢 -->
<XtxGuess ref="guessRef" />
<!-- 底部占位空盒子 -->
<view class="toolbar-height"></view>
</scroll-view>
</template>
<style lang="scss">
//
:host {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: #f7f7f8;
}
//
.scroll-view {
flex: 1;
background-color: #f7f7f8;
}
//
.cart-list {
padding: 0 20rpx;
//
.tips {
display: flex;
align-items: center;
line-height: 1;
margin: 30rpx 10rpx;
font-size: 26rpx;
color: #666;
.label {
color: #fff;
padding: 7rpx 15rpx 5rpx;
border-radius: 4rpx;
font-size: 24rpx;
background-color: #27ba9b;
margin-right: 10rpx;
}
}
//
.goods {
display: flex;
padding: 20rpx 20rpx 20rpx 80rpx;
border-radius: 10rpx;
background-color: #fff;
position: relative;
.navigator {
display: flex;
}
.checkbox {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 80rpx;
height: 100%;
&::before {
content: '\e6cd';
font-family: 'erabbit' !important;
font-size: 40rpx;
color: #444;
}
&.checked::before {
content: '\e6cc';
color: #27ba9b;
}
}
.picture {
width: 170rpx;
height: 170rpx;
}
.meta {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 20rpx;
}
.name {
height: 72rpx;
font-size: 26rpx;
color: #444;
}
.attrsText {
line-height: 1.8;
padding: 0 15rpx;
font-size: 24rpx;
align-self: flex-start;
border-radius: 4rpx;
color: #888;
background-color: #f7f7f8;
}
.price {
line-height: 1;
font-size: 26rpx;
color: #444;
margin-bottom: 2rpx;
color: #cf4444;
&::before {
content: '¥';
font-size: 80%;
}
}
//
.count {
position: absolute;
bottom: 20rpx;
right: 5rpx;
display: flex;
justify-content: space-between;
align-items: center;
width: 220rpx;
height: 48rpx;
.text {
height: 100%;
padding: 0 20rpx;
font-size: 32rpx;
color: #444;
}
.input {
height: 100%;
text-align: center;
border-radius: 4rpx;
font-size: 24rpx;
color: #444;
background-color: #f6f6f6;
}
}
}
.cart-swipe {
display: block;
margin: 20rpx 0;
}
.cart-swipe-right {
display: flex;
height: 100%;
.button {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
padding: 6px;
line-height: 1.5;
color: #fff;
font-size: 26rpx;
border-radius: 0;
}
.delete-button {
background-color: #cf4444;
}
}
}
//
.cart-blank,
.login-blank {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 60vh;
.image {
width: 400rpx;
height: 281rpx;
}
.text {
color: #444;
font-size: 26rpx;
margin: 20rpx 0;
}
.button {
width: 240rpx !important;
height: 60rpx;
line-height: 60rpx;
margin-top: 20rpx;
font-size: 26rpx;
border-radius: 60rpx;
color: #fff;
background-color: #27ba9b;
}
}
//
.toolbar {
position: fixed;
left: 0;
right: 0;
bottom: calc(var(--window-bottom));
z-index: 1;
height: 100rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
border-top: 1rpx solid #ededed;
border-bottom: 1rpx solid #ededed;
background-color: #fff;
box-sizing: content-box;
.all {
margin-left: 25rpx;
font-size: 14px;
color: #444;
display: flex;
align-items: center;
}
.all::before {
font-family: 'erabbit' !important;
content: '\e6cd';
font-size: 40rpx;
margin-right: 8rpx;
}
.checked::before {
content: '\e6cc';
color: #27ba9b;
}
.text {
margin-right: 8rpx;
margin-left: 32rpx;
color: #444;
font-size: 14px;
}
.amount {
font-size: 20px;
color: #cf4444;
.decimal {
font-size: 12px;
}
&::before {
content: '¥';
font-size: 12px;
}
}
.button-grounp {
margin-left: auto;
display: flex;
justify-content: space-between;
text-align: center;
line-height: 72rpx;
font-size: 13px;
color: #fff;
.button {
width: 240rpx;
margin: 0 10rpx;
border-radius: 72rpx;
}
.payment-button {
background-color: #27ba9b;
&.disabled {
opacity: 0.6;
}
}
}
}
//
.toolbar-height {
height: 100rpx;
}
</style>

@ -0,0 +1,98 @@
<script setup lang="ts">
import { getCategoryTopAPI } from '@/services/category'
import { getHomeBannerAPI } from '@/services/home'
import type { CategoryTopItem } from '@/types/category'
import type { BannerItem } from '@/types/home'
import { onLoad } from '@dcloudio/uni-app'
import { computed, ref } from 'vue'
import PageSkeleton from './components/PageSkeleton.vue'
//
const bannerList = ref<BannerItem[]>([])
const getBannerData = async () => {
const res = await getHomeBannerAPI(2)
bannerList.value = res.result
}
//
const categoryList = ref<CategoryTopItem[]>([])
const activeIndex = ref(0)
const getCategoryTopData = async () => {
const res = await getCategoryTopAPI()
categoryList.value = res.result
}
//
const isFinish = ref(false)
//
onLoad(async () => {
await Promise.all([getBannerData(), getCategoryTopData()])
isFinish.value = true
})
//
const subCategoryList = computed(() => {
return categoryList.value[activeIndex.value]?.children || []
})
</script>
<template>
<view class="viewport" v-if="isFinish">
<!-- 搜索框 -->
<view class="search">
<view class="input">
<text class="icon-search">女靴</text>
</view>
</view>
<!-- 分类 -->
<view class="categories">
<!-- 左侧一级分类 -->
<scroll-view class="primary" scroll-y>
<view
v-for="(item, index) in categoryList"
:key="item.id"
class="item"
:class="{ active: index === activeIndex }"
@tap="activeIndex = index"
>
<text class="name">
{{ item.name }}
</text>
</view>
</scroll-view>
<!-- 右侧二级分类 -->
<scroll-view enable-back-to-top class="secondary" scroll-y>
<!-- 焦点图 -->
<XtxSwiper class="banner" :list="bannerList" />
<!-- 内容区域 -->
<view class="panel" v-for="item in subCategoryList" :key="item.id">
<view class="title">
<text class="name">{{ item.name }}</text>
<navigator class="more" hover-class="none">全部</navigator>
</view>
<view class="section">
<navigator
v-for="goods in item.goods"
:key="goods.id"
class="goods"
hover-class="none"
:url="`/pages/goods/goods?id=${goods.id}`"
>
<image class="image" :src="goods.picture"></image>
<view class="name ellipsis">{{ goods.name }}</view>
<view class="price">
<text class="symbol">¥</text>
<text class="number">{{ goods.price }}</text>
</view>
</navigator>
</view>
</view>
</scroll-view>
</view>
</view>
<PageSkeleton v-else />
</template>
<style lang="scss">
@import './styles/category.scss';
</style>

@ -0,0 +1,510 @@
<template name="skeleton">
<view class="sk-container">
<view class="viewport">
<view class="search">
<view class="input">
<text
class="icon-search sk-transparent sk-text-14-2857-225 sk-text sk-pseudo sk-pseudo-circle"
>女靴</text
>
</view>
</view>
<view class="categories">
<scroll-view :scroll-y="true" class="primary">
<view class="item active sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-218 sk-text">居家</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-495 sk-text">美食</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-628 sk-text">服饰</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-163 sk-text">母婴</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-690 sk-text">个护</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-302 sk-text">严选</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-730 sk-text">数码</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-584 sk-text">运动</text>
</view>
<view class="item sk-pseudo sk-pseudo-circle">
<text class="name sk-transparent sk-text-14-2857-895 sk-text">杂项</text>
</view>
</scroll-view>
<scroll-view :scroll-y="true" class="secondary">
<view is="components/XtxSwiper" class="banner">
<view class="carousel XtxSwiper--carousel">
<view class="indicator XtxSwiper--indicator">
<text class="dot XtxSwiper--dot active XtxSwiper--active"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
</view>
</view>
</view>
<view class="panel">
<view class="title">
<text class="name sk-transparent sk-text-26-6667-885 sk-text">居家生活用品</text>
<navigator
class="more sk-transparent sk-text-30-0000-892 sk-text sk-pseudo sk-pseudo-circle"
hover-class="none"
>全部</navigator
>
</view>
<view class="section">
<navigator class="goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-648 sk-text"
>极光限定 珠光蓝珐琅锅</view
>
<view class="price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-708 sk-text">199.00</text>
</view>
</navigator>
<navigator class="goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-832 sk-text"
>钻石陶瓷涂层多用锅18cm 小奶锅</view
>
<view class="price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-349 sk-text">149.00</text>
</view>
</navigator>
</view>
</view>
<view class="panel">
<view class="title">
<text class="name sk-transparent sk-text-26-6667-486 sk-text">收纳</text>
<navigator
class="more sk-transparent sk-text-30-0000-520 sk-text sk-pseudo sk-pseudo-circle"
hover-class="none"
>全部</navigator
>
</view>
<view class="section">
<navigator class="goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-582 sk-text"
>开发员自留款带滚轮双层脏衣篓</view
>
<view class="price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-938 sk-text">125.00</text>
</view>
</navigator>
<navigator class="goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-108 sk-text"
>换季好帮手大容量防尘衣物收纳袋</view
>
<view class="price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-564 sk-text">69.00</text>
</view>
</navigator>
<navigator class="goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-507 sk-text"
>可水洗的布艺收纳盒</view
>
<view class="price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-503 sk-text">29.90</text>
</view>
</navigator>
<navigator class="goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-75 sk-text"
>爆款明星好物抽屉式透明储物柜</view
>
<view class="price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-965 sk-text">129.00</text>
</view>
</navigator>
<navigator class="goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-71 sk-text"
>给衣柜减减肥真空防潮压缩袋</view
>
<view class="price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-530 sk-text">79.00</text>
</view>
</navigator>
<navigator class="goods" hover-class="none">
<image class="image sk-image"></image>
<view class="name ellipsis sk-transparent sk-text-14-2857-151 sk-text"
>拉开抽屉不凌乱磨砂抽屉整理盒套装</view
>
<view class="price">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="number sk-transparent sk-text-14-2857-641 sk-text">49.00</text>
</view>
</navigator>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<style lang="scss">
/* #ifdef H5 || APP-PLUS */
/* 修复 H5 端骨架屏样式异常 */
@import '@/components/styles/XtxSwiper.scss';
@import '../styles/category.scss';
/* #endif */
.sk-transparent {
color: transparent !important;
}
.sk-text-14-2857-225 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 39.2rpx;
position: relative !important;
}
.sk-text {
background-origin: content-box !important;
background-clip: content-box !important;
background-color: transparent !important;
color: transparent !important;
background-repeat: repeat-y !important;
}
.sk-text-14-2857-218 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-495 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-628 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-163 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-690 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-302 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-730 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-584 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-895 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-26-6667-885 {
background-image: linear-gradient(
transparent 26.6667%,
#eeeeee 0%,
#eeeeee 73.3333%,
transparent 0%
) !important;
background-size: 100% 60rpx;
position: relative !important;
}
.sk-text-30-0000-892 {
background-image: linear-gradient(
transparent 30%,
#eeeeee 0%,
#eeeeee 70%,
transparent 0%
) !important;
background-size: 100% 60rpx;
position: relative !important;
}
.sk-text-14-2857-648 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 30.8rpx;
position: relative !important;
}
.sk-opacity {
opacity: 0 !important;
}
.sk-text-14-2857-708 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-832 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 30.8rpx;
position: relative !important;
}
.sk-text-14-2857-349 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-26-6667-486 {
background-image: linear-gradient(
transparent 26.6667%,
#eeeeee 0%,
#eeeeee 73.3333%,
transparent 0%
) !important;
background-size: 100% 60rpx;
position: relative !important;
}
.sk-text-30-0000-520 {
background-image: linear-gradient(
transparent 30%,
#eeeeee 0%,
#eeeeee 70%,
transparent 0%
) !important;
background-size: 100% 60rpx;
position: relative !important;
}
.sk-text-14-2857-582 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 30.8rpx;
position: relative !important;
}
.sk-text-14-2857-938 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-108 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 30.8rpx;
position: relative !important;
}
.sk-text-14-2857-564 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-507 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 30.8rpx;
position: relative !important;
}
.sk-text-14-2857-503 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-75 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 30.8rpx;
position: relative !important;
}
.sk-text-14-2857-965 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-71 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 30.8rpx;
position: relative !important;
}
.sk-text-14-2857-530 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-151 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 30.8rpx;
position: relative !important;
}
.sk-text-14-2857-641 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-image {
background: #efefef !important;
}
.sk-pseudo::before,
.sk-pseudo::after {
background: #efefef !important;
background-image: none !important;
color: transparent !important;
border-color: transparent !important;
}
.sk-pseudo-rect::before,
.sk-pseudo-rect::after {
border-radius: 0 !important;
}
.sk-pseudo-circle::before,
.sk-pseudo-circle::after {
border-radius: 50% !important;
}
.sk-container {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: transparent;
}
</style>

@ -0,0 +1,138 @@
page {
height: 100%;
overflow: hidden;
}
.viewport {
height: 100%;
display: flex;
flex-direction: column;
}
.search {
padding: 20rpx 30rpx;
background-color: #fff;
.input {
display: flex;
align-items: center;
justify-content: space-between;
height: 64rpx;
padding-left: 26rpx;
color: #8b8b8b;
font-size: 28rpx;
border-radius: 32rpx;
background-color: #f3f4f4;
}
}
.icon-search {
&::before {
margin-right: 10rpx;
}
}
/* 分类 */
.categories {
flex: 1;
min-height: 400rpx;
display: flex;
}
/* 一级分类 */
.primary {
overflow: hidden;
width: 180rpx;
flex: none;
background-color: #f6f6f6;
.item {
display: flex;
justify-content: center;
align-items: center;
height: 96rpx;
font-size: 26rpx;
color: #595c63;
position: relative;
&::after {
content: '';
position: absolute;
left: 42rpx;
bottom: 0;
width: 96rpx;
border-top: 1rpx solid #e3e4e7;
}
}
.active {
background-color: #fff;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 8rpx;
height: 100%;
background-color: #27ba9b;
}
}
}
.primary .item:last-child::after,
.primary .active::after {
display: none;
}
/* 二级分类 */
.secondary {
background-color: #fff;
.carousel {
height: 200rpx;
margin: 0 30rpx 20rpx;
border-radius: 4rpx;
overflow: hidden;
}
.panel {
margin: 0 30rpx 0rpx;
}
.title {
height: 60rpx;
line-height: 60rpx;
color: #333;
font-size: 28rpx;
border-bottom: 1rpx solid #f7f7f8;
.more {
float: right;
padding-left: 20rpx;
font-size: 24rpx;
color: #999;
}
}
.more {
&::after {
font-family: 'erabbit' !important;
content: '\e6c2';
}
}
.section {
width: 100%;
display: flex;
flex-wrap: wrap;
padding: 20rpx 0;
.goods {
width: 150rpx;
margin: 0rpx 20rpx 20rpx 0;
&:nth-child(3n) {
margin-right: 0;
}
.image {
width: 150rpx;
height: 150rpx;
}
.name {
padding: 5rpx;
font-size: 22rpx;
color: #333;
}
.price {
padding: 5rpx;
font-size: 18rpx;
color: #cf4444;
}
.number {
font-size: 24rpx;
margin-left: 2rpx;
}
}
}
}

@ -0,0 +1,124 @@
<script setup lang="ts">
//
</script>
<template>
<view class="address-panel">
<!-- 关闭按钮 -->
<text class="close icon-close"></text>
<!-- 标题 -->
<view class="title">配送至</view>
<!-- 内容 -->
<view class="content">
<view class="item">
<view class="user">李明 13824686868</view>
<view class="address">北京市顺义区后沙峪地区安平北街6号院</view>
<text class="icon icon-checked"></text>
</view>
<view class="item">
<view class="user">王东 13824686868</view>
<view class="address">北京市顺义区后沙峪地区安平北街6号院</view>
<text class="icon icon-ring"></text>
</view>
<view class="item">
<view class="user">张三 13824686868</view>
<view class="address">北京市朝阳区孙河安平北街6号院</view>
<text class="icon icon-ring"></text>
</view>
</view>
<view class="footer">
<view class="button primary"> 新建地址 </view>
<view v-if="false" class="button primary"></view>
</view>
</view>
</template>
<style lang="scss">
.address-panel {
padding: 0 30rpx;
border-radius: 10rpx 10rpx 0 0;
position: relative;
background-color: #fff;
}
.title {
line-height: 1;
padding: 40rpx 0;
text-align: center;
font-size: 32rpx;
font-weight: normal;
border-bottom: 1rpx solid #ddd;
color: #444;
}
.close {
position: absolute;
right: 24rpx;
top: 24rpx;
}
.content {
min-height: 300rpx;
max-height: 540rpx;
overflow: auto;
padding: 20rpx;
.item {
padding: 30rpx 50rpx 30rpx 60rpx;
background-size: 40rpx;
background-repeat: no-repeat;
background-position: 0 center;
background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/locate.png);
position: relative;
}
.icon {
color: #999;
font-size: 40rpx;
transform: translateY(-50%);
position: absolute;
top: 50%;
right: 0;
}
.icon-checked {
color: #27ba9b;
}
.icon-ring {
color: #444;
}
.user {
font-size: 28rpx;
color: #444;
font-weight: 500;
}
.address {
font-size: 26rpx;
color: #666;
}
}
.footer {
display: flex;
justify-content: space-between;
padding: 20rpx 0 40rpx;
font-size: 28rpx;
color: #444;
.button {
flex: 1;
height: 72rpx;
text-align: center;
line-height: 72rpx;
margin: 0 20rpx;
color: #fff;
border-radius: 72rpx;
}
.primary {
color: #fff;
background-color: #27ba9b;
}
.secondary {
background-color: #ffa868;
}
}
</style>

@ -0,0 +1,96 @@
<script setup lang="ts">
//
const emit = defineEmits<{
(event: 'close'): void
}>()
</script>
<template>
<view class="service-panel">
<!-- 关闭按钮 -->
<text class="close icon-close" @tap="emit('close')"></text>
<!-- 标题 -->
<view class="title">服务说明</view>
<!-- 内容 -->
<view class="content">
<view class="item">
<view class="dt">无忧退货</view>
<view class="dd">
自收到商品之日起30天内可在线申请无忧退货服务食品等特殊商品除外
</view>
</view>
<view class="item">
<view class="dt">快速退款</view>
<view class="dd">
收到退货包裹并确认无误后将在48小时内办理退款
退款将原路返回不同银行处理时间不同预计1-5个工作日到账
</view>
</view>
<view class="item">
<view class="dt">满88元免邮费</view>
<view class="dd">
单笔订单金额(不含运费)满88元可免邮费不满88元 单笔订单收取10元邮费
</view>
</view>
</view>
</view>
</template>
<style lang="scss">
.service-panel {
padding: 0 30rpx;
border-radius: 10rpx 10rpx 0 0;
position: relative;
background-color: #fff;
}
.title {
line-height: 1;
padding: 40rpx 0;
text-align: center;
font-size: 32rpx;
font-weight: normal;
border-bottom: 1rpx solid #ddd;
color: #444;
}
.close {
position: absolute;
right: 24rpx;
top: 24rpx;
}
.content {
padding: 20rpx 20rpx 100rpx 20rpx;
.item {
margin-top: 20rpx;
}
.dt {
margin-bottom: 10rpx;
font-size: 28rpx;
color: #333;
font-weight: 500;
position: relative;
&::before {
content: '';
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background-color: #eaeaea;
transform: translateY(-50%);
position: absolute;
top: 50%;
left: -20rpx;
}
}
.dd {
line-height: 1.6;
font-size: 26rpx;
color: #999;
}
}
</style>

@ -0,0 +1,533 @@
<script setup lang="ts">
import type {
SkuPopupEvent,
SkuPopupInstance,
SkuPopupLocaldata,
} from '@/components/vk-data-goods-sku-popup/vk-data-goods-sku-popup'
import { postMemberCartAPI } from '@/services/cart'
import { getGoodsByIdAPI } from '@/services/goods'
import type { GoodsResult } from '@/types/goods'
import { onLoad } from '@dcloudio/uni-app'
import { computed, ref } from 'vue'
import AddressPanel from './components/AddressPanel.vue'
import ServicePanel from './components/ServicePanel.vue'
//
const { safeAreaInsets } = uni.getSystemInfoSync()
//
const query = defineProps<{
id: string
}>()
//
const goods = ref<GoodsResult>()
const getGoodsByIdData = async () => {
const res = await getGoodsByIdAPI(query.id)
goods.value = res.result
// SKU
localdata.value = {
_id: res.result.id,
name: res.result.name,
goods_thumb: res.result.mainPictures[0],
spec_list: res.result.specs.map((v) => {
return {
name: v.name,
list: v.values,
}
}),
sku_list: res.result.skus.map((v) => {
return {
_id: v.id,
goods_id: res.result.id,
goods_name: res.result.name,
image: v.picture,
price: v.price * 100, // 100
stock: v.inventory,
sku_name_arr: v.specs.map((vv) => vv.valueName),
}
}),
}
}
//
onLoad(() => {
getGoodsByIdData()
})
//
const currentIndex = ref(0)
const onChange: UniHelper.SwiperOnChange = (ev) => {
currentIndex.value = ev.detail.current
}
//
const onTapImage = (url: string) => {
//
uni.previewImage({
current: url,
urls: goods.value!.mainPictures,
})
}
// uni-ui ref
const popup = ref<{
open: (type?: UniHelper.UniPopupType) => void
close: () => void
}>()
//
const popupName = ref<'address' | 'service'>()
const openPopup = (name: typeof popupName.value) => {
//
popupName.value = name
popup.value?.open()
}
// SKU
const isShowSku = ref(false)
//
const localdata = ref({} as SkuPopupLocaldata)
//
enum SkuMode {
Both = 1,
Cart = 2,
Buy = 3,
}
const mode = ref<SkuMode>(SkuMode.Cart)
// SKU
const openSkuPopup = (val: SkuMode) => {
// SKU
isShowSku.value = true
//
mode.value = val
}
// SKU
const skuPopupRef = ref<SkuPopupInstance>()
//
const selectArrText = computed(() => {
return skuPopupRef.value?.selectArr?.join(' ').trim() || '请选择商品规格'
})
//
const onAddCart = async (ev: SkuPopupEvent) => {
await postMemberCartAPI({ skuId: ev._id, count: ev.buy_num })
uni.showToast({ title: '添加成功' })
isShowSku.value = false
}
//
const onBuyNow = (ev: SkuPopupEvent) => {
uni.navigateTo({ url: `/pagesOrder/create/create?skuId=${ev._id}&count=${ev.buy_num}` })
}
</script>
<template>
<!-- SKU弹窗组件 -->
<vk-data-goods-sku-popup
v-model="isShowSku"
:localdata="localdata"
:mode="mode"
add-cart-background-color="#FFA868"
buy-now-background-color="#27BA9B"
ref="skuPopupRef"
:actived-style="{
color: '#27BA9B',
borderColor: '#27BA9B',
backgroundColor: '#E9F8F5',
}"
@add-cart="onAddCart"
@buy-now="onBuyNow"
/>
<scroll-view enable-back-to-top scroll-y class="viewport">
<!-- 基本信息 -->
<view class="goods">
<!-- 商品主图 -->
<view class="preview">
<swiper @change="onChange" circular>
<swiper-item v-for="item in goods?.mainPictures" :key="item">
<image class="image" @tap="onTapImage(item)" mode="aspectFill" :src="item" />
</swiper-item>
</swiper>
<view class="indicator">
<text class="current">{{ currentIndex + 1 }}</text>
<text class="split">/</text>
<text class="total">{{ goods?.mainPictures.length }}</text>
</view>
</view>
<!-- 商品简介 -->
<view class="meta">
<view class="price">
<text class="symbol">¥</text>
<text class="number">{{ goods?.price }}</text>
</view>
<view class="name ellipsis">{{ goods?.name }}</view>
<view class="desc"> {{ goods?.desc }} </view>
</view>
<!-- 操作面板 -->
<view class="action">
<view @tap="openSkuPopup(SkuMode.Both)" class="item arrow">
<text class="label">选择</text>
<text class="text ellipsis"> {{ selectArrText }} </text>
</view>
<view @tap="openPopup('address')" class="item arrow">
<text class="label">送至</text>
<text class="text ellipsis"> 请选择收获地址 </text>
</view>
<view @tap="openPopup('service')" class="item arrow">
<text class="label">服务</text>
<text class="text ellipsis"> 无忧退 快速退款 免费包邮 </text>
</view>
</view>
</view>
<!-- 商品详情 -->
<view class="detail panel">
<view class="title">
<text>详情</text>
</view>
<view class="content">
<view class="properties">
<!-- 属性详情 -->
<view class="item" v-for="item in goods?.details.properties" :key="item.name">
<text class="label">{{ item.name }}</text>
<text class="value">{{ item.value }}</text>
</view>
</view>
<!-- 图片详情 -->
<image
class="image"
v-for="item in goods?.details.pictures"
:key="item"
mode="widthFix"
:src="item"
></image>
</view>
</view>
<!-- 同类推荐 -->
<view class="similar panel">
<view class="title">
<text>同类推荐</text>
</view>
<view class="content">
<navigator
v-for="item in goods?.similarProducts"
:key="item.id"
class="goods"
hover-class="none"
:url="`/pages/goods/goods?id=${item.id}`"
>
<image class="image" mode="aspectFill" :src="item.picture"></image>
<view class="name ellipsis">{{ item.name }}</view>
<view class="price">
<text class="symbol">¥</text>
<text class="number">{{ item.price }}</text>
</view>
</navigator>
</view>
</view>
</scroll-view>
<!-- 用户操作 -->
<view v-if="goods" class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
<view class="icons">
<button class="icons-button"><text class="icon-heart"></text>收藏</button>
<!-- #ifdef MP-WEIXIN -->
<button class="icons-button" open-type="contact">
<text class="icon-handset"></text>客服
</button>
<!-- #endif -->
<navigator class="icons-button" url="/pages/cart/cart2" open-type="navigate">
<text class="icon-cart"></text>购物车
</navigator>
</view>
<view class="buttons">
<view @tap="openSkuPopup(SkuMode.Cart)" class="addcart"> 加入购物车 </view>
<view @tap="openSkuPopup(SkuMode.Buy)" class="payment"> 立即购买 </view>
</view>
</view>
<!-- uni-ui 弹出层 -->
<uni-popup ref="popup" type="bottom" background-color="#fff">
<AddressPanel v-if="popupName === 'address'" @close="popup?.close()" />
<ServicePanel v-if="popupName === 'service'" @close="popup?.close()" />
</uni-popup>
</template>
<style lang="scss">
page {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.viewport {
background-color: #f4f4f4;
}
.panel {
margin-top: 20rpx;
background-color: #fff;
.title {
display: flex;
justify-content: space-between;
align-items: center;
height: 90rpx;
line-height: 1;
padding: 30rpx 60rpx 30rpx 6rpx;
position: relative;
text {
padding-left: 10rpx;
font-size: 28rpx;
color: #333;
font-weight: 600;
border-left: 4rpx solid #27ba9b;
}
navigator {
font-size: 24rpx;
color: #666;
}
}
}
.arrow {
&::after {
position: absolute;
top: 50%;
right: 30rpx;
content: '\e6c2';
color: #ccc;
font-family: 'erabbit' !important;
font-size: 32rpx;
transform: translateY(-50%);
}
}
/* 商品信息 */
.goods {
background-color: #fff;
.preview {
height: 750rpx;
position: relative;
.image {
width: 750rpx;
height: 750rpx;
}
.indicator {
height: 40rpx;
padding: 0 24rpx;
line-height: 40rpx;
border-radius: 30rpx;
color: #fff;
font-family: Arial, Helvetica, sans-serif;
background-color: rgba(0, 0, 0, 0.3);
position: absolute;
bottom: 30rpx;
right: 30rpx;
.current {
font-size: 26rpx;
}
.split {
font-size: 24rpx;
margin: 0 1rpx 0 2rpx;
}
.total {
font-size: 24rpx;
}
}
}
.meta {
position: relative;
border-bottom: 1rpx solid #eaeaea;
.price {
height: 130rpx;
padding: 25rpx 30rpx 0;
color: #fff;
font-size: 34rpx;
box-sizing: border-box;
background-color: #35c8a9;
}
.number {
font-size: 56rpx;
}
.brand {
width: 160rpx;
height: 80rpx;
overflow: hidden;
position: absolute;
top: 26rpx;
right: 30rpx;
}
.name {
max-height: 88rpx;
line-height: 1.4;
margin: 20rpx;
font-size: 32rpx;
color: #333;
}
.desc {
line-height: 1;
padding: 0 20rpx 30rpx;
font-size: 24rpx;
color: #cf4444;
}
}
.action {
padding-left: 20rpx;
.item {
height: 90rpx;
padding-right: 60rpx;
border-bottom: 1rpx solid #eaeaea;
font-size: 26rpx;
color: #333;
position: relative;
display: flex;
align-items: center;
&:last-child {
border-bottom: 0 none;
}
}
.label {
width: 60rpx;
color: #898b94;
margin: 0 16rpx 0 10rpx;
}
.text {
flex: 1;
-webkit-line-clamp: 1;
}
}
}
/* 商品详情 */
.detail {
padding-left: 20rpx;
.content {
margin-left: -20rpx;
.image {
width: 100%;
}
}
.properties {
padding: 0 20rpx;
margin-bottom: 30rpx;
.item {
display: flex;
line-height: 2;
padding: 10rpx;
font-size: 26rpx;
color: #333;
border-bottom: 1rpx dashed #ccc;
}
.label {
width: 200rpx;
}
.value {
flex: 1;
}
}
}
/* 同类推荐 */
.similar {
.content {
padding: 0 20rpx 20rpx;
background-color: #f4f4f4;
display: flex;
flex-wrap: wrap;
.goods {
width: 340rpx;
padding: 24rpx 20rpx 20rpx;
margin: 20rpx 7rpx;
border-radius: 10rpx;
background-color: #fff;
}
.image {
width: 300rpx;
height: 260rpx;
}
.name {
height: 80rpx;
margin: 10rpx 0;
font-size: 26rpx;
color: #262626;
}
.price {
line-height: 1;
font-size: 20rpx;
color: #cf4444;
}
.number {
font-size: 26rpx;
margin-left: 2rpx;
}
}
navigator {
&:nth-child(even) {
margin-right: 0;
}
}
}
/* 底部工具栏 */
.toolbar {
position: fixed;
left: 0;
right: 0;
bottom: calc((var(--window-bottom)));
z-index: 1;
background-color: #fff;
height: 100rpx;
padding: 0 20rpx;
border-top: 1rpx solid #eaeaea;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: content-box;
.buttons {
display: flex;
& > view {
width: 220rpx;
text-align: center;
line-height: 72rpx;
font-size: 26rpx;
color: #fff;
border-radius: 72rpx;
}
.addcart {
background-color: #ffa868;
}
.payment {
background-color: #27ba9b;
margin-left: 20rpx;
}
}
.icons {
padding-right: 20rpx;
display: flex;
align-items: center;
flex: 1;
// H5 App
.navigator-wrap,
.icons-button {
flex: 1;
text-align: center;
line-height: 1.4;
padding: 0;
margin: 0;
border-radius: 0;
font-size: 20rpx;
color: #333;
background-color: #fff;
&::after {
border: none;
}
}
text {
display: block;
font-size: 34rpx;
}
}
}
</style>

@ -0,0 +1,221 @@
<script setup lang="ts">
import { getHotRecommendAPI } from '@/services/hot'
import type { SubTypeItem } from '@/types/hot'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
// url
const urlMap = [
{ type: '1', title: '特惠推荐', url: '/hot/preference' },
{ type: '2', title: '爆款推荐', url: '/hot/inVogue' },
{ type: '3', title: '一站买全', url: '/hot/oneStop' },
{ type: '4', title: '新鲜好物', url: '/hot/new' },
]
// uniapp
const query = defineProps<{
type: string
}>()
// console.log(query)
const currUrlMap = urlMap.find((v) => v.type === query.type)
//
uni.setNavigationBarTitle({ title: currUrlMap!.title })
//
const bannerPicture = ref('')
//
const subTypes = ref<(SubTypeItem & { finish?: boolean })[]>([])
//
const activeIndex = ref(0)
//
const getHotRecommendData = async () => {
const res = await getHotRecommendAPI(currUrlMap!.url, {
// 便
page: import.meta.env.DEV ? 30 : 1,
pageSize: 10,
})
// console.log(res.result.title)
bannerPicture.value = res.result.bannerPicture
subTypes.value = res.result.subTypes
}
//
onLoad(() => {
getHotRecommendData()
})
//
const onScrolltolower = async () => {
//
const currsubTypes = subTypes.value[activeIndex.value]
//
if (currsubTypes.goodsItems.page < currsubTypes.goodsItems.pages) {
//
currsubTypes.goodsItems.page++
} else {
//
currsubTypes.finish = true
// 退
return uni.showToast({ icon: 'none', title: '没有更多数据了~' })
}
// API
const res = await getHotRecommendAPI(currUrlMap!.url, {
subType: currsubTypes.id,
page: currsubTypes.goodsItems.page,
pageSize: currsubTypes.goodsItems.pageSize,
})
//
const newsubTypes = res.result.subTypes[activeIndex.value]
//
currsubTypes.goodsItems.items.push(...newsubTypes.goodsItems.items)
}
</script>
<template>
<view class="viewport">
<!-- 推荐封面图 -->
<view class="cover">
<image class="image" mode="widthFix" :src="bannerPicture"></image>
</view>
<!-- 推荐选项 -->
<view class="tabs">
<text
v-for="(item, index) in subTypes"
:key="item.id"
class="text"
:class="{ active: index === activeIndex }"
@tap="activeIndex = index"
>{{ item.title }}</text
>
</view>
<!-- 推荐列表 -->
<scroll-view
enable-back-to-top
v-for="(item, index) in subTypes"
:key="item.id"
v-show="activeIndex === index"
scroll-y
class="scroll-view"
@scrolltolower="onScrolltolower"
>
<view class="goods">
<navigator
hover-class="none"
class="navigator"
v-for="goods in item.goodsItems.items"
:key="goods.id"
:url="`/pages/goods/goods?id=${goods.id}`"
>
<image class="thumb" :src="goods.picture"></image>
<view class="name ellipsis">{{ goods.name }}</view>
<view class="price">
<text class="symbol">¥</text>
<text class="number">{{ goods.price }}</text>
</view>
</navigator>
</view>
<view class="loading-text">
{{ item.finish ? '没有更多数据了~' : '正在加载...' }}
</view>
</scroll-view>
</view>
</template>
<style lang="scss">
page {
height: 100%;
background-color: #f4f4f4;
}
.viewport {
display: flex;
flex-direction: column;
height: 100%;
padding: 180rpx 0 0;
position: relative;
}
.cover {
width: 750rpx;
height: 225rpx;
border-radius: 0 0 40rpx 40rpx;
overflow: hidden;
position: absolute;
left: 0;
top: 0;
.image {
width: 750rpx;
}
}
.scroll-view {
flex: 1;
}
.tabs {
display: flex;
justify-content: space-evenly;
height: 100rpx;
line-height: 90rpx;
margin: 0 20rpx;
font-size: 28rpx;
border-radius: 10rpx;
box-shadow: 0 4rpx 5rpx rgba(200, 200, 200, 0.3);
color: #333;
background-color: #fff;
position: relative;
z-index: 9;
.text {
margin: 0 20rpx;
position: relative;
}
.active {
&::after {
content: '';
width: 40rpx;
height: 4rpx;
transform: translate(-50%);
background-color: #27ba9b;
position: absolute;
left: 50%;
bottom: 24rpx;
}
}
}
.goods {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0 20rpx 20rpx;
.navigator {
width: 342rpx;
padding: 20rpx;
margin-top: 20rpx;
border-radius: 10rpx;
background-color: #fff;
}
.thumb {
width: 305rpx;
height: 305rpx;
}
.name {
height: 88rpx;
font-size: 26rpx;
}
.price {
line-height: 1;
color: #cf4444;
font-size: 30rpx;
}
.symbol {
font-size: 70%;
}
.decimal {
font-size: 70%;
}
}
.loading-text {
text-align: center;
font-size: 28rpx;
color: #666;
padding: 20rpx 0 50rpx;
}
</style>

@ -0,0 +1,41 @@
<script setup lang="ts">
import type { CategoryItem } from '@/types/home'
// props
defineProps<{
list: CategoryItem[]
}>()
</script>
<template>
<view class="category">
<view class="module-title">
预约上门回收
</view>
<view class="cont">
<navigator
class="category-item"
hover-class="none"
url="/pages/index/index"
v-for="item in list"
:key="item.id"
>
<view class="left">
<view class="title">{{ item.name }}</view>
<view class="desc">{{ item.desc }}</view>
</view>
<view class="right">
<image class="icon" :src="item.icon"></image>
</view>
</navigator>
</view>
</view>
</template>
<style lang="scss">
@import '../styles/category.scss';
</style>

@ -0,0 +1,71 @@
<script setup lang="ts">
//
const { safeAreaInsets } = uni.getSystemInfoSync()
</script>
<template>
<view class="navbar" :style="{ paddingTop: safeAreaInsets!.top + 10 + 'px' }">
<!-- logo文字 -->
<view class="logo">
<image class="logo-image" src="@/static/images/logo.png"></image>
<!-- <text class="logo-text">新鲜 · 亲民 · 快捷</text> -->
</view>
<!-- 搜索条 -->
<view class="search">
<text class="icon-search">搜索商品</text>
<text class="icon-scan"></text>
</view>
</view>
</template>
<style lang="scss">
/* 自定义导航条 */
.navbar {
background-image: url(@/static/images/navigator_bg.png);
background-size: cover;
position: relative;
display: flex;
flex-direction: column;
padding-top: 20px;
.logo {
display: flex;
align-items: center;
height: 64rpx;
padding-left: 30rpx;
.logo-image {
width: 166rpx;
height: 39rpx;
}
.logo-text {
flex: 1;
line-height: 28rpx;
color: #fff;
margin: 2rpx 0 0 20rpx;
padding-left: 20rpx;
border-left: 1rpx solid #fff;
font-size: 26rpx;
}
}
.search {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10rpx 0 26rpx;
height: 64rpx;
margin: 16rpx 20rpx;
color: #fff;
font-size: 28rpx;
border-radius: 32rpx;
background-color: rgba(255, 255, 255, 0.5);
}
.icon-search {
&::before {
margin-right: 10rpx;
}
}
.icon-scan {
font-size: 30rpx;
padding: 15rpx;
}
}
</style>

@ -0,0 +1,33 @@
<script setup lang="ts">
import type { HotItem } from '@/types/home'
// props
defineProps<{
list: HotItem[]
}>()
</script>
<template>
<!-- 推荐专区 -->
<view class="panel hot">
<view class="item" v-for="item in list" :key="item.id">
<view class="title">
<text class="title-text">{{ item.title }}</text>
<text class="title-desc">{{ item.alt }}</text>
</view>
<navigator hover-class="none" :url="`/pages/hot/hot?type=${item.type}`" class="cards">
<image
v-for="src in item.pictures"
:key="src"
class="image"
mode="aspectFit"
:src="src"
></image>
</navigator>
</view>
</view>
</template>
<style lang="scss">
@import '../styles/hot.scss';
</style>

@ -0,0 +1,394 @@
<template name="skeleton">
<view is="components/XtxSwiper">
<view class="carousel XtxSwiper--carousel">
<!-- App 端控制台未知报错删除 swiper 错误消失 -->
<view class="indicator XtxSwiper--indicator">
<text class="dot XtxSwiper--dot active XtxSwiper--active"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
<text class="dot XtxSwiper--dot"></text>
</view>
</view>
</view>
<view is="pages/index/components/CategoryPanel">
<view class="category CategoryPanel--category">
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-382 sk-text"
>居家</text
>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-248 sk-text"
>锦鲤</text
>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-184 sk-text"
>服饰</text
>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-202 sk-text"
>母婴</text
>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-664 sk-text"
>个护</text
>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-805 sk-text"
>严选</text
>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-445 sk-text"
>数码</text
>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-512 sk-text"
>运动</text
>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-61 sk-text">杂项</text>
</navigator>
<navigator class="category-item CategoryPanel--category-item" hover-class="none">
<image class="icon CategoryPanel--icon sk-image"></image>
<text class="text CategoryPanel--text sk-transparent sk-text-14-2857-863 sk-text"
>品牌</text
>
</navigator>
</view>
</view>
<view is="pages/index/components/HotPanel">
<view class="panel HotPanel--panel hot HotPanel--hot">
<view class="item HotPanel--item">
<view class="title HotPanel--title">
<text class="title-text HotPanel--title-text sk-transparent sk-text-14-2857-739 sk-text"
>特惠推荐</text
>
<text class="title-desc HotPanel--title-desc sk-transparent sk-text-14-2857-229 sk-text"
>精选全攻略</text
>
</view>
<navigator class="cards HotPanel--cards" hover-class="none">
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
</navigator>
</view>
<view class="item HotPanel--item">
<view class="title HotPanel--title">
<text class="title-text HotPanel--title-text sk-transparent sk-text-14-2857-73 sk-text"
>爆款推荐</text
>
<text class="title-desc HotPanel--title-desc sk-transparent sk-text-14-2857-879 sk-text"
>最受欢迎</text
>
</view>
<navigator class="cards HotPanel--cards" hover-class="none">
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
</navigator>
</view>
<view class="item HotPanel--item">
<view class="title HotPanel--title">
<text class="title-text HotPanel--title-text sk-transparent sk-text-14-2857-323 sk-text"
>一站买全</text
>
<text class="title-desc HotPanel--title-desc sk-transparent sk-text-14-2857-50 sk-text"
>精心优选</text
>
</view>
<navigator class="cards HotPanel--cards" hover-class="none">
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
</navigator>
</view>
<view class="item HotPanel--item">
<view class="title HotPanel--title">
<text class="title-text HotPanel--title-text sk-transparent sk-text-14-2857-70 sk-text"
>新鲜好物</text
>
<text class="title-desc HotPanel--title-desc sk-transparent sk-text-14-2857-877 sk-text"
>生活加分项</text
>
</view>
<navigator class="cards HotPanel--cards" hover-class="none">
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
<image class="image HotPanel--image sk-image" mode="aspectFit"></image>
</navigator>
</view>
</view>
</view>
</template>
<style lang="scss">
/* #ifdef H5 || APP-PLUS */
/* 修复 H5 端骨架屏样式异常 */
/* 原因H5 端样式自动开启 scoped 隔离,导致骨架屏的基础样式被隔离 */
@import '../styles/category.scss';
@import '../styles/hot.scss';
@import '@/components/styles/XtxSwiper.scss';
/* #endif */
.sk-transparent {
color: transparent !important;
}
.sk-text-3-5714-536 {
background-image: linear-gradient(
transparent 3.5714%,
#eeeeee 0%,
#eeeeee 96.4286%,
transparent 0%
) !important;
background-size: 100% 28rpx;
position: relative !important;
}
.sk-text {
background-origin: content-box !important;
background-clip: content-box !important;
background-color: transparent !important;
color: transparent !important;
background-repeat: repeat-y !important;
}
.sk-text-14-2857-28 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 39.2rpx;
position: relative !important;
}
.sk-text-14-2857-382 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-248 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-184 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-202 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-664 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-805 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-445 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-512 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-61 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-863 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-739 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 44.8rpx;
position: relative !important;
}
.sk-text-14-2857-229 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-73 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 44.8rpx;
position: relative !important;
}
.sk-text-14-2857-879 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-323 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 44.8rpx;
position: relative !important;
}
.sk-text-14-2857-50 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-70 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 44.8rpx;
position: relative !important;
}
.sk-text-14-2857-877 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-0-0000-363 {
background-image: linear-gradient(
transparent 0%,
#eeeeee 0%,
#eeeeee 100%,
transparent 0%
) !important;
background-size: 100% 32rpx;
position: relative !important;
}
.sk-image {
background: #efefef !important;
}
.sk-pseudo::before,
.sk-pseudo::after {
background: #efefef !important;
background-image: none !important;
color: transparent !important;
border-color: transparent !important;
}
.sk-pseudo-rect::before,
.sk-pseudo-rect::after {
border-radius: 0 !important;
}
.sk-pseudo-circle::before,
.sk-pseudo-circle::after {
border-radius: 50% !important;
}
.sk-container {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: transparent;
}
</style>

@ -0,0 +1,156 @@
<script setup lang="ts">
import { getHomeBannerAPI, getHomeCategoryAPI, getHomeHotAPI } from '@/services/home'
import type { BannerItem, CategoryItem, HotItem } from '@/types/home'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
import CustomNavbar from './components/CustomNavbar.vue'
import CategoryPanel from './components/CategoryPanel.vue'
import HotPanel from './components/HotPanel.vue'
import PageSkeleton from './components/PageSkeleton.vue'
import { useGuessList } from '@/composables'
//
const bannerList = ref<BannerItem[]>([])
const getHomeBannerData = async () => {
const res = await getHomeBannerAPI()
bannerList.value = res.result
}
//
const categoryList = ref<CategoryItem[]>([])
const getHomeCategoryData = async () => {
// const res = await getHomeCategoryAPI()
// console.log(JSON.stringify(res.result,null, 4))
// debugger
// categoryList.value = res.result
const data = [
{
"id": "1019000",
"name": "旧衣服",
"desc": '旧衣服回收不浪费',
"icon": "http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-05-06/4b02f01f-a365-4b6c-9f7a-8b0f591dda02.png?quality=95&imageView"
},
{
"id": "1043000",
"name": "家用电器",
"desc": '旧衣服回收不浪费',
"icon": "http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-05-06/9660870d-6a59-4624-8064-b3a8cbf50d5c.png?quality=95&imageView"
},
{
"id": "109243029",
"name": "废纸废书",
"desc": '旧衣服回收不浪费',
"icon": "http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-05-06/7d19752c-baff-49b6-bd02-5ece1d729214.png?quality=95&imageView"
},
{
"id": "19999999",
"name": "废金属",
"desc": '旧衣服回收不浪费',
"icon": "http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-05-06/4ff20b9e-8150-4bd3-87a3-0cd6766938dd.png?quality=95&imageView"
},
{
"id": "999999",
"name": "废玻璃",
"desc": '旧衣服回收不浪费',
"icon": "http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-05-06/d9ee4919-0d2c-4383-9916-2dd25f12610c.png?quality=95&imageView"
},
{
"id": "999999",
"name": "废塑料",
"desc": '旧衣服回收不浪费',
"icon": "http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-05-06/d9ee4919-0d2c-4383-9916-2dd25f12610c.png?quality=95&imageView"
}
]
categoryList.value = data
}
//
const hotList = ref<HotItem[]>([])
const getHomeHotData = async () => {
const res = await getHomeHotAPI()
hotList.value = res.result
}
//
const isLoading = ref(false)
//
onLoad(async () => {
isLoading.value = true
await Promise.all([getHomeBannerData(), getHomeCategoryData(), getHomeHotData()])
isLoading.value = false
})
//
const { guessRef, onScrolltolower } = useGuessList()
//
const isTriggered = ref(false)
//
const onRefresherrefresh = async () => {
//
isTriggered.value = true
//
// await getHomeBannerData()
// await getHomeCategoryData()
// await getHomeHotData()
//
guessRef.value?.resetData()
await Promise.all([
getHomeBannerData(),
getHomeCategoryData(),
getHomeHotData(),
guessRef.value?.getMore(),
])
//
isTriggered.value = false
}
</script>
<template>
<view class="viewport">
<!-- 自定义导航栏 -->
<CustomNavbar />
<!-- 滚动容器 -->
<scroll-view
enable-back-to-top
refresher-enabled
@refresherrefresh="onRefresherrefresh"
:refresher-triggered="isTriggered"
@scrolltolower="onScrolltolower"
class="scroll-view"
scroll-y
>
<PageSkeleton v-if="isLoading" />
<template v-else>
<!-- 自定义轮播图 -->
<XtxSwiper :list="bannerList" />
<!-- 分类面板 -->
<CategoryPanel :list="categoryList" />
<!-- 热门推荐 -->
<HotPanel :list="hotList" />
<!-- 猜你喜欢 -->
<XtxGuess ref="guessRef" />
</template>
</scroll-view>
</view>
</template>
<style lang="scss">
page {
background-color: #f7f7f7;
height: 100%;
overflow: hidden;
}
.viewport {
height: 100%;
display: flex;
flex-direction: column;
}
.scroll-view {
flex: 1;
overflow: hidden;
}
</style>

@ -0,0 +1,46 @@
/* 前台类目 */
.category {
.module-title {
margin: 32rpx 32rpx 0;
font-size: 30rpx;
color: #0D0D26;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #F2F2F2;
}
.cont {
// margin: 20rpx 0 0;
padding: 10rpx 0;
display: flex;
justify-content: center;
flex-wrap: wrap;
min-height: 328rpx;
.category-item {
width: 48%;
border-radius: 16rpx;
margin: 8rpx;
padding: 16rpx 0;
display: flex;
justify-content: center; // flex-direction: row;
align-items: center;
box-sizing: border-box;
background: #FCF4FB;
.left {
.title {
font-size: 26rpx;
color: #666;
}
.desc {
font-size: 24rpx;
color: #7BAFBD;
}
}
.right {
.icon {
width: 100rpx;
height: 100rpx;
}
}
}
}
}

@ -0,0 +1,44 @@
@charset "UTF-8";
/* 前台类目 */
.category .module-title {
margin: 32rpx 32rpx 0;
font-size: 30rpx;
color: #0D0D26;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #F2F2F2;
}
.category .cont {
padding: 10rpx 0;
display: flex;
justify-content: center;
flex-wrap: wrap;
min-height: 328rpx;
}
.category .cont .category-item {
width: 48%;
border-radius: 16rpx;
margin: 8rpx;
padding: 16rpx 0;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
background: #FCF4FB;
}
.category .cont .category-item .left .title {
font-size: 26rpx;
color: #666;
}
.category .cont .category-item .left .desc {
font-size: 24rpx;
color: #7BAFBD;
}
.category .cont .category-item .right .icon {
width: 100rpx;
height: 100rpx;
}

@ -0,0 +1,51 @@
/* 热门推荐 */
.hot {
display: flex;
flex-wrap: wrap;
min-height: 508rpx;
margin: 20rpx 20rpx 0;
border-radius: 10rpx;
background-color: #fff;
.title {
display: flex;
align-items: center;
padding: 24rpx 24rpx 0;
font-size: 32rpx;
color: #262626;
position: relative;
.title-desc {
font-size: 24rpx;
color: #7f7f7f;
margin-left: 18rpx;
}
}
.item {
display: flex;
flex-direction: column;
width: 50%;
height: 254rpx;
border-right: 1rpx solid #eee;
border-top: 1rpx solid #eee;
.title {
justify-content: start;
}
&:nth-child(2n) {
border-right: 0 none;
}
&:nth-child(-n + 2) {
border-top: 0 none;
}
.image {
width: 150rpx;
height: 150rpx;
}
}
.cards {
flex: 1;
padding: 15rpx 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
}

@ -0,0 +1,59 @@
@charset "UTF-8";
/* 热门推荐 */
.hot {
display: flex;
flex-wrap: wrap;
min-height: 508rpx;
margin: 20rpx 20rpx 0;
border-radius: 10rpx;
background-color: #fff;
}
.hot .title {
display: flex;
align-items: center;
padding: 24rpx 24rpx 0;
font-size: 32rpx;
color: #262626;
position: relative;
}
.hot .title .title-desc {
font-size: 24rpx;
color: #7f7f7f;
margin-left: 18rpx;
}
.hot .item {
display: flex;
flex-direction: column;
width: 50%;
height: 254rpx;
border-right: 1rpx solid #eee;
border-top: 1rpx solid #eee;
}
.hot .item .title {
justify-content: start;
}
.hot .item:nth-child(2n) {
border-right: 0 none;
}
.hot .item:nth-child(-n + 2) {
border-top: 0 none;
}
.hot .item .image {
width: 150rpx;
height: 150rpx;
}
.hot .cards {
flex: 1;
padding: 15rpx 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}

@ -0,0 +1,308 @@
<script setup lang="ts">
import { postLoginAPI, postLoginWxMinAPI, postLoginWxMinSimpleAPI } from '@/services/login'
import { useMemberStore } from '@/stores'
import type { LoginResult } from '@/types/member'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
// #ifdef MP-WEIXIN
// code
let code = ''
onLoad(async () => {
const res = await wx.login()
code = res.code
})
//
const onGetphonenumber: UniHelper.ButtonOnGetphonenumber = async (ev) => {
await checkedAgreePrivacy()
const { encryptedData, iv } = ev.detail
const res = await postLoginWxMinAPI({ code, encryptedData, iv })
loginSuccess(res.result)
}
// #endif
//
const onGetphonenumberSimple = async () => {
await checkedAgreePrivacy()
const res = await postLoginWxMinSimpleAPI('13123456789')
loginSuccess(res.result)
}
const loginSuccess = (profile: LoginResult) => {
//
const memberStore = useMemberStore()
memberStore.setProfile(profile)
//
uni.showToast({ icon: 'success', title: '登录成功' })
setTimeout(() => {
//
// uni.switchTab({ url: '/pages/my/my' })
uni.navigateBack()
}, 500)
}
// #ifdef H5
// 13123456789 123456使
const form = ref({
account: '13123456789',
password: '',
})
//
const onSubmit = async () => {
await checkedAgreePrivacy()
const res = await postLoginAPI(form.value)
loginSuccess(res.result)
}
// #endif
//
const isAgreePrivacy = ref(false)
const isAgreePrivacyShakeY = ref(false)
const checkedAgreePrivacy = async () => {
if (!isAgreePrivacy.value) {
uni.showToast({
icon: 'none',
title: '请先阅读并勾选协议',
})
//
isAgreePrivacyShakeY.value = true
setTimeout(() => {
isAgreePrivacyShakeY.value = false
}, 500)
//
return Promise.reject(new Error('请先阅读并勾选协议'))
}
}
const onOpenPrivacyContract = () => {
// #ifdef MP-WEIXIN
//
wx.openPrivacyContract({})
// #endif
}
</script>
<template>
<view class="viewport">
<view class="logo">
<image
src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/logo_icon.png"
></image>
</view>
<view class="login">
<!-- 网页端表单登录 -->
<!-- #ifdef H5 -->
<input v-model="form.account" class="input" type="text" placeholder="请输入用户名/手机号码" />
<input v-model="form.password" class="input" type="text" password placeholder="请输入密码" />
<button @tap="onSubmit" class="button phone">登录</button>
<!-- #endif -->
<!-- 小程序端授权登录 -->
<!-- #ifdef MP-WEIXIN -->
<view class="button-privacy-wrap">
<button
:hidden="isAgreePrivacy"
class="button-opacity button phone"
@tap="checkedAgreePrivacy"
>
请先阅读并勾选协议
</button>
<button class="button phone" open-type="getPhoneNumber" @getphonenumber="onGetphonenumber">
<text class="icon icon-phone"></text>
手机号快捷登录
</button>
</view>
<!-- #endif -->
<view class="extra">
<view class="caption">
<text>其他登录方式</text>
</view>
<view class="options">
<!-- 通用模拟登录 -->
<button @tap="onGetphonenumberSimple">
<text class="icon icon-phone">模拟快捷登录</text>
</button>
</view>
</view>
<view class="tips" :class="{ animate__shakeY: isAgreePrivacyShakeY }">
<label class="label" @tap="isAgreePrivacy = !isAgreePrivacy">
<radio class="radio" color="#28bb9c" :checked="isAgreePrivacy" />
<text>登录/注册即视为你同意</text>
</label>
<navigator class="link" hover-class="none" url="./protocal">服务条款</navigator>
<text class="link" @tap="onOpenPrivacyContract"></text>
</view>
</view>
</view>
</template>
<style lang="scss">
page {
height: 100%;
}
.viewport {
display: flex;
flex-direction: column;
height: 100%;
padding: 20rpx 40rpx;
}
.logo {
flex: 1;
text-align: center;
image {
width: 220rpx;
height: 220rpx;
margin-top: 15vh;
}
}
.login {
display: flex;
flex-direction: column;
height: 60vh;
padding: 40rpx 20rpx 20rpx;
.input {
width: 100%;
height: 80rpx;
font-size: 28rpx;
border-radius: 72rpx;
border: 1px solid #ddd;
padding-left: 30rpx;
margin-bottom: 20rpx;
}
.button {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 80rpx;
font-size: 28rpx;
border-radius: 72rpx;
color: #fff;
.icon {
font-size: 40rpx;
margin-right: 6rpx;
}
}
.phone {
background-color: #28bb9c;
}
.wechat {
background-color: #06c05f;
}
.extra {
flex: 1;
padding: 70rpx 70rpx 0;
.caption {
width: 440rpx;
line-height: 1;
border-top: 1rpx solid #ddd;
font-size: 26rpx;
color: #999;
position: relative;
text {
transform: translate(-40%);
background-color: #fff;
position: absolute;
top: -12rpx;
left: 50%;
}
}
.options {
display: flex;
justify-content: center;
align-items: center;
margin-top: 70rpx;
button {
padding: 0;
background-color: transparent;
&::after {
border: none;
}
}
}
.icon {
font-size: 24rpx;
color: #444;
display: flex;
flex-direction: column;
align-items: center;
&::before {
display: flex;
align-items: center;
justify-content: center;
width: 80rpx;
height: 80rpx;
margin-bottom: 6rpx;
font-size: 40rpx;
border: 1rpx solid #444;
border-radius: 50%;
}
}
.icon-weixin::before {
border-color: #06c05f;
color: #06c05f;
}
}
}
@keyframes animate__shakeY {
0% {
transform: translate(0, 0);
}
50% {
transform: translate(0, -5rpx);
}
100% {
transform: translate(0, 0);
}
}
.animate__shakeY {
animation: animate__shakeY 0.2s ease-in-out 3;
}
.button-privacy-wrap {
position: relative;
.button-opacity {
opacity: 0;
position: absolute;
z-index: 1;
}
}
.tips {
position: absolute;
bottom: 80rpx;
left: 20rpx;
right: 20rpx;
font-size: 22rpx;
color: #999;
text-align: center;
.radio {
transform: scale(0.6);
margin-right: -4rpx;
margin-top: -4rpx;
vertical-align: middle;
}
.link {
display: inline;
color: #28bb9c;
}
}
</style>

@ -0,0 +1,267 @@
<template>
<article class="article">
<h1 class="h1 tac">小兔鲜儿用户服务协议</h1>
<p class="p tac">版本生效日期2023年11月6日</p>
<h2 class="h2">服务条款的确认及接受</h2>
<p class="p">
() 小兔鲜儿 网站各项电子服务的所有权和运作权归属于传智教育所有<strong class="strong"
>仅用于IT培训教学使用为保障您的个人信息安全请勿向平台录入任何个人敏感信息如手机号身份证号等</strong
>本网站提供的服务将完全按照其发布的服务条款和操作规则严格执行您确认所有服务条款并完成注册程序时本协议在您与本网站间成立并发生法律效力同时您成为本网站正式用户
</p>
<p class="p">
()
小兔鲜儿隐私政策平台规则单独协议均为本协议的补充协议与本协议不可分割且具有同等法律效力如您使用小兔鲜儿平台服务视为您同意上述补充协议
</p>
<p class="p">
()
您在使用本网站某一特定服务时该服务可能会另有单独的协议相关业务规则等以下统称为单独协议您在使用该项服务前请阅读并同意相关的单独协议您使用前述特定服务即视为您已阅读并同意接受相关单独协议
</p>
<p class="p">
()
根据国家法律法规变化及本网站运营需要传智教育有权对本协议条款及相关规则不时地进行修改修改后的内容一旦依法以任何形式公布在本网站上即生效并取代此前相关内容您应不时关注本网站公告提示信息及协议规则等相关内容的变动您知悉并确认如您不同意更新后的内容应立即停止使用本网站如您继续使用本网站即视为知悉变动内容并同意接受
</p>
<p class="p">
()
除非另有明确的书面说明,小兔鲜儿不对本网站的运营及其包含在本网站上的信息内容材料产品包括软件或服务作任何形式的明示或默示的声明或担保根据中华人民共和国法律另有规定的以外
</p>
<p class="p">
()
本协议内容中以加粗方式显著标识的条款请您着重阅读您点击同意按钮即视为您完全接受本协议在点击之前请您再次确认已知悉并完全理解本协议的全部内容
</p>
<h2 class="h2">账户注册管理及使用</h2>
<h4 class="h4">() 用户资格</h4>
<p class="p">
您确认在您开始注册程序使用小兔鲜儿平台服务前您应当具备中华人民共和国法律规定的与您行为相适应的民事行为能力若您不具备前述与您行为相适应的民事行为能力需要在监护人的监护参与下才能注册并使用本网站您及您的监护人应依照法律规定承担因此而导致的一切后果
此外您还需确保您不是任何国家地区或国际组织实施的贸易限制经济制裁或其他法律法规限制的对象也未直接或间接为前述对象提供资金商品或服务否则您应当停止使用小兔鲜儿平台服务同时您理解违反前述要求可能会造成您无法正常注册及使用小兔鲜儿平台服务
</p>
<h4 class="h4">() 账户注册</h4>
<p class="p">
1
您按照注册页面提示填写信息阅读并同意本协议且完成全部注册程序后您可获得小兔鲜儿平台账户并成为小兔鲜儿平台用户您设置的小兔鲜儿账号名/昵称/头像等不得违反国家法律法规及小兔鲜儿平台相关规则关于账号注册的管理规定未经他人许可不得用他人名义包括但不限于冒用他人姓名名称字号头像等或采取其他足以让人引起混淆的方式开设账号不得恶意注册小兔鲜儿账号否则小兔鲜儿平台经营者有权拒绝设置或收回您的账号名/昵称账号名的回收不影响您以邮箱手机号码登录小兔鲜儿平台并使用小兔鲜儿平台服务
</p>
<p class="p">
2
小兔鲜儿平台原则上只允许每位用户使用一个小兔鲜儿平台账户如有证据证明或小兔鲜儿根据小兔鲜儿平台规则判断您存在不当注册或不当使用多个小兔鲜儿平台账户的情形小兔鲜儿平台可采取冻结或关闭账户取消订单拒绝提供服务等措施如给小兔鲜儿平台及相关方造成损失的您还应承担赔偿责任
</p>
<p class="p">
3
在使用小兔鲜儿平台服务时您应当按小兔鲜儿平台页面的提示准确完整地提供您的信息包括您的姓名及电子邮件地址联系电话联系地址等以便小兔鲜儿或其他服务方与您联系您了解并同意您有义务保持您提供信息的真实性及有效性
</p>
<h4 class="h4">() 账户登录</h4>
<p class="p">
您有权使用您设置或确认的小兔鲜儿用户名手机号码以下简称账户名称及您设置的密码账户名称及密码合称账户或通过授权使用您合法拥有的第三方软件或平台用户账号如微信账号QQ账号等登录小兔鲜儿平台但第三方软件或平台对此有限制或禁止的除外当您以前述已有账号登录使用的同样适用本协议中的相关条款
</p>
<p class="p">
您理解并同意除您登录小兔鲜儿平台以外您还可以使用小兔鲜儿账号登录小兔鲜儿及其关联方或其他合作方提供的其他软件服务您以小兔鲜儿账号登录前述服务的同样应受其他软件服务实际提供方的用户协议及其他协议条款约束
</p>
<h4 class="h4">() 实名认证</h4>
<p class="p">
作为小兔鲜儿平台经营者为使您更好地使用小兔鲜儿平台的各项服务保障您的账户安全小兔鲜儿可要求您按照相关法律法规规定完成实名认证
</p>
<h4 class="h4">() 更新维护</h4>
<p class="p">
您应当及时更新您提供的信息在法律有明确规定要求小兔鲜儿作为平台服务提供者必须对部分用户的信息进行核实的情况下小兔鲜儿将依法不时地对您的信息进行检查核实您应当配合提供最新真实完整有效的信息
</p>
<p class="p">
如小兔鲜儿按您最后一次提供的信息与您联系未果您未按小兔鲜儿的要求及时提供信息您提供的信息存在明显不实或行政司法机关核实您提供的信息无效的您将承担因此对您自身他人及小兔鲜儿造成的全部损失与不利后果小兔鲜儿可向您发出询问或要求整改的通知并要求您进行重新认证直至中止终止对您提供部分或全部小兔鲜儿平台服务小兔鲜儿对此不承担责任
</p>
<h4 class="h4">() 账户管理</h4>
<p class="p">
1
由于您的小兔鲜儿平台账户关联您的个人信息及小兔鲜儿平台商业信息您的小兔鲜儿平台账户仅限您本人使用您应对您账户下的所有行为结果负责不得以任何方式转让否则小兔鲜儿平台有权追究您的违约责任且由此产生的责任及后果均由您自行承担您直接或间接授权第三方使用您小兔鲜儿平台账户或获取您账户项下信息的行为后果亦由您自行承担如小兔鲜儿根据小兔鲜儿平台规则中约定的违约认定程序及标准判断您小兔鲜儿平台账户的使用可能危及您的账户安全及/或小兔鲜儿平台信息安全的小兔鲜儿平台可拒绝提供相应服务或终止本协议
</p>
<p class="p">
2
您的账户为您自行设置并由您保管小兔鲜儿任何时候均不会主动要求您提供您的账户密码因此建议您务必保管好您的账户并确保您在每个上网时段结束时退出登录并以正确步骤离开小兔鲜儿平台
</p>
<p class="p">
3
账户因您主动泄露或因您遭受他人攻击诈骗等行为导致的损失及后果小兔鲜儿并不承担责任您应通过司法行政等救济途径向侵权行为人追偿如发现任何未经授权使用您账户登录小兔鲜儿平台或其他可能导致您账户遭窃遗失的情况建议您立即通知小兔鲜儿您理解小兔鲜儿对您的任何请求采取行动均需要合理时间且小兔鲜儿应您请求而采取的行动可能无法避免或阻止侵害后果的形成或扩大除小兔鲜儿存在法定过错外小兔鲜儿不承担责任
</p>
<p class="p">
4
您理解并同意为充分使用账号资源如您在注册后未及时进行初次登录使用或连续12个月未登录任一小兔鲜儿平台且不存在未到期的有效业务的小兔鲜儿有权收回您的账号您可能无法通过您此前持有的账号登录小兔鲜儿平台您该账号下任何个性化设置如头像/昵称即将无法恢复在收回您的账号之前小兔鲜儿将以适当方式做出提示如您在收到相关提示后一定期限内仍未登录使用账号小兔鲜儿将收回账号
</p>
<h4 class="h4">() 账号注销</h4>
<p class="p">
如您需要终止使用小兔鲜儿账号服务符合以下条件的您可以申请注销您的账号1您仅能申请注销您本人的账号并依照小兔鲜儿账号注销须知进行注销2您仍应对您在注销账号前使用小兔鲜儿账号服务期间的行为承担责任特别提示您您申请同意注销账户的视为您放弃账户信息以及该账户在平台的资产虚拟权益等且小兔鲜儿无法为您恢复前述服务这可能对您主张售后服务带来不便
</p>
<p class="p"></p>
<h3 class="h3">小兔鲜儿平台服务</h3>
<p class="p">
您有权在小兔鲜儿平台上享受商品及/或服务的浏览收藏购买与评价配送和交付平台活动交易争议处理信息交流及分享等服务详情您可登录小兔鲜儿平台查看
</p>
<h4 class="h4">() 商品及/或服务的浏览收藏</h4>
<p class="p">
在您浏览我们网站或客户端的过程中小兔鲜儿为您提供了信息分类关键词检索筛选收藏及关注等功能以更好地匹配您的需求您可以对您感兴趣的商品及/或服务进行收藏添加至购物车关注您所感兴趣的店铺/品牌等
</p>
<h4 class="h4">() 商品及/或服务的购买</h4>
<p class="p">
1
当您在小兔鲜儿平台购买商品及/或服务时请您仔细确认所购商品的名称价格数量型号规格尺寸联系地址电话收货人等信息收货人与您本人不一致的收货人的行为和意思表示视为您的行为和意思表示您应对收货人的行为及意思表示的法律后果承担连带责任
</p>
<p class="p">
2
您的购买行为应当符合法律规定为生活消费需要购买使用商品或者接受服务不得存在对商品及/或服务实施恶意购买恶意维权等扰乱小兔鲜儿平台正常交易秩序的行为基于维护小兔鲜儿平台交易秩序及交易安全的需要小兔鲜儿发现上述情形时可主动执行包括但不限于暂停或停止服务取消订单等小兔鲜儿认为有必要的管控措施等操作
</p>
<p class="p">
3 您理解并同意本网站上销售商展示的商品和价格等信息<strong class="strong"
>仅用于教学展示使用</strong
>如果您下单购买商品或服务并完成支付不会产生实际购买商品或服务的合同关系小兔鲜儿平台无需发货该购买行为仅视为教学测试使用
</p>
<h4 class="h4">() 配送和交付</h4>
<p class="p">
您在本网站购买的商品<strong class="strong">仅为教学测试使用</strong
>并未实际产生交易金额因此小兔鲜儿平台不会根据您填写的物流信息进行实际配送服务
</p>
<p class="p"></p>
<h3 class="h3">用户个人信息保护及授权</h3>
<h4 class="h4">
() 您知悉并同意为方便您使用本网站相关服务本网站将存储您在使用时的必要信息
</h4>
<p class="p">
包括但不限于您的真实姓名性别生日配送地址联系方式通讯录相册日历定位信息等除法律法规规定的情形外未经您的许可小兔鲜儿不会向第三方公开透露您的个人信息小兔鲜儿对相关信息采取专业加密存储与传输方式利用合理措施保障用户个人信息的安全如小兔鲜儿平台网站或客户端未设置独立隐私政策但使用了小兔鲜儿平台账号登陆相应网站或客户端的为保护您的隐私权我们将参照适用小兔鲜儿隐私政策的要求对您的个人信息进行收集存储使用披露和保护小兔鲜儿希望通过隐私政策向您清楚地介绍小兔鲜儿对您个人信息的处理方式因此小兔鲜儿建议您完整地阅读隐私政策以帮助您更好地保护您的隐私权
</p>
<h4 class="h4">() 您充分理解并同意</h4>
<p class="p">
1
您理解并同意通过邮件短信电话消息等平台渠道形式接收相关订单信息促销活动商品推荐等内容您有权通过您注册时填写的手机号码或者电子邮箱获取您感兴趣的商品广告信息促销优惠等商业性信息推广或信息包括商业或非商业信息您如果不愿意接收此类信息您有权通过小兔鲜儿提供的相应的退订功能进行退订
</p>
<p class="p">
2
为配合行政监管机关司法机关执行工作在法律规定范围内小兔鲜儿有权向上述行政司法机关提供您在使用本网站时所储存的相关信息包括但不限于您的注册信息等或使用相关信息进行证据保全包括但不限于公证见证等
</p>
<p class="p">
3
小兔鲜儿依法保障您在安装或使用过程中的知情权和选择权在您使用本网站服务过程中涉及您设备自带功能的服务会提前征得您同意您一经确认小兔鲜儿有权开启包括但不限于收集地理位置读取通讯录使用摄像头启用录音等提供服务必要的辅助功能
</p>
<p class="p">
4
小兔鲜儿有权根据实际情况在法律规定范围内自行决定单个用户在本网站及服务中数据的最长储存期限以及用户日志的储存期限并在服务器上为其分配数据最大存储空间等
</p>
<p class="p"></p>
<h3 class="h3">用户行为规范</h3>
<h4 class="h4">() 您同意严格遵守法律法规规章规定依法遵守以下义务</h4>
<p class="p">
1
不得制作传输或发表以下违法信息资料反对宪法所确定的基本原则煽动抗拒破坏宪法和法律法规实施的危害国家安全泄露国家秘密颠覆国家政权破坏国家统一的煽动推翻社会主义制度的损害国家荣誉和利益的歪曲丑化亵渎否定英雄烈士事迹和精神侵害英雄烈士的姓名肖像名誉荣誉的宣扬或煽动实施恐怖主义极端主义及其活动的煽动民族仇恨民族歧视破坏民族团结的言论破坏国家宗教政策宣扬邪教和封建迷信的散布谣言扰乱经济秩序和社会秩序的散布淫秽色情暴力或者教唆犯罪的侮辱或者诽谤他人侵害他人名誉隐私和其他合法权益的法律行政法规禁止的其他内容
</p>
<p class="p">
2
防范和抵制制作复制发布含有下列内容的不良信息资料标题严重夸张发表内容与标题严重不符的不当评述自然灾害重大事故等灾难的煽动人群歧视地域歧视等的宣扬低俗庸俗媚俗内容的违反社会公德行为的侵犯未成年人合法权益的其他对网络生态造成不良影响的内容
</p>
<h4 class="h4">() 本协议依据国家相关法律法规规章制定您亦同意严格遵守以下义务</h4>
<p class="p">1 从中国大陆向境外传输资料信息时必须符合中国有关法规</p>
<p class="p">2 不得利用本网站从事洗钱窃取商业秘密窃取个人信息等违法犯罪活动</p>
<p class="p">
3
不得企图干扰破坏小兔鲜儿系统或网站的正常运转故意传播恶意程序或病毒以及其他破坏干扰正常网络信息服务的行为
</p>
<p class="p">
4
不得传输或发表任何违法犯罪的骚扰性的中伤他人的辱骂性的恐吓性的伤害性的庸俗的不文明的等信息资料
</p>
<p class="p">5 不得教唆他人从事违法违规或本协议平台规则所禁止的行为</p>
<p class="p">
6
不得利用在本网站注册的账户进行不当利益获取恶意攻击违法经营活动传播不良内容等违反法律法规的行为
</p>
<p class="p">7 不得发布任何侵犯他人个人信息著作权商标权等知识产权或合法权利的内容</p>
<p class="p">
8
不得冒充他人或利用他人的名义使用小兔鲜儿软件服务或传播任何信息恶意使用注册账号导致其他用户误认的
</p>
<p class="p">
9
不得存在可能破坏篡改删除影响小兔鲜儿平台任何系统正常运行或未经授权秘密获取小兔鲜儿平台及其他用户的数据个人资料的病毒木马爬虫等恶意软件程序代码的
</p>
<p class="p">10不得恶意注册小兔鲜儿账号包括但不限于频繁批量注册账号注销账号</p>
<p class="p">11不得发布其他对网络生态造成不良影响的内容及法律行政法规禁止的其他内容</p>
<h4 class="h4">()您须对自己在网上的言论和行为承担法律责任</h4>
<p class="p">
您若在本网站上散布和传播反动色情或其它违反国家法律的信息本网站的系统记录有可能作为您违反法律的证据小兔鲜儿可对您发布的信息依法或依本协议进行删除或屏蔽
</p>
<h4 class="h4">() 除非法律允许或小兔鲜儿书面许可您使用本网站过程中不得从事下列行为</h4>
<p class="p">1 删除本网站及其副本上关于著作权的信息</p>
<p class="p">
2 对本网站进行反向工程反向汇编反向编译或者以其他方式尝试发现本网站的源代码
</p>
<p class="p">
3
对小兔鲜儿拥有知识产权的内容进行使用出租出借复制修改链接转载汇编发表出版建立镜像站点等
</p>
<p class="p">
4
对本网站或者本网站运行过程中释放到任何终端内存中的数据网站运行过程中客户端与服务器端的交互数据以及本网站运行所必需的系统数据进行复制修改增加删除挂接运行或创作任何衍生作品形式包括但不限于使用插件外挂或非经小兔鲜儿授权的第三方工具/服务接入本网站和相关系统
</p>
<p class="p">
5
通过修改或伪造网站运行中的指令数据增加删减变动网站的功能或运行效果或者将用于上述用途的软件方法进行运营或向公众传播无论这些行为是否为商业目的
</p>
<p class="p">
6
通过非小兔鲜儿开发授权的第三方软件插件外挂系统登录或使用本网站及服务或制作发布传播上述工具
</p>
<p class="p">7 自行或者授权他人第三方软件对本网站及其组件模块数据进行干扰</p>
<h3 class="h3">不可抗力或其他免责事由</h3>
<h4 class="h4">
()
您理解并同意在使用本服务的过程中可能会遇到不可抗力等风险因素使本服务协议下的服务发生中断或终止
</h4>
<p class="p">
不可抗力是指不能预见不能克服并不能避免且对一方或双方造成重大影响的客观事件包括但不限于信息网络设备维护信息网络连接故障电脑通讯或其他系统的故障电力故障罢工劳动争议暴乱起义骚乱生产力或生产资料不足火灾洪水风暴爆炸战争政府行为法律法规变动司法行政机关的命令其他不可抗力或第三方的不作为而造成的不能服务或延迟服务等行为出现上述情况时小兔鲜儿将努力在第一时间与相关部门配合及时进行修复但是由此给您造成的损失小兔鲜儿在法律允许的范围内免责
</p>
<h4 class="h4">
() 您理解并同意在法律允许的范围内小兔鲜儿对以下事由所导致的服务中断或终止不承担责任
</h4>
<p class="p">1 受到计算机病毒木马或其他恶意程序黑客攻击的破坏</p>
<p class="p">2 用户或小兔鲜儿的电脑软件系统硬件和通信线路出现故障</p>
<p class="p">3 用户操作不当</p>
<p class="p">4 用户通过非小兔鲜儿授权的方式使用本服务</p>
<p class="p">5 其他小兔鲜儿无法控制或合理预见的情形</p>
</article>
</template>
<style lang="scss" scoped>
.article {
padding: 1em;
}
.h1,
.h2,
.h3,
.h4,
.h5,
.h6 {
font-weight: bold;
margin: 0.5em 0;
}
.h1 {
font-size: 1.3em;
}
.h2 {
font-size: 1.2em;
}
.h3 {
font-size: 1.1em;
}
.p {
text-indent: 2em;
line-height: 1.5;
}
.strong {
font-weight: bold;
display: inline;
text-decoration: underline;
}
.tac {
text-align: center;
text-indent: 0;
}
</style>

@ -0,0 +1,232 @@
<script setup lang="ts">
import { useGuessList } from '@/composables'
import { useMemberStore } from '@/stores'
//
const { safeAreaInsets } = uni.getSystemInfoSync()
//
const orderTypes = [
{ type: '1', text: '待付款', icon: 'icon-currency' },
{ type: '2', text: '待发货', icon: 'icon-gift' },
{ type: '3', text: '待收货', icon: 'icon-check' },
{ type: '4', text: '待评价', icon: 'icon-comment' },
]
//
const memberStore = useMemberStore()
const { guessRef, onScrolltolower } = useGuessList()
</script>
<template>
<scroll-view enable-back-to-top @scrolltolower="onScrolltolower" class="viewport" scroll-y>
<!-- 个人资料 -->
<view class="profile" :style="{ paddingTop: safeAreaInsets!.top + 'px' }">
<!-- 情况1已登录 -->
<view class="overview" v-if="memberStore.profile">
<navigator url="/pagesMember/profile/profile" hover-class="none">
<image class="avatar" :src="memberStore.profile.avatar" mode="aspectFill"></image>
</navigator>
<view class="meta">
<view class="nickname">
{{ memberStore.profile.nickname || memberStore.profile.account }}
</view>
<navigator class="extra" url="/pagesMember/profile/profile" hover-class="none">
<text class="update">更新头像昵称</text>
</navigator>
</view>
</view>
<!-- 情况2未登录 -->
<view class="overview" v-else>
<navigator url="/pages/login/login" hover-class="none">
<image
class="avatar gray"
mode="aspectFill"
src="https://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-04-06/db628d42-88a7-46e7-abb8-659448c33081.png"
></image>
</navigator>
<view class="meta">
<navigator url="/pages/login/login" hover-class="none" class="nickname">
未登录
</navigator>
<view class="extra">
<text class="tips">点击登录账号</text>
</view>
</view>
</view>
<navigator class="settings" url="/pagesMember/settings/settings" hover-class="none">
设置
</navigator>
</view>
<!-- 我的订单 -->
<view class="orders">
<view class="title">
我的订单
<navigator class="navigator" url="/pagesOrder/list/list?type=0" hover-class="none">
查看全部订单<text class="icon-right"></text>
</navigator>
</view>
<view class="section">
<!-- 订单 -->
<navigator
v-for="item in orderTypes"
:key="item.type"
:class="item.icon"
:url="`/pagesOrder/list/list?type=${item.type}`"
class="navigator"
hover-class="none"
>
{{ item.text }}
</navigator>
<!-- 客服 -->
<!-- #ifdef MP-WEIXIN -->
<button class="contact icon-handset" open-type="contact">售后</button>
<!-- #endif -->
</view>
</view>
<!-- 猜你喜欢 -->
<view class="guess">
<XtxGuess ref="guessRef" />
</view>
</scroll-view>
</template>
<style lang="scss">
page {
height: 100%;
overflow: hidden;
background-color: #f7f7f8;
}
.viewport {
height: 100%;
background-repeat: no-repeat;
background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/center_bg.png);
background-size: 100% auto;
}
/* 用户信息 */
.profile {
margin-top: 30rpx;
position: relative;
.overview {
display: flex;
height: 120rpx;
padding: 0 36rpx;
color: #fff;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background-color: #eee;
}
.gray {
filter: grayscale(100%);
}
.meta {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
line-height: 30rpx;
padding: 16rpx 0;
margin-left: 20rpx;
}
.nickname {
max-width: 180rpx;
margin-bottom: 16rpx;
font-size: 30rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.extra {
display: flex;
font-size: 20rpx;
}
.tips {
font-size: 22rpx;
}
.update {
padding: 3rpx 10rpx 1rpx;
color: rgba(255, 255, 255, 0.8);
border: 1rpx solid rgba(255, 255, 255, 0.8);
margin-right: 10rpx;
border-radius: 30rpx;
}
.settings {
position: absolute;
bottom: 0;
right: 40rpx;
font-size: 30rpx;
color: #fff;
}
}
/* 我的订单 */
.orders {
position: relative;
z-index: 99;
padding: 30rpx;
margin: 50rpx 20rpx 0;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 4rpx 6rpx rgba(240, 240, 240, 0.6);
.title {
height: 40rpx;
line-height: 40rpx;
font-size: 28rpx;
color: #1e1e1e;
.navigator {
font-size: 24rpx;
color: #939393;
float: right;
}
}
.section {
width: 100%;
display: flex;
justify-content: space-between;
padding: 40rpx 20rpx 10rpx;
.navigator,
.contact {
text-align: center;
font-size: 24rpx;
color: #333;
&::before {
display: block;
font-size: 60rpx;
color: #ff9545;
}
&::after {
border: none;
}
}
.contact {
padding: 0;
margin: 0;
border: 0;
background-color: transparent;
line-height: inherit;
}
}
}
/* 猜你喜欢 */
.guess {
background-color: #f7f7f8;
margin-top: 20rpx;
}
</style>

@ -0,0 +1,263 @@
<script setup lang="ts">
import {
getMemberAddressByIdAPI,
postMemberAddressAPI,
putMemberAddressByIdAPI,
} from '@/services/address'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
//
const form = ref({
receiver: '', //
contact: '', //
fullLocation: '', // ()
provinceCode: '', // ()
cityCode: '', // ()
countyCode: '', // /()
address: '', //
isDefault: 0, // 10
})
//
const query = defineProps<{
id?: string
}>()
//
const getMemberAddressByIdData = async () => {
if (query.id) {
//
const res = await getMemberAddressByIdAPI(query.id)
//
Object.assign(form.value, res.result)
}
}
//
onLoad(() => {
getMemberAddressByIdData()
})
//
uni.setNavigationBarTitle({ title: query.id ? '修改地址' : '新建地址' })
//
const onRegionChange: UniHelper.RegionPickerOnChange = (ev) => {
// ()
form.value.fullLocation = ev.detail.value.join(' ')
// ()
const [provinceCode, cityCode, countyCode] = ev.detail.code!
// form.value.provinceCode = provinceCode
Object.assign(form.value, { provinceCode, cityCode, countyCode })
}
//
const onSwitchChange: UniHelper.SwitchOnChange = (ev) => {
form.value.isDefault = ev.detail.value ? 1 : 0
}
//
const rules: UniHelper.UniFormsRules = {
receiver: {
rules: [{ required: true, errorMessage: '请输入收货人姓名' }],
},
contact: {
rules: [
{ required: true, errorMessage: '请输入联系方式' },
{ pattern: /^1[3-9]\d{9}$/, errorMessage: '手机号格式不正确' },
],
},
countyCode: {
rules: [{ required: true, errorMessage: '请选择所在地区' }],
},
address: {
rules: [{ required: true, errorMessage: '请选择详细地址' }],
},
}
//
const formRef = ref<UniHelper.UniFormsInstance>()
//
const onSubmit = async () => {
try {
//
await formRef.value?.validate?.()
//
if (query.id) {
//
await putMemberAddressByIdAPI(query.id, form.value)
} else {
//
await postMemberAddressAPI(form.value)
}
//
uni.showToast({ icon: 'success', title: query.id ? '修改成功' : '添加成功' })
//
setTimeout(() => {
uni.navigateBack()
}, 400)
} catch (error) {
uni.showToast({ icon: 'error', title: '请填写完整信息' })
}
}
// #ifdef H5 || APP-PLUS
const onCityChange: UniHelper.UniDataPickerOnChange = (ev) => {
//
const [province, city, county] = ev.detail.value
// code
Object.assign(form.value, {
provinceCode: province.value,
cityCode: city.value,
countyCode: county.value,
})
}
// #endif
</script>
<template>
<view class="content">
<uni-forms :rules="rules" :model="form" ref="formRef">
<!-- 表单内容 -->
<uni-forms-item name="receiver" class="form-item">
<text class="label">收货人</text>
<input class="input" placeholder="请填写收货人姓名" v-model="form.receiver" />
</uni-forms-item>
<uni-forms-item name="contact" class="form-item">
<text class="label">手机号码</text>
<input
class="input"
placeholder="请填写收货人手机号码"
:maxlength="11"
v-model="form.contact"
/>
</uni-forms-item>
<uni-forms-item name="countyCode" class="form-item">
<text class="label">所在地区</text>
<!-- #ifdef MP-WEIXIN -->
<picker
@change="onRegionChange"
class="picker"
mode="region"
:value="form.fullLocation.split(' ')"
>
<view v-if="form.fullLocation">{{ form.fullLocation }}</view>
<view v-else class="placeholder">请选择省//()</view>
</picker>
<!-- #endif -->
<!-- #ifdef H5 || APP-PLUS -->
<uni-data-picker
placeholder="请选择地址"
popup-title="请选择城市"
collection="opendb-city-china"
field="code as value, name as text"
orderby="value asc"
:step-searh="true"
self-field="code"
parent-field="parent_code"
@change="onCityChange"
:clear-icon="false"
v-model="form.countyCode"
/>
<!-- #endif -->
</uni-forms-item>
<uni-forms-item name="address" class="form-item">
<text class="label">详细地址</text>
<input class="input" placeholder="街道、楼牌号等信息" v-model="form.address" />
</uni-forms-item>
<view class="form-item">
<label class="label">设为默认地址</label>
<switch
@change="onSwitchChange"
class="switch"
color="#27ba9b"
:checked="form.isDefault === 1"
/>
</view>
</uni-forms>
</view>
<!-- 提交按钮 -->
<button @tap="onSubmit" class="button">保存并使用</button>
</template>
<style lang="scss">
// uni-data-picker
:deep(.selected-area) {
flex: 0 1 auto;
height: auto;
}
page {
background-color: #f4f4f4;
}
.content {
margin: 20rpx 20rpx 0;
padding: 0 20rpx;
border-radius: 10rpx;
background-color: #fff;
.form-item,
.uni-forms-item {
display: flex;
align-items: center;
min-height: 96rpx;
padding: 25rpx 10rpx;
background-color: #fff;
font-size: 28rpx;
border-bottom: 1rpx solid #ddd;
position: relative;
margin-bottom: 0;
// uni-forms
.uni-forms-item__content {
display: flex;
}
.uni-forms-item__error {
margin-left: 200rpx;
}
&:last-child {
border: none;
}
.label {
width: 200rpx;
color: #333;
}
.input {
flex: 1;
display: block;
height: 46rpx;
}
.switch {
position: absolute;
right: -20rpx;
transform: scale(0.8);
}
.picker {
flex: 1;
}
.placeholder {
color: #808080;
}
}
}
.button {
height: 80rpx;
margin: 30rpx 20rpx;
color: #fff;
border-radius: 80rpx;
font-size: 30rpx;
background-color: #27ba9b;
}
</style>

@ -0,0 +1,194 @@
<script setup lang="ts">
import { deleteMemberAddressByIdAPI, getMemberAddressAPI } from '@/services/address'
import { useAddressStore } from '@/stores/modules/address'
import type { AddressItem } from '@/types/address'
import { onShow } from '@dcloudio/uni-app'
import { ref } from 'vue'
//
const addressList = ref<AddressItem[]>([])
const getMemberAddressData = async () => {
const res = await getMemberAddressAPI()
addressList.value = res.result
}
// ()
onShow(() => {
getMemberAddressData()
})
//
const onDeleteAddress = (id: string) => {
//
uni.showModal({
content: '删除地址?',
confirmColor: '#27BA9B',
success: async (res) => {
if (res.confirm) {
// id
await deleteMemberAddressByIdAPI(id)
//
getMemberAddressData()
}
},
})
}
//
const onChangeAddress = (item: AddressItem) => {
//
const addressStore = useAddressStore()
addressStore.changeSelectedAddress(item)
//
uni.navigateBack()
}
</script>
<template>
<view class="viewport">
<!-- 地址列表 -->
<scroll-view enable-back-to-top class="scroll-view" scroll-y>
<view v-if="addressList.length" class="address">
<uni-swipe-action class="address-list">
<!-- 收货地址项 -->
<uni-swipe-action-item class="item" v-for="item in addressList" :key="item.id">
<view class="item-content" @tap="onChangeAddress(item)">
<view class="user">
{{ item.receiver }}
<text class="contact">{{ item.contact }}</text>
<text v-if="item.isDefault" class="badge"></text>
</view>
<view class="locate">{{ item.fullLocation }} {{ item.address }}</view>
<!-- H5 端需添加 .prevent 阻止链接的默认行为 -->
<navigator
class="edit"
hover-class="none"
:url="`/pagesMember/address-form/address-form?id=${item.id}`"
@tap.stop="() => {}"
@tap.prevent="() => {}"
>
修改
</navigator>
</view>
<!-- 右侧插槽 -->
<template #right>
<button @tap="onDeleteAddress(item.id)" class="delete-button">删除</button>
</template>
</uni-swipe-action-item>
</uni-swipe-action>
</view>
<view v-else class="blank">暂无收货地址</view>
</scroll-view>
<!-- 添加按钮 -->
<view class="add-btn">
<navigator hover-class="none" url="/pagesMember/address-form/address-form">
新建地址
</navigator>
</view>
</view>
</template>
<style lang="scss">
page {
height: 100%;
overflow: hidden;
}
/* 删除按钮 */
.delete-button {
display: flex;
justify-content: center;
align-items: center;
width: 50px;
height: 100%;
font-size: 28rpx;
color: #fff;
border-radius: 0;
padding: 0;
background-color: #cf4444;
}
.viewport {
display: flex;
flex-direction: column;
height: 100%;
background-color: #f4f4f4;
.scroll-view {
padding-top: 20rpx;
}
}
.address {
padding: 0 20rpx;
margin: 0 20rpx;
border-radius: 10rpx;
background-color: #fff;
.item-content {
line-height: 1;
padding: 40rpx 10rpx 38rpx;
border-bottom: 1rpx solid #ddd;
position: relative;
.edit {
position: absolute;
top: 36rpx;
right: 30rpx;
padding: 2rpx 0 2rpx 20rpx;
border-left: 1rpx solid #666;
font-size: 26rpx;
color: #666;
line-height: 1;
}
}
.item:last-child .item-content {
border: none;
}
.user {
font-size: 28rpx;
margin-bottom: 20rpx;
color: #333;
.contact {
color: #666;
}
.badge {
display: inline-block;
padding: 4rpx 10rpx 2rpx 14rpx;
margin: 2rpx 0 0 10rpx;
font-size: 26rpx;
color: #27ba9b;
border-radius: 6rpx;
border: 1rpx solid #27ba9b;
}
}
.locate {
line-height: 1.6;
font-size: 26rpx;
color: #333;
}
}
.blank {
margin-top: 300rpx;
text-align: center;
font-size: 32rpx;
color: #888;
}
.add-btn {
height: 80rpx;
text-align: center;
line-height: 80rpx;
margin: 30rpx 20rpx;
color: #fff;
border-radius: 80rpx;
font-size: 30rpx;
background-color: #27ba9b;
}
</style>

@ -0,0 +1,331 @@
<script setup lang="ts">
import { getMemberProfileAPI, putMemberProfileAPI } from '@/services/profile'
import { useMemberStore } from '@/stores'
import type { Gender, ProfileDetail } from '@/types/member'
import { formatDate } from '@/utils'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
//
const { safeAreaInsets } = uni.getSystemInfoSync()
//
const profile = ref({} as ProfileDetail)
const getMemberProfileData = async () => {
const res = await getMemberProfileAPI()
profile.value = res.result
// Store
memberStore.profile!.avatar = res.result.avatar
memberStore.profile!.nickname = res.result.nickname
}
onLoad(() => {
getMemberProfileData()
})
const memberStore = useMemberStore()
//
const onAvatarChange = () => {
// /
//
// #ifdef H5 || APP-PLUS
// 2.21.0 wx.chooseImage 使 uni.chooseMedia
uni.chooseImage({
count: 1,
success: (res) => {
//
const tempFilePaths = res.tempFilePaths
//
uploadFile(tempFilePaths[0])
},
})
// #endif
// #ifdef MP-WEIXIN
// uni.chooseMedia
uni.chooseMedia({
//
count: 1,
//
mediaType: ['image'],
success: (res) => {
//
const { tempFilePath } = res.tempFiles[0]
//
uploadFile(tempFilePath)
},
})
// #endif
}
// -H5App
const uploadFile = (file: string) => {
//
uni.uploadFile({
url: '/member/profile/avatar',
name: 'file',
filePath: file,
success: (res) => {
if (res.statusCode === 200) {
const avatar = JSON.parse(res.data).result.avatar
//
profile.value.avatar = avatar
// Store
memberStore.profile!.avatar = avatar
uni.showToast({ icon: 'success', title: '更新成功' })
} else {
uni.showToast({ icon: 'error', title: '出现错误' })
}
},
})
}
//
const onGenderChange: UniHelper.RadioGroupOnChange = (ev) => {
profile.value.gender = ev.detail.value as Gender
}
//
const onBirthdayChange: UniHelper.DatePickerOnChange = (ev) => {
profile.value.birthday = ev.detail.value
}
//
let fullLocationCode: [string, string, string] = ['', '', '']
const onFullLocationChange: UniHelper.RegionPickerOnChange = (ev) => {
//
profile.value.fullLocation = ev.detail.value.join(' ')
//
fullLocationCode = ev.detail.code!
}
//
const onSubmit = async () => {
const { nickname, gender, birthday, profession } = profile.value
const res = await putMemberProfileAPI({
nickname,
gender,
birthday,
profession,
provinceCode: fullLocationCode[0] || undefined,
cityCode: fullLocationCode[1] || undefined,
countyCode: fullLocationCode[2] || undefined,
})
// Store
memberStore.profile!.nickname = res.result.nickname
uni.showToast({ icon: 'success', title: '保存成功' })
setTimeout(() => {
uni.navigateBack()
}, 400)
}
</script>
<template>
<view class="viewport">
<!-- 导航栏 -->
<view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
<navigator open-type="navigateBack" class="back icon-left" hover-class="none"></navigator>
<view class="title">个人信息</view>
</view>
<!-- 头像 -->
<view class="avatar">
<view @tap="onAvatarChange" class="avatar-content">
<image class="image" :src="profile?.avatar" mode="aspectFill" />
<text class="text">点击修改头像</text>
</view>
</view>
<!-- 表单 -->
<view class="form">
<!-- 表单内容 -->
<view class="form-content">
<view class="form-item">
<text class="label">账号</text>
<text class="account placeholder">{{ profile?.account }}</text>
</view>
<view class="form-item">
<text class="label">昵称</text>
<input class="input" type="text" placeholder="请填写昵称" v-model="profile.nickname" />
</view>
<view class="form-item">
<text class="label">性别</text>
<radio-group @change="onGenderChange">
<label class="radio">
<radio value="男" color="#27ba9b" :checked="profile?.gender === '男'" />
</label>
<label class="radio">
<radio value="女" color="#27ba9b" :checked="profile?.gender === '女'" />
</label>
</radio-group>
</view>
<view class="form-item">
<text class="label">生日</text>
<picker
@change="onBirthdayChange"
mode="date"
class="picker"
:value="profile?.birthday"
start="1900-01-01"
:end="formatDate(new Date())"
>
<view v-if="profile?.birthday">{{ profile?.birthday }}</view>
<view class="placeholder" v-else></view>
</picker>
</view>
<!-- 只有微信小程序端内置了省市区数据 -->
<!-- #ifdef MP-WEIXIN -->
<view class="form-item">
<text class="label">城市</text>
<picker
@change="onFullLocationChange"
mode="region"
class="picker"
:value="profile?.fullLocation?.split(' ')"
>
<view v-if="profile?.fullLocation">{{ profile.fullLocation }}</view>
<view class="placeholder" v-else></view>
</picker>
</view>
<!-- #endif -->
<view class="form-item">
<text class="label">职业</text>
<input class="input" type="text" placeholder="请填写职业" v-model="profile.profession" />
</view>
</view>
<!-- 提交按钮 -->
<button @tap="onSubmit" class="form-button"> </button>
</view>
</view>
</template>
<style lang="scss">
page {
background-color: #f4f4f4;
}
.viewport {
display: flex;
flex-direction: column;
height: 100%;
background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/order_bg.png);
background-size: auto 420rpx;
background-repeat: no-repeat;
}
//
.navbar {
position: relative;
.title {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
font-weight: 500;
color: #fff;
}
.back {
position: absolute;
height: 40px;
width: 40px;
left: 0;
font-size: 20px;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
}
}
//
.avatar {
text-align: center;
width: 100%;
height: 260rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.image {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background-color: #eee;
}
.text {
display: block;
padding-top: 20rpx;
line-height: 1;
font-size: 26rpx;
color: #fff;
}
}
//
.form {
background-color: #f4f4f4;
&-content {
margin: 20rpx 20rpx 0;
padding: 0 20rpx;
border-radius: 10rpx;
background-color: #fff;
}
&-item {
display: flex;
height: 96rpx;
line-height: 46rpx;
padding: 25rpx 10rpx;
background-color: #fff;
font-size: 28rpx;
border-bottom: 1rpx solid #ddd;
&:last-child {
border: none;
}
.label {
width: 180rpx;
color: #333;
}
.account {
color: #666;
}
.input {
flex: 1;
display: block;
height: 46rpx;
}
.radio {
margin-right: 20rpx;
}
.picker {
flex: 1;
}
.placeholder {
color: #808080;
}
}
&-button {
height: 80rpx;
text-align: center;
line-height: 80rpx;
margin: 30rpx 20rpx;
color: #fff;
border-radius: 80rpx;
font-size: 30rpx;
background-color: #27ba9b;
}
}
</style>

@ -0,0 +1,108 @@
<script setup lang="ts">
import { useMemberStore } from '@/stores'
const memberStore = useMemberStore()
// 退
const onLogout = () => {
//
uni.showModal({
content: '是否退出登录?',
confirmColor: '#27BA9B',
success: res => {
if (res.confirm) {
//
memberStore.clearProfile()
//
uni.navigateBack()
}
},
})
}
</script>
<template>
<view class="viewport">
<!-- 列表1 -->
<view class="list" v-if="memberStore.profile">
<navigator url="/pagesMember/address/address" hover-class="none" class="item arrow">
我的收货地址
</navigator>
</view>
<!-- #ifdef MP-WEIXIN -->
<!-- 列表2 -->
<view class="list">
<button hover-class="none" class="item arrow" open-type="openSetting">授权管理</button>
<button hover-class="none" class="item arrow" open-type="feedback">问题反馈</button>
<button hover-class="none" class="item arrow" open-type="contact">联系我们</button>
</view>
<!-- #endif -->
<!-- 操作按钮 -->
<view class="action" v-if="memberStore.profile">
<view @tap="onLogout" class="button">退出登录</view>
</view>
</view>
</template>
<style lang="scss">
page {
background-color: #f4f4f4;
}
.viewport {
padding: 20rpx;
}
/* 列表 */
.list {
padding: 0 20rpx;
background-color: #fff;
margin-bottom: 20rpx;
border-radius: 10rpx;
.item {
line-height: 90rpx;
padding-left: 10rpx;
font-size: 30rpx;
color: #333;
border-top: 1rpx solid #ddd;
position: relative;
text-align: left;
border-radius: 0;
background-color: #fff;
&::after {
width: auto;
height: auto;
left: auto;
border: none;
}
&:first-child {
border: none;
}
&::after {
right: 5rpx;
}
}
.arrow::after {
content: '\e6c2';
position: absolute;
top: 50%;
color: #ccc;
font-family: 'erabbit' !important;
font-size: 32rpx;
transform: translateY(-50%);
}
}
/* 操作按钮 */
.action {
text-align: center;
line-height: 90rpx;
margin-top: 40rpx;
font-size: 32rpx;
color: #333;
.button {
background-color: #fff;
margin-bottom: 20rpx;
border-radius: 10rpx;
}
}
</style>

@ -0,0 +1,392 @@
<script setup lang="ts">
import {
getMemberOrderPreAPI,
getMemberOrderPreNowAPI,
getMemberOrderRepurchaseByIdAPI,
postMemberOrderAPI,
} from '@/services/order'
import { useAddressStore } from '@/stores/modules/address'
import type { OrderPreResult } from '@/types/order'
import { onLoad } from '@dcloudio/uni-app'
import { computed, ref } from 'vue'
//
const { safeAreaInsets } = uni.getSystemInfoSync()
//
const buyerMessage = ref('')
//
const deliveryList = ref([
{ type: 1, text: '时间不限 (周一至周日)' },
{ type: 2, text: '工作日送 (周一至周五)' },
{ type: 3, text: '周末配送 (周六至周日)' },
])
//
const activeIndex = ref(0)
//
const activeDelivery = computed(() => deliveryList.value[activeIndex.value])
//
const onChangeDelivery: UniHelper.SelectorPickerOnChange = (ev) => {
activeIndex.value = ev.detail.value
}
//
const query = defineProps<{
skuId?: string
count?: string
orderId?: string
}>()
//
const orderPre = ref<OrderPreResult>()
const getMemberOrderPreData = async () => {
if (query.count && query.skuId) {
const res = await getMemberOrderPreNowAPI({
count: query.count,
skuId: query.skuId,
})
orderPre.value = res.result
} else if (query.orderId) {
//
const res = await getMemberOrderRepurchaseByIdAPI(query.orderId)
orderPre.value = res.result
} else {
const res = await getMemberOrderPreAPI()
orderPre.value = res.result
}
}
onLoad(() => {
getMemberOrderPreData()
})
const addressStore = useAddressStore()
//
const selecteAddress = computed(() => {
return addressStore.selectedAddress || orderPre.value?.userAddresses.find((v) => v.isDefault)
})
//
const onOrderSubmit = async () => {
//
if (!selecteAddress.value?.id) {
return uni.showToast({ icon: 'none', title: '请选择收货地址' })
}
//
const res = await postMemberOrderAPI({
addressId: selecteAddress.value?.id,
buyerMessage: buyerMessage.value,
deliveryTimeType: activeDelivery.value.type,
goods: orderPre.value!.goods.map((v) => ({ count: v.count, skuId: v.skuId })),
payChannel: 2,
payType: 1,
})
// id
uni.redirectTo({ url: `/pagesOrder/detail/detail?id=${res.result.id}` })
}
</script>
<template>
<scroll-view enable-back-to-top scroll-y class="viewport">
<!-- 收货地址 -->
<navigator
v-if="selecteAddress"
class="shipment"
hover-class="none"
url="/pagesMember/address/address?from=order"
>
<view class="user"> {{ selecteAddress.receiver }} {{ selecteAddress.contact }} </view>
<view class="address"> {{ selecteAddress.fullLocation }} {{ selecteAddress.address }} </view>
<text class="icon icon-right"></text>
</navigator>
<navigator
v-else
class="shipment"
hover-class="none"
url="/pagesMember/address/address?from=order"
>
<view class="address"> 请选择收货地址 </view>
<text class="icon icon-right"></text>
</navigator>
<!-- 商品信息 -->
<view class="goods">
<navigator
v-for="item in orderPre?.goods"
:key="item.skuId"
:url="`/pages/goods/goods?id=${item.id}`"
class="item"
hover-class="none"
>
<image class="picture" :src="item.picture" />
<view class="meta">
<view class="name ellipsis"> {{ item.name }} </view>
<view class="attrs">{{ item.attrsText }}</view>
<view class="prices">
<view class="pay-price symbol">{{ item.payPrice }}</view>
<view class="price symbol">{{ item.price }}</view>
</view>
<view class="count">x{{ item.count }}</view>
</view>
</navigator>
</view>
<!-- 配送及支付方式 -->
<view class="related">
<view class="item">
<text class="text">配送时间</text>
<picker :range="deliveryList" range-key="text" @change="onChangeDelivery">
<view class="icon-fonts picker">{{ activeDelivery.text }}</view>
</picker>
</view>
<view class="item">
<text class="text">订单备注</text>
<input
class="input"
:cursor-spacing="30"
placeholder="选题,建议留言前先与商家沟通确认"
v-model="buyerMessage"
/>
</view>
</view>
<!-- 支付金额 -->
<view class="settlement">
<view class="item">
<text class="text">商品总价: </text>
<text class="number symbol">{{ orderPre?.summary.totalPrice.toFixed(2) }}</text>
</view>
<view class="item">
<text class="text">运费: </text>
<text class="number symbol">{{ orderPre?.summary.postFee.toFixed(2) }}</text>
</view>
</view>
</scroll-view>
<!-- 吸底工具栏 -->
<view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
<view class="total-pay symbol">
<text class="number">{{ orderPre?.summary.totalPayPrice.toFixed(2) }}</text>
</view>
<view class="button" :class="{ disabled: !selecteAddress?.id }" @tap="onOrderSubmit">
提交订单
</view>
</view>
</template>
<style lang="scss">
page {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background-color: #f4f4f4;
}
.symbol::before {
content: '¥';
font-size: 80%;
margin-right: 5rpx;
}
.shipment {
margin: 20rpx;
padding: 30rpx 30rpx 30rpx 84rpx;
font-size: 26rpx;
border-radius: 10rpx;
background: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/locate.png)
20rpx center / 50rpx no-repeat #fff;
position: relative;
.icon {
font-size: 36rpx;
color: #333;
transform: translateY(-50%);
position: absolute;
top: 50%;
right: 20rpx;
}
.user {
color: #333;
margin-bottom: 5rpx;
}
.address {
color: #666;
}
}
.goods {
margin: 20rpx;
padding: 0 20rpx;
border-radius: 10rpx;
background-color: #fff;
.item {
display: flex;
padding: 30rpx 0;
border-top: 1rpx solid #eee;
&:first-child {
border-top: none;
}
.picture {
width: 170rpx;
height: 170rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.meta {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
}
.name {
height: 80rpx;
font-size: 26rpx;
color: #444;
}
.attrs {
line-height: 1.8;
padding: 0 15rpx;
margin-top: 6rpx;
font-size: 24rpx;
align-self: flex-start;
border-radius: 4rpx;
color: #888;
background-color: #f7f7f8;
}
.prices {
display: flex;
align-items: baseline;
margin-top: 6rpx;
font-size: 28rpx;
.pay-price {
margin-right: 10rpx;
color: #cf4444;
}
.price {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
}
}
.count {
position: absolute;
bottom: 0;
right: 0;
font-size: 26rpx;
color: #444;
}
}
}
.related {
margin: 20rpx;
padding: 0 20rpx;
border-radius: 10rpx;
background-color: #fff;
.item {
display: flex;
justify-content: space-between;
align-items: center;
min-height: 80rpx;
font-size: 26rpx;
color: #333;
}
.input {
flex: 1;
text-align: right;
margin: 20rpx 0;
padding-right: 20rpx;
font-size: 26rpx;
color: #999;
}
.item .text {
width: 125rpx;
}
.picker {
color: #666;
}
.picker::after {
content: '\e6c2';
}
}
/* 结算清单 */
.settlement {
margin: 20rpx;
padding: 0 20rpx;
border-radius: 10rpx;
background-color: #fff;
.item {
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
font-size: 26rpx;
color: #333;
}
.danger {
color: #cf4444;
}
}
/* 吸底工具栏 */
.toolbar {
position: fixed;
left: 0;
right: 0;
bottom: calc(var(--window-bottom));
z-index: 1;
background-color: #fff;
height: 100rpx;
padding: 0 20rpx;
border-top: 1rpx solid #eaeaea;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: content-box;
.total-pay {
font-size: 40rpx;
color: #cf4444;
.decimal {
font-size: 75%;
}
}
.button {
width: 220rpx;
text-align: center;
line-height: 72rpx;
font-size: 26rpx;
color: #fff;
border-radius: 72rpx;
background-color: #27ba9b;
}
.disabled {
opacity: 0.6;
}
}
</style>

@ -0,0 +1,430 @@
<template name="skeleton">
<view class="sk-container">
<scroll-view
:scroll-y="true"
class="viewport sk-transparent"
id="scroller"
:enable-back-to-top="true"
>
<view class="overview sk-image" style="padding-top: 64px">
<view class="status sk-transparent sk-text-0-0000-826 sk-text">待收货</view>
</view>
<view class="shipment">
<navigator class="logistics sk-image sk-pseudo sk-pseudo-circle" hover-class="none">
<view class="message sk-transparent sk-text-14-2857-512 sk-text"
>到了小福家里请签收</view
>
<view class="date sk-transparent sk-text-14-2857-990 sk-text">2023-04-15 23:23:04</view>
</navigator>
<view class="locate sk-image">
<view class="user sk-transparent sk-text-14-2857-630 sk-text">苏东坡 13633336666</view>
<view class="address sk-transparent sk-text-14-2857-606 sk-text"
>广东省 广州市 天河区吉山幼儿园</view
>
</view>
</view>
<view class="goods">
<view class="item">
<navigator class="navigator" hover-class="none">
<image class="cover sk-image"></image>
<view class="meta">
<view class="name ellipsis sk-transparent sk-text-14-2857-474 sk-text"
>厚厚一按就干爽埃及进口长绒棉毛巾</view
>
<view class="type sk-transparent sk-text-22-2222-237 sk-text"
>超值4条装灰蓝色+粉色+银灰+嫩黄</view
>
<view class="price">
<view class="actual">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="sk-transparent sk-text-14-2857-102 sk-text">68</text>
</view>
</view>
<view class="quantity sk-transparent sk-opacity">x1</view>
</view>
</navigator>
<navigator class="navigator" hover-class="none">
<image class="cover sk-image"></image>
<view class="meta">
<view class="name ellipsis sk-transparent sk-text-14-2857-969 sk-text"
>KJE金属色系轻量电动车骑行盔男女通用</view
>
<view class="type sk-transparent sk-text-22-2222-510 sk-text">玫瑰金L</view>
<view class="price">
<view class="actual">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="sk-transparent sk-text-14-2857-431 sk-text">120</text>
</view>
</view>
<view class="quantity sk-transparent sk-opacity">x1</view>
</view>
</navigator>
<navigator class="navigator" hover-class="none">
<image class="cover sk-image"></image>
<view class="meta">
<view class="name ellipsis sk-transparent sk-text-14-2857-130 sk-text"
>源自澳洲进口羊毛儿童奢暖羊毛被升级款</view
>
<view class="type sk-transparent sk-text-22-2222-110 sk-text"
>春秋款 100%羊毛款150x200cm适合1.2/1.35米床</view
>
<view class="price">
<view class="actual">
<text class="symbol sk-transparent sk-opacity">¥</text>
<text class="sk-transparent sk-text-14-2857-273 sk-text">289</text>
</view>
</view>
<view class="quantity sk-transparent sk-opacity">x1</view>
</view>
</navigator>
</view>
<view class="total">
<view class="row">
<view class="text sk-transparent sk-text-0-0000-302 sk-text">商品总价: </view>
<view
class="symbol sk-transparent sk-text-0-0000-998 sk-text sk-pseudo sk-pseudo-circle"
>477</view
>
</view>
<view class="row">
<view class="text sk-transparent sk-text-0-0000-912 sk-text">运费: </view>
<view
class="symbol sk-transparent sk-text-0-0000-208 sk-text sk-pseudo sk-pseudo-circle"
>2</view
>
</view>
<view class="row">
<view class="text sk-transparent sk-text-0-0000-538 sk-text">应付金额: </view>
<view
class="symbol primary sk-transparent sk-text-0-0000-858 sk-text sk-pseudo sk-pseudo-circle"
>479</view
>
</view>
</view>
</view>
<view class="detail">
<view class="title sk-transparent sk-text-0-0000-66 sk-text">订单信息</view>
<view class="row">
<view class="item sk-transparent">
订单编号: 1645809639951962113
<text class="copy sk-transparent sk-text-0-0000-522 sk-text">复制</text>
</view>
<view class="item sk-transparent sk-text-0-0000-353 sk-text"
>下单时间: 2023-04-11 23:22:50</view
>
</view>
</view>
<view class="toolbar" style="padding-bottom: 34px">
<view
class="button primary sk-transparent sk-text-31-9444-411 sk-text"
style="background-position-x: 50%"
>再次购买</view
>
</view>
</scroll-view>
</view>
</template>
<style>
.sk-transparent {
color: transparent !important;
}
.sk-text-14-2857-107 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 44.8rpx;
position: relative !important;
}
.sk-text {
background-origin: content-box !important;
background-clip: content-box !important;
background-color: transparent !important;
color: transparent !important;
background-repeat: repeat-y !important;
}
.sk-text-0-0000-826 {
background-image: linear-gradient(
transparent 0%,
#eeeeee 0%,
#eeeeee 100%,
transparent 0%
) !important;
background-size: 100% 36rpx;
position: relative !important;
}
.sk-text-14-2857-512 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-990 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-630 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-14-2857-606 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-474 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-22-2222-237 {
background-image: linear-gradient(
transparent 22.2222%,
#eeeeee 0%,
#eeeeee 77.7778%,
transparent 0%
) !important;
background-size: 100% 43.2rpx;
position: relative !important;
}
.sk-opacity {
opacity: 0 !important;
}
.sk-text-14-2857-102 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-969 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-22-2222-510 {
background-image: linear-gradient(
transparent 22.2222%,
#eeeeee 0%,
#eeeeee 77.7778%,
transparent 0%
) !important;
background-size: 100% 43.2rpx;
position: relative !important;
}
.sk-text-14-2857-431 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-14-2857-130 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 36.4rpx;
position: relative !important;
}
.sk-text-22-2222-110 {
background-image: linear-gradient(
transparent 22.2222%,
#eeeeee 0%,
#eeeeee 77.7778%,
transparent 0%
) !important;
background-size: 100% 43.2rpx;
position: relative !important;
}
.sk-text-14-2857-273 {
background-image: linear-gradient(
transparent 14.2857%,
#eeeeee 0%,
#eeeeee 85.7143%,
transparent 0%
) !important;
background-size: 100% 33.6rpx;
position: relative !important;
}
.sk-text-0-0000-302 {
background-image: linear-gradient(
transparent 0%,
#eeeeee 0%,
#eeeeee 100%,
transparent 0%
) !important;
background-size: 100% 26rpx;
position: relative !important;
}
.sk-text-0-0000-998 {
background-image: linear-gradient(
transparent 0%,
#eeeeee 0%,
#eeeeee 100%,
transparent 0%
) !important;
background-size: 100% 26rpx;
position: relative !important;
}
.sk-text-0-0000-912 {
background-image: linear-gradient(
transparent 0%,
#eeeeee 0%,
#eeeeee 100%,
transparent 0%
) !important;
background-size: 100% 26rpx;
position: relative !important;
}
.sk-text-0-0000-208 {
background-image: linear-gradient(
transparent 0%,
#eeeeee 0%,
#eeeeee 100%,
transparent 0%
) !important;
background-size: 100% 26rpx;
position: relative !important;
}
.sk-text-0-0000-538 {
background-image: linear-gradient(
transparent 0%,
#eeeeee 0%,
#eeeeee 100%,
transparent 0%
) !important;
background-size: 100% 26rpx;
position: relative !important;
}
.sk-text-0-0000-858 {
background-image: linear-gradient(
transparent 0%,
#eeeeee 0%,
#eeeeee 100%,
transparent 0%
) !important;
background-size: 100% 36rpx;
position: relative !important;
}
.sk-text-0-0000-66 {
background-image: linear-gradient(
transparent 0%,
#eeeeee 0%,
#eeeeee 100%,
transparent 0%
) !important;
background-size: 100% 30rpx;
position: relative !important;
}
.sk-text-0-0000-522 {
background-image: linear-gradient(
transparent 0%,
#eeeeee 0%,
#eeeeee 100%,
transparent 0%
) !important;
background-size: 100% 20rpx;
position: relative !important;
}
.sk-text-0-0000-353 {
background-image: linear-gradient(
transparent 0%,
#eeeeee 0%,
#eeeeee 100%,
transparent 0%
) !important;
background-size: 100% 26rpx;
position: relative !important;
}
.sk-text-0-0000-375 {
background-image: linear-gradient(
transparent 0%,
#eeeeee 0%,
#eeeeee 100%,
transparent 0%
) !important;
background-size: 100% 32rpx;
position: relative !important;
}
.sk-text-31-9444-411 {
background-image: linear-gradient(
transparent 31.9444%,
#eeeeee 0%,
#eeeeee 68.0556%,
transparent 0%
) !important;
background-size: 100% 72rpx;
position: relative !important;
}
.sk-image {
background: #efefef !important;
}
.sk-pseudo::before,
.sk-pseudo::after {
background: #efefef !important;
background-image: none !important;
color: transparent !important;
border-color: transparent !important;
}
.sk-pseudo-rect::before,
.sk-pseudo-rect::after {
border-radius: 0 !important;
}
.sk-pseudo-circle::before,
.sk-pseudo-circle::after {
border-radius: 50% !important;
}
.sk-container {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: transparent;
}
</style>

@ -0,0 +1,843 @@
<script setup lang="ts">
import { useGuessList } from '@/composables'
import { OrderState, orderStateList } from '@/services/constants'
import {
deleteMemberOrderAPI,
getMemberOrderByIdAPI,
getMemberOrderCancelByIdAPI,
getMemberOrderLogisticsByIdAPI,
getMemberOrderConsignmentByIdAPI,
putMemberOrderReceiptByIdAPI,
} from '@/services/order'
import type { LogisticItem, OrderResult } from '@/types/order'
import { onLoad, onReady } from '@dcloudio/uni-app'
import { ref } from 'vue'
import PageSkeleton from './components/PageSkeleton.vue'
import { getPayMockAPI, getPayWxPayMiniPayAPI } from '@/services/pay'
//
const { safeAreaInsets } = uni.getSystemInfoSync()
//
const { guessRef, onScrolltolower } = useGuessList()
//
const popup = ref<UniHelper.UniPopupInstance>()
//
const reasonList = ref([
'商品无货',
'不想要了',
'商品信息填错了',
'地址信息填写错误',
'商品降价',
'其它',
])
//
const reason = ref('')
//
const onCopy = (id: string) => {
//
uni.setClipboardData({ data: id })
}
//
const query = defineProps<{
id: string
}>()
//
const pages = getCurrentPages()
// Page uni-app Page
type PageInstance = Page.PageInstance & WechatMiniprogram.Page.InstanceMethods<any>
// #ifdef MP-WEIXIN
//
const pageInstance = pages.at(-1) as PageInstance
//
onReady(() => {
// ,
pageInstance.animate(
'.navbar',
[{ backgroundColor: 'transparent' }, { backgroundColor: '#f8f8f8' }],
1000,
{
scrollSource: '#scroller',
timeRange: 1000,
startScrollOffset: 0,
endScrollOffset: 50,
},
)
// ,
pageInstance.animate('.navbar .title', [{ color: 'transparent' }, { color: '#000' }], 1000, {
scrollSource: '#scroller',
timeRange: 1000,
startScrollOffset: 0,
endScrollOffset: 50,
})
// ,
pageInstance.animate('.navbar .back', [{ color: '#fff' }, { color: '#000' }], 1000, {
scrollSource: '#scroller',
timeRange: 1000,
startScrollOffset: 0,
endScrollOffset: 50,
})
})
// #endif
//
const order = ref<OrderResult>()
const getMemberOrderByIdData = async () => {
const res = await getMemberOrderByIdAPI(query.id)
order.value = res.result
if (
[OrderState.DaiShouHuo, OrderState.DaiPingJia, OrderState.YiWanCheng].includes(
order.value.orderState,
)
) {
getMemberOrderLogisticsByIdData()
}
}
//
const logisticList = ref<LogisticItem[]>([])
const getMemberOrderLogisticsByIdData = async () => {
const res = await getMemberOrderLogisticsByIdAPI(query.id)
logisticList.value = res.result.list
}
onLoad(() => {
getMemberOrderByIdData()
})
//
const onTimeup = () => {
//
order.value!.orderState = OrderState.YiQuXiao
}
//
const onOrderPay = async () => {
if (import.meta.env.DEV) {
//
await getPayMockAPI({ orderId: query.id })
} else {
// #ifdef MP-WEIXIN
// 1.2.API
// const res = await getPayWxPayMiniPayAPI({ orderId: query.id })
// await wx.requestPayment(res.result)
// 线 0.01
await getPayMockAPI({ orderId: query.id })
// #endif
// #ifdef H5 || APP-PLUS
// H5 App -
await getPayMockAPI({ orderId: query.id })
// #endif
}
//
uni.redirectTo({ url: `/pagesOrder/payment/payment?id=${query.id}` })
}
//
const isDev = import.meta.env.DEV
//
const onOrderSend = async () => {
if (isDev) {
await getMemberOrderConsignmentByIdAPI(query.id)
uni.showToast({ icon: 'success', title: '模拟发货完成' })
//
order.value!.orderState = OrderState.DaiShouHuo
}
}
//
const onOrderConfirm = () => {
//
uni.showModal({
content: '为保障您的权益,请收到货并确认无误后,再确认收货',
confirmColor: '#27BA9B',
success: async (success) => {
if (success.confirm) {
const res = await putMemberOrderReceiptByIdAPI(query.id)
//
order.value = res.result
}
},
})
}
//
const onOrderDelete = () => {
//
uni.showModal({
content: '是否删除订单',
confirmColor: '#27BA9B',
success: async (success) => {
if (success.confirm) {
await deleteMemberOrderAPI({ ids: [query.id] })
uni.redirectTo({ url: '/pagesOrder/list/list' })
}
},
})
}
//
const onOrderCancel = async () => {
//
const res = await getMemberOrderCancelByIdAPI(query.id, { cancelReason: reason.value })
//
order.value = res.result
//
popup.value?.close!()
//
uni.showToast({ icon: 'none', title: '订单取消成功' })
}
</script>
<template>
<!-- 自定义导航栏: 默认透明不可见, scroll-view 滚动到 50 时展示 -->
<view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }">
<view class="wrap">
<navigator
v-if="pages.length > 1"
open-type="navigateBack"
class="back icon-left"
></navigator>
<navigator v-else url="/pages/index/index" open-type="switchTab" class="back icon-home">
</navigator>
<view class="title">订单详情</view>
</view>
</view>
<scroll-view
enable-back-to-top
scroll-y
class="viewport"
id="scroller"
@scrolltolower="onScrolltolower"
>
<template v-if="order">
<!-- 订单状态 -->
<view class="overview" :style="{ paddingTop: safeAreaInsets!.top + 20 + 'px' }">
<!-- 待付款状态:展示倒计时 -->
<template v-if="order.orderState === OrderState.DaiFuKuan">
<view class="status icon-clock">等待付款</view>
<view class="tips">
<text class="money">应付金额: ¥ {{ order.payMoney }}</text>
<text class="time">支付剩余</text>
<uni-countdown
:second="order.countdown"
color="#fff"
splitor-color="#fff"
:show-day="false"
:show-colon="false"
@timeup="onTimeup"
/>
</view>
<view class="button" @tap="onOrderPay"></view>
</template>
<!-- 其他订单状态:展示再次购买按钮 -->
<template v-else>
<!-- 订单状态文字 -->
<view class="status"> {{ orderStateList[order.orderState].text }} </view>
<view class="button-group">
<navigator
class="button"
:url="`/pagesOrder/create/create?orderId=${query.id}`"
hover-class="none"
>
再次购买
</navigator>
<!-- 待发货状态模拟发货,开发期间使用,用于修改订单状态为已发货 -->
<view
v-if="isDev && order.orderState == OrderState.DaiFaHuo"
@tap="onOrderSend"
class="button"
>
模拟发货
</view>
<!-- 待收货状态: 展示确认收货按钮 -->
<view
v-if="order.orderState === OrderState.DaiShouHuo"
@tap="onOrderConfirm"
class="button"
>
确认收货
</view>
</view>
</template>
</view>
<!-- 配送状态 -->
<view class="shipment">
<!-- 订单物流信息 -->
<view v-for="item in logisticList" :key="item.id" class="item">
<view class="message">
{{ item.text }}
</view>
<view class="date"> {{ item.time }} </view>
</view>
<!-- 用户收货地址 -->
<view class="locate">
<view class="user"> {{ order.receiverContact }} {{ order.receiverMobile }} </view>
<view class="address"> {{ order.receiverAddress }} </view>
</view>
</view>
<!-- 商品信息 -->
<view class="goods">
<view class="item">
<navigator
class="navigator"
v-for="item in order.skus"
:key="item.id"
:url="`/pages/goods/goods?id=${item.spuId}`"
hover-class="none"
>
<image class="cover" :src="item.image"></image>
<view class="meta">
<view class="name ellipsis">{{ item.name }}</view>
<view class="type">{{ item.attrsText }}</view>
<view class="price">
<view class="actual">
<text class="symbol">¥</text>
<text>{{ item.curPrice }}</text>
</view>
</view>
<view class="quantity">x{{ item.quantity }}</view>
</view>
</navigator>
<!-- 待评价状态:展示按钮 -->
<view class="action" v-if="order.orderState === OrderState.DaiPingJia">
<view class="button primary">申请售后</view>
<navigator url="" class="button"> 去评价 </navigator>
</view>
</view>
<!-- 合计 -->
<view class="total">
<view class="row">
<view class="text">商品总价: </view>
<view class="symbol">{{ order.totalMoney }}</view>
</view>
<view class="row">
<view class="text">运费: </view>
<view class="symbol">{{ order.postFee }}</view>
</view>
<view class="row">
<view class="text">应付金额: </view>
<view class="symbol primary">{{ order.payMoney }}</view>
</view>
</view>
</view>
<!-- 订单信息 -->
<view class="detail">
<view class="title">订单信息</view>
<view class="row">
<view class="item">
订单编号: {{ query.id }} <text class="copy" @tap="onCopy(query.id)"></text>
</view>
<view class="item">下单时间: {{ order.createTime }}</view>
</view>
</view>
<!-- 猜你喜欢 -->
<XtxGuess ref="guessRef" />
<!-- 底部操作栏 -->
<view class="toolbar-height" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }"></view>
<view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
<!-- 待付款状态:展示支付按钮 -->
<template v-if="order.orderState === OrderState.DaiFuKuan">
<view class="button primary" @tap="onOrderPay"> </view>
<view class="button" @tap="popup?.open?.()"> </view>
</template>
<!-- 其他订单状态:按需展示按钮 -->
<template v-else>
<navigator
class="button secondary"
:url="`/pagesOrder/create/create?orderId=${query.id}`"
hover-class="none"
>
再次购买
</navigator>
<!-- 待收货状态: 展示确认收货 -->
<view
class="button primary"
v-if="order.orderState === OrderState.DaiShouHuo"
@tap="onOrderConfirm"
>
确认收货
</view>
<!-- 待评价状态: 展示去评价 -->
<view class="button" v-if="order.orderState === OrderState.DaiPingJia"> </view>
<!-- 待评价/已完成/已取消 状态: 展示删除订单 -->
<view
class="button delete"
v-if="order.orderState >= OrderState.DaiPingJia"
@tap="onOrderDelete"
>
删除订单
</view>
</template>
</view>
</template>
<template v-else>
<!-- 骨架屏组件 -->
<PageSkeleton />
</template>
</scroll-view>
<!-- 取消订单弹窗 -->
<uni-popup ref="popup" type="bottom" background-color="#fff">
<view class="popup-root">
<view class="title">订单取消</view>
<view class="description">
<view class="tips">请选择取消订单的原因</view>
<view class="cell" v-for="item in reasonList" :key="item" @tap="reason = item">
<text class="text">{{ item }}</text>
<text class="icon" :class="{ checked: item === reason }"></text>
</view>
</view>
<view class="footer">
<view class="button" @tap="popup?.close?.()"></view>
<view class="button primary" @tap="onOrderCancel"></view>
</view>
</view>
</uni-popup>
</template>
<style lang="scss">
page {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.navbar {
width: 750rpx;
color: #000;
position: fixed;
top: 0;
left: 0;
z-index: 9;
/* background-color: #f8f8f8; */
background-color: transparent;
.wrap {
position: relative;
.title {
height: 44px;
display: flex;
justify-content: center;
align-items: center;
font-size: 32rpx;
/* color: #000; */
color: transparent;
}
.back {
position: absolute;
left: 0;
height: 44px;
width: 44px;
font-size: 44rpx;
display: flex;
align-items: center;
justify-content: center;
/* color: #000; */
color: #fff;
}
}
}
.viewport {
background-color: #f7f7f8;
}
.overview {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1;
padding-bottom: 30rpx;
color: #fff;
background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/order_bg.png);
background-size: cover;
.status {
font-size: 36rpx;
}
.status::before {
margin-right: 6rpx;
font-weight: 500;
}
.tips {
margin: 30rpx 0;
display: flex;
font-size: 14px;
align-items: center;
.money {
margin-right: 30rpx;
}
}
.button-group {
margin-top: 30rpx;
display: flex;
justify-content: center;
align-items: center;
}
.button {
width: 260rpx;
height: 64rpx;
margin: 0 10rpx;
text-align: center;
line-height: 64rpx;
font-size: 28rpx;
color: #27ba9b;
border-radius: 68rpx;
background-color: #fff;
}
}
.shipment {
line-height: 1.4;
padding: 0 20rpx;
margin: 20rpx 20rpx 0;
border-radius: 10rpx;
background-color: #fff;
.locate,
.item {
min-height: 120rpx;
padding: 30rpx 30rpx 25rpx 75rpx;
background-size: 50rpx;
background-repeat: no-repeat;
background-position: 6rpx center;
}
.locate {
background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/locate.png);
.user {
font-size: 26rpx;
color: #444;
}
.address {
font-size: 24rpx;
color: #666;
}
}
.item {
background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/car.png);
border-bottom: 1rpx solid #eee;
position: relative;
.message {
font-size: 26rpx;
color: #444;
}
.date {
font-size: 24rpx;
color: #666;
}
}
}
.goods {
margin: 20rpx 20rpx 0;
padding: 0 20rpx;
border-radius: 10rpx;
background-color: #fff;
.item {
padding: 30rpx 0;
border-bottom: 1rpx solid #eee;
.navigator {
display: flex;
margin: 20rpx 0;
}
.cover {
width: 170rpx;
height: 170rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.meta {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
}
.name {
height: 80rpx;
font-size: 26rpx;
color: #444;
}
.type {
line-height: 1.8;
padding: 0 15rpx;
margin-top: 6rpx;
font-size: 24rpx;
align-self: flex-start;
border-radius: 4rpx;
color: #888;
background-color: #f7f7f8;
}
.price {
display: flex;
margin-top: 6rpx;
font-size: 24rpx;
}
.symbol {
font-size: 20rpx;
}
.original {
color: #999;
text-decoration: line-through;
}
.actual {
margin-left: 10rpx;
color: #444;
}
.text {
font-size: 22rpx;
}
.quantity {
position: absolute;
bottom: 0;
right: 0;
font-size: 24rpx;
color: #444;
}
.action {
display: flex;
flex-direction: row-reverse;
justify-content: flex-start;
padding: 30rpx 0 0;
.button {
width: 200rpx;
height: 60rpx;
text-align: center;
justify-content: center;
line-height: 60rpx;
margin-left: 20rpx;
border-radius: 60rpx;
border: 1rpx solid #ccc;
font-size: 26rpx;
color: #444;
}
.primary {
color: #27ba9b;
border-color: #27ba9b;
}
}
}
.total {
line-height: 1;
font-size: 26rpx;
padding: 20rpx 0;
color: #666;
.row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10rpx 0;
}
.symbol::before {
content: '¥';
font-size: 80%;
margin-right: 3rpx;
}
.primary {
color: #cf4444;
font-size: 36rpx;
}
}
}
.detail {
line-height: 1;
padding: 30rpx 20rpx 0;
margin: 20rpx 20rpx 0;
font-size: 26rpx;
color: #666;
border-radius: 10rpx;
background-color: #fff;
.title {
font-size: 30rpx;
color: #444;
}
.row {
padding: 20rpx 0;
.item {
padding: 10rpx 0;
display: flex;
align-items: center;
}
.copy {
border-radius: 20rpx;
font-size: 20rpx;
border: 1px solid #ccc;
padding: 5rpx 10rpx;
margin-left: 10rpx;
}
}
}
.toolbar-height {
height: 100rpx;
box-sizing: content-box;
}
.toolbar {
position: fixed;
left: 0;
right: 0;
bottom: calc(var(--window-bottom));
z-index: 1;
height: 100rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
flex-direction: row-reverse;
border-top: 1rpx solid #ededed;
border-bottom: 1rpx solid #ededed;
background-color: #fff;
box-sizing: content-box;
.button {
display: flex;
justify-content: center;
align-items: center;
width: 200rpx;
height: 72rpx;
margin-left: 15rpx;
font-size: 26rpx;
border-radius: 72rpx;
border: 1rpx solid #ccc;
color: #444;
}
.delete {
order: 4;
color: #cf4444;
}
.button {
order: 3;
}
.secondary {
order: 2;
color: #27ba9b;
border-color: #27ba9b;
}
.primary {
order: 1;
color: #fff;
background-color: #27ba9b;
}
}
.popup-root {
padding: 30rpx 30rpx 0;
border-radius: 10rpx 10rpx 0 0;
overflow: hidden;
.title {
font-size: 30rpx;
text-align: center;
margin-bottom: 30rpx;
}
.description {
font-size: 28rpx;
padding: 0 20rpx;
.tips {
color: #444;
margin-bottom: 12rpx;
}
.cell {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15rpx 0;
color: #666;
}
.icon::before {
content: '\e6cd';
font-family: 'erabbit' !important;
font-size: 38rpx;
color: #999;
}
.icon.checked::before {
content: '\e6cc';
font-size: 38rpx;
color: #27ba9b;
}
}
.footer {
display: flex;
justify-content: space-between;
padding: 30rpx 0 40rpx;
font-size: 28rpx;
color: #444;
.button {
flex: 1;
height: 72rpx;
text-align: center;
line-height: 72rpx;
margin: 0 20rpx;
color: #444;
border-radius: 72rpx;
border: 1rpx solid #ccc;
}
.primary {
color: #fff;
background-color: #27ba9b;
border: none;
}
}
}
</style>

@ -0,0 +1,388 @@
<script setup lang="ts">
import { OrderState } from '@/services/constants'
import { orderStateList } from '@/services/constants'
import { putMemberOrderReceiptByIdAPI } from '@/services/order'
import { deleteMemberOrderAPI } from '@/services/order'
import { getMemberOrderAPI } from '@/services/order'
import { getPayMockAPI, getPayWxPayMiniPayAPI } from '@/services/pay'
import type { OrderItem } from '@/types/order'
import type { OrderListParams } from '@/types/order'
import { onMounted, ref } from 'vue'
//
const { safeAreaInsets } = uni.getSystemInfoSync()
// porps
const props = defineProps<{
orderState: number
}>()
//
const queryParams: Required<OrderListParams> = {
page: 1,
pageSize: 5,
orderState: props.orderState,
}
//
const orderList = ref<OrderItem[]>([])
//
const isLoading = ref(false)
const getMemberOrderData = async () => {
// 退
if (isLoading.value) return
// 退
if (isFinish.value === true) {
return uni.showToast({ icon: 'none', title: '没有更多数据~' })
}
//
isLoading.value = true
//
const res = await getMemberOrderAPI(queryParams)
//
isLoading.value = false
//
orderList.value.push(...res.result.items)
//
if (queryParams.page < res.result.pages) {
//
queryParams.page++
} else {
//
isFinish.value = true
}
}
onMounted(() => {
getMemberOrderData()
})
//
const onOrderPay = async (id: string) => {
if (import.meta.env.DEV) {
//
await getPayMockAPI({ orderId: id })
} else {
// #ifdef MP-WEIXIN
// 1.2.API
// const res = await getPayWxPayMiniPayAPI({ orderId: id })
// await wx.requestPayment(res.result)
// 线 0.01
await getPayMockAPI({ orderId: id })
// #endif
}
//
uni.showToast({ title: '模拟支付成功' })
//
setTimeout(() => {
wx.showModal({
title: '温馨提示',
content: '此交易是模拟支付,您并未付款,不会导致实际购买商品或服务',
confirmText: '知道了',
showCancel: false,
})
}, 2000)
//
const order = orderList.value.find((v) => v.id === id)
order!.orderState = OrderState.DaiFaHuo
}
//
const onOrderConfirm = (id: string) => {
uni.showModal({
content: '为保障您的权益,请收到货并确认无误后,再确认收货',
confirmColor: '#27BA9B',
success: async (res) => {
if (res.confirm) {
await putMemberOrderReceiptByIdAPI(id)
uni.showToast({ icon: 'success', title: '确认收货成功' })
//
const order = orderList.value.find((v) => v.id === id)
order!.orderState = OrderState.DaiPingJia
}
},
})
}
//
const onOrderDelete = (id: string) => {
uni.showModal({
content: '你确定要删除该订单?',
confirmColor: '#27BA9B',
success: async (res) => {
if (res.confirm) {
await deleteMemberOrderAPI({ ids: [id] })
//
const index = orderList.value.findIndex((v) => v.id === id)
orderList.value.splice(index, 1)
}
},
})
}
//
const isFinish = ref(false)
//
const isTriggered = ref(false)
//
const onRefresherrefresh = async () => {
//
isTriggered.value = true
//
queryParams.page = 1
orderList.value = []
isFinish.value = false
//
await getMemberOrderData()
//
isTriggered.value = false
}
</script>
<template>
<scroll-view
enable-back-to-top
scroll-y
class="orders"
refresher-enabled
:refresher-triggered="isTriggered"
@refresherrefresh="onRefresherrefresh"
@scrolltolower="getMemberOrderData"
>
<view class="card" v-for="order in orderList" :key="order.id">
<!-- 订单信息 -->
<view class="status">
<text class="date">{{ order.createTime }}</text>
<!-- 订单状态文字 -->
<text>{{ orderStateList[order.orderState].text }}</text>
<!-- 待评价/已完成/已取消 状态: 展示删除订单 -->
<text
v-if="order.orderState >= OrderState.DaiPingJia"
class="icon-delete"
@tap="onOrderDelete(order.id)"
></text>
</view>
<!-- 商品信息点击商品跳转到订单详情不是商品详情 -->
<navigator
v-for="item in order.skus"
:key="item.id"
class="goods"
:url="`/pagesOrder/detail/detail?id=${order.id}`"
hover-class="none"
>
<view class="cover">
<image class="image" mode="aspectFit" :src="item.image"></image>
</view>
<view class="meta">
<view class="name ellipsis">{{ item.name }}</view>
<view class="type">{{ item.attrsText }}</view>
</view>
</navigator>
<!-- 支付信息 -->
<view class="payment">
<text class="quantity">{{ order.totalNum }}件商品</text>
<text>实付</text>
<text class="amount"> <text class="symbol">¥</text>{{ order.payMoney }}</text>
</view>
<!-- 订单操作按钮 -->
<view class="action">
<!-- 待付款状态显示去支付按钮 -->
<template v-if="order.orderState === OrderState.DaiFuKuan">
<view class="button primary" @tap="onOrderPay(order.id)"></view>
</template>
<template v-else>
<navigator
class="button secondary"
:url="`/pagesOrder/create/create?orderId=${order.id}`"
hover-class="none"
>
再次购买
</navigator>
<!-- 待收货状态: 展示确认收货 -->
<view
v-if="order.orderState === OrderState.DaiShouHuo"
class="button primary"
@tap="onOrderConfirm(order.id)"
>
确认收货
</view>
</template>
</view>
</view>
<!-- 底部提示文字 -->
<view class="loading-text" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">
{{ isFinish ? '没有更多数据~' : '正在加载...' }}
</view>
</scroll-view>
</template>
<style lang="scss">
//
.orders {
.card {
min-height: 100rpx;
padding: 20rpx;
margin: 20rpx 20rpx 0;
border-radius: 10rpx;
background-color: #fff;
&:last-child {
padding-bottom: 40rpx;
}
}
.status {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 28rpx;
color: #999;
margin-bottom: 15rpx;
.date {
color: #666;
flex: 1;
}
.primary {
color: #ff9240;
}
.icon-delete {
line-height: 1;
margin-left: 10rpx;
padding-left: 10rpx;
border-left: 1rpx solid #e3e3e3;
}
}
.goods {
display: flex;
margin-bottom: 20rpx;
.cover {
width: 170rpx;
height: 170rpx;
margin-right: 20rpx;
border-radius: 10rpx;
overflow: hidden;
position: relative;
.image {
width: 170rpx;
height: 170rpx;
}
}
.quantity {
position: absolute;
bottom: 0;
right: 0;
line-height: 1;
padding: 6rpx 4rpx 6rpx 8rpx;
font-size: 24rpx;
color: #fff;
border-radius: 10rpx 0 0 0;
background-color: rgba(0, 0, 0, 0.6);
}
.meta {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.name {
height: 80rpx;
font-size: 26rpx;
color: #444;
}
.type {
line-height: 1.8;
padding: 0 15rpx;
margin-top: 10rpx;
font-size: 24rpx;
align-self: flex-start;
border-radius: 4rpx;
color: #888;
background-color: #f7f7f8;
}
.more {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
color: #333;
}
}
.payment {
display: flex;
justify-content: flex-end;
align-items: center;
line-height: 1;
padding: 20rpx 0;
text-align: right;
color: #999;
font-size: 28rpx;
border-bottom: 1rpx solid #eee;
.quantity {
font-size: 24rpx;
margin-right: 16rpx;
}
.amount {
color: #444;
margin-left: 6rpx;
}
.symbol {
font-size: 20rpx;
}
}
.action {
display: flex;
justify-content: flex-end;
align-items: center;
padding-top: 20rpx;
.button {
width: 180rpx;
height: 60rpx;
display: flex;
justify-content: center;
align-items: center;
margin-left: 20rpx;
border-radius: 60rpx;
border: 1rpx solid #ccc;
font-size: 26rpx;
color: #444;
}
.secondary {
color: #27ba9b;
border-color: #27ba9b;
}
.primary {
color: #fff;
background-color: #27ba9b;
border-color: #27ba9b;
}
}
.loading-text {
text-align: center;
font-size: 28rpx;
color: #666;
padding: 20rpx 0;
}
}
</style>

@ -0,0 +1,105 @@
<script setup lang="ts">
import { ref } from 'vue'
import OrderList from './components/OrderList.vue'
//
const query = defineProps<{
type: string
}>()
// tabs
const orderTabs = ref([
{ orderState: 0, title: '全部', isRender: false },
{ orderState: 1, title: '待付款', isRender: false },
{ orderState: 2, title: '待发货', isRender: false },
{ orderState: 3, title: '待收货', isRender: false },
{ orderState: 4, title: '待评价', isRender: false },
])
//
const activeIndex = ref(orderTabs.value.findIndex((v) => v.orderState === Number(query.type)))
//
orderTabs.value[activeIndex.value].isRender = true
</script>
<template>
<view class="viewport">
<!-- tabs -->
<view class="tabs">
<text
class="item"
v-for="(item, index) in orderTabs"
:key="item.title"
@tap="
() => {
activeIndex = index
item.isRender = true
}
"
>
{{ item.title }}
</text>
<!-- 游标 -->
<view class="cursor" :style="{ left: activeIndex * 20 + '%' }"></view>
</view>
<!-- 滑动容器 -->
<swiper class="swiper" :current="activeIndex" @change="activeIndex = $event.detail.current">
<!-- 滑动项 -->
<swiper-item v-for="item in orderTabs" :key="item.title">
<!-- 订单列表 -->
<OrderList v-if="item.isRender" :order-state="item.orderState" />
</swiper-item>
</swiper>
</view>
</template>
<style lang="scss">
page {
height: 100%;
overflow: hidden;
}
.viewport {
height: 100%;
display: flex;
flex-direction: column;
background-color: #fff;
}
// tabs
.tabs {
display: flex;
justify-content: space-around;
line-height: 60rpx;
margin: 0 10rpx;
background-color: #fff;
box-shadow: 0 4rpx 6rpx rgba(240, 240, 240, 0.6);
position: relative;
z-index: 9;
.item {
flex: 1;
text-align: center;
padding: 20rpx;
font-size: 28rpx;
color: #262626;
}
.cursor {
position: absolute;
left: 0;
bottom: 0;
width: 20%;
height: 6rpx;
padding: 0 50rpx;
background-color: #27ba9b;
/* 过渡效果 */
transition: all 0.4s;
}
}
// swiper
.swiper {
background-color: #f7f7f8;
}
</style>

@ -0,0 +1,115 @@
<script setup lang="ts">
import { useGuessList } from '@/composables'
import { onLoad } from '@dcloudio/uni-app'
//
const query = defineProps<{
id: string
}>()
//
const { guessRef, onScrolltolower } = useGuessList()
//
onLoad(() => {
wx.showModal({
title: '温馨提示',
content: '此交易是模拟支付,您并未付款,不会导致实际购买商品或服务',
confirmText: '知道了',
showCancel: false,
})
})
</script>
<template>
<scroll-view enable-back-to-top class="viewport" scroll-y @scrolltolower="onScrolltolower">
<!-- 订单状态 -->
<view class="overview">
<view class="status icon-checked">模拟支付成功</view>
<view class="tips">提示: 本小程序仅为教学演示用途并未实际支付或购买商品或服务</view>
<view class="buttons">
<navigator
hover-class="none"
class="button navigator"
url="/pages/index/index"
open-type="switchTab"
>
返回首页
</navigator>
<navigator
hover-class="none"
class="button navigator"
:url="`/pagesOrder/detail/detail?id=${query.id}`"
open-type="redirect"
>
查看订单
</navigator>
</view>
</view>
<!-- 猜你喜欢 -->
<XtxGuess ref="guessRef" />
</scroll-view>
</template>
<style lang="scss">
page {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.viewport {
background-color: #f7f7f8;
}
.overview {
line-height: 1;
padding: 50rpx 0;
color: #fff;
background-color: #27ba9b;
.tips {
width: 70%;
font-size: 24rpx;
text-align: center;
line-height: 1.5;
margin: 60rpx auto;
}
.status {
font-size: 36rpx;
font-weight: 500;
text-align: center;
}
.status::before {
display: block;
font-size: 110rpx;
margin-bottom: 20rpx;
}
.buttons {
height: 60rpx;
line-height: 60rpx;
display: flex;
justify-content: center;
align-items: center;
margin-top: 60rpx;
}
.button {
text-align: center;
margin: 0 10rpx;
font-size: 28rpx;
color: #fff;
&:first-child {
width: 200rpx;
border-radius: 64rpx;
border: 1rpx solid #fff;
}
}
}
</style>

@ -0,0 +1,59 @@
import type { AddressItem, AddressParams } from '@/types/address'
import { http } from '@/utils/http'
/**
*
* @param data
*/
export const postMemberAddressAPI = (data: AddressParams) => {
return http({
method: 'POST',
url: '/member/address',
data,
})
}
/**
*
*/
export const getMemberAddressAPI = () => {
return http<AddressItem[]>({
method: 'GET',
url: '/member/address',
})
}
/**
*
* @param id id()
*/
export const getMemberAddressByIdAPI = (id: string) => {
return http<AddressItem>({
method: 'GET',
url: `/member/address/${id}`,
})
}
/**
*
* @param id id()
* @param data ()
*/
export const putMemberAddressByIdAPI = (id: string, data: AddressParams) => {
return http({
method: 'PUT',
url: `/member/address/${id}`,
data,
})
}
/**
*
* @param id id()
*/
export const deleteMemberAddressByIdAPI = (id: string) => {
return http({
method: 'DELETE',
url: `/member/address/${id}`,
})
}

@ -0,0 +1,63 @@
import type { CartItem } from '@/types/cart'
import { http } from '@/utils/http'
/**
*
* @param data
*/
export const postMemberCartAPI = (data: { skuId: string; count: number }) => {
return http({
method: 'POST',
url: '/member/cart',
data,
})
}
/**
*
*/
export const getMemberCartAPI = () => {
return http<CartItem[]>({
method: 'GET',
url: '/member/cart',
})
}
/**
* /
* @param data ids SKUID
*/
export const deleteMemberCartAPI = (data: { ids: string[] }) => {
return http({
method: 'DELETE',
url: '/member/cart',
data,
})
}
/**
*
* @param skuId SKUID
* @param data selected count
*/
export const putMemberCartBySkuIdAPI = (
skuId: string,
data: { selected?: boolean; count?: number },
) => {
return http({
method: 'PUT',
url: `/member/cart/${skuId}`,
data,
})
}
/**
* /
* @param data selected
*/
export const putMemberCartSelectedAPI = (data: { selected: boolean }) => {
return http({
method: 'PUT',
url: '/member/cart/selected',
data,
})
}

@ -0,0 +1,12 @@
import type { CategoryTopItem } from '@/types/category'
import { http } from '@/utils/http'
/**
* -
*/
export const getCategoryTopAPI = () => {
return http<CategoryTopItem[]>({
method: 'GET',
url: '/category/top',
})
}

@ -0,0 +1,25 @@
/** 订单状态枚举 */
export enum OrderState {
/** 待付款 */
DaiFuKuan = 1,
/** 待发货 */
DaiFaHuo = 2,
/** 待收货 */
DaiShouHuo = 3,
/** 待评价 */
DaiPingJia = 4,
/** 已完成 */
YiWanCheng = 5,
/** 已取消 */
YiQuXiao = 6,
}
/** 订单状态列表 */
export const orderStateList = [
{ id: 0, text: '' },
{ id: 1, text: '待付款' },
{ id: 2, text: '待发货' },
{ id: 3, text: '待收货' },
{ id: 4, text: '待评价' },
{ id: 5, text: '已完成' },
{ id: 6, text: '已取消' },
]

@ -0,0 +1,14 @@
import type { GoodsResult } from '@/types/goods'
import { http } from '@/utils/http'
/**
*
* @param id id
*/
export const getGoodsByIdAPI = (id: string) => {
return http<GoodsResult>({
method: 'GET',
url: '/goods',
data: { id },
})
}

@ -0,0 +1,48 @@
import type { PageParams, PageResult } from '@/types/global'
import type { BannerItem, CategoryItem, GuessItem, HotItem } from '@/types/home'
import { http } from '@/utils/http'
/**
* -广-
* @param distributionSite 广 12 1
*/
export const getHomeBannerAPI = (distributionSite = 1) => {
return http<BannerItem[]>({
method: 'GET',
url: '/home/banner',
data: {
distributionSite,
},
})
}
/**
* --
*/
export const getHomeCategoryAPI = () => {
return http<CategoryItem[]>({
method: 'GET',
url: '/home/category/mutli',
})
}
/**
* --
*/
export const getHomeHotAPI = () => {
return http<HotItem[]>({
method: 'GET',
url: '/home/hot/mutli',
})
}
/**
* -
*/
export const getHomeGoodsGuessLikeAPI = (data?: PageParams) => {
return http<PageResult<GuessItem>>({
method: 'GET',
url: '/home/goods/guessLike',
data,
})
}

@ -0,0 +1,17 @@
import { http } from '@/utils/http'
import type { PageParams } from '@/types/global'
import type { HotResult } from '@/types/hot'
type HotParams = PageParams & { subType?: string }
/**
*
* @param url
* @param data
*/
export const getHotRecommendAPI = (url: string, data?: HotParams) => {
return http<HotResult>({
method: 'GET',
url,
data,
})
}

@ -0,0 +1,49 @@
import type { LoginResult } from '@/types/member'
import { http } from '@/utils/http'
type LoginWxMinParams = {
code: string
encryptedData?: string
iv?: string
}
/**
*
* @param data
*/
export const postLoginWxMinAPI = (data: LoginWxMinParams) => {
return http<LoginResult>({
method: 'POST',
url: '/login/wxMin',
data,
})
}
/**
* _
* @param phoneNumber
*/
export const postLoginWxMinSimpleAPI = (phoneNumber: string) => {
return http<LoginResult>({
method: 'POST',
url: '/login/wxMin/simple',
data: {
phoneNumber,
},
})
}
type LoginParams = {
account: string
password: string
}
/**
* -+
* @param data
*/
export const postLoginAPI = (data: LoginParams) => {
return http<LoginResult>({
method: 'POST',
url: '/login',
data,
})
}

@ -0,0 +1,142 @@
import type { OrderListResult } from '@/types/order'
import type {
OrderCreateParams,
OrderListParams,
OrderLogisticResult,
OrderPreResult,
OrderResult,
} from '@/types/order'
import { http } from '@/utils/http'
/**
* -
*/
export const getMemberOrderPreAPI = () => {
return http<OrderPreResult>({
method: 'GET',
url: '/member/order/pre',
})
}
/**
* -
*/
export const getMemberOrderPreNowAPI = (data: {
skuId: string
count: string
addressId?: string
}) => {
return http<OrderPreResult>({
method: 'GET',
url: '/member/order/pre/now',
data,
})
}
/**
* -
* @param id id
*/
export const getMemberOrderRepurchaseByIdAPI = (id: string) => {
return http<OrderPreResult>({
method: 'GET',
url: `/member/order/repurchase/${id}`,
})
}
/**
*
* @param data
*/
export const postMemberOrderAPI = (data: OrderCreateParams) => {
return http<{ id: string }>({
method: 'POST',
url: '/member/order',
data,
})
}
/**
*
* @param id id
*/
export const getMemberOrderByIdAPI = (id: string) => {
return http<OrderResult>({
method: 'GET',
url: `/member/order/${id}`,
})
}
/**
* -
* @description DEV使
* @param id id
*/
export const getMemberOrderConsignmentByIdAPI = (id: string) => {
return http({
method: 'GET',
url: `/member/order/consignment/${id}`,
})
}
/**
*
* @description
* @param id id
*/
export const putMemberOrderReceiptByIdAPI = (id: string) => {
return http<OrderResult>({
method: 'PUT',
url: `/member/order/${id}/receipt`,
})
}
/**
*
* @description
* @param id id
*/
export const getMemberOrderLogisticsByIdAPI = (id: string) => {
return http<OrderLogisticResult>({
method: 'GET',
url: `/member/order/${id}/logistics`,
})
}
/**
*
* @description
* @param data ids
*/
export const deleteMemberOrderAPI = (data: { ids: string[] }) => {
return http({
method: 'DELETE',
url: `/member/order`,
data,
})
}
/**
*
* @description
* @param id id
* @param data cancelReason
*/
export const getMemberOrderCancelByIdAPI = (id: string, data: { cancelReason: string }) => {
return http<OrderResult>({
method: 'PUT',
url: `/member/order/${id}/cancel`,
data,
})
}
/**
*
* @param data orderState
*/
export const getMemberOrderAPI = (data: OrderListParams) => {
return http<OrderListResult>({
method: 'GET',
url: `/member/order`,
data,
})
}

@ -0,0 +1,25 @@
import { http } from '@/utils/http'
/**
*
* @param data orderId id
*/
export const getPayWxPayMiniPayAPI = (data: { orderId: string }) => {
return http<WechatMiniprogram.RequestPaymentOption>({
method: 'GET',
url: '/pay/wxPay/miniPay',
data,
})
}
/**
* -
* @param data orderId id
*/
export const getPayMockAPI = (data: { orderId: string }) => {
return http({
method: 'GET',
url: '/pay/mock',
data,
})
}

@ -0,0 +1,24 @@
import type { ProfileDetail, ProfileParams } from '@/types/member'
import { http } from '@/utils/http'
/**
*
*/
export const getMemberProfileAPI = () => {
return http<ProfileDetail>({
method: 'GET',
url: '/member/profile',
})
}
/**
*
* @param data
*/
export const putMemberProfileAPI = (data: ProfileParams) => {
return http<ProfileDetail>({
method: 'PUT',
url: '/member/profile',
data,
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

@ -0,0 +1,13 @@
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
// 创建 pinia 实例
const pinia = createPinia()
// 使用持久化存储插件
pinia.use(persist)
// 默认导出,给 main.ts 使用
export default pinia
// 模块统一导出
export * from './modules/member'

@ -0,0 +1,16 @@
import type { AddressItem } from '@/types/address'
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useAddressStore = defineStore('address', () => {
const selectedAddress = ref<AddressItem>()
const changeSelectedAddress = (val: AddressItem) => {
selectedAddress.value = val
}
return {
selectedAddress,
changeSelectedAddress,
}
})

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save