feat: init

master
xjs 2025-07-23 16:27:22 +08:00
commit edfeb770ff
152 changed files with 12020 additions and 0 deletions

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env
.env.local
.env.development.local

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict = true

1
.nvmdrc Normal file
View File

@ -0,0 +1 @@
23.8.0

12
.prettierignore Normal file
View File

@ -0,0 +1,12 @@
/dist/*
.local
.output.js
.github
.vscode
/node_modules/**
*.html
**/*.svg
**/*.sh
/public/**

10
.prettierrc.json Normal file
View File

@ -0,0 +1,10 @@
{
"printWidth": 160,
"arrowParens": "always",
"bracketSameLine": true,
"bracketSpacing": true,
"semi": true,
"singleAttributePerLine": false,
"vueIndentScriptAndStyle": true,
"htmlWhitespaceSensitivity": "ignore"
}

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

27
ALIYUN.pem Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA2SOM6Heb+BNGha/ucoV+GopddM7ckyGWALhGPJp/Z7P5jgw6
NSJ1G/E7CFEukaLC50eupji9mA4o8Emtrgn8y7uMIc5lafHe0IPy+WA90PZyien4
0u7dD0NrEbKH41SIEuZFbGev0CgQJsxmkS8CmOytmglyJ0JDBpJD9tQLwSuG9kad
DrYPArQdQu+ZALt4gyG7m14c4mZ2hAcIUwNYqFY9g/HIp0q9al8SW3WqzkB1U7GS
kh3i/MwtUvZWPI16aKW4/tdUX7y8PPguHQ1MWLd6DI0iGo9tqTQt35o9ax41O+0S
EY4MgU1Q3XJaMrmrYUpZ2/Y4xJbHOHgyi6afzQIDAQABAoIBAGms4ovUgkSmZOD1
MU/s5eVWx4rsje7RHqa1CAHAkxbOQTq/eqiXX3U83qT6lXZtRvu2KCpfXO4eng/r
W6pi0/P3D4j4YOTBwNWsEdkJ3KvQ9QdnpiBJ/a3K+tW/FGEvp5XDGbBbefYNOWcY
fSZVQadZMFfSFwtCNUqCbq82nY3hkoFVGiQ9EHUlvNCQM4y5VeJuCPzsl8rzAsyo
gpkHyKxU/CNg+f5UuPwostR5eTXgkp6nlpa65yDK0szsww78keE/J1tOB0d4r+Oe
12ZVzYLQrzQwt2CwIGT9KkAUv7eO7ZTMDsG8MYNnRPGXKgjSZqBSW0MCq7ksz1/P
dHTJmSECgYEA9Q8VbnLkdk5NlRCNTONfjLhNUGmAqt/qLPi7tnJa0wfSpr3tg1Aj
AxnV455fT81vbDP8V8tGDNx+/d5jBkIsdsCYOMa6dqZr1HOfSgbH8wETtU9mbKdB
Gt/frdM3rkJvyd5RjoqMex4U7x4f1OWfThHVzvi8TzqfRSXNpbXBMdUCgYEA4tVa
TBQhwgnjMHl9OYrW1urtf2fKEGOZA/uGJI55O7IL2SoYnVjFUHjlE4D94K/hdijp
l2oFydD0GrWVkWULPvTdUFzMTRPH+SUfXbVqXBMJfHJYrRuZHsJBH2jIwIzu4LaA
hliletIdJUtK8mUvwEb+4TlVUwfCYqkveu1EuhkCgYAHjK5pV6rIJkNnmznvK3YP
HMJs/sMTAJDzT7pgtYcsxynrLyC5EefyOYKIX6GqELclCzjz73Q6AzT6VzaPw8wg
4HAQF7c43oml4uX+XtUcHGViCY8rO7/atxjp/v7RJITTIEE89fG7/UJB15i9c1GE
EzKWDL2oZzLu62o5d676/QKBgQDgiCBhvmvMDs18ZkW2d+BBzTpaKvqxTmVgs9EM
zpripFNmG21SE1T9Wy4mKEEl7/NVaxoObzxbkSKQbb4ntcV0BB4uNi1k/ner/zsV
H0aw7YcuUGHGuNLQx6h+1tIhB2BNv1lposXq1aFUETuWxOKHib8yYfY7wiqATshY
/hRRwQKBgCkT2Wt/XjbXLCpX1viEJOLzYYQkrNyPvruXknCnQQ+0/cq4LoRqWZZR
4RjS0RkoOF4vM38u/hgICiUUCR4bYUy/4O7uhNu5tDle6lIeB+ZxVpRRedE0OfQT
tCpQd0yN9iONBlGyfAtpq7oNzuXgsl1OR4usZ8wCinfImChAl6wv
-----END RSA PRIVATE KEY-----

1
README.md Normal file
View File

@ -0,0 +1 @@
# 康养项目

72
components.d.ts vendored Normal file
View File

@ -0,0 +1,72 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
BackButton: typeof import('./src/components/back-button/index.vue')['default']
Carte: typeof import('./src/components/restaurant-cart/carte.vue')['default']
CheckoutBtn: typeof import('./src/components/restaurant-cart/checkout-btn.vue')['default']
ChooseLoation: typeof import('./src/components/checkout/chooseLoation.vue')['default']
ChooseLocation: typeof import('./src/components/checkout/chooseLocation.vue')['default']
ChoosePeople: typeof import('./src/components/checkout/choosePeople.vue')['default']
ChooseTime: typeof import('./src/components/checkout/chooseTime.vue')['default']
ElDivider: typeof import('element-plus/es')['ElDivider']
HealthCare: typeof import('./src/components/shopping-cart/health-care.vue')['default']
NutActionSheet: typeof import('@nutui/nutui')['ActionSheet']
NutBadge: typeof import('@nutui/nutui')['Badge']
NutButton: typeof import('@nutui/nutui')['Button']
NutCalendar: typeof import('@nutui/nutui')['Calendar']
NutCard: typeof import('@nutui/nutui')['Card']
NutCell: typeof import('@nutui/nutui')['Cell']
NutCellGroup: typeof import('@nutui/nutui')['CellGroup']
NutCheckbox: typeof import('@nutui/nutui')['Checkbox']
NutCheckboxGroup: typeof import('@nutui/nutui')['CheckboxGroup']
NutDivider: typeof import('@nutui/nutui')['Divider']
NutDrag: typeof import('@nutui/nutui')['Drag']
NutElevator: typeof import('@nutui/nutui')['Elevator']
NutForm: typeof import('@nutui/nutui')['Form']
NutFormItem: typeof import('@nutui/nutui')['FormItem']
NutGrid: typeof import('@nutui/nutui')['Grid']
NutGridItem: typeof import('@nutui/nutui')['GridItem']
NutImage: typeof import('@nutui/nutui')['Image']
NutImagePreview: typeof import('@nutui/nutui')['ImagePreview']
NutInput: typeof import('@nutui/nutui')['Input']
NutInputNumber: typeof import('@nutui/nutui')['InputNumber']
NutList: typeof import('@nutui/nutui')['List']
NutMenu: typeof import('@nutui/nutui')['Menu']
NutMenuItem: typeof import('@nutui/nutui')['MenuItem']
NutNavbar: typeof import('@nutui/nutui')['Navbar']
NutPopup: typeof import('@nutui/nutui')['Popup']
NutPrice: typeof import('@nutui/nutui')['Price']
NutRadio: typeof import('@nutui/nutui')['Radio']
NutRadioGroup: typeof import('@nutui/nutui')['RadioGroup']
NutSearchbar: typeof import('@nutui/nutui')['Searchbar']
NutSticky: typeof import('@nutui/nutui')['Sticky']
NutSwiper: typeof import('@nutui/nutui')['Swiper']
NutSwiperItem: typeof import('@nutui/nutui')['SwiperItem']
NutSwitch: typeof import('@nutui/nutui')['Switch']
NutTabbar: typeof import('@nutui/nutui')['Tabbar']
NutTabbarItem: typeof import('@nutui/nutui')['TabbarItem']
NutTabPane: typeof import('@nutui/nutui')['TabPane']
NutTabs: typeof import('@nutui/nutui')['Tabs']
NutTextarea: typeof import('@nutui/nutui')['Textarea']
NutToast: typeof import('@nutui/nutui')['Toast']
NutUploader: typeof import('@nutui/nutui')['Uploader']
NutVideo: typeof import('@nutui/nutui')['Video']
Product: typeof import('./src/components/shopping-cart/product.vue')['default']
ResaurantBtn: typeof import('./src/components/restaurant-cart/resaurant-btn.vue')['default']
RestaurantCart: typeof import('./src/components/restaurant-cart/index.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Share: typeof import('./src/components/shopping-cart/share.vue')['default']
ShoppingCart: typeof import('./src/components/shopping-cart/index.vue')['default']
SvgComponent: typeof import('./src/components/SvgComponent.vue')['default']
SvgIcon: typeof import('./src/components/svg-icon/SvgIcon.vue')['default']
Tabbar: typeof import('./src/components/tabbar/index.vue')['default']
}
}

20
index.html Normal file
View File

@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta charset="utf-8" name="referrer" content="strict-origin-when-cross-origin" />
<!-- WX JS SDK 微信小程序一定要引入这个文件 可以自己去官方下载最新的-->
<script type="text/javascript" src="/js/jweixin-1.6.0.js"></script>
<script type="text/javascript" src="/js/uni.webview.1.5.6.js" defer></script>
<!-- <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<script>
var vConsole = new window.VConsole();
</script> -->
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

54
package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "vue-app-template",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vue-tsc -b && vite build",
"upload": "bash ./upload.sh",
"build-and-upload": "pnpm run build && pnpm run upload",
"preview": "vite preview",
"check-env": "node ./scripts/checkVersions.cjs"
},
"dependencies": {
"@nutui/nutui": "^4.3.13",
"@types/crypto-js": "^4.2.2",
"@types/node": "^22.10.1",
"@unocss/reset": "^66.3.2",
"axios": "^1.7.9",
"crypto-js": "^4.2.0",
"echarts": "^5.6.0",
"lucide-vue-next": "^0.525.0",
"pinia": "^2.3.0",
"pinia-plugin-persistedstate": "^4.1.3",
"semver": "^7.6.3",
"vue": "^3.5.13",
"vue-echarts": "^7.0.3",
"vue-router": "4",
"vue-virtual-draglist": "^3.3.8"
},
"devDependencies": {
"@iconify-json/carbon": "^1.2.8",
"@nutui/auto-import-resolver": "^1.0.0",
"@types/qs": "^6.9.17",
"@unocss-applet/preset-rem-rpx": "^0.9.0",
"@unocss/preset-wind": "^0.65.2",
"@vitejs/plugin-basic-ssl": "^1.2.0",
"@vitejs/plugin-vue": "^5.2.1",
"prettier": "3.4.2",
"sass-embedded": "^1.86.0",
"svg-sprite-loader": "^6.0.11",
"typescript": "~5.6.2",
"unocss": "^0.65.2",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vite": "^6.0.1",
"vue-tsc": "^2.1.10"
},
"engines": {
"node": ">=22.11.0",
"pnpm": ">=9.13.2"
},
"preinstall": "npx only-allow pnpm"
}

70
plugins/svgBuilder.ts Normal file
View File

@ -0,0 +1,70 @@
// /plugins/svgBuilder.ts
import {readFileSync, readdirSync} from 'fs'
import {join as pathJoin} from 'path'
import {Plugin} from 'vite'
let idPrefix = ''
const svgTitle = /<svg([^>+].*?)>/
const clearHeightWidth = /(width|height)="([^>+].*?)"/g
const clearFill = /fill="[^>+].*?"/g;
const hasViewBox = /(viewBox="[^>+].*?")/g
const clearReturn = /(\r)|(\n)/g
const findSvgFile = (dir: string) => {
const svgRes: string[] = []
const directory = readdirSync(dir, {withFileTypes: true})
for (const dirent of directory) {
if (dirent?.isDirectory()) {
svgRes.push(...findSvgFile(pathJoin(dir, dirent.name, '/')))
} else {
const svg = readFileSync(pathJoin(dir, dirent.name))
.toString()
.replace(clearReturn, '')
.replace(clearFill,'')
.replace(svgTitle, (_$1: string, $2: string) => {
let width = '0'
let height = '0'
let content = $2.replace(
clearHeightWidth,
(_s1, s2:string, s3:string) => {
if (s2 === 'width') {
width = s3
} else if (s2 === 'height') {
height = s3
}
return ''
}
)
if (!hasViewBox.test($2)) {
content += `viewBox="0 0 ${width} ${height}"`
}
return `<symbol id="${idPrefix}-${dirent.name.replace(
'.svg',
''
)}" ${content}>`
})
.replace('</svg>', '</symbol>')
svgRes.push(svg)
}
}
return svgRes
}
const svgBuilder = (path: string, prefix = 'icon'): Plugin => {
idPrefix = prefix
const res = findSvgFile(path)
return {
name: 'svg-transform',
transformIndexHtml(html) {
return html.replace(
'<body>',
`<body><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0">${res.join('')}</svg>`
)
}
}
}
export default svgBuilder

5693
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/images/mine/bowl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/images/shop/bath.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/images/shop/care.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

18
scripts/checkVersions.cjs Normal file
View File

@ -0,0 +1,18 @@
const semver = require('semver')
const { engines } = require('../package')// 这里的package是你的package.json文件路径
const version = engines.node
if (!semver.satisfies(process.version, version)) {
console.log(
[
'node环境变量版本错误',
'你的node环境启动位置' + process.execPath + '.',
'要求环境版本' + version,
'你的环境版本' + process.version
].join('\n')
)
process.exit(1)
}else{
console.log(`echo check environment successfully`)
}

29
src/App.vue Normal file
View File

@ -0,0 +1,29 @@
<template>
<div class="flex flex-col h-screen w-screen pb-safe">
<router-view v-slot="{ Component,route }">
<keep-alive :include="include" :exclude="excludes">
<component :is="Component" :key="route.path" class="flex-auto overflow-auto"/>
</keep-alive>
</router-view>
<router-view name="tabbar">
</router-view>
<router-view name="backButton"></router-view>
</div>
</template>
<script setup lang="ts">
import { excludes } from '@/router/keepAlive'
const include:string[] = []
// window.addEventListener('message', function(event) {
// // event.data
// const message = event.data;
// console.log('', message);
// //
// });
</script>
<style lang="scss" scoped>
</style>

62
src/api/customFetch.ts Normal file
View File

@ -0,0 +1,62 @@
/*
* @Author: HideInMatrix
* @Date: 2024-07-15
* @LastEditors: error: git config user.name & please set dead value or install git
* @LastEditTime: 2024-09-01
* @Description:
* @FilePath: /free-music-react/src/lib/customFetch.ts
*/
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
interface FetchOptions extends RequestInit {
headers?: Record<string, string>;
}
interface ApiResponse {
result?: unknown;
error?: string;
code?: number;
success?: boolean;
message?:string;
}
const apiClient = (method: HttpMethod) => {
return async (
url: string,
data?: unknown,
options: FetchOptions = {}
): Promise<ApiResponse> => {
const config: FetchOptions = {
method,
...options,
};
if (method !== "GET" && data) {
config.body = JSON.stringify(data);
} else if (method === "GET" && data) {
const _params = [];
for (const [key, value] of Object.entries(data)) {
_params.push(`${key}=${value}`);
}
url += `?${_params.join("&")}`;
}
const response = await fetch(`${url}`, config);
const result = await response.json();
if (!response.ok) {
return {
error: result.message || "Request failed",
code: response.status,
};
}
return result;
};
};
export const getRequest = apiClient("GET");
export const postRequest = apiClient("POST");
export const putRequest = apiClient("PUT");
export const deleteRequest = apiClient("DELETE");

12
src/api/encryptUrl.ts Normal file
View File

@ -0,0 +1,12 @@
// 对指定请求链接进行加密
import { AxiosRequestConfig } from "axios";
import { useUserStore } from "@/store/user";
export const handleUrl = async (config: (AxiosRequestConfig & {unEncrypt?: boolean})) => {
const userStore = useUserStore()
if(userStore.getToken){
config.headers!!["Authorization"] = `Bearer ${userStore.getToken}`
}
return config;
}

View File

@ -0,0 +1,92 @@
const baseUrl = ''
export const getMapSearch = () => {
return `http://api.tianditu.gov.cn/v2/search`
}
export const getLocationByReverseGeography = () => {
return `http://api.tianditu.gov.cn/geocoder`
}
export const getAdministrative =() => {
return `http://api.tianditu.gov.cn/v2/administrative`
}
export const getProductCategory = () => {
return `${baseUrl}/api/mall/category/list`
}
export const getMallRecommend = () => {
return `${baseUrl}/api/mall/recommend`
}
export const getUserAddressList = () => {
return `${baseUrl}/api/user/address/list`
}
export const setUserDefaultAddress = () => {
return `${baseUrl}/api/user/address/set-default`
}
export const getProductInfo = () => {
return `${baseUrl}/api/product/detail`
}
export const getNotifyList = () => {
return `${baseUrl}/api/notice/list`
}
export const getRestaurantFoodList = () => {
return `${baseUrl}/api/product/simple-list`
}
export const getRestaurantFoodDetail = () => {
return `${baseUrl}/api/meal/product/detail`
}
export const getRestaurantFoodSet = () => {
return `${baseUrl}/api/product/setmeal-list`
}
export const getLessonCategory = () => {
return `${baseUrl}/api/lesson/category`
}
export const getLessonList = () => {
return `${baseUrl}/api/lesson/list`
}
export const getCourseDetail = () => {
return `${baseUrl}/api/course/detail`
}
export const getLessonCatalog = () => {
return `${baseUrl}/api/lesson/catalog`
}
export const getNursingHomeList = () => {
return `${baseUrl}/api/nursing-home/list`
}
export const getNursingDetail = () => {
return `${baseUrl}/api/nursing/detail`
}
export const getTravelList = () => {
return `${baseUrl}/api/travel/list`
}
export const getHospitalCompanionList = () =>{
return `${baseUrl}/api/nursing/hospital-companion/list`
}
export const getServiceDetail = () =>{
return `${baseUrl}/api/service/detail`
}
export const getHousekeepingCleaningList = () =>{
return `${baseUrl}/api/housekeeping/cleaning/list`
}
export const getHousekeepingCleaningDetail = () =>{
return `${baseUrl}/api/housekeeping/cleaning/detail`
}

88
src/auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,88 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const createPinia: typeof import('pinia')['createPinia']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const effectScope: typeof import('vue')['effectScope']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

View File

@ -0,0 +1,18 @@
<template>
<div class="block">
<i v-html="content"></i>
</div>
</template>
<script setup lang="ts">
defineProps({
content:{
type:String,
default: ""
}
})
</script>

View File

@ -0,0 +1,27 @@
<template>
<nut-drag direction="y" attract :style="[props.bottom ? {bottom:props.bottom} : {top:props.top}]" class="rounded-full bg-[#fff] border-solid border-black border-[3rpx] left-[30rpx]" style="z-index: 999 !important;">
<ChevronLeft class="w-[88rpx] h-[88rpx] text-black" :stroke-width="2" @click="goBack"/>
</nut-drag>
</template>
<script lang="ts" setup>
import { ChevronLeft } from 'lucide-vue-next';
import { useRouter } from 'vue-router';
const props = defineProps({
bottom:{
type:String,
default:""
},
top:{
type:String,
default:"8vh"
}
})
const router = useRouter()
const goBack = () => {
router.back();
}
</script>

View File

@ -0,0 +1,110 @@
<template>
<nut-action-sheet v-model:visible="innerShow">
<div class="flex flex-col bg-[#F5F5F5]">
<div class="relative py-[32rpx] text-center bg-white">
<div class="text-[36rpx] font-500">选择收货地址</div>
<div class="w-[60rpx] h-[60rpx] bg-[#f5f5f5] rounded-full absolute top-[20rpx] right-[20rpx] flex items-center justify-center" @click="handleClose">
<X class="text-[25rpx]" />
</div>
</div>
<div class="px-[30rpx] py-[20rpx] min-h-0 flex-auto overflow-auto">
<div
v-for="(item, index) in recvAddrList"
:key="index"
class="items-center justify-between h-[210rpx] rounded-[24rpx] bg-white pt-[30rpx] pl-[30rpx] pb-[22rpx] pr-[34rpx] flex mt-[30rpx]"
:class="{ 'active-address': item.isDefault }"
@click="handleSelectAddress(item)">
<div class="flex flex-col justify-evenly flex-1 h-full">
<div class="text-[36rpx] font-600">{{ item.fullAddress }}</div>
<div class="text-[32rpx] text-[#333] font-400 flex items-center mt-[14rpx]">
<div class="mr-[20rpx]">{{ item.name }}</div>
<div>{{ item.phone }}</div>
</div>
</div>
<div class="h-[125rpx] py-[12rpx] mr-[30rpx]">
<div class="border-r-[#CCCCCC] border-dashed border-r-[2rpx] h-full w-[1rpx]"></div>
</div>
<div class="flex flex-col items-center" @click="handleEditAddress(item)">
<PencilLine class="w-[48rpx] h-[48rpx] text-[#29bebb]" />
<div class="text-[#28BEBB] text-[32rpx] font-400 mt-[8rpx]">修改</div>
</div>
<Check class="text-[#fff] absolute bottom-[-10rpx] right-[-5rpx] font-700" v-if="item.isDefault" />
</div>
</div>
<div class="px-[30rpx] py-[16rpx] bg-white">
<div class="pb-safe">
<div class="flex items-center justify-center bg-[#28BEBB] rounded-[20rpx] px-[80rpx] py-[28rpx]" @click="toAddAddressPage">
<CirclePlus class="text-white mr-[16rpx]" fill="#fff" stroke="#29bebb" :stroke-width="2"/>
<span class="text-white">新增收货地址</span>
</div>
</div>
</div>
</div>
</nut-action-sheet>
</template>
<script lang="ts" setup>
import { useUserStore } from "@/store/user";
import { X, PencilLine, CirclePlus, Check } from "lucide-vue-next";
const props = defineProps({
show: {
type: Boolean,
default: false,
},
});
const innerShow = computed({
get() {
return props.show;
},
set(value) {
emit("update:show", value);
},
});
const userStore = useUserStore();
const recvAddrList = computed(() => userStore.recvAddrList);
const router = useRouter();
const emit = defineEmits(["update:show"]);
const handleEditAddress = (item: any) => {
router.push({ path: "/mine/address", query: { id: item.id } });
};
const toAddAddressPage = () => {
router.push({ path: "/mine/add/address" });
};
const handleSelectAddress = (item: any) => {
userStore.setDefaultAddress(item);
};
const handleClose = () => {
emit("update:show", false);
};
</script>
<style lang="scss" scoped>
.active-address {
outline: 2px solid #000000;
position: relative;
& ::before {
content: "";
position: absolute;
width: 0;
height: 0;
right: 0;
bottom: 0;
border: 15px solid;
border-color: transparent #000 #000 transparent;
border-bottom-right-radius: 10px;
}
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<nut-action-sheet v-model:visible="innerShow">
<div class="flex flex-col bg-[#F5F5F5]">
<div class="relative py-[32rpx] text-center bg-white">
<div class="text-[36rpx] font-500">选择被护理人</div>
<div class="w-[60rpx] h-[60rpx] bg-[#f5f5f5] rounded-full absolute top-[20rpx] right-[20rpx] flex items-center justify-center" @click="handleClose">
<X class="text-[25rpx]" />
</div>
</div>
<div class="px-[30rpx] py-[20rpx] min-h-0 flex-auto overflow-auto">
<div
v-for="(item, index) in recvAddrList"
:key="index"
class="items-center justify-between h-[210rpx] rounded-[24rpx] bg-white p-[30rpx] flex mt-[30rpx]"
:class="{ 'active-address': item.isDefault }"
@click="handleSelectAddress(item)">
<div class="flex flex-col justify-evenly flex-1 h-full text-[32rpx] font-400 text-[#333]">
<div class="flex items-center mt-[14rpx]">
<div class="w-[72rpx] h-[38rpx] rounded-[10rpx] bg-[#e8f2ff] text-[26rpx] text-[#1D86FF] flex items-center justify-center mr-[10rpx]" v-if="item.isDefault"></div>
<div class="mr-[20rpx]">{{ item.name }}</div>
</div>
<div>{{ item.phone }}</div>
</div>
<div class="h-[125rpx] py-[12rpx] mr-[30rpx]">
<div class="border-r-[#CCCCCC] border-dashed border-r-[2rpx] h-full w-[1rpx]"></div>
</div>
<div class="flex flex-col items-center" @click="handleEditAddress(item)">
<PencilLine class="w-[48rpx] h-[48rpx] text-[#29bebb]" />
<div class="text-[#28BEBB] text-[32rpx] font-400 mt-[8rpx]">修改</div>
</div>
<Check class="text-[#fff] absolute bottom-[-10rpx] right-[-5rpx] font-700" v-if="item.isDefault" />
</div>
</div>
<div class="px-[30rpx] py-[16rpx] bg-white">
<div class="pb-safe">
<div class="flex items-center justify-center bg-[#28BEBB] rounded-[20rpx] px-[80rpx] py-[28rpx]" @click="toAddAddressPage">
<CirclePlus class="text-white mr-[16rpx] size-[54rpx]" fill="#fff" stroke="#29bebb" :stroke-width="2"/>
<span class="text-white text-[44rpx]">新增被护理人</span>
</div>
</div>
</div>
</div>
</nut-action-sheet>
</template>
<script lang="ts" setup>
import { useUserStore } from "@/store/user";
import { X, PencilLine, CirclePlus, Check } from "lucide-vue-next";
const props = defineProps({
show: {
type: Boolean,
default: false,
},
});
const innerShow = computed({
get() {
return props.show;
},
set(value) {
emit("update:show", value);
},
});
const userStore = useUserStore();
const recvAddrList = ref([
{ name: "王大力(女,67岁)", phone: "310108196005127765", isDefault: true },
{ name: "王小力(女,67岁)", phone: "310108196005127765", isDefault: false },
]);
const router = useRouter();
const emit = defineEmits(["update:show"]);
const handleEditAddress = (item: any) => {
router.push({ path: "/mine/add/paramedic", query: { id: item.id } });
};
const toAddAddressPage = () => {
router.push({ path: "/mine/add/paramedic" });
};
const handleSelectAddress = (item: any) => {
recvAddrList.value = recvAddrList.value.map(item => {
if (item.isDefault) {
item.isDefault = false;
}
return item;
});
item.isDefault = true;
// userStore.setDefaultAddress(item);
};
const handleClose = () => {
emit("update:show", false);
};
</script>
<style lang="scss" scoped>
.active-address {
outline: 2px solid #000000;
position: relative;
& ::before {
content: "";
position: absolute;
width: 0;
height: 0;
right: 0;
bottom: 0;
border: 15px solid;
border-color: transparent #000 #000 transparent;
border-bottom-right-radius: 10px;
}
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<nut-action-sheet v-model:visible="innerShow">
<div class="flex flex-col bg-[#F5F5F5]">
<div class="relative py-[32rpx] text-center bg-white">
<div class="text-[36rpx] font-500">选择服务时长</div>
<div class="w-[60rpx] h-[60rpx] bg-[#f5f5f5] rounded-full absolute top-[20rpx] right-[20rpx] flex items-center justify-center" @click="handleClose">
<X class="text-[25rpx]" />
</div>
</div>
<div class="px-[30rpx] py-[20rpx] min-h-0 flex-auto overflow-auto">
<nut-radio-group v-model="chooseTime" class="flex time-radio-group" direction="horizontal">
<nut-radio v-for="item in timeList" :key="item.value" :label="item.value" shape="button">{{ item.label }}</nut-radio>
</nut-radio-group>
<div class="text-[32rpx] text-[#999AA3] mt-[20rpx]">2小时适合面积50平米房屋</div>
</div>
<div class="px-[30rpx] py-[16rpx] bg-white">
<div class="pb-safe">
<div class="flex items-center justify-center bg-[#28BEBB] rounded-[20rpx] px-[80rpx] py-[28rpx]" @click="submit">
<span class="text-white">确定</span>
</div>
</div>
</div>
</div>
</nut-action-sheet>
</template>
<script lang="ts" setup>
import { X } from "lucide-vue-next";
const props = defineProps({
show: {
type: Boolean,
default: false,
},
});
const innerShow = computed({
get() {
return props.show;
},
set(value) {
emit("update:show", value);
},
});
const chooseTime = ref(2);
const timeList = [
{
label: "2小时",
value: 2,
},
{
label: "3小时",
value: 3,
},
{
label: "4小时",
value: 4,
},
{
label: "5小时",
value: 5,
},
{
label: "6小时",
value: 6,
},
];
const emit = defineEmits(["update:show",'onGetTime']);
const submit = () => {
handleClose();
emit("onGetTime",chooseTime.value)
};
const handleClose = () => {
emit("update:show", false);
};
</script>
<style lang="scss" scoped>
.active-address {
outline: 2px solid #000000;
position: relative;
& ::before {
content: "";
position: absolute;
width: 0;
height: 0;
right: 0;
bottom: 0;
border: 15px solid;
border-color: transparent #000 #000 transparent;
border-bottom-right-radius: 10px;
}
}
.time-radio-group {
--nut-radio-button-padding: 13px 32px;
--nut-radio-button-border-radius: 5px;
--nut-radio-label-font-active-color:#0AB3B0;
--nut-radio-label-button-border-color: #28BEBB;
--nut-radio-label-button-background: #e9f8f8;
--nut-radio-button-font-size:18px;
:deep(.nut-radio__button){
background-color: #fff;
}
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<nut-list v-if="carte.length > 0" :list-data="carte" @scroll-bottom="onScrollBottom" class="mt-[30rpx] flex-1 min-h-0 overflow-auto">
<template #default="{ item }">
<div class="bg-white mx-[30rpx] rounded-[20rpx] flex mb-[30rpx]">
<img :src="item.cover" alt="food" class="h-[176rpx] aspect-square rounded-[20rpx_0_0_20rpx] object-cover" />
<div class="px-[20rpx] flex-auto flex flex-col justify-between">
<p class="text-[40rpx] font-500">{{ item.name }}</p>
<p class="text-[30rpx] font-400 text-[#666] mb-[26rpx] mt-[10rpx]">{{ item.desc }}</p>
<div class="flex items-center justify-between">
<div class="flex items-baseline">
<nut-price :price="item.price" :decimal-places="2" size="normal" class="font-600" />
<span class="text-[28rpx] font-400 text-[#adafaf]">/</span>
</div>
<nut-input-number v-model="item.count" :readonly="true" @reduce="(event) => handleMinus(event, item)" @add="handlePlus(item)" :min="0">
<template #left-icon>
<div class="w-[52rpx] h-[52rpx] bg-white rounded-full flex items-center justify-center border-[#28BEBB] border-[4rpx] border-solid">
<Minus :stroke-width="3" class="text-[#28BEBB]" />
</div>
</template>
<template #right-icon>
<div class="w-[52rpx] h-[52rpx] bg-[#28BEBB] rounded-full flex items-center justify-center">
<Plus :stroke-width="3" class="text-white" />
</div>
</template>
</nut-input-number>
</div>
</div>
</div>
</template>
</nut-list>
<div v-else class="flex-1 min-h-0 overflow-auto flex items-center justify-center">
<p class="text-[32rpx] font-500">暂无商品</p>
</div>
</template>
<script lang="ts" setup>
import { Plus, Minus } from "lucide-vue-next";
import { useShoppingCartStore } from "@/store/shoppingCart";
const shoppingCartStore = useShoppingCartStore();
const props = defineProps({
carte: {
type: Array as PropType<any[]>,
default: () => [],
},
});
const onScrollBottom = () => {
console.log("onScrollBottom");
};
const handleMinus = (event: any, item: any) => {
const orderCount = Number(item.count) - 1;
if(orderCount <= 0){
shoppingCartStore.removeShoppingCart(item.id);
}
};
const handlePlus = (item: any) => {
// console.log("handlePlus", item);
};
</script>
<style scoped lang="scss">
.nut-input-number {
--nut-inputnumber-input-background-color: #fff;
--nut-inputnumber-input-margin: 0;
--nut-inputnumber-input-font-size: 18px;
--nut-inputnumber-input-width: 20px;
}
</style>

View File

@ -0,0 +1,78 @@
<template>
<div class="h-[120rpx] rounded-full bg-[#25262A] flex">
<nut-badge :value="shoppingCartTotal" top="10" right="5" @click="showShoppingCart">
<img src="/images/checkout/basket.png" alt="" class="w-[100rpx] h-[100rpx] ml-[30rpx] my-[10rpx]" />
</nut-badge>
<div class="flex-1 min-w-0 flex flex-col justify-center ml-[6rpx]">
<nut-price :price="shoppingCartTotalPrice" size="normal" class="font-700 custom-price mb-[6rpx]" />
<div class="flex items-center text-white text-[26rpx]">
<div class="mr-[10rpx]">含运费 ¥{{ shippingFee }}</div>
<div>打包费 ¥{{ packingFee }}</div>
</div>
</div>
<div class="checkout-button w-[224rpx] relative text-white flex items-center justify-center" @click="handleCheckoutBtn">
<div class="text-white z-2 text-[40rpx] font-600">去结算</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { packingFee, shippingFee } from "@/composables/useRestaurant";
import { useShoppingCartStore } from "@/store/shoppingCart";
const shoppingCartStore = useShoppingCartStore();
const shoppingCartTotal = computed(() => {
return shoppingCartStore.getShoppingCartByRestaurant.reduce((total, item) => total + Number(item.count), 0);
});
const shoppingCartTotalPrice = computed(() => {
return shoppingCartStore.getShoppingCartByRestaurant.reduce((total, item) => total + item.price * item.count, 0);
});
const emit = defineEmits(["showShoppingCart","handleCheckoutBtn"]);
const showShoppingCart = () => {
emit("showShoppingCart");
};
const handleCheckoutBtn = () => {
emit("handleCheckoutBtn");
};
</script>
<style scoped lang="scss">
.custom-price {
--nut-price-symbol-medium-size: 18px;
--nut-price-decimal-medium-size: 18px;
--nut-price-medium-size: 24px;
--nut-primary-color: #fff;
}
.checkout-button {
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 50px;
bottom: 0;
border-radius: 20px 100% 100% 20px;
background: #f68e1d;
transform: skewX(-15deg);
z-index: 1;
}
&::before {
content: "";
position: absolute;
top: 0;
right: 0;
border-radius: 0 113px 113px 0;
width: 80%;
height: 100%;
background: #f68e1d;
z-index: 1;
}
}
</style>

View File

@ -0,0 +1,114 @@
<template>
<div>
<CheckoutBtn @showShoppingCart="handleShowShoppingCart" @handleCheckoutBtn="handleShowCheckoutCart" />
<nut-action-sheet v-model:visible="show">
<div class="flex flex-col pb-safe max-h-[60vh] min-h-[60vh]">
<div class="bg-[#F5F5F5] px-[30rpx] py-[18rpx] flex items-center">
<span class="text-[#666666] text-[30rpx] font-400">已选菜品</span>
<div class="text-[#333] font-400 text-[30rpx]">
(含运费&nbsp;
<span class="text-[#F03B25] text-[30rpx] font-400">¥{{ shippingFee }}</span>
&nbsp;打包费&nbsp;¥<span class="text-[#F03B25] text-[30rpx] font-400">{{ packingFee }}</span>)
</div>
<div class="flex items-center ml-auto">
<Trash2 :stroke-width="1.5" class="w-[32rpx] h-[32rpx] text-[#ccc]"/>
<span class="text-[#666] font-400 text-[32rpx]" @click="handleClearCart"></span>
</div>
</div>
<Carte :carte="shoppingCartList" />
<div class="bg-white px-[30rpx] pb-[16rpx]">
<CheckoutBtn />
</div>
</div>
</nut-action-sheet>
<nut-action-sheet v-model:visible="showCheckoutCart">
<div class="flex flex-col pb-safe max-h-[80vh] min-h-[60vh]">
<div class="flex items-center w-full px-[30rpx] py-[30rpx] bg-[#F5F5F5]" @click="handleShowChooseLocation">
<img src="/images/checkout/location.png" alt="location" class="w-[92rpx] h-[92rpx]">
<div class="flex flex-col ml-[20rpx] flex-auto">
<p class="text-[36rpx] font-600">{{ defaultAddr?.fullAddress }}</p>
<div class="flex items-center text-[32rpx] text-[#333] mt-[14rpx]">
<span class="mr-[20rpx]">{{ defaultAddr?.name }}</span>
<span>{{ defaultAddr?.phone }}</span>
</div>
</div>
<ChevronRight :stroke-width="1.5" class="w-[60rpx] h-[60rpx] ml-auto"/>
</div>
<Carte :carte="shoppingCartList" class="px-[30rpx]"/>
<div class="mx-[30rpx] border-b-dashed border-b-[#D8D8D8] border-b-[2rpx] mb-[40rpx] mt-[20rpx]"></div>
<div class="flex flex-col items-center px-[30rpx]">
<div class="flex items-center justify-between w-full">
<div class="text-[#3d3d3d] text-[30rpx] font-400">打包费</div>
<div class="text-[#F03B25] text-[30rpx] font-400">¥{{ packingFee }}</div>
</div>
<div class="flex items-center justify-between w-full mt-[20rpx] mb-[40rpx]">
<div class="text-[#3d3d3d] text-[30rpx] font-400">运费</div>
<div class="text-[#F03B25] text-[30rpx] font-400">¥{{ shippingFee }}</div>
</div>
</div>
<div class="bg-white px-[30rpx] pb-[16rpx]">
<CheckoutBtn @handleCheckoutBtn="toCheckout"/>
</div>
</div>
</nut-action-sheet>
<ChooseLocation v-model:show="showChooseLocation" />
</div>
</template>
<script lang="ts" setup>
import { packingFee, shippingFee } from "@/composables/useRestaurant";
import { Trash2, ChevronRight } from "lucide-vue-next";
import CheckoutBtn from "./checkout-btn.vue";
import { useShoppingCartStore } from "@/store/shoppingCart";
import Carte from "./carte.vue";
import { useUserStore } from "@/store/user";
import ChooseLocation from "@/components/checkout/chooseLocation.vue";
import { useRouter } from "vue-router";
import { CartTypeEnum } from "@/types/cart";
const router = useRouter();
const userStore = useUserStore();
const { recvAddrList } = storeToRefs(userStore);
const defaultAddr = computed(() => {
return recvAddrList.value.find((item:any) => item.isDefault);
});
const shoppingCartStore = useShoppingCartStore();
const shoppingCartList = computed(() => {
return shoppingCartStore.getShoppingCartByRestaurant;
});
const show = ref(false);
const showCheckoutCart = ref(false);
const handleShowShoppingCart = () => {
show.value = true;
};
const handleShowCheckoutCart = () => {
showCheckoutCart.value = true;
};
const showChooseLocation = ref(false);
const handleShowChooseLocation = () => {
showChooseLocation.value = true;
};
const toCheckout = () => {
router.push({ path: "/checkout/payment", query: { type: CartTypeEnum.Restaurant } });
};
const handleClearCart = () => {
shoppingCartStore.clearShoppingCartByType(CartTypeEnum.Restaurant);
};
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,77 @@
<template>
<ul class="px-[30rpx] bg-[#f5f5f5] flex-auto w-full min-h-0 overflow-auto pb-[20rpx]">
<li class="p-[20rpx] rounded-[20rpx] bg-white w-full mt-[30rpx]" v-for="(item, index) in shoppingCartList" :key="index">
<div class="flex">
<img :src="item.cover" alt="cover" class="w-[192rpx] h-[192rpx] rounded-[20rpx]" />
<div class="flex flex-col ml-[16rpx] justify-between">
<div class="text-[40rpx] font-500">{{ item.name }}</div>
<div class="text-[30rpx] text-[#666] font-400 mt-[10rpx]">{{ item.desc }}</div>
</div>
</div>
<div class="flex justify-between items-center mt-[30rpx]">
<div class="text-[32rpx] text-[#333] font-400">产品单价</div>
<nut-price :price="item.price" size="normal" class="text-[36rpx] font-500" />
</div>
<div class="flex justify-between items-center mt-[22rpx]">
<div class="text-[32rpx] text-[#333] font-400">购买数量</div>
<nut-input-number v-model="item.count" readonly :min="0" @overlimit="(event, type) => handleOverLimit(event, type, item)"/>
</div>
</li>
</ul>
<div class="flex pl-[24rpx] py-[16rpx] pr-[30rpx] items-center justify-between">
<nut-price :price="totalPrice" size="large" class="font-500" />
<div class="w-[416rpx] h-[100rpx] rounded-[20rpx] bg-[#28BEBB] text-white text-[44rpx] font-600 flex items-center justify-center" @click="toPayment"></div>
</div>
</template>
<script lang="ts" setup>
import { useShoppingCartStore } from "@/store/shoppingCart";
import { CartTypeEnum } from "@/types/cart";
import { useRouter } from "vue-router";
const router = useRouter();
const shoppingCartStore = useShoppingCartStore();
const shoppingCartList = ref(shoppingCartStore.getShoppingCartByMall);
const totalPrice = computed(() => {
return shoppingCartList.value.reduce((acc, item) => acc + item.price * item.count, 0);
});
const toPayment = () => {
router.push({ path: "/checkout/payment", query: { type: CartTypeEnum.Mall } });
};
const handleOverLimit = (_event:any,type:string,item:any) => {
if(type === "reduce"){
shoppingCartStore.removeShoppingCart(item.id);
}
};
</script>
<style lang="scss" scoped>
:deep(.nut-input-number) {
--nut-inputnumber-height: 32px;
--nut-inputnumber-input-margin: 0 2px;
--nut-inputnumber-input-border-radius: 0;
& .nut-input-number__icon {
height: 32px;
width: 32px;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
}
& :nth-child(1) {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
& :nth-child(3) {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
</style>

View File

@ -0,0 +1,40 @@
<template>
<nut-action-sheet v-model:visible="innerShow" class="share-action">
<div class="flex flex-col relative pt-[20rpx] px-[20rpx] pb-[42rpx] bg-gradient-to-b from-[#28BEBB] to-[#28BEBB00] rounded-[40rpx_40rpx_0_0]">
<img src="/images/product/share-title.png" alt="share-title" class="w-[276rpx] h-[90rpx] absolute top-[-10%] left-[36rpx]" />
<div class="rounded-full bg-white w-[60rpx] h-[60rpx] flex items-center justify-center self-end" @click="handleClose">
<X />
</div>
<div class="m-[34rpx] flex items-center">
<img src="/images/product/share-wx.png" alt="share-qrcode" class="w-[320rpx] h-[284rpx]" />
<img src="/images/product/share-friend.png" alt="share-friend" class="w-[320rpx] h-[284rpx]" />
</div>
</div>
</nut-action-sheet>
</template>
<script lang="ts" setup>
import { X } from "lucide-vue-next";
const props = defineProps({
show: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(["update:show"]);
const innerShow = computed({
get() {
return props.show;
},
set(value) {
emits("update:show", value);
},
});
const handleClose = () => {
emits("update:show", false);
};
</script>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
export interface SvgIconProps {
name: string
}
const props = defineProps<SvgIconProps>()
const styles = useCssModule()
const attrs = useAttrs()
const iconName = computed(() => `#icon-${props.name}`);
const svgClass = computed(() => {
const className = [styles['svg-icon']]
if (props.name) className.push(`icon-${props.name}`)
return className
})
</script>
<template>
<svg :class="svgClass" v-bind="attrs">
<use :xlink:href="iconName"></use>
</svg>
</template>
<style module>
.svg-icon {
width: 1em;
height: 1em;
fill: currentColor;
vertical-align: middle;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,10 @@
import SvgIcon from './SvgIcon.vue'
import type {SvgIconProps} from './SvgIcon.vue'
export {
SvgIcon
}
export type {
SvgIconProps
}

View File

@ -0,0 +1,49 @@
<template>
<nut-tabbar v-model="activeIndex" @tab-switch="tabSwitch" class="">
<nut-tabbar-item v-for="(item,index) in menus" :key="index" :to="`${item.href}`">
<template #icon="props">
<div class="text-[36rpx] font-500 pt-[36rpx] mt-[8rpx]" :class="props.active ? 'active-bg w-[104rpx] h-[86rpx] text-[40rpx]':''">{{ item.name }}</div>
</template>
</nut-tabbar-item>
</nut-tabbar>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useRoute } from "vue-router";
const route = useRoute()
const activeIndex = ref(0);
const menus = [{name:'首页',href:'/home'},{name:'健康',href:"/medical"},{name:'商城',href:'/shop'},{name:'我的',href:'/mine'}]
// const routeQuery = Object.entries(route.query).map(([key, value]) => `${key}=${value}`).join("&");
const tabSwitch = (item: Record<string, unknown>, index: number) => {
console.log(item, index);
};
const useMatchPath = () => {
const path = route.path;
activeIndex.value = menus.findIndex(item => item.href === path)
}
useMatchPath();
</script>
<style lang="scss" scoped>
.active-bg{
background-image: url("/images/tabbar/menu-active.png");
background-position: center;
background-repeat: no-repeat;
background-size: 100% 100%;
color: #fff;
}
:deep(.nut-badge){
height: 100%;
}
</style>
<style>
:root{
--nut-tabbar-border-bottom:0px solid;
}
</style>

View File

@ -0,0 +1,24 @@
import { getRequest } from "@/api/customFetch";
import { getLocationByReverseGeography } from "@/api/interfaceDocument";
import { mapKey } from "@/utils/useMap";
import { useUserStore } from "@/store/user";
import { Address } from "@/types/tdt";
export const useLocationName = (longitude: string, latitude: string) => {
const userStore = useUserStore();
userStore.setCoordinate(longitude, latitude);
userStore.setDeviceCoordinate(longitude, latitude);
getRequest(getLocationByReverseGeography(), {
postStr: encodeURIComponent(JSON.stringify({ lon: longitude, lat: latitude, ver: 1 })),
type: "geocode",
tk: mapKey,
}).then((resp) => {
let _result = resp.result as {
addressComponent:Address;
};
const address = _result.addressComponent.town + _result.addressComponent.road;
userStore.setAddress(address);
userStore.setDeviceAddress({ ..._result.addressComponent });
});
};

View File

@ -0,0 +1,46 @@
import { getRequest } from "@/api/customFetch";
import { getCourseDetail, getLessonCatalog, getLessonCategory, getLessonList } from "@/api/interfaceDocument";
export const useGetLessonCategory = () => {
const lessonCategory = ref<any[]>([]);
getRequest(getLessonCategory()).then(resp => {
if(resp.code === 200){
lessonCategory.value = (resp.result as {categories:any[]}).categories;
}
})
return { lessonCategory }
}
export const useGetLessonList = () => {
const lessonList = ref<any[]>([]);
getRequest(getLessonList()).then(resp => {
if(resp.code === 200){
lessonList.value = (resp.result as {items:any[]}).items;
}
})
return { lessonList }
}
export const useGetCourseDetail = () => {
const courseDetail = ref<any>({});
getRequest(getCourseDetail()).then(resp => {
if(resp.code === 200){
courseDetail.value = resp.result;
}
})
return { courseDetail }
}
export const useGetCourseCatalog = () => {
const courseCatalog = ref<any[]>([]);
getRequest(getLessonCatalog()).then(resp => {
if(resp.code === 200){
courseCatalog.value = (resp.result as {chapters:any[]}).chapters;
}
})
return { courseCatalog }
}

View File

@ -0,0 +1,11 @@
import { getRequest } from "@/api/customFetch";
import { getServiceDetail } from "@/api/interfaceDocument";
export const useGetServiceDetail = () => {
const serviceDetail = ref<any>(null);
getRequest(getServiceDetail()).then(res => {
serviceDetail.value = res.result;
});
return { serviceDetail };
}

View File

@ -0,0 +1,22 @@
import { getHousekeepingCleaningList,getHousekeepingCleaningDetail } from "@/api/interfaceDocument";
import { getRequest } from "@/api/customFetch";
export const useGetHousekeepingCleaningList = () => {
const cleaningList = ref<any[]>([]);
getRequest(getHousekeepingCleaningList()).then((res) => {
cleaningList.value = res.result as any[]
})
return {
cleaningList
}
}
export const useGetHousekeepingCleaningDetail = () => {
const cleaningDetail = ref<any>({});
getRequest(getHousekeepingCleaningDetail()).then((res) => {
cleaningDetail.value = res.result as any
})
return {
cleaningDetail
}
}

View File

@ -0,0 +1,14 @@
import { getRequest } from "@/api/customFetch"
import { getNotifyList } from "@/api/interfaceDocument"
export const useGetNoticeList = () => {
const noticeList = ref<any[]>([])
getRequest(getNotifyList()).then(resp => {
if(resp.code === 200){
noticeList.value = (resp.result as {list:any[]}).list
}
})
return { noticeList }
}

View File

@ -0,0 +1,22 @@
import { getNursingHomeList, getNursingDetail } from "@/api/interfaceDocument";
import { getRequest } from "@/api/customFetch";
export const useNursingHomeList = () => {
const nursingHomeList = ref<any[]>([]);
getRequest(getNursingHomeList()).then(resp => {
if(resp.code === 200){
nursingHomeList.value = (resp.result as { list: any[] }).list;
}
});
return {nursingHomeList}
}
export const useNursingDetail = () => {
const nursingDetail = ref<any>({});
getRequest(getNursingDetail()).then(resp => {
if(resp.code === 200){
nursingDetail.value = (resp.result as { data: any }).data;
}
});
return { nursingDetail };
}

View File

@ -0,0 +1,37 @@
import { getRequest } from "@/api/customFetch";
import { getRestaurantFoodDetail, getRestaurantFoodList, getRestaurantFoodSet } from "@/api/interfaceDocument";
export const foodList = ref<any[]>([]);
export const packingFee = ref(0);
export const shippingFee = ref(0);
export const useRestaurantFoodList = () => {
getRequest(getRestaurantFoodList()).then((resp) => {
if (resp.code === 200) {
foodList.value = (resp.result as { items: any[] }).items;
packingFee.value = (resp.result as { packingFee: number }).packingFee;
shippingFee.value = (resp.result as { shippingFee: number }).shippingFee;
}
});
};
export const foodSetList = ref<any[]>([]);
export const useRestaurantFoodSet = () => {
getRequest(getRestaurantFoodSet()).then((resp) => {
if (resp.code === 200) {
foodSetList.value = (resp.result as { items: any[] }).items;
}
});
};
export const useRestaurantFoodDetail = () => {
const foodDetail = ref<any>({});
getRequest(getRestaurantFoodDetail()).then((resp) => {
if (resp.code === 200) {
foodDetail.value = resp.result;
}
});
return {foodDetail}
};

View File

@ -0,0 +1,16 @@
import { getRequest } from "@/api/customFetch";
import { getTravelList } from "@/api/interfaceDocument";
export const useTravelList = () => {
const travelList = ref<any[]>([]);
const travelCategory = ref<any[]>([]);
getRequest(getTravelList()).then((resp) => {
if (resp.code === 200) {
travelList.value = (resp.result as { data:{products: any[]} }).data.products;
travelCategory.value = (resp.result as {data: {categories: any[]} }).data.categories.map((item,index) => ({title:item,paneKey:index.toString()}));
}
});
return { travelList,travelCategory };
};

View File

@ -0,0 +1,27 @@
import { getUserAddressList } from "@/api/interfaceDocument"
import { getRequest } from "@/api/customFetch"
import { useUserStore } from "@/store/user"
const userStore = useUserStore();
export const address = ref<any[]>([])
export const defaultAddress = ref(1);
export const useMatchDefault = () => {
const matchAddress = address.value.find(item => item.isDefault);
if(matchAddress){
defaultAddress.value = matchAddress.id
}else{
defaultAddress.value = 1
}
}
export const useGetUserAddressList = () => {
getRequest(getUserAddressList()).then(resp => {
if(resp.code === 200){
address.value = (resp.result as {list:any[]}).list
useMatchDefault()
userStore.setRecvAddrList(address.value)
}
})
}

16
src/main.ts Normal file
View File

@ -0,0 +1,16 @@
import { createApp } from 'vue'
import '@unocss/reset/tailwind-compat.css'
import './style.css'
import 'uno.css';
import App from './App.vue'
// Pinia 持久化
import store from '@/store/index'
//导入路由
import router from '@/router/index'
const app = createApp(App)
// 注册已经加上了持久化的pinia
app.use(store).use(router)
app.mount('#app')

46
src/router/index.ts Normal file
View File

@ -0,0 +1,46 @@
import { createRouter, createWebHistory } from "vue-router";
import { publicRoutes } from "./publicRoutes";
import { privateRoutes } from "./privateRoutes";
function getRoutes() {
const routes = [ // 私有路由,请在这里添加
...privateRoutes,
// 公共路由
...publicRoutes,
];
/**
* routes
*/
return routes;
}
const router = createRouter({
history: createWebHistory(),
routes: getRoutes(),
});
// 全局前置守卫,这边可以对身份进行验证
router.beforeEach((to, _from, next) => {
let userRole = "admin";
// 如果目标路由没有角色限制
if (!to.meta.role) {
next();
}
// 判断当前用户角色是否在目标路由的允许角色列表中
if ((to.meta.role as string[]).includes(userRole)) {
// 如果角色匹配,允许进入目标路由
next();
} else {
// 如果角色不匹配,跳转到 unauthorized 页面
next({ path: "/unauthorized" });
}
});
// 监听路由变化,动态设置网页标题
router.afterEach((to) => {
if (to.meta.title) {
document.title = to.meta.title as string;
}
});
export default router;

13
src/router/keepAlive.ts Normal file
View File

@ -0,0 +1,13 @@
import { ComponentInternalInstance, ref } from 'vue'
export const excludes = ref<string[]>([])
export function removeKeepAliveCache(instance: ComponentInternalInstance) {
if (!excludes.value.includes(instance.type.name!)) {
excludes.value.push(instance.type.name!)
}
}
export function resetKeepAliveCache(instance: ComponentInternalInstance) {
excludes.value = excludes.value.filter((item) => item !== instance.type.name)
}

View File

@ -0,0 +1,3 @@
import { RouteRecordRaw } from "vue-router";
export const privateRoutes:RouteRecordRaw[] = [];

317
src/router/publicRoutes.ts Normal file
View File

@ -0,0 +1,317 @@
import { RouteRecordRaw } from "vue-router";
export const publicRoutes:RouteRecordRaw[] = [
{
path: "/home",
name: "home",
meta: {
title: "康乐云家",
},
components: {
default: () => import("../views/home/index.vue"),
tabbar: () => import("../components/tabbar/index.vue")
},
},
{
path: "/location/map",
name: "map",
meta: {
title: "新增地址",
},
components: {default:() => import("../views/location/map.vue"),backButton:() => import("../components/back-button/index.vue")},
},
{
path: "/location/administrative",
name: "administrative",
meta: {
title: "选择城市",
},
components: {default:() => import("../views/location/administrative.vue")},
},
{
path: "/medical",
name: "medical",
meta: {
title: "康乐云家",
},
components: {
default: () => import("../views/medical/index.vue"),
tabbar: () => import("../components/tabbar/index.vue")
},
},
{
path: "/shop",
name: "shop",
meta: {
title: "康乐云家",
},
components: {
default: () => import("../views/shop/index.vue"),
tabbar: () => import("../components/tabbar/index.vue")
},
},
{
path: "/shop/product",
name: "product",
meta: {
title: "商品详情",
},
components: {default:() => import("../views/shop/productInfo.vue"),tabbar: () => import("../components/back-button/index.vue")},
},
{
path: "/checkout",
name: "checkout",
meta: {
title: "确认订单",
},
components: {default:() => import("../views/checkout/index.vue"),backButton:() => import("../components/back-button/index.vue")},
props:{
backButton:{bottom:"15vh"}
}
},
{
path: "/checkout/payment",
name: "payment",
meta: {
title: "支付成功",
},
component: () => import("../views/checkout/payment.vue"),
},
{
path:"/checkout/health-care",
name:"health-care-checkout",
meta:{
title:"确认订单"
},
components:{default:() => import("../views/checkout/healthCare.vue"),backButton:() => import("../components/back-button/index.vue")},
props:{
backButton:{bottom:"15vh"}
}
},
{
path:"/housekeeping/order/:id",
name:"housekeeping-checkout",
meta:{
title:"确认订单"
},
components:{default:() => import("../views/housekeeping/order.vue"),backButton:() => import("../components/back-button/index.vue")},
props:{
backButton:{bottom:"15vh"}
}
},
{
path: "/mine",
name: "mine",
meta: {
title: "康乐云家",
},
components: {
default: () => import("../views/mine/index.vue"),
tabbar: () => import("../components/tabbar/index.vue")
},
},
{
path:'/mine/address',
name:'my-address',
meta:{
title:'收获地址管理'
},
component: () => import("../views/mine/addressManagement.vue")
},
{
path:'/mine/add/address',
name:'add-address',
meta:{
title:'收获地址管理'
},
component: () => import("../views/mine/addAddress.vue")
},
{
path:'/mine/add/paramedic',
name:'add-paramedic',
meta:{
title:'新增被护理人'
},
component: () => import("../views/mine/addParamedic.vue")
},
{
path:'/mine/notice',
name:'notice',
meta:{
title:'消息中心'
},
components: {default:() => import("../views/mine/notice.vue"),backButton:() => import("../components/back-button/index.vue")},
props:{
backButton:{bottom:"15vh"}
}
},
{
path:'/mine/orders',
name:"mine-orders",
meta:{
title:'我的订单'
},
components: {default:() => import("../views/mine/orders.vue"),backButton:() => import("../components/back-button/index.vue")},
props:{
backButton:{bottom:"15vh"}
}
},
{
path:'/mine/user-info',
name:'mine-user-info',
meta:{
title:'个人资料'
},
component: () => import("../views/mine/userInfo.vue")
},
{
path:'/restaurant',
name:'restaurant',
meta:{
title:'社区点餐'
},
components: {default:() => import("../views/restaurant/index.vue"),backButton:() => import("../components/back-button/index.vue")},
props:{
backButton:{bottom:"15vh"}
}
},
{
path:"/restaurant/detail/:id",
name:"restaurant-detail",
meta:{
title:"商品详情"
},
components: {default:() => import("../views/restaurant/detail.vue"),backButton:() => import("../components/back-button/index.vue")}
},
{
path:'/courses',
name:'courses',
meta:{
title:'课程学习'
},
components: {default:() => import("../views/courses/index.vue"),backButton:() => import("../components/back-button/index.vue")},
props:{
backButton:{bottom:"15vh"}
}
},
{
path:'/courses/detail/:id',
name:'courses-detail',
meta:{
title:'课程详情'
},
components: {default:() => import("../views/courses/detail.vue"),backButton:() => import("../components/back-button/index.vue")}
},
{
path:'/courses/cart/:id',
name:'courses-cart',
meta:{
title:'购买课程'
},
component: () => import("../views/courses/courseCart.vue")
},
{
path:'/travel',
name:'travel',
meta:{
title:'乐享夕阳行'
},
components: {default:() => import("../views/travel/index.vue"),backButton:() => import("../components/back-button/index.vue")},
props:{
backButton:{bottom:"15vh"}
}
},
{
path:'/health-care',
name:'health-care',
meta:{
title:'康养护理'
},
components: {default:() => import("../views/healthCare/index.vue"),backButton:() => import("../components/back-button/index.vue")},
props:{
backButton:{bottom:"15vh"}
}
},
{
path:'/health-care/order/:id',
name:'health-care-order',
meta:{
title:'预约'
},
components: {default:() => import("../views/healthCare/order.vue"),backButton:() => import("../components/back-button/index.vue")},
},
{
path:'/jinze',
name:'jinze',
meta:{
title:'锦泽安康'
},
components: {default:() => import("../views/jinze/index.vue"),backButton:() => import("../components/back-button/index.vue")},
props:{
backButton:{bottom:"15vh"}
}
},
{
path:'/jinze/detail/:id',
name:'jinze-detail',
meta:{
title:'养老院详情'
},
components: {default:() => import("../views/jinze/detail.vue"),backButton:() => import("../components/back-button/index.vue")},
},
{
path:'/jinze/images/:id',
name:'jinze-images',
meta:{
title:'泰康之家·锦绣府'
},
components: {default:() => import("../views/jinze/images.vue"),backButton:() => import("../components/back-button/index.vue")},
props:{
backButton:{bottom:"15vh"}
}
},
{
path:'/jinze/patient-situation',
name:'jinze-patient-situation',
meta:{
title:'情况描述'
},
component: () => import("../views/jinze/patientSituation.vue")
},
{
path:'/home/address',
name:'current-address',
meta:{
title:'选择定位'
},
component: () => import("../views/home/currentLocation.vue")
},
{
path:'/housekeeping',
name:'housekeeping',
meta:{
title:'预约家政'
},
components: {default:() => import("../views/housekeeping/index.vue"),backButton:() => import("../components/back-button/index.vue")},
props:{
backButton:{bottom:"15vh"}
}
},
{
path:"/housekeeping/detail/:id",
name:"housekeeping-detail",
meta:{
title:"家政详情"
},
components: {default:() => import("../views/housekeeping/detail.vue"),backButton:() => import("../components/back-button/index.vue")},
},
{
path: "/unauthorized",
name: "unauthorized",
meta: {
title: "unauthorized",
},
component: () => import("../views/unauthorized.vue"),
},
];

13
src/store/index.ts Normal file
View File

@ -0,0 +1,13 @@
// pinia数据持久化存储
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persistedstate'
import { SelfStorage } from './secureStore'
// 第一个参数是应用程序中 store 的唯一 id
const store = createPinia()
store.use(
createPersistedState({
storage: SelfStorage
})
)
export default store

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