first commit

This commit is contained in:
2026-02-26 09:25:47 +08:00
commit 40665dda67
708 changed files with 100122 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"VNode": true,
"WritableComputedRef": true,
"acceptHMRUpdate": true,
"computed": true,
"createApp": true,
"createPinia": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"defineStore": true,
"effectScope": true,
"getActivePinia": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"mapActions": true,
"mapGetters": true,
"mapState": true,
"mapStores": true,
"mapWritableState": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onAddToFavorites": true,
"onBackPress": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onError": true,
"onErrorCaptured": true,
"onHide": true,
"onLaunch": true,
"onLoad": true,
"onMounted": true,
"onNavigationBarButtonTap": true,
"onNavigationBarSearchInputChanged": true,
"onNavigationBarSearchInputClicked": true,
"onNavigationBarSearchInputConfirmed": true,
"onNavigationBarSearchInputFocusChanged": true,
"onPageNotFound": true,
"onPageScroll": true,
"onPullDownRefresh": true,
"onReachBottom": true,
"onReady": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onResize": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onShareAppMessage": true,
"onShareTimeline": true,
"onShow": true,
"onTabItemTap": true,
"onThemeChange": true,
"onUnhandledRejection": true,
"onUnload": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"setActivePinia": true,
"setMapStoreSuffix": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"storeToRefs": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"unref": true,
"useAreaCode": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useEventEmit": true,
"useGetMsgCode": true,
"useI18n": true,
"useId": true,
"useModel": true,
"useNetworkStatusChange": true,
"usePage": true,
"useScrollThreshold": true,
"useSlots": true,
"useTemplateRef": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
distMap
*.local
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
/unpackage/
docs
+25
View File
@@ -0,0 +1,25 @@
{
// launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
"version" : "0.0",
"configurations" : [
{
"app-plus" : {
"launchtype" : "local"
},
"default" : {
"launchtype" : "local"
},
"type" : "uniCloud"
},
{
"customPlaygroundType" : "local",
"playground" : "standard",
"type" : "uni-app:app-android"
},
{
"playground" : "custom",
"type" : "uni-app:app-ios"
}
]
}
+21
View File
@@ -0,0 +1,21 @@
{
"i18n-ally.localesPaths": [
"src/locale",
],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": true,
"i18n-ally.enabledParsers": [
"json"
],
"i18n-ally.sourceLanguage": "zh-Hans",
"i18n-ally.displayLanguage": "zh-Hans",
"i18n-ally.translate.engines": [
"google-cn",
"google",
"deepl"
],
"i18n-ally.enabledFrameworks": [
"vue",
],
}
Binary file not shown.
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<trees>
<tree path="/src/components/reservation-item" title="预定"/>
<tree path="/src/pages-user/pages/home-order/refund-order.vue" title="退款订单"/>
<tree path="/src/pages-user/pages/home-order/reservation.vue" title="预定菜品"/>
<tree path="/src/pages-user/pages/home-order/today.vue" title="今日订单"/>
<tree path="/src/pages-user/pages/home-order/future.vue" title="未来订单" extension="" presentableText=""
tooltipTitle="" icon="" textColor="" backgroundColor=""/>
<tree path="/src/pages-user/pages/home-order/month-order.vue" title="月订单"/>
<tree path="/src/pages-user/pages/store-management" title="店铺管理"/>
<tree path="/src/pages-user/pages/complaints" title="反馈意见"/>
<tree path="/src/pages-user/pages/income" title="我的收益"/>
<tree path="/src/pages-user/pages/sales-record" title="销售记录"/>
<tree path="/src/pages-user/pages/recipe" title="菜谱"/>
<tree path="/src/pages-user/pages/store-settle-in" title="店铺认证"/>
<tree path="/src/pages-user/pages/coupons" title="优惠券"/>
<tree path="/src/pages-user/pages/menu" title="菜单"/>
<tree path="/src/pages-user/pages/food" title="食物"/>
<tree path="/src/pages-user/pages/scan-code" title="扫码" extension="" presentableText="" tooltipTitle="" icon=""
textColor="" backgroundColor=""/>
<tree path="/src/pages-user/pages/cooking-machine" title="炒菜机"/>
<tree path="/src/pages-user/pages/food/add-item.vue" title="配菜"/>
</trees>
Vendored
+8
View File
@@ -0,0 +1,8 @@
VITE_APP_TITLE=ChefLinkStore
VITE_APP_PORT=8091
# h5是否需要配置代理
VITE_APP_PROXY=true
VITE_APP_PROXY_PREFIX=/api
# 默认语言
VITE_FALLBACK_LOCALE='zh-Hans'
+11
View File
@@ -0,0 +1,11 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV=development
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE=false
#本地环境
#VITE_SERVER_BASEURL=http://liuyao.nat100.top/meiguowaimai
VITE_SERVER_BASEURL=http://192.168.5.23:8889
#VITE_SERVER_BASEURL=https://howhowfresh.com/prod-api
#VITE_SERVER_BASEURL=http://192.168.1.8:8811
#VITE_SERVER_BASEURL=http://mifengchuantou.natapp1.cc/meiguowaimai
+10
View File
@@ -0,0 +1,10 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV=production
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE=true
#正式环境
VITE_SERVER_BASEURL=https://howhowfresh.com/prod-api
#VITE_SERVER_BASEURL=http://liuyao.nat100.top/meiguowaimai
+5
View File
@@ -0,0 +1,5 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE = false
+20
View File
@@ -0,0 +1,20 @@
<!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>
</body>
</html>
+32
View File
@@ -0,0 +1,32 @@
import type {GenerateServiceProps} from 'openapi-ts-request'
// 将路径转换为大驼峰命名
function pathToCamelCase(path: string): string {
return path
.split('/')
.filter(segment => segment && segment !== '')
.map(segment => segment.charAt(0).toUpperCase() + segment.slice(1))
.join('')
}
export default [
{
schemaPath: 'http://127.0.0.1:4523/export/openapi/4?version=3.0',
serversPath: './src/service',
requestLibPath: `import request from '@/http/vue-query';\n import type { CustomRequestOptions } from '@/http/types'`,
requestOptionsType: 'CustomRequestOptions',
isGenReactQuery: false,
reactQueryMode: 'vue',
isGenJavaScript: false,
hook: {
// 自定义接口名称生成规则
customFunctionName: (data: any) => {
const {path, method} = data
// 将路径转换为大驼峰,然后加上请求方法
const camelCasePath = pathToCamelCase(path)
const methodSuffix = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase()
return `${camelCasePath}${methodSuffix}`
}
}
},
] as GenerateServiceProps[]
+114
View File
@@ -0,0 +1,114 @@
{
"name": "uni-preset-vue",
"version": "0.0.0",
"scripts": {
"openapi": "openapi-ts",
"dev:custom": "uni -p",
"dev": "uni",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-jd": "uni -p mp-jd",
"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:mp-xhs": "uni -p mp-xhs",
"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: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-jd": "uni build -p mp-jd",
"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:mp-xhs": "uni build -p mp-xhs",
"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",
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4050620250312001",
"@dcloudio/uni-app-harmony": "3.0.0-4050620250312001",
"@dcloudio/uni-app-plus": "3.0.0-4050620250312001",
"@dcloudio/uni-components": "3.0.0-4050620250312001",
"@dcloudio/uni-h5": "3.0.0-4050620250312001",
"@dcloudio/uni-i18n": "3.0.0-4050620250312001",
"@dcloudio/uni-mp-alipay": "3.0.0-4050620250312001",
"@dcloudio/uni-mp-baidu": "3.0.0-4050620250312001",
"@dcloudio/uni-mp-jd": "3.0.0-4050620250312001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4050620250312001",
"@dcloudio/uni-mp-lark": "3.0.0-4050620250312001",
"@dcloudio/uni-mp-qq": "3.0.0-4050620250312001",
"@dcloudio/uni-mp-toutiao": "3.0.0-4050620250312001",
"@dcloudio/uni-mp-weixin": "3.0.0-4050620250312001",
"@dcloudio/uni-mp-xhs": "3.0.0-4050620250312001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4050620250312001",
"@googlemaps/js-api-loader": "1.16.8",
"@iconify-json/fluent": "1.2.14",
"@stripe/stripe-js": "^7.3.0",
"big.js": "6.2.2",
"crypto-js": "4.2.0",
"dayjs": "1.11.10",
"fuse.js": "^7.1.0",
"gsap": "^3.13.0",
"image-tools": "1.4.0",
"js-base64": "3.7.7",
"nanoid": "^5.1.5",
"pinia": "2.0.36",
"pinia-plugin-persistedstate": "3.2.1",
"qs": "6.5.3",
"ramda": "0.30.1",
"rxjs": "^7.8.2",
"throttle-debounce": "5.0.2",
"vue": "3.4.21",
"vue-i18n": "9.1.9",
"wot-design-uni": "1.9.1",
"zod": "3.24.2"
},
"devDependencies": {
"@dcloudio/types": "3.4.14",
"@dcloudio/uni-automator": "3.0.0-4050620250312001",
"@dcloudio/uni-cli-shared": "3.0.0-4050620250312001",
"@dcloudio/uni-stacktracey": "3.0.0-4050620250312001",
"@dcloudio/vite-plugin-uni": "3.0.0-4050620250312001",
"@iconify-json/carbon": "^1.2.8",
"@types/big.js": "6.2.2",
"@types/crypto-js": "4.2.2",
"@types/node": "20.17.19",
"@types/qs": "6.9.18",
"@types/ramda": "0.30.2",
"@types/throttle-debounce": "5.0.2",
"@types/wechat-miniprogram": "3.4.8",
"@uni-helper/vite-plugin-uni-layouts": "0.1.10",
"@vue/runtime-core": "3.5.13",
"@vue/tsconfig": "0.1.3",
"openapi-ts-request": "^1.6.6",
"sass": "1.78.0",
"typescript": "4.9.5",
"unocss": "0.58.0",
"unocss-applet": "0.7.8",
"unocss-preset-animations": "^1.1.1",
"unplugin-auto-import": "0.17.8",
"vite": "5.4.14",
"vite-plugin-restart": "0.4.2",
"vite-plugin-vue-devtools": "7.7.2",
"vue-tsc": "1.8.27"
},
"engines": {
"node": ">=18.12.0",
"pnpm": ">=9.11.0"
},
"volta": {
"node": "20.18.0"
}
}
+10717
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
/// <reference types='@dcloudio/types' />
import 'vue'
declare module '@vue/runtime-core' {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {
}
}
+103
View File
@@ -0,0 +1,103 @@
<script lang="ts" setup>
import {useConfigStore} from "@/store";
import {setDayjsLocale, setWotDesignLocale} from "@/plugin";
const configStore = useConfigStore()
onLaunch(initConfig);
onShow(() => {
console.log('%c外卖商户端', 'background: #FF6106; color: white; padding: 3px; border-radius: 2px;');
console.log("App Show");
});
onHide(() => {
console.log("App Hide");
});
onError((error: string) => {
console.log('App Error', error)
})
function initApp() {
// #ifdef APP-PLUS
try {
plus.screen.lockOrientation('portrait-primary')
const color = plus.android.newObject("android.graphics.Color");
const ac = plus.android.runtimeMainActivity();
const c2int = plus.android.invoke(color, "parseColor", "#FFFFFF");
console.log("c2int===" + JSON.stringify(c2int))
const win = plus.android.invoke(ac, "getWindow");
console.log("win===" + JSON.stringify(win))
plus.android.invoke(win, "setNavigationBarColor", c2int);
} catch (e) {
console.log('error', e)
}
// #endif
}
// 初始化配置
function initConfig() {
// #ifdef APP-PLUS
plus.screen.lockOrientation('portrait-primary')
// #endif
const {statusBarHeight, windowHeight, windowWidth, screenWidth, screenHeight, safeAreaInsets} =
uni.getWindowInfo()
const {deviceId} =
uni.getDeviceInfo()
const {appVersion} =
uni.getAppBaseInfo()
const {
uniPlatform,
osName,
osVersion,
} =
uni.getSystemInfoSync()
configStore.deviceId = deviceId
configStore.osName = osName
configStore.osVersion = osVersion
configStore.appVersion = appVersion
configStore.uniPlatform = uniPlatform
configStore.statusBarHeight = statusBarHeight ?? 0
configStore.windowHeight = windowHeight ?? 0
configStore.windowWidth = windowWidth ?? 0
configStore.screenWidth = screenWidth ?? 0
configStore.screenHeight = screenHeight ?? 0
configStore.safeAreaInsets = safeAreaInsets ?? {
bottom: 0,
top: 0,
}
setWotDesignLocale()
setDayjsLocale()
if (osName === 'android') {
initApp()
}
if (!configStore.isShowedLanguageSelectPage) {
console.log('未展示过语言选择页面,导航到语言选择页面')
uni.navigateTo({
url: '/pages-login/pages/choose-language/index',
success() {
configStore.isShowedLanguageSelectPage = true
console.log('导航到语言选择页面成功')
},
})
}
}
</script>
<style lang="scss">
@import "@/styles/index.scss";
view {
box-sizing: border-box;
}
</style>
+64
View File
@@ -0,0 +1,64 @@
<script setup lang="ts">
const props = defineProps<{
modelValue: string
}>()
const emits = defineEmits<{
'update:modelValue': [value: string]
}>()
interface AreaCode {
title: string
value: string
}
const isFilterShow = ref(false)
const areaCodeList = ref<AreaCode[]>([{
title: 'Singapore',
value: '+65',
}])
function handleChooseCode(item: AreaCode) {
close()
emits('update:modelValue', item.value)
}
function close() {
isFilterShow.value = false
}
defineExpose({
close
})
</script>
<template>
<view class="relative">
<view class="center" @click.stop="isFilterShow = !isFilterShow">
<text class="text-28rpx text-primary font-bold">{{ props.modelValue }}</text>
<image class="shrink-0 ml-6rpx w-16rpx h-16rpx transition-all"
:class="{'rotate-180':isFilterShow}"
src="@img/2134@2x.png"></image>
</view>
<wd-transition :show="isFilterShow" name="fade" :duration="100">
<view
class="absolute z-99 top-100% left-[-50%] mt-10rpx py-20rpx px-20rpx bg-white rounded-12rpx shadow-[0rpx_6rpx_36rpx_rgba(0,0,0,0.16)]">
<view
class="center py-16rpx px-48rpx rounded-12rpx transition-all"
:class="{ 'bg-common': props.modelValue === item.value }"
v-for="(item, index) in areaCodeList"
:key="item.value"
@click="handleChooseCode(item)"
>
<text class="text-28rpx text-primary font-bold">{{ item.value }}</text>
</view>
</view>
</wd-transition>
</view>
</template>
<style scoped lang="scss">
</style>
+336
View File
@@ -0,0 +1,336 @@
<template>
<view
class="ca-switch"
:class="[{ 'is-active': isChecked, 'is-loading': loading, 'is-disabled': disabled }, `is-${shape}`]"
:style="[switchStyle]" @click="handleClick()">
<!-- 球的轨道 -->
<view class="ca-switch__chute" :style="[chuteStyle]">
<!-- -->
<view class="ca-switch__slide" :style="[slideStyle]">
<text v-if="loading" class="ca-switch__loading" :style="[loadingStyle]" />
</view>
<!-- 选中文字 -->
<text
v-if="onText"
class="ca-switch__text is-on"
:style="[textStyle_, trendsTextStyle(true), textStyle]">
{{ onText }}
</text>
<!-- 未选中文字 -->
<text
v-if="offText"
class="ca-switch__text is-off"
:style="[textStyle_, trendsTextStyle(false), textStyle]">
{{ offText }}
</text>
</view>
</view>
</template>
<script>
import props from './props'
import { defineComponent, computed, watch, ref, getCurrentInstance, nextTick } from 'vue'
export default defineComponent({
name: 'CaSwitch',
props,
emits: ['click', 'update:modelValue', 'change', 'asyncChange'],
setup(props, { emit }) {
// 组件宽度
const switchWidth = ref(0)
const { proxy } = getCurrentInstance()
// 球跟轨道之间的间隙
const spaceSize = parseInt(addUnit(props.space))
// 滑块大小
const switchHeight = parseInt(addUnit(props.size))
// 球大小
const slideSize = switchHeight - 2 * spaceSize
// 文本的宽度(要取最大值)
const textMaxWidth = ref(0)
// 双向绑定
const switchValue = ref(props.offVal)
const options = {
immediate: true,
deep: true
}
watch(() => props.modelValue, (value) => {
switchValue.value = value
}, options)
watch(switchValue, (value) => {
emit('update:modelValue', value)
emit('change', value)
}, options)
// 是否激活
const isChecked = computed(() => switchValue.value === props.onVal)
// 文字颜色(on,off
const textStyle_ = computed(() => {
return {
fontSize: addUnit(props.textSize),
minWidth: `${textMaxWidth.value}px`,
zIndex: `${props.textDisplay ? 3 : 1}`
}
})
// 文字颜色
const trendsTextStyle = computed(() => {
return (isOn) => {
return isOn
? {
opacity: props.textDisplay || isChecked.value ? 1 : 0,
color: props.textDisplay ? (isChecked.value ? props.textColor : props.textSelColor) : (isChecked.value ? props.textSelColor : props.textColor)
}
: {
opacity: props.textDisplay || !isChecked.value ? 1 : 0,
color: props.textDisplay ? (isChecked.value ? props.textSelColor : props.textColor) : (isChecked.value ? props.textSelColor : props.textColor)
}
}
})
// switch外层样式
const switchStyle = computed(() => {
return {
width: `${spaceSize > 0 ? switchWidth.value : switchWidth.value - 2 * spaceSize}px`,
height: `${spaceSize > 0 ? switchHeight.value : switchHeight.value - 2 * spaceSize}px`,
opacity: switchWidth.value ? props.disabled ? 0.6 : 1 : 0
}
})
// 滑槽样式
const chuteStyle = computed(() => {
const chuteColor = isChecked.value ? props.chuteSelColor : props.chuteColor
const chuteStyle = {
height: `${switchHeight}px`,
width: `${switchWidth.value}px`,
[chuteColor.includes('linear-gradient') ? 'background' : 'backgroundColor']: chuteColor,
borderRadius: props.shape === 'circle' ? `${switchHeight}px` : '6rpx',
padding: spaceSize > 0 ? `${spaceSize}px` : 0,
opacity: switchWidth.value ? 1 : 0
}
return chuteStyle
})
// 球样式
const slideStyle = computed(() => {
const slideStyle = {
width: `${props.textDisplay ? textMaxWidth.value : slideSize}px`,
borderRadius: props.shape === 'circle' ? `${Math.ceil(slideSize / 2)}px` : '4rpx',
height: `${slideSize}px`,
transform: `translateX(${isChecked.value ? textMaxWidth.value : spaceSize > 0 ? 0 : spaceSize}px)`,
boxShadow: props.chuteShadow
}
return slideStyle
})
const loadingStyle = computed(() => {
const size = addUnit(props.loadingSize)
return {
width: size,
height: size,
color: props.loadingColor || (isChecked.value ? props.chuteSelColor : props.chuteColor)
}
})
watch(() => [props.onText, props.offText], () => {
getSwitchWidth()
}, {
immediate: true
})
// 获取switch高度
async function getSwitchWidth() {
const slideRect = await getRect('.ca-switch__text')
const textInfo = slideRect ? ([].concat(slideRect)).map(o => o.width) : []
// 文字最小宽度
textMaxWidth.value = Math.ceil(Math.max(...textInfo, slideSize * 0.8))
if (props.textDisplay) {
switchWidth.value = (textMaxWidth.value + spaceSize) * 2
} else {
switchWidth.value = slideSize + textMaxWidth.value + spaceSize * 2
}
}
// 状态切换
async function handleClick() {
if (props.readonly || props.loading) return
if (props.disabled) {
props.disabledText && uni.showToast({ title: props.disabledText })
return
}
const res = isChecked.value ? props.offVal : props.onVal
if (props.async) {
emit('asyncChange', res)
return
}
switchValue.value = res
}
/**
* 单位转换
* value [String, Number] 要转换的值
* unit [String] 默认单位
* expUnit [String] 期望单位
*/
function addUnit(value = 'auto', unit = 'rpx', expUnit = 'px') {
if (!value) return `0${unit}`
value = isNaN(value) ? value : `${value}${unit}`
if (expUnit === 'px' && /upx|rpx/.test(value)) {
// 所有数值(含单位)
const strArr = value.replace(/\s+/g, ' ').split(' ')
return strArr.map(o => {
const matcher = o.match(/^(-?\d+)(.*)/)
return `${/upx|rpx/.test(matcher[2]) ? uni.upx2px(matcher[1]) : matcher[1]}${expUnit}`
}).join(' ')
}
return value
}
/**
* @description: 获取节点信息
* @param {String} selecter 要查询节点的id或者class名称
* @param {Object} fields 需要查询返回的额外节点信息
* @return {Promise} 返回需要查询的节点信息
*/
function getRect(selecter, fields) {
function formatReturn(data) {
return Array.isArray(data) ? (data.length > 1 ? data : data[0]) : data
}
return new Promise((resolve, reject) => {
nextTick(() => {
const querySelect = uni.createSelectorQuery()
// #ifndef MP-ALIPAY
querySelect.in(proxy)
// #endif
querySelect.selectAll(selecter)
.fields(
{
size: true,
rect: true,
...fields
},
data => {
resolve(formatReturn(data))
}
)
.exec()
})
})
}
return {
chuteStyle,
isChecked,
textStyle_,
slideStyle,
slideSize,
switchStyle,
loadingStyle,
trendsTextStyle,
handleClick
}
}
})
</script>
<style scoped>
.ca-switch {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s linear;
will-change: opacity, width, height;
box-sizing: border-box;
}
.ca-switch__chute {
transition: opacity 0.3s linear, background-color 0.3s linear;
will-change: opacity, width, height;
box-sizing: border-box;
display: inline-flex;
align-items: center;
}
.ca-switch__slide {
background-color: #fff;
border-radius: 100%;
transition: transform 0.2s cubic-bezier(0.65, 0.05, 0.36, 1);
transform: translateX(0);
will-change: transform;
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.ca-switch__loading{
color: rgb(41, 121, 255);
width: 30rpx;
height: 30rpx;
border-radius: 100%;
animation: rotate 1s linear infinite;
box-sizing: border-box;
}
.ca-switch__loading::after,.ca-switch__loading::before{
content: '';
color: inherit;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
border-radius: 100%;
border: 2px solid currentColor;
box-sizing: border-box;
}
.ca-switch__loading::before{
z-index: 1;
opacity: 0.15;
}
.ca-switch__loading::after{
z-index: 2;
border-color: currentColor transparent transparent;
}
.ca-switch__text {
font-size: 24rpx;
color: #ffffff;
position: absolute;
z-index: 1;
opacity: 0;
top: 0;
bottom: 0;
display: inline-flex;
align-items: center;
justify-content: center;
transition: opacity 0.1s linear;
white-space: nowrap;
padding: 0 12rpx;
line-height: 1;
box-sizing: border-box;
}
.ca-switch__text.is-on{
left: 0;
}
.ca-switch__text.is-off{
right: 0;
}
/* 解决视觉偏差问题 */
.ca-switch.is-circle .is-on{
padding: 0 12rpx 0 18rpx;
}
/* 解决视觉偏差问题 */
.ca-switch.is-circle .is-off{
padding: 0 18rpx 0 12rpx;
}
.ca-switch.is-disabled .ca-switch__chute {
cursor: no-drop;
pointer-events: none;
}
@keyframes rotate{
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(1turn);
transform: rotate(1turn);
}
}
</style>
+125
View File
@@ -0,0 +1,125 @@
export default {
// 双向绑定的值
modelValue: {
type: [String, Number, Boolean],
default: false
},
space: {
type: [Number, String],
default: 4
},
// 开关尺寸,默认单位是rpx
size: {
type: [Number, String],
default: 56
},
textSize: {
type: [String, Number],
default: 26
},
// 选中的值
onVal: {
type: [String, Number, Boolean],
default: true
},
// 未选中的值
offVal: {
type: [String, Number, Boolean],
default: false
},
// 开启展示的值
onText: {
type: String,
default: ''
},
// 关闭展示的值
offText: {
type: String,
default: ''
},
textStyle: {
type: [String, Object],
default: ''
},
// 字体颜色
textColor: {
type: String,
default: '#666'
},
// 选中时字体颜色
textSelColor: {
type: String,
default: '#fff'
},
// 文字常显
textDisplay: {
type: Boolean,
default: false
},
// 滑槽阴影,同css的box-shadow
chuteShadow: {
type: String,
default: '-2rpx 2rpx 4rpx 0 rgba(0,0,0,.25)'
},
// 开关底座颜色(可以是渐变色), 球的轨道
chuteColor: {
type: String,
default: '#eee'
},
// 开关底座选中颜色, 球的轨道
chuteSelColor: {
type: String,
default: '#007aff'
},
// 只读
readonly: {
type: Boolean,
default: false
},
// 禁止滑动
disabled: {
type: Boolean,
default: false
},
// 禁止滑动的提示
disabledText: {
type: String,
default: ''
},
// 滑动球颜色
slideColor: {
type: String,
default: '#fff'
},
// 开关开启后滑动球颜色
slideSelColor: {
type: String,
default: '#fff'
},
// 形状
shape: {
type: String,
values: ['circle', 'square'],
default: 'circle'
},
// 异步关闭
async: {
type: Boolean,
default: false
},
// loading大小,默认单位rpx
loadingSize: {
type: [String, Number],
default: 40
},
// 加载状态
loading: {
type: Boolean,
default: false
},
// loading转圈颜色
loadingColor: {
type: String,
default: ''
}
}
+486
View File
@@ -0,0 +1,486 @@
<template>
<view class="flex items-center pb-36rpx">
<image
class="w-44rpx h-44rpx shrink-0 mr-12rpx"
mode="aspectFill"
src="@img/chef/1330.png"
/>
{{ t('common.comment') }}({{ props.tableTotal }})
</view>
<template v-if="dataList && dataList.length">
<view v-for="(item1, index1) in dataList" :key="item1.id" class="c_comment">
<!-- 一级评论 -->
<CommonComp
:data="item1"
@deleteClick="() => deleteClick({ item1, index1 })"
@likeClick="() => likeClick({ item1, index1 })"
@replyClick="() => replyClick({ item1, index1 })"
/>
<view v-if="item1.childrenShow && item1.childrenShow.length> 0" class="children_item bg-#F5F7FB rounded-16rpx">
<!-- 二级评论 -->
<CommonComp
v-for="(item2, index2) in item1.childrenShow"
:key="item2.id"
:data="item2"
:pData="item1"
@deleteClick="() => deleteClick({ item1, index1, item2, index2 })"
@likeClick="() => likeClick({ item1, index1, item2, index2 })"
@replyClick="() => replyClick({ item1, index1, item2, index2 })"
/>
</view>
<view v-if="item1.children || item1.commentCount> 0" class="flex items-center pl-100rpx text-#666 pt-10rpx">
<!-- 展开二级评论 -->
<view
class="flex items-center"
@click="expandReply(item1)"
>
<text>{{ t('pages-user.recipe.expandReply') }}</text>
<wd-icon class="mt-4rpx" name="chevron-down" size="22px"></wd-icon>
</view>
<!-- 折叠二级评论 -->
<view
class="flex items-center"
@click="shrinkReply(item1)"
>
<text>{{ t('pages-user.recipe.collapseReply') }}</text>
<wd-icon class="mt-4rpx" name="chevron-up" size="22px"></wd-icon>
</view>
</view>
</view>
</template>
<!-- 空盒子 -->
<view v-else class="empty_box">
<view class="py-100rpx center">
<image class="w-250rpx h-250rpx" src="@img/chef/16033@2x.png"></image>
</view>
</view>
<!-- 评论弹窗 -->
<wd-popup ref="cPopupRef" v-model="cPopupShow" custom-style="border-radius:16rpx 16rpx 0 0;" position="bottom"
@change="popChange">
<view class="w-full rounded-16rpx px-30rpx py-16rpx">
<view class="flex items-center">
<template v-if="Object.keys(replyTemp).length">
<text class="text_aid">{{ t('pages-user.recipe.replyTo') }}</text>
<image :src="replyTemp.item2 ? replyTemp.item2.user_avatar : replyTemp.item1.user_avatar"
class="w-68rpx h-68rpx mx-10rpx rounded-50%"/>
<text class="text_main">{{ replyTemp.item2 ? replyTemp.item2.user_name : replyTemp.item1.user_name }}</text>
</template>
</view>
<view class="w-full flex-center-sb gap-30rpx bg-white py-12rpx">
<view class="w-full h-74rpx center bg-#F6F6F6 rounded-16rpx px-28rpx">
<wd-input
v-model="commentValue"
:focus-when-clear="false"
:placeholder="commentPlaceholder"
clearable
confirm-type="send"
custom-class="flex items-center !text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #6D6D6D; font-weight: 500;"
use-prefix-slot
@confirm="sendClick"
>
</wd-input>
</view>
<wd-button class="!h-74rpx !w-120rpx !rounded-16rpx !bg-#333 !text-white !text-30rpx" @click="sendClick">
{{ t('common.send') }}
</wd-button>
</view>
</view>
</wd-popup>
<wd-message-box/>
</template>
<script setup>
import CommonComp from "./componets/common";
import {appCommentDeleteCommentIdDelete, appCommentPublishCommentPost, appCommentReplyListPost} from "@/service";
import {useMessage} from "wot-design-uni";
import {formatTimestampWithMonthName} from "@/utils/utils";
const message = useMessage();
const {t} = useI18n();
const props = defineProps({
/** 登陆用户信息
* id: number // 登陆用户id
* user_name: number // 登陆用户名
* user_avatar: string // 登陆用户头像地址
*/
myInfo: {
type: Object,
default: () => {
},
},
/** 文章作者信息
* id: number // 文章作者id
* user_name: number // 文章作者名
* user_avatar: string // 文章作者头像地址
*/
userInfo: {
type: Object,
default: () => {
},
},
/** 评论列表
* id: number // 评论id
* parent_id: number // 父级评论id
* reply_id: number // 被回复人评论id
* reply_name: string // 被回复人名称
* user_name: string // 用户名
* user_avatar: string // 评论者头像地址
* user_content: string // 评论内容
* is_like: boolean // 是否点赞
* like_count: number // 点赞数统计
* create_time: string // 创建时间
*/
tableData: {
type: Array,
default: () => [],
},
// 评论总数
tableTotal: {
type: Number,
default: 0,
},
// 评论删除模式
// bind - 当被删除的一级评论存在回复评论, 那么该评论内容变更显示为[当前评论内容已被移除]
// only - 仅删除当前评论(后端删除相关联的回复评论, 否则总数显示不对)
// all - 删除所有评论包括回复评论
deleteMode: {
type: String,
default: "all",
},
});
const emit = defineEmits([
"update:tableTotal",
"likeFun", // 点赞事件
"replyFun", // 回复事件
"deleteFun", // 删除事件
]);
// 渲染数据(前端的格式)
let dataList = ref([]);
watch(
() => props.tableData,
(newVal) => {
if (newVal.length !== dataList.value.length) {
let temp = props.tableData;
dataList.value = treeTransForm(temp);
}
},
{deep: true, immediate: true}
);
// 数据转换
function treeTransForm(data) {
let newData = JSON.parse(JSON.stringify(data));
let result = [];
let map = {};
newData.forEach((item, i) => {
item.owner = item.user_id === props.myInfo.user_id; // 是否为当前登陆用户 可以对自己的评论进行删除 不能回复
// item.author = item.user_id === props.userInfo.user_id; // 是否为作者 显示标记
map[item.id] = item;
});
newData.forEach((item) => {
let parent = map[item.parent_id];
if (parent) {
(parent.children || (parent.children = [])).push(item); // 所有回复
if (parent.children.length === 1) {
(parent.childrenShow = []).push(item); // 显示的回复
}
} else {
result.push(item);
}
});
return result;
}
// 点赞
let setLike = (item) => {
item.is_like = !item.is_like;
item.like_count = item.is_like ? item.like_count + 1 : item.like_count - 1;
};
function likeClick({item1, index1, item2, index2}) {
let item = item2 || item1;
setLike(item);
emit("likeFun", {params: item}, (res) => {
// 请求后端失败, 重置点赞
setLike(item);
});
}
// 回复
let cPopupRef = ref(null); // 弹窗实例
const cPopupShow = ref(false); // 弹窗显示状态
let replyTemp = reactive({}); // 临时数据
function replyClick({item1, index1, item2, index2}) {
replyTemp = JSON.parse(JSON.stringify({item1, index1, item2, index2}));
cPopupShow.value = true;
}
// 发起新评论
let isNewComment = ref(false); // 是否为新评论
defineExpose({newCommentFun});
function newCommentFun() {
isNewComment.value = true;
cPopupShow.value = true;
}
// 评论弹窗
let focus = ref(false);
function popChange(e) {
// 关闭弹窗
if (!e.show) {
commentValue.value = ""; // 清空输入框值
replyTemp = {}; // 清空被回复人信息
isNewComment.value = false; // 恢复是否为新评论默认值
}
focus.value = e.show;
}
let commentValue = ref(""); // 输入框值
let commentPlaceholder = ref("说点什么..."); // 输入框占位符
// 发送评论
function sendClick({item1, index1, item2, index2} = replyTemp) {
console.log('replyTemp', replyTemp.item1)
console.log('replyTemp', replyTemp.item2)
const data = replyTemp.item2 || replyTemp.item1
appCommentPublishCommentPost({
body: {
topId: replyTemp.item2 ? replyTemp.item1.id : data.id,
parentId: data.id,
targetId: data.target_id,
targetType: 1,
content: commentValue.value,
}
}).then(res => {
console.log(res)
emit("update");
cPopupShow.value = false;
commentValue.value = ""; // 清空输入框值
// shrinkReply(replyTemp.item1)
dataList.value.forEach((item, i) => {
if (item.id === replyTemp.item1.id) {
shrinkReply(item)
}
})
})
}
function expandReply(item1) {
// item1.childrenShow = item1.children
console.log(item1)
appCommentReplyListPost({
params: {
pageNum: 1,
pageSize: 100,
},
body: {
parentId: item1.id,
}
}).then(res => {
console.log('回复列表', res)
item1.children = res.rows.map(item => {
let userInfo = {}
// 普通用户
if (+item.userPort === 1) {
userInfo.user_id = item.userVo.id
userInfo.user_name = `${item.userVo.firstName} ${item.userVo.surname}`
userInfo.user_avatar = item.userVo.avatar
} else {
userInfo.user_id = item.merchantVo.userId
userInfo.user_name = item.merchantVo.merchantName
userInfo.user_avatar = item.merchantVo.logo
}
return {
id: item.id,
topId: item.topId,
parent_id: null, // 评论父级的id
reply_id: null, // 被回复评论的id
reply_name: item.parentUserVo ? `${item.parentUserVo.firstName} ${item.parentUserVo.surname}` : null, // 被回复人名称
target_id: item1.target_id,
commentCount: item.commentCount,
user_id: userInfo.user_id, // 用户id
user_name: userInfo.user_name, // 用户名
user_avatar: userInfo.user_avatar, // 用户头像地址
user_content: item.content, // 用户评论内容
create_time: formatTimestampWithMonthName(item.createTime), // 创建时间
owner: userInfo.user_id === props.myInfo.user_id, // 是否为当前登陆用户 可以对自己的评论进行删除 不能回复
}
})
item1.childrenShow = item1.children
})
}
function shrinkReply(item1) {
item1.childrenShow = []
}
// 删除
const delPopupRef = ref(null);
let delTemp = reactive({}); // 临时数据
function deleteClick({item1, index1, item2, index2}) {
console.log('删123除', item1, index1, item2, index2)
message
.confirm({
title: t("common.prompt.system-prompt"),
msg: `${t("common.prompt.system-prompt-delete")}`,
confirmButtonText: t("common.yes"),
cancelButtonText: t("common.no"),
cancelButtonProps: {
customClass:
"!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !border-#666666 !rounded-20rpx",
},
confirmButtonProps: {
customClass:
"!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !bg-primary !rounded-20rpx",
},
})
.then(async () => {
appCommentDeleteCommentIdDelete({
params: {
id: item2.id ? item2.id : item1.id,
}
}).then(res => {
console.log('删除成功', res)
emit("deleteFun");
shrinkReply(item1)
})
})
.catch(() => {
});
}
// 展开评论if
function expandTxtShow({item1, index1}) {
return item1.childrenShow?.length && item1.children.length - item1.childrenShow.length;
}
// 展开更多评论
function expandReplyFun({item1, index1}) {
let csLen = dataList.value[index1].childrenShow.length;
dataList.value[index1].childrenShow.push(
...dataList.value[index1].children.slice(csLen, csLen + 6) // 截取5条评论
);
}
// 收起评论if
function shrinkTxtShow({item1, index1}) {
return item1.childrenShow?.length >= 2 && item1.children.length - item1.childrenShow.length === 0;
}
// 收起更多评论
function shrinkReplyFun({item1, index1}) {
let csLen = dataList.value[index1].childrenShow.length;
dataList.value[index1].childrenShow = [];
dataList.value[index1].childrenShow.push(
...dataList.value[index1].children.slice(0, 1) // 截取1条评论
);
}
</script>
<style lang="scss" scoped>
////////////////////////
.center {
display: flex;
align-items: center;
}
////////////////////////
.c_total {
//padding: 20rpx 30rpx 0 30rpx;
font-size: 28rpx;
}
.empty_box {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 150rpx 10rpx;
font-size: 28rpx;
.txt {
//color: $uni-text-color-disable;
}
.click {
//color: $uni-color-primary;
}
}
.c_comment {
//padding: 20rpx 30rpx;
font-size: 28rpx;
.children_item {
padding: 20rpx 30rpx;
margin-top: 10rpx;
margin-left: 80rpx;
//background-color: $uni-bg-color-grey;
.expand_reply,
.shrink_reply {
margin-top: 10rpx;
margin-left: 80rpx;
.txt {
font-weight: 600;
//color: $uni-color-primary;
}
}
}
}
.c_popup_box {
background-color: #fff;
.reply_text {
@extend .center;
padding: 20rpx 20rpx 0 20rpx;
font-size: 26rpx;
.text_aid {
//color: $uni-text-color-grey;
margin-right: 5rpx;
}
.user_avatar {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
margin-right: 6rpx;
margin-left: 12rpx;
}
.text_main {
}
}
.content {
@extend .center;
.text_area {
flex: 1;
padding: 20rpx;
}
.send_btn {
@extend .center;
justify-content: center;
width: 120rpx;
height: 60rpx;
border-radius: 20rpx;
font-size: 28rpx;
color: #fff;
//background-color: $uni-color-primary;
margin-right: 20rpx;
margin-left: 5rpx;
}
}
}
</style>
@@ -0,0 +1,176 @@
<template>
<view class="comment_item">
<view class="top">
<view class="top_left !text-#999 !text-26rpx">
<image v-if="+props.data.topId === 0" :src="props.data.user_avatar" class="w-80rpx h-80rpx rounded-50% mr-24rpx"
mode="aspectFill"/>
<text class="user_name">{{ props.data.user_name }}</text>
<text class="user_name">{{ cReplyName }}</text>
</view>
</view>
<view :class="[+props.data.topId === 0 ? '!pl-34rpx ml-70rpx' : '']" class="content !text-#333 !text-26rpx"
@tap="replyClick(props.data)">
{{ c_content }}
</view>
<view :class="[+props.data.topId === 0 ? '!pl-100rpx' : '']" class="">
<text class="create_time !text-#999 !text-26rpx">{{ props.data.create_time }}</text>
<text v-if="props.data.owner" class="bg-#E6E6E6 rounded-4rpx px-8rpx text-22rpx text-#333 mx-10rpx"
@tap="deleteClick(props.data)">{{ t('common.remove') }}
</text>
<text class="bg-#E6E6E6 rounded-4rpx px-8rpx text-22rpx text-#333 ml-10rpx">{{ t('common.reply') }}</text>
</view>
</view>
</template>
<script setup>
import {computed, ref, watch} from "vue";
const {t} = useI18n();
const props = defineProps({
// 评论数据
data: {
type: Object,
default: () => {
},
},
// 父级评论数据
pData: {
type: Object,
default: () => {
},
},
});
// 被回复人名称
const cReplyName = computed(() => {
return props.data?.reply_name ? `` + props.data?.reply_name : "";
});
// 点赞数显示
const cLikeCount = computed(() => {
return props.data.like_count === 0 ? "" : props.data.like_count > 99 ? `99+` : props.data.like_count;
});
// 评论过长处理
let contentShowLength = 70; // 默认显示评论字符
let user_content = props.data.user_content;
let isShrink = ref(user_content.length > contentShowLength); // 是否收缩评论
let c_content = ref("");
watch(
() => isShrink.value,
(newVal) => {
c_content.value = newVal ? user_content.slice(0, contentShowLength + 1) : user_content;
},
{
immediate: true,
}
);
// 删除变更显示定制
watch(
() => props.data.user_content,
(newVal, oldVal) => {
if (newVal !== oldVal) {
c_content.value = newVal;
}
}
);
// 展开文字
function expandContentFun() {
isShrink.value = false;
}
// 收起文字
function shrinkContentFun() {
isShrink.value = true;
}
const emit = defineEmits(["likeClick", "replyClick", "deleteClick"]);
// 点赞
function likeClick(item) {
emit("likeClick", item);
}
// 回复
function replyClick(item) {
emit("replyClick", item);
}
// 删除
function deleteClick(item) {
emit("deleteClick", item);
}
</script>
<style lang="scss" scoped>
////////////////////////
.center {
display: flex;
align-items: center;
}
.ellipsis {
overflow: hidden; //超出文本隐藏
white-space: nowrap; //溢出不换行
text-overflow: ellipsis; //溢出省略号显示
}
////////////////////////
.comment_item {
font-size: 28rpx;
.top {
@extend .center;
justify-content: space-between;
.top_left {
display: flex;
align-items: center;
overflow: hidden;
.user_avatar {
width: 68rpx;
height: 68rpx;
border-radius: 50%;
margin-right: 12rpx;
}
.tag {
margin-right: 6rpx;
}
.user_name {
@extend .ellipsis;
max-width: 180rpx;
//color: $uni-text-color-grey;
}
}
.top_right {
@extend .center;
.like_count {
//color: $uni-text-color-grey;
&.active {
//color: $uni-color-primary;
}
}
}
}
.content {
padding: 10rpx 0;
//color: $uni-text-color;
&:active {
//background-color: $uni-bg-color-hover;
}
.shrink {
padding: 20rpx 20rpx 20rpx 0rpx;
//color: $uni-color-primary;
}
}
}
</style>
@@ -0,0 +1,29 @@
{
"compileType": "miniprogram",
"setting": {
"coverView": true,
"es6": true,
"postcss": true,
"minified": true,
"enhance": true,
"showShadowRootInWxmlPanel": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"ignoreUploadUnusedFiles": true
},
"condition": {},
"editorSetting": {
"tabIndent": "tab",
"tabSize": 2
},
"libVersion": "3.6.3",
"packOptions": {
"ignore": [],
"include": []
},
"appid": ""
}
@@ -0,0 +1,7 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "cc-comment",
"setting": {
"compileHotReLoad": true
}
}
+140
View File
@@ -0,0 +1,140 @@
<script setup lang="ts">
import * as R from 'ramda'
import {debounce} from "throttle-debounce";
import Config from "@/config";
import {useConfigStore} from "@/store";
interface DayItem {
date: string
name: string
time: number
dataFormat: string
selected: boolean
}
const props = defineProps<{ date?: string }>();
const emits = defineEmits<{
success: [DayItem]
}>()
const {t} = useI18n();
const configStore = useConfigStore();
const $dayjs = inject('dayjs') as any
const show = ref(false);
// 选择的日期
const date = ref<{ fulldate: string } | null>(null)
watch(() => props.date, (value) => {
if (value) {
date.value = {fulldate: value}
}
}, {
immediate: true
})
function init() {
show.value = true;
}
function close() {
show.value = false;
}
function submit() {
close();
const weekdays = $dayjs.weekdaysShort()
const selectedDate: string = date.value?.fulldate || ''
let day = $dayjs(selectedDate)
emits('success', {
date: day.date(),
name: weekdays[day.day()],
time: day.valueOf(),
dataFormat: selectedDate,
selected: false,
})
}
const handleSubmit = debounce(Config.debounceLongTime, submit, {
atBegin: true
});
function handleCalendarChange(event: any) {
console.log('event', event)
date.value = event
}
defineOptions({
name: "ChooseDate",
});
defineExpose({
show,
init,
close,
});
</script>
<template>
<wd-popup
custom-class="rounded-t-30rpx"
position="bottom"
safe-area-inset-bottom
v-model="show"
@close="close"
>
<view class="box-border flex flex-col"
>
<view class="relative p-[40rpx+30rpx] flex items-center justify-between border-bottom">
<view class="text-42rpx text-primary font-bold">
{{ t('common.select-date') }}
</view>
<image class="absolute right-30rpx w-32rpx h-32rpx" src="@img-store/2106@2x.png" @click="close"></image>
</view>
<view class="mb-30rpx">
<wu-calendar
:todayDefaultStyle="false"
:showMonth="false"
:monthShowCurrentMonth="true"
:insert="true"
color="#333"
:itemHeight="52"
:startDate="$dayjs(configStore.serverTime).format('YYYY-MM-DD')"
@change="handleCalendarChange"></wu-calendar>
</view>
</view>
<fixed-bottom-large-btn class="" :text="t('common.confirm')"
@click="handleSubmit"
/>
</wd-popup>
</template>
<style scoped lang="scss">
:deep(.wu-calendar) {
.wu-calendar-item__weeks-lunar-text {
display: none;
}
.wu-calendar-item__weeks-box-item {
width: 43px !important;
border-radius: 16rpx !important;
}
.wu-calendar-item__weeks-box-item::after {
content: '';
display: block;
width: 8rpx;
height: 8rpx;
background-color: #fff;
border-radius: 50%;
}
}
</style>
@@ -0,0 +1,105 @@
<script lang="ts" setup>
import {uploadToS3} from '@/utils/upload/ymx'
const props = withDefaults(
defineProps<{
count?: number
isUpload?: boolean
}>(),
{
count: 1,
isUpload: true,
},
)
const emits = defineEmits(['change', 'update:modelValue'])
const {t} = useI18n()
const show = ref(false)
function init() {
show.value = true
}
function close() {
show.value = false
}
// 选择图片
function handleChooseImage(type: 'album' | 'camera') {
close()
chooseImage(type)
}
function chooseImage(type: 'album' | 'camera') {
// #ifdef APP-PLUS
uni.chooseMedia({
count: props.count,
mediaType: ['image'],
sourceType: type === 'album' ? ['album'] : ['camera'],
maxDuration: 30,
camera: 'back',
async success(res) {
let files: string | string[] = []
const asyncList = []
console.log('res.tempFiles', res.tempFiles)
if (res.tempFiles.length === 0) return
files = res.tempFiles.map((item) => item.tempFilePath)
if (props.count > 0) {
// 根据count截取数组长度
files = files.slice(0, props.count)
}
if (!props.isUpload) {
return emits('change', files)
}
await uni.showLoading({
title: t('common.loading') + '...',
mask: true,
})
for (let i = 0; i < files.length; i++) {
// asyncList.push(upload(files[i]))
asyncList.push(uploadToS3(files[i]))
console.log(asyncList)
}
Promise.all(asyncList)
.then((results) => {
console.log(results)
emits('change', results)
})
.finally(() => {
uni.hideLoading()
})
}
})
// #endif
}
defineExpose({
show,
init,
close,
})
</script>
<template>
<view>
<wd-popup v-model="show" custom-style="border-radius: 30rpx 30rpx 0 0;" position="bottom" safe-area-inset-bottom
@close="close">
<slot name="top"></slot>
<view class="p-[32rpx+20rpx] text-34rpx text-primary text-center font-bold" @click="handleChooseImage('album')">
<text>{{ t('common.select-from-album') }}</text>
</view>
<view class="p-[32rpx+20rpx] text-34rpx text-primary text-center font-bold" @click="handleChooseImage('camera')">
<text>{{ t('common.photograph') }}</text>
</view>
<view class="h-20rpx bg-common"></view>
<view class="p-[32rpx+20rpx] text-34rpx text-primary text-center" @click="close">{{ t('common.cancel') }}</view>
</wd-popup>
</view>
</template>
<style lang="scss" scoped></style>
+150
View File
@@ -0,0 +1,150 @@
<template>
<view class="collection-component" @click.stop="handleCollectionClick">
<view class="icon-container">
<!-- 未收藏图标 -->
<image
v-show="!isCollected"
:class="['collection-icon', 'collected-icon', { 'slide-in': isAnimating && !isCollected }]"
:src="uncollectedIcon"
mode="aspectFit"
/>
<!-- 已收藏图标 -->
<image
v-show="isCollected"
:class="['collection-icon', 'uncollected-icon', { 'slide-out': isAnimating && isCollected }]"
:src="collectedIcon"
mode="aspectFit"
/>
</view>
</view>
</template>
<script lang="ts" setup>
import {debounce, throttle} from 'throttle-debounce'
// 定义组件属性
interface Props {
/** 是否已收藏 */
isCollected: boolean
/** 节流延迟时间(毫秒) */
throttleDelay?: number
/** 防抖延迟时间(毫秒) */
debounceDelay?: number
/** 是否禁用点击 */
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isCollected: false,
throttleDelay: 300,
debounceDelay: 500,
disabled: false
})
// 定义事件
const emit = defineEmits<{
/** 收藏状态改变事件 */
'collection-change': [isCollected: boolean]
/** 点击事件 */
'click': [event: any]
}>()
// 图标路径
const uncollectedIcon = '/static/images/chef/196.png'
const collectedIcon = '/static/images/chef/197.png'
// 动画状态
const isAnimating = ref(false)
// 节流处理函数
const throttledEmit = throttle(props.throttleDelay, (isCollected: boolean) => {
emit('collection-change', isCollected)
})
// 防抖处理函数
const debouncedEmit = debounce(props.debounceDelay, (isCollected: boolean) => {
emit('collection-change', isCollected)
})
// 点击处理函数
const handleCollectionClick = (event: any) => {
if (props.disabled) {
return
}
// 触发点击事件
emit('click', event)
// 开始动画
isAnimating.value = true
// 立即触发状态变化
throttledEmit(!props.isCollected)
debouncedEmit(!props.isCollected)
// 动画结束后重置状态
setTimeout(() => {
isAnimating.value = false
}, 300)
}
</script>
<style lang="scss" scoped>
.collection-component {
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
&:active {
transform: scale(0.95);
}
.icon-container {
position: relative;
width: 40rpx;
height: 40rpx;
overflow: hidden;
}
.collection-icon {
position: absolute;
top: 0;
left: 0;
width: 40rpx;
height: 40rpx;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&.uncollected-icon {
transform: translateY(0);
opacity: 1;
&.slide-out {
transform: translateY(-100%);
opacity: 0;
}
}
&.collected-icon {
transform: translateY(0);
opacity: 1;
&.slide-in {
animation: slide-in-from-bottom 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
}
}
}
@keyframes slide-in-from-bottom {
0% {
transform: translateY(100%);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
</style>
@@ -0,0 +1,47 @@
<script lang="ts" setup>
import {useConfigStore} from "@/store";
const props = withDefaults(defineProps<{
text: string,
fixed?: boolean,
loading?: boolean
}>(), {
fixed: false,
loading: false
})
const emit = defineEmits<{
click: [event: any]
}>()
const configStore = useConfigStore()
function handleClick(event: any) {
emit('click', event)
}
</script>
<template>
<view class="">
<view v-if="props.fixed" :style="[configStore.iosSafeBottomPlaceholder]" class="h-118rpx"></view>
<view :style="[props.fixed&&{
position: 'fixed',
bottom: '0',
left: '0',
right: '0',
}]" class="z-1 bg-#fff shadow-[0rpx_-6rpx_24rpx_rgba(0,0,0,0.16)]">
<view class="h-118rpx p-[10rpx+30rpx] box-border">
<wd-button :loading="props.loading" block
custom-class="!h-98rpx !text-30rpx !lh-42rpx !font-bold !rounded-20rpx !bg-#14181B"
@click="handleClick">
{{ props.text }}
</wd-button>
</view>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</view>
</view>
</template>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,84 @@
<script setup lang="ts">
import Config from '@/config'
import {throttle} from 'throttle-debounce'
const {t} = useI18n()
const props = withDefaults(
defineProps<{
modelValue?: string
disabled?: boolean
focus?: boolean
placeholder?: string
}>(),
{
modelValue: '',
disabled: false,
focus: false,
},
)
const emits = defineEmits<{
'update:modelValue': [value: string]
search: []
}>()
function search() {
emits('search')
}
const handleSearch = throttle(Config.throttleTime, search)
function handleInputUpdateModelValue(value: string) {
console.log(value)
emits('update:modelValue', value)
}
function handleClickLeft() {
uni.navigateBack()
}
defineOptions({
name: 'HeaderSearch',
})
</script>
<template>
<view
class="relative z-1 bg-#fff"
>
<status-bar/>
<view class="flex items-center px-20rpx h-88rpx">
<view class="shrink-0" @click="handleClickLeft">
<view class="i-fluent:ios-arrow-ltr-24-filled text-36rpx text-#333"></view>
</view>
<view
class="px-20rpx w-650rpx h-80rpx flex items-center bg-common rounded-20rpx "
>
<wd-input
no-border
clearable
:focus-when-clear="false"
:disabled="disabled"
:focus="focus"
confirm-type="search"
use-prefix-slot
custom-class="flex items-center !text-30rpx !bg-transparent flex-1"
placeholderStyle="font-size: 30rpx;color: #999;"
:modelValue="modelValue"
:placeholder="placeholder || t('common.input-placeholder.enter-keywords-to-search')"
@update:modelValue="handleInputUpdateModelValue"
@confirm="handleSearch"
>
</wd-input>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
:deep(.wd-input__clear) {
background: transparent !important;
}
</style>
+87
View File
@@ -0,0 +1,87 @@
<script setup lang="ts">
</script>
<template>
<view class="container-animation">
<view class="part one"></view>
<view class="part two"></view>
<view class="part three"></view>
<view class="part four"></view>
</view>
</template>
<style scoped lang="scss">
$border-width: 10px;
$border-color: #333;
.container-animation {
position: relative;
animation: rotate 2s linear infinite;
}
.part {
position: absolute;
height: 30px;
width: 30px;
box-sizing: border-box;
}
.one {
top: -30px;
left: -30px;
border-radius: 100% 0 0 0;
animation: animato1 1s linear 1s alternate infinite;
border-top: $border-width solid $border-color;
border-left: $border-width solid $border-color;
}
.two {
top: -30px;
border-radius: 0 100% 0 0;
animation: animato2 1s linear alternate infinite;
border-top: $border-width solid $border-color;
border-right: $border-width solid $border-color;
}
.three {
left: -30px;
border-radius: 0 0 0 100%;
animation: animato3 1s linear alternate infinite;
border-bottom: $border-width solid $border-color;
border-left: $border-width solid $border-color;
}
.four {
top: 0px;
left: 0px;
border-radius: 0 0 100% 0;
animation: animato4 1s linear 1s alternate infinite;
border-bottom: $border-width solid $border-color;
border-right: $border-width solid $border-color;
}
@keyframes rotate {
to {
transform: rotate(360deg);
background-color: orange;
}
}
@keyframes animato1 {
to {
width: 20rpx; height: 20rpx; top: -20rpx; left: -20rpx;
}
}
@keyframes animato2 {
to {
width: 20rpx; height: 20rpx; top: -20rpx;
}
}
@keyframes animato3 {
to {
width: 20rpx; height: 20rpx; left: -20rpx;
}
}
@keyframes animato4 {
to {
width: 20rpx; height: 20rpx;
}
}
</style>
+41
View File
@@ -0,0 +1,41 @@
<script lang="ts" setup>
const props = withDefaults(defineProps<{
title?: string;
fixed?: boolean
showLeft?: boolean
customClass?: string
}>(), {
fixed: true,
showLeft: true
});
function handleClickLeft() {
uni.navigateBack()
}
</script>
<template>
<wd-navbar
:bordered="false"
:custom-class="props.customClass"
:fixed="props.fixed"
:placeholder="props.fixed"
:title="props.title"
safeAreaInsetTop
@click-left="handleClickLeft">
<template #left>
<view v-if="showLeft" class="shrink-0">
<view class="i-carbon:chevron-left text-50rpx text-primary ml-[-10rpx]"></view>
</view>
</template>
<template #right>
<slot name="right"></slot>
</template>
</wd-navbar>
</template>
<style lang="scss" scoped>
:deep(.wd-navbar) {
z-index: 2 !important;
}
</style>
+125
View File
@@ -0,0 +1,125 @@
<script lang="ts" setup>
import {OrderStatus} from "@/constant/enums";
import {formatTimestampWithMonthName, thumbnailImg} from "@/utils/utils";
const {t} = useI18n()
const props = defineProps({
item: {
type: Object,
default: () => ({})
}
})
function navigateTo(url: string) {
uni.navigateTo({url})
}
</script>
<template>
<view @click="navigateTo('/pages-user/pages/order/index?id=' + item.id)">
<view class="flex-center-sb">
<view class="flex items-center">
<image
:src="thumbnailImg(item?.userVo?.avatar)"
class="w-42rpx h-42rpx rounded-50% mr-6rpx"
mode="aspectFill"
/>
<view class="text-30rpx lh-30rpx text-#333 mr-10rpx">
<text class="mr-6rpx">{{ item?.userVo?.firstName }} {{ item?.userVo?.surname }}</text>
<text>{{ item?.phone }}</text>
</view>
<!-- <view class="px-10rpx h-32rpx bg-#FF6106 rounded-6rpx center text-22rpx lh-22rpx text-#fff">-->
<!-- </view>-->
<!--收货方式(1-派送 2-自取)-->
<view v-if="+item.receiveMethod === 1"
class="bg-#FF6106 rounded-6rpx h-32rpx text-#fff text-22rpx center px-10rpx ml-20rpx">
{{ t('pages.order.DEL') }}
</view>
<view v-if="+item.receiveMethod === 2"
class="bg-#00A76D rounded-6rpx h-32rpx text-#fff text-22rpx center px-10rpx ml-20rpx">
{{ t('pages.order.PU') }}
</view>
</view>
<view class="text-30rpx lh-30rpx font-500 text-#00A76D">
<template v-if="+item.orderStatus === OrderStatus.CANCELLED">{{ t('pages.order.cancel') }}</template>
<template v-if="+item.orderStatus === OrderStatus.REFUNDED">{{ t('pages.order.refund') }}</template>
<template v-if="+item.orderStatus === OrderStatus.HAS_PENDING_PAYMENT">{{ t('pages.order.CONF') }}</template>
<template v-if="+item.orderStatus === OrderStatus.MERCHANT_ACCEPTED">{{ t('pages.order.ACPT') }}</template>
<template v-if="+item.orderStatus === OrderStatus.DELIVERING">{{ t('pages.order.OTW') }}</template>
<text class="!text-#333">
<template v-if="+item.orderStatus === OrderStatus.COMPLETED">{{ t('pages.order.DCOMP') }}</template>
</text>
</view>
</view>
<view class="relative mt-22rpx pb-12rpx pr-196rpx">
<scroll-view :scroll-x="true">
<view class="flex">
<template v-for="(food, index) in item.merchantOrderDishVoList" :key="index">
<view class="mr-20rpx flex flex-col items-center w-120rpx !inline-block">
<image
:src="food?.merchantDishVo?.dishImage?.split(',')[0]"
class="w-120rpx h-120rpx rounded-16rpx mb-26rpx"
mode="aspectFill"
/>
<view class="text-24rpx lh-24rpx text-#7D7D7D line-clamp-1 w-120rpx">{{
food?.merchantDishVo?.dishName
}}
</view>
</view>
</template>
</view>
</scroll-view>
<view
class="absolute top--1rpx right--2rpx w-196rpx h-180rpx bg-#F6F6F6 flex flex-col items-center justify-center">
<text class="text-34rpx lh-34rpx font-500 text-#333">${{ item.paidAmount }}</text>
<view class="text-24rpx lh-24rpx mt-24rpx text-#7D7D7D">
{{ t('common.itemNum') }}
{{ item.merchantOrderDishVoList.length }}
{{ t('common.item') }}
</view>
</view>
</view>
<view class="flex-center-sb">
<text class="text-28rpx lh-28rpx text-#7D7D7D">{{ formatTimestampWithMonthName(item.endScheduledTime) }}</text>
<slot name="rightBtn">
<wd-button
v-if="+item.receiveMethod === 1 && +item.orderStatus === OrderStatus.HAS_PENDING_PAYMENT"
class="!h-56rpx !rounded-56rpx !text-28rpx text-#fff"
>{{ t('pages-user.order.receiving') }}
</wd-button>
<!--配送订单 已经接单-->
<wd-button
v-if="+item.receiveMethod === 1 && +item.orderStatus === OrderStatus.MERCHANT_ACCEPTED"
class="!h-56rpx !rounded-56rpx !text-28rpx text-#fff"
>{{ t('pages-user.order.startDelivery') }}
</wd-button>
<!--配送订单 配送中-->
<wd-button
v-if="+item.receiveMethod === 1 && +item.orderStatus === OrderStatus.DELIVERING"
class="!h-56rpx !rounded-56rpx !text-28rpx text-#fff"
>{{ t('pages-user.order.delivered') }}
</wd-button>
<!--自取订单 未接单-->
<wd-button
v-if="+item.receiveMethod === 2 && +item.orderStatus === OrderStatus.HAS_PENDING_PAYMENT"
class="!h-56rpx !rounded-56rpx !text-28rpx text-#fff"
>{{ t('pages-user.order.receiving') }}
</wd-button>
<!--自取订单 已经接单-->
<view v-if="+item.receiveMethod === 2 && +item.orderStatus === OrderStatus.MERCHANT_ACCEPTED"
@click.stop="navigateTo('/pages-user/pages/scan-code/index')">
<wd-button class="!h-56rpx !rounded-56rpx !text-28rpx text-#fff">
{{ t('pages-user.order.writeOff') }}
</wd-button>
</view>
</slot>
</view>
</view>
</template>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,55 @@
<script lang="ts" setup>
import type SetPassword from "@/components/set-password/set-password.vue";
import type PasswordInput from "@/components/password-input/password-input.vue";
import {useUserStore} from "@/store";
const emits = defineEmits<{
success: [password: string];
}>();
const userStore = useUserStore();
const setPasswordRef = ref<InstanceType<typeof SetPassword> | null>(null);
const passwordInputRef = ref<InstanceType<typeof PasswordInput> | null>(null);
function showSetPassword() {
setPasswordRef.value?.init();
}
function showPasswordInput() {
if (!userStore.userInfo?.payPwd) {
showSetPassword()
return
}
passwordInputRef.value?.init?.();
}
function handleSuccess(password: string) {
emits("success", password);
}
defineOptions({
name: "PasswordContainer",
});
defineExpose({
showSetPassword,
showPasswordInput,
});
</script>
<template>
<view class="">
<set-password ref="setPasswordRef"/>
<password-input ref="passwordInputRef" @success="handleSuccess"/>
</view>
</template>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,122 @@
<script lang="ts" setup>
import {useConfigStore} from "@/store";
const emits = defineEmits<{
success: [password: string]
}>()
const {t} = useI18n()
const configStore = useConfigStore()
const show = ref(false)
const password = ref('')
const keyboardHeight = ref<number>(0)
const showKeyboard = ref<boolean>(false);
function init() {
password.value = ''
show.value = true
}
function close() {
show.value = false
password.value = ''
}
function handleSubmit() {
console.log(password.value)
if (password.value.length === 6) {
emits('success', password.value)
close()
} else {
uni.showToast({
title: t('common.please-enter-6-digit-payment-password'),
icon: 'none',
})
}
}
const listener = function (res: any) {
console.log(res)
keyboardHeight.value = res.height
}
onMounted(() => {
// #ifdef APP-PLUS
uni.onKeyboardHeightChange(listener)
// #endif
})
onUnmounted(() => {
// #ifdef APP-PLUS
uni.offKeyboardHeightChange(listener)
// #endif
})
defineOptions({
name: 'PasswordInput',
styleIsolation: 'shared',
})
defineExpose({
init,
close,
})
</script>
<template>
<wd-popup
v-model="show"
custom-class="rounded-t-30rpx !p-[40rpx+20rpx+48rpx]"
position="bottom"
safe-area-inset-bottom
>
<view
class="mb-48rpx relative flex justify-center text-center text-34rpx text-primary font-bold"
>
<text>{{ t('common.please-enter-your-payment-password') }}</text>
</view>
<view class="relative">
<wd-password-input v-model="password"
:focused="showKeyboard"
:length="6" custom-class="flex-1 h-50px"/>
<input
v-model="password"
:cursor-spacing="20"
:maxlength="6"
class="absolute top-0 left-0 opacity-0 w-ful h-full"
cursor-color="transparent"
type="number"
@blur="showKeyboard = false"
@focus="showKeyboard = true"
/>
</view>
<view class="mt-60rpx">
<wd-button
custom-class="!w-full !h-88rpx !m-0 !text-30rpx !font-bold !rounded-20rpx"
@click="handleSubmit"
>{{ t('common.confirm') }}
</wd-button
>
</view>
<view :style="[configStore.isIos?{
height: keyboardHeight + 'px',
transition: 'height 0.3s ease-in-out',
overflow: 'hidden',
}:{}]" class=""></view>
</wd-popup>
</template>
<style lang="scss" scoped>
:deep(.wd-password-input) {
margin: 0;
}
:deep(.wd-password-input__item) {
background-color: transparent;
}
</style>
@@ -0,0 +1,198 @@
<script setup lang="ts">
import * as R from 'ramda'
import {getCardList} from "@/service";
import {useUserStore} from "@/store";
const props = defineProps<{
modelValue?: number
userCardId?: string
}>()
const emits = defineEmits<{
"update:modelValue": [value: number];
"update:userCardId": [value: string];
}>();
const {t} = useI18n()
const userStore = useUserStore()
// 支付方式:1-paynow支付 2-信用卡支付 2-余额支付
const payMethod = ref(props.modelValue || 1);
const form = ref({
cardNumber: '',
userCardId: '',
})
function handleChoosePayWay(method: number) {
payMethod.value = method
emits(
"update:modelValue",
method
);
}
function handleSetFromData(data: {
cardNumber: string,
id: string,
}) {
console.log(data)
const {cardNumber, id} = R.pick(['cardNumber', 'id'], data || {})
form.value = {
cardNumber,
userCardId: id,
}
emits(
"update:userCardId",
id
);
}
function handleReplaceCard() {
uni.navigateTo({
url: '/pages-shop/pages/select-credit-card/index',
events: {
// 为指定事件添加一个监听器,获取被打开页面传送到当前页面的数据
acceptDataFromOpenedPage: function (data: {
cardNumber: string,
id: string,
}) {
handleSetFromData(data)
},
}
})
}
function handleAddCard() {
uni.navigateTo({
url: '/pages-user/pages/credit-card/add/index',
events: {
// 为指定事件添加一个监听器,获取被打开页面传送到当前页面的数据
acceptDataFromOpenedPage: function (data: {
cardNumber: string,
id: string,
}) {
handleSetFromData(data)
},
}
})
}
function getPayInfo() {
if (R.equals(payMethod.value, 2)) {
return {
cardNumber: form.value.cardNumber,
userCardId: form.value.userCardId
}
}
}
async function getUserCar() {
try {
const res = await getCardList({}, {
pageNum: 1,
pageSize: 1,
})
console.log('car', res)
if (Array.isArray(res.rows) && res.rows.length > 0) {
handleSetFromData(res.rows[0] || {})
}
} catch (e) {
}
}
onMounted(() => {
getUserCar()
})
defineOptions({
name: "PaymentMethod",
});
defineExpose({
getPayInfo
})
</script>
<template>
<view class="bg-white rounded-20rpx">
<view class="py-28rpx px-24rpx">
<view class="text-28-bold">{{ t('common.payment-method') }}</view>
<view class="mt-30rpx flex items-center space-x-10rpx">
<view
class="box-border w-160rpx h-106rpx pl-16rpx pt-20rpx flex flex-col border-2rpx border-solid border-#DFDFDF rounded-16rpx transition-all"
:class="[payMethod===1&&'!border-#ED7EEF']"
@click="handleChoosePayWay(1)"
>
<image class="w-110rpx h-32rpx shrink-0" src="@img/2118@2x.png"></image>
<view class="mt-8rpx text-20-bold">{{ t('common.pay-now') }}</view>
</view>
<view
class="box-border w-160rpx h-106rpx pl-16rpx pt-20rpx flex flex-col border-2rpx border-solid border-#DFDFDF rounded-16rpx transition-all"
:class="[payMethod===2&&'!border-#ED7EEF']"
@click="handleChoosePayWay(2)"
>
<image class="w-40rpx h-32rpx shrink-0" src="@img/1851@2x.png"></image>
<view class="mt-8rpx text-20-bold">{{ t('common.credit-card') }}</view>
</view>
<view
class="relative box-border w-160rpx h-106rpx pl-16rpx pt-20rpx flex flex-col border-2rpx border-solid border-#DFDFDF rounded-16rpx transition-all"
:class="[payMethod===3&&'!border-#ED7EEF']"
@click="handleChoosePayWay(3)"
>
<view class="absolute right-0 top-0 px-8rpx h-32rpx center bg-#FED1FF rounded-[0rpx+16rpx+0rpx+10rpx]"
v-if="userStore.isMember"
>
<text class="text-16rpx text-primary font-bold lh-16rpx">{{ t('common.discount') }}!</text>
</view>
<image class="w-32rpx h-32rpx shrink-0" src="@img/icon_pay_wallet@2x.png"></image>
<view class="mt-8rpx text-20-bold">{{ t('common.store-credits') }}</view>
</view>
</view>
<view class="mt-20rpx text-24rpx text-#999 font-bold lh-34rpx">
<text class="" v-show="payMethod===1">{{ t('common.pay-now-description') }}</text>
<text class="" v-show="payMethod===2">{{ t('common.credit-card-description') }}</text>
</view>
</view>
<view class="p-[24rpx+30rpx+32rpx] flex items-center border-top" v-if="R.equals(+payMethod,1)">
<image class="shrink-0 mr-20rpx w-36rpx h-36rpx" src="@img/1850@2x.png"></image>
<view class="text-24rpx text-#999 font-bold lh-34rpx">
{{ t('common.pay-now-presentation-for-payment') }}
</view>
</view>
<view class="p-[24rpx+30rpx+32rpx] flex items-center justify-between border-top"
v-if="R.equals(+payMethod,2)">
<view class="flex-1 flex items-center">
<image class="w-198rpx h-42rpx shrink-0 mr-20rpx" src="@img/20226@2x.png"></image>
<view class="text-28-bold">{{ form.cardNumber }}</view>
</view>
<view class="inline-flex items-center ml-20rpx">
<wd-button plain
custom-class="!h-40rpx !w-114rpx !min-w-auto !text-24rpx !lh-24rpx "
block
v-if="form.cardNumber"
@click="handleReplaceCard"
>
{{ t('common.replace') }}
</wd-button>
<wd-button plain
custom-class="!h-40rpx !w-114rpx !min-w-auto !text-24rpx !lh-24rpx "
block
v-else
@click="handleAddCard"
>
{{ t('common.add') }}
</wd-button>
</view>
</view>
</view>
</template>
<style scoped lang="scss"></style>
@@ -0,0 +1,24 @@
<script lang="ts" setup>
const props = defineProps({
item: {
type: Object,
default: () => {
},
}
})
const {t} = useI18n()
</script>
<template>
<view class="flex-center-sb px-24rpx py-22rpx bg-white rounded-16rpx">
<text class="text-28rpx lh-28rpx text-#333">{{ item.dishName }}</text>
<view class="flex items-center">
<text class="text-22rpx lh-22rpx text-#999">{{ t('pages-user.order.copies') }}</text>
<text class="text-28rpx lh-28rpx text-#333">{{ item.copies }}{{ t('pages-user.order.share') }}</text>
</view>
</view>
</template>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,147 @@
<script lang="ts" setup>
import { getCaptcha } from '@/service'
const emits = defineEmits<{
success: [{ code: string; uuid: string }]
}>()
const imgCode = ref('')
const captcha = ref({
img: '',
uuid: '',
})
const show = ref(false)
function init() {
initData()
show.value = true
}
function close() {
show.value = false
imgCode.value = ''
captcha.value = {
img: '',
uuid: '',
}
}
function handleSubmit() {
if (!imgCode.value) {
return uni.showToast({ title: '请输入图形验证码', icon: 'none' })
}
emits('success', { code: imgCode.value, uuid: captcha.value.uuid })
close()
}
async function initData() {
try {
const res = await getCaptcha()
captcha.value = {
img: 'data:image/jpeg;base64,' + res?.data?.img,
uuid: res?.data?.uuid,
}
} catch (e) {}
}
defineOptions({
})
defineExpose({
init,
close,
})
</script>
<template>
<wd-popup :close-on-click-modal="false" custom-style="border-radius: 20rpx;" v-model="show">
<view class="content">
<view class="content__header">
<view class="content__title">请输入图形验证码</view>
<view class="content__close-icon" @click="close">
<image src="@/pages-login/static/images/icon_del@2x.png"></image>
</view>
</view>
<view class="content__body">
<view
class="flex items-center justify-between border border-solid border-gray-3 rounded-50rpx"
>
<wd-input
no-border
v-model.trim="imgCode"
placeholderStyle="font-size: 28rpx;line-height: 40rpx;color: #999999;"
placeholder="请输入"
custom-class="flex-1 p-[15rpx+30rpx] !bg-transparent "
></wd-input>
<view class="flex items-center">
<image :src="captcha.img" class="w-180rpx h-80rpx rounded-r-50rpx"></image>
</view>
</view>
<view class="content__tip" @click="initData">看不清换一张</view>
</view>
<view class="content__footer">
<wd-button block @click="handleSubmit">确定</wd-button>
</view>
</view>
</wd-popup>
</template>
<style lang="scss" scoped>
.content {
box-sizing: border-box;
width: 630rpx;
padding: 30rpx 40rpx 40rpx;
background-color: #fff;
border-radius: 16rpx;
&__header {
position: relative;
text-align: center;
}
&__body {
margin: 42rpx 0 40rpx;
.uni-easyinput {
height: 90rpx;
display: flex;
align-items: center;
border-radius: $uni-border-radius-lg;
border: 1rpx solid #d1d1d1;
}
}
&__close-icon {
position: absolute;
right: 0;
top: 8rpx;
width: 32rpx;
height: 32rpx;
image {
width: 100%;
height: 100%;
}
}
&__title {
font-size: 32rpx;
font-weight: bold;
color: #333;
line-height: 48rpx;
letter-spacing: 2rpx;
}
&__tip {
margin-top: 10rpx;
text-align: right;
font-size: 24rpx;
font-weight: 400;
line-height: 33rpx;
color: $uni-color-primary;
letter-spacing: 2rpx;
}
}
</style>
@@ -0,0 +1,60 @@
<script lang="ts" setup>
const {t} = useI18n()
const show = ref(false)
function init() {
show.value = true
}
function close() {
show.value = false
}
function handleConfirm() {
close()
uni.navigateTo({
url: '/pages-user/pages/pay-password/set/index',
})
}
defineExpose({
show,
init,
close,
})
</script>
<template>
<wd-popup
custom-class="rounded-20rpx"
safe-area-inset-bottom
v-model="show"
@close="close"
>
<view class="w-630rpx box-border p-[40rpx+44rpx] flex flex-col">
<view class="mt-18rpx text-34-bold text-center">
{{ t('common.prompt.not-setup-payment-password') }}
</view>
<view class="mt-58rpx flex items-center justify-between">
<wd-button
custom-class="!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !rounded-20rpx"
@click="close"
>
{{ t('common.cancel') }}
</wd-button>
<wd-button plain
custom-class="!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !border-#666666 !rounded-20rpx"
@click="handleConfirm"
>
{{ t('common.go-to-settings') }}
</wd-button>
</view>
</view>
</wd-popup>
</template>
<style scoped lang="scss"></style>
+40
View File
@@ -0,0 +1,40 @@
<script lang="ts" setup>
import {useConfigStore} from "@/store";
const configStore = useConfigStore()
function getSystemInfo(): any {
let systemInfo: any
// #ifdef MP-WEIXIN
try {
// const systemSetting = uni.getSystemSetting() // 暂时不需要
const deviceInfo = uni.getDeviceInfo()
const windowInfo = uni.getWindowInfo()
const appBaseInfo = uni.getAppBaseInfo()
systemInfo = {
...deviceInfo,
...windowInfo,
...appBaseInfo
}
} catch (error) {
console.warn('获取系统信息失败,降级使用uni.getSystemInfoSync:', error)
// 降级处理,使用原来的方法
systemInfo = uni.getSystemInfoSync()
}
// #endif
// #ifndef MP-WEIXIN
systemInfo = uni.getSystemInfoSync()
// #endif
return systemInfo
}
const { statusBarHeight } = getSystemInfo()
</script>
<template>
<view :style="[{ height: statusBarHeight + 'px' }]" class="w-750rpx shrink-0"></view>
</template>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,54 @@
<script lang="ts" setup>
import {useUserStore} from "@/store";
const {t} = useI18n()
const userStore = useUserStore()
const emits = defineEmits(['changeStore'])
function clickChangeStore() {
// 当前用户已经有店铺
if (userStore.merchants.length !== 0) {
emits('changeStore')
} else {
userStore.getFirstShopSettled()
nextTick(() => {
console.log('hhhhhhhhhhhhhh', userStore.firstShopSettled)
// 判断当前用户是否已经认证过了(进入首次创建店铺流程)
if (userStore.firstShopSettled) {
uni.navigateTo({
url: '/pages-user/pages/store-settle-in/list',
})
} else {
uni.navigateTo({
url: '/pages-user/pages/store-settle-in/index?from=top',
})
}
})
}
}
</script>
<template>
<view>
<view v-if="userStore.isLogin" class="flex items-center" @click="clickChangeStore">
<view class="text-36rpx text-#333 lh-36rpx tracking-[.04em] mr-12rpx font-bold">
<template v-if="userStore.merchants.length === 0">
{{ t('pages.home.createStore') }}
</template>
<template v-else>
<template v-if="userStore.currenMerchantInfo && userStore.currenMerchantInfo.merchantName">
{{ userStore.currenMerchantInfo.merchantName }}
</template>
<template v-else>
{{ t('toast.pleaseSelectStore') }}
</template>
</template>
</view>
<image v-if="userStore.merchants.length !== 0" class="w-14rpx h-14rpx" src="@img/chef/108.png"></image>
</view>
</view>
</template>
<style lang="scss" scoped>
</style>
+86
View File
@@ -0,0 +1,86 @@
<script lang="ts" setup>
import {debounce} from "throttle-debounce";
import Config from "@/config";
import {useConfigStore} from "@/store";
const emits = defineEmits<{
success: []
}>()
const {t} = useI18n();
const configStore = useConfigStore();
const verificationCode = ref('');
const show = ref(false);
function init(code: string) {
verificationCode.value = code;
show.value = true;
}
function close() {
show.value = false;
}
function submit() {
close();
emits('success')
}
const handleSubmit = debounce(Config.debounceLongTime, submit, {
atBegin: true
});
function handleCancel() {
close()
navigateTo("/pages-shop/pages/booking-service/make-appointment/index")
}
function navigateTo(url: string) {
uni.navigateTo({
url,
})
}
defineOptions({
name: "UseCode",
});
defineExpose({
show,
init,
close,
});
</script>
<template>
<wd-popup
v-model="show"
custom-class="!bg-transparent"
safe-area-inset-bottom
@close="close"
>
<view class="box-border ">
<view class="p-[40rpx+40rpx+34rpx] bg-#fff rounded-20rpx"
>
<view class="text-44rpx text-primary font-bold lh-62rpx text-center">
{{ t('pages-store.order-detail.use-code') }}
</view>
<view class="mt-40rpx center">
<uqrcode ref="uqrcode" :options="{}" :size="512"
:value="verificationCode"
canvas-id="qrcode"
sizeUnit="rpx"></uqrcode>
</view>
</view>
<!-- <view class="center mt-52rpx">-->
<!-- <image class="w-80rpx h-80rpx shrink-0" src="@img/20241@2x.png" @click="close"></image>-->
<!-- </view>-->
</view>
</wd-popup>
</template>
<style lang="scss" scoped></style>
+52
View File
@@ -0,0 +1,52 @@
const Config = {
appName: "CHEFLINK Merchant",
googleMapKey: "AIzaSyDvTA1j9_hPPg7kev4fzf6RlGpf_yYhdoo",
// stripeKey:
// "pk_test_51RcyzCQVf8HI8x55xHQ11F0ydksiLEscmWuut6o0eCHV8fCYOWI9F9VrddMlKarnux65EjSIQmFb8rTwrrjrRzgG00CyQVpyRQ",
stripeKey:
"pk_live_51Rcyz0HqArf2IYTZTxK8mXrmUoxLJuC6QpNYgG76CEGSD6D3pUi48QkIwuyEAtqklEwaLC6cHGP5vntuiAFWB7cY000m2o2AU1",
defaultLanguage: "en",
timezone: "Asia/Shanghai",
debounceLongTime: 1000,
debounceShortTime: 500,
throttleTime: 1000,
throttleShortTime: 500,
iosId: "6615065357",
weixinServiceUrl: `https://chatbot.weixin.qq.com/webapp/BunZ9sgxnZPgogWooE5vth0H1IY0Xk?robotName=Nail%20Lover`,
weixinServiceName: "Nail Lover",
// 登录页
loginPath: "/pages-login/pages/index",
// 首页
indexPath: "/pages/home/index",
// 引导页
guidePath: "/pages/guide-page/index",
shareLink: "http://47.84.53.93/h5/#/",
// 手机区号数组
phoneCodeList: [
"+86", "+886", "+852", "+853", "+93", "+355", "+213", "+684", "+376", "+244",
"+1264", "+672", "+1268", "+54", "+374", "+297", "+61", "+43", "+994", "+973",
"+880", "+1246", "+375", "+32", "+501", "+229", "+1441", "+975", "+591", "+387",
"+267", "+55", "+1284", "+673", "+359", "+226", "+95", "+257", "+855", "+237",
"+1", "+238", "+1345", "+236", "+235", "+56", "+61", "+61", "+57", "+269", "+243",
"+242", "+682", "+506", "+225", "+385", "+53", "+357", "+420", "+45", "+253",
"+1767", "+1809", "+593", "+20", "+503", "+240", "+291", "+372", "+251", "+500",
"+298", "+679", "+358", "+33", "+594", "+689", "+241", "+995", "+49", "+233",
"+350", "+30", "+299", "+1473", "+590", "+1671", "+502", "+1481", "+224", "+245",
"+592", "+509", "+379", "+504", "+36", "+354", "+91", "+62", "+98", "+964", "+353",
"+972", "+39", "+1876", "+81", "+962", "+73", "+254", "+686", "+850", "+82", "+965",
"+996", "+856", "+371", "+961", "+266", "+231", "+218", "+423", "+370", "+352",
"+389", "+261", "+265", "+60", "+960", "+223", "+356", "+692", "+596", "+222",
"+230", "+269", "+52", "+691", "+373", "+377", "+976", "+1664", "+212", "+258",
"+264", "+674", "+977", "+31", "+599", "+687", "+64", "+505", "+227", "+234",
"+683", "+6723", "+1", "+47", "+968", "+92", "+680", "+507", "+675", "+595",
"+51", "+63", "+48", "+351", "+1809", "+974", "+262", "+40", "+7", "+250", "+290",
"+1869", "+1758", "+508", "+1784", "+685", "+378", "+239", "+966", "+221", "+381",
"+248", "+232", "+65", "+421", "+386", "+677", "+252", "+27", "+34", "+94", "+249",
"+597", "+47", "+268", "+46", "+41", "+963", "+992", "+255", "+66", "+1242", "+220",
"+228", "+690", "+676", "+1868", "+216", "+90", "+993", "+1649", "+688", "+256",
"+380", "+971", "+44", "+1", "+598", "+998", "+678", "+58", "+84", "+1340", "+681",
"+967", "+260", "+263"
]
};
export default Config;
+175
View File
@@ -0,0 +1,175 @@
/** 协议 */
export enum Agreement {
/** 用户须知及使用协议- */
USER_AGREEMENT = "chef_user_agreement",
/** 隐私政策 */
PRIVACY_POLICY = "chef_mer_privacy_policy",
/** 关于我们 */
ABOUT_US = "ABOUT_US",
/** 收益说明 */
CHEF_BENEFIT_DESCRIPTION = "chef_benefit_description",
/** 会员说明 */
CHEF_WITHDRAWAL_AGREEMENT = "chef_withdrawal_agreement",
/** 兑换码发布协议 */
CHEF_M_CODE_AGREEMENT = "chef_m_code_agreement",
/** 支付说明 */
DEPOSIT_EXPLANATION = "DEPOSIT_EXPLANATION",
/** GST说明 */
GST_EXPLANATION = "GST_EXPLANATION",
}
/** 短信验证码类型 */
export enum SmsType {
/** 绑定手机号 */
USER_BIND_PHONE_NUMBER = 1,
/** 邮箱注册 */
USER_EMAIL_REGISTER = 2,
/** 忘记密码 */
USER_FORGET_PASSWORD = 3,
/** 忘记支付密码 */
USER_FORGET_PAYMENT_PASSWORD = 4,
/** 设置支付密码 */
USER_SET_PAYMENT_PASSWORD = 5
}
/** 登录类型 */
export enum LoginType {
/** 邮箱登录 */
EMAIL = 1,
/** 苹果登录 */
APPLE = 2,
/** 脸书登录 */
FACEBOOK = 3,
/** 谷歌登录 */
GOOGLE = 4,
/** 账号密码登录 */
ACCOUNT = 5,
}
/** 字典 */
export enum DictType {
/** 订单相关 */
ABOUT_ORDER = "about_order",
/** 平台相关 */
ABOUT_PLATFORM = "about_platform",
/** 价格范围 */
PRICE_RANGE = "price_range",
/** 购买须知 */
PURCHASE_NOTES = "purchase_notes",
/** 活动说明 */
ACTIVITY_DESCRIPTION = "activity_description",
/** 预约提示 */
APPOINTMENT_REMINDER = "appointment_reminder",
}
/** 字典 */
export enum DictValue {
/** 待付款订单取消时间 */
PENDING_PAYMENT_ORDER_CANCEL_TIME = "pending_payment_order_cancel_time",
/** 退款订单自动审核时间 */
REFUND_ORDER_AUTO_AUDIT_TIME = "refund_order_auto_audit_time",
/** 平台联系方式 */
CONTACT_METHOD = "contact_method",
/** 多人预约折扣提示 */
GROUP_DISCOUNT_EN = "group_discount_en",
/** 多人预约折扣提示 */
GROUP_DISCOUNT_ZH = "group_discount_zh",
/**
卸甲预约、补甲预约免费时长提示 */
FREE_REMOVAL_SERVICE_AVAILABLE_EN = "free_removal_service_available_en",
/**
卸甲预约、补甲预约免费时长提示 */
FREE_REMOVAL_SERVICE_AVAILABLE_ZH = "free_removal_service_available_zh"
}
/** 自定义事件 */
export enum EventEnum {
/** 裁剪头像 */
CROPPER_AVATAR = "CROPPER_AVATAR",
/** 更新提现账号 */
UPDATE_WITHDRAWAL_ACCOUNT = "update-withdrawal-account",
/** 选择提现账号 */
CHOOSE_WITHDRAWAL_ACCOUNT = "choose-withdrawal-account",
/** 更新地址信息 */
UPDATE_ADDRESS = "UPDATE_ADDRESS",
/** 获取平台默认门店信息 */
PLATFORM_DEFAULT_STORE_INFO = "PLATFORM_DEFAULT_STORE_INFO",
/** 选择门店 */
CHOOSE_ADDRESS = "CHOOSE_ADDRESS",
/** 选择店铺类型 */
CHOOSE_STORE_TYPE = "CHOOSE_STORE_TYPE",
/** 选择菜单时间 */
CHOOSE_MENU_TIME = "CHOOSE_MENU_TIME",
/** 选择支付方式 */
CHOOSE_PAYMENT_METHOD = "CHOOSE_PAYMENT_METHOD",
}
// 订单状态
export enum OrderStatus {
/** 已退款 */
REFUNDED = -2,
/** 已取消 */
CANCELLED = -1,
/** 待付款 */
PENDING_PAYMENT = 1,
/** 已付款 */
HAS_PENDING_PAYMENT = 2,
/** 商家已接单 */
MERCHANT_ACCEPTED = 3,
/** 配送中 */
DELIVERING = 4,
/** 已核销(已送达) */
COMPLETED = 5,
/** 商家拒绝接单 */
MERCHANT_REJECTED = 6,
}
// 订单取消状态
export enum OrderCancelStatus {
/**
* 申请退款
*/
APPLIED = 1,
/**
* 商家同意
*/
APPROVED = 2,
/**
* 商家拒绝
*/
REJECTED = 3,
}
/** 消息类型 */
export enum MessageTypeEnum {
/** 有人评论了你 */
COMMENTED = 1,
/** 有人回复了你 */
REPLIED = 2,
/** 用户下单 */
ORDER_CREATED = 3,
/** 商家接单 */
MERCHANT_ACCEPTED = 4,
/** 商家同意退款 */
REFUND_AGREED = 5,
/** 商家拒绝退款 */
REFUND_REJECTED = 6,
/** 商家开始配送 */
DELIVERY_STARTED = 7,
/** 商家已送达 */
DELIVERY_ARRIVED = 8,
/** 订单已核销 */
ORDER_WRITTEN_OFF = 9,
/** 商家入驻审核通过 */
SETTLEMENT_APPROVED = 10,
/** 商家入驻审核未通过 */
SETTLEMENT_REJECTED = 11,
/** 评论审核通过 */
COMMENT_APPROVED = 13,
/** 评论审核未通过 */
COMMENT_REJECTED = 14,
}
+29
View File
@@ -0,0 +1,29 @@
/// <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
}
interface ImportMetaEnv {
/** 网站标题,应用名称 */
readonly VITE_APP_TITLE: string
/** 服务端口号 */
readonly VITE_SERVER_PORT: string
/** 后台接口地址 */
readonly VITE_SERVER_BASEURL: string
/** H5是否需要代理 */
readonly VITE_APP_PROXY: 'true' | 'false'
/** H5是否需要代理,需要的话有个前缀 */
readonly VITE_APP_PROXY_PREFIX: string // 一般是/api
/** 是否清除console */
readonly VITE_DELETE_CONSOLE: string
/** 是否是开发环境 */
readonly DEV: boolean
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
+18
View File
@@ -0,0 +1,18 @@
import type AreaCode from "@/components/area-code/area-code.vue";
export default function useAreaCode() {
const areaCodeRef = ref<InstanceType<typeof AreaCode>>()
const defaultAreaCode = ref("+1")
function handlePageClick() {
areaCodeRef.value?.close()
}
return {
areaCodeRef,
defaultAreaCode,
handlePageClick
}
}
+6
View File
@@ -0,0 +1,6 @@
export default function useEventEmit(eventName: string, callback: (result: any) => void) {
onLoad(() => uni.$on(eventName, callback))
onUnload(() => uni.$off(eventName))
}
+41
View File
@@ -0,0 +1,41 @@
import {appSmsSendPost} from "@/service";
// 获取短信验证码的自定义 Hook
export default function useGetMsgCode() {
const {t} = useI18n()
const isSend = ref(0)
async function getMsgCode({
type, phone, areaCode
}: {
type: string | number;
phone: string | number,
areaCode: string | number,
}) {
const res = await appSmsSendPost({
body: {
areaCode,
phone,
// 1 :用户端绑定手机号 2 :用户端邮箱注册 3 :用户端忘记密码 4 :用户端忘记支付密码 5 :用户端设置支付密码
type,
}
})
if (res) {
await uni.showToast({title: t('common.verification-code-sent-successfully'), icon: 'none'})
isSend.value = 60
const timer = setInterval(() => {
if (isSend.value === 0) {
clearInterval(timer)
} else {
isSend.value -= 1
}
}, 1000)
}
}
return {
isSend,
getMsgCode
}
}
+20
View File
@@ -0,0 +1,20 @@
// 监听网络状态变化的自定义 Hook
export default function useNetworkStatusChange(callback: () => void) {
const OnNetworkStatusChange = (res: any) => {
console.log('=>(useNetworkStatusChange)', res)
if (res.isConnected) {
callback && callback()
}
}
onLoad(() => {
uni.onNetworkStatusChange(OnNetworkStatusChange)
})
onUnload(() => {
uni.offNetworkStatusChange(OnNetworkStatusChange)
})
}
+42
View File
@@ -0,0 +1,42 @@
// 自定义分页 Hook
export default function usePage<T>(
queryFun: (pageNum: number, pageSize: number) => Promise<IResData<T>>,
callback?: (params?: any) => void,
) {
const paging = ref<ZPagingInstance<T> | null>(null)
const loading = ref(true)
const firstLoaded = ref(false)
const dataList = ref<T[]>([])
const totalRows = ref(0)
async function queryList(pageNum: number, pageSize: number) {
try {
const res = await queryFun(pageNum, pageSize)
console.log('接口响应', res)
await paging.value?.complete(res?.rows || [])
totalRows.value = res?.total
firstLoaded.value = true
setTimeout(() => {
callback && callback({...res, pageNum, pageSize})
}, 100)
} catch (error) {
await paging.value?.complete(false)
setTimeout(() => {
callback && callback({pageNum, pageSize})
}, 100)
} finally {
setTimeout(() => {
loading.value = false
}, 100)
}
}
return {
paging,
firstLoaded,
loading,
dataList,
totalRows,
queryList,
}
}
+32
View File
@@ -0,0 +1,32 @@
import { ref, onMounted, onUnmounted } from 'vue'
import { throttle } from 'throttle-debounce'
export function useScrollThreshold(threshold = 250, delay = 150) {
const isThresholdReached = ref(false)
// 创建节流处理函数 (使用箭头函数保持this指向)
const throttledHandler = throttle(delay, (scrollTop: number) => {
const newState = scrollTop > threshold
// 仅当状态变化时更新
if (isThresholdReached.value !== newState) {
isThresholdReached.value = newState
console.log("阈值状态更新:", newState, "| 滚动位置:", scrollTop)
}
})
const handleScroll = (e: any) => {
// 传递滚动位置给节流函数
throttledHandler(e.scrollTop)
}
onMounted(() => {
uni.$on('page-scroll', handleScroll)
})
onUnmounted(() => {
uni.$off('page-scroll', handleScroll)
throttledHandler.cancel() // 清除节流函数残留任务
})
return isThresholdReached
}
+50
View File
@@ -0,0 +1,50 @@
import type { CustomRequestOptions } from '@/http/types'
import {http as httpUtils} from "@/utils/http";
export function http<T>(options: CustomRequestOptions) {
// 1. 返回 Promise 对象
return httpUtils<T>(options)
}
/**
* GET
* @param url
* @param query query参数
* @param header json格式
* @returns
*/
export function httpGet<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
return http<T>({
url,
query,
method: 'GET',
header,
...options,
})
}
/**
* POST
* @param url
* @param data body参数
* @param query query参数post请求也支持query
* @param header json格式
* @returns
*/
export function httpPost<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
return http<T>({
url,
query,
data,
method: 'POST',
header,
...options,
})
}
http.get = httpGet
http.post = httpPost
// 支持与 alovaJS 类似的API调用
http.Get = httpGet
http.Post = httpPost
+33
View File
@@ -0,0 +1,33 @@
/**
* uniapp RequestOptions IUniUploadFileOptions
*/
export interface CustomRequestOptions extends UniApp.RequestOptions {
query?: Record<string, any>
/** 出错时是否隐藏错误提示 */
hideErrorToast?: boolean
}
// 通用响应格式
export interface IResponse<T = any> {
code: number | string
data: T
message: string
status: string | number
}
// 分页请求参数
export interface PageParams {
page: number
pageSize: number
[key: string]: any
}
// 分页响应数据
export interface PageResult<T> {
list: T[]
total: number
page: number
pageSize: number
}
+30
View File
@@ -0,0 +1,30 @@
import type { CustomRequestOptions } from '@/http/types'
import { http } from './http'
/*
* openapi-ts-request request
*/
export default function request<T = unknown>(
url: string,
options: Omit<CustomRequestOptions, 'url'> & {
params?: Record<string, unknown>
headers?: Record<string, unknown>
},
) {
const requestOptions = {
url,
...options,
}
if (options.params) {
requestOptions.query = requestOptions.params
delete requestOptions.params
}
if (options.headers) {
requestOptions.header = options.headers
delete requestOptions.headers
}
return http<T>(requestOptions)
}
+2
View File
@@ -0,0 +1,2 @@
export { routeInterceptor } from './route'
export { requestInterceptor } from './request'
+79
View File
@@ -0,0 +1,79 @@
import qs from 'qs'
import {useUserStore} from '@/store'
import {platform} from '@/utils/platform'
export type CustomRequestOptions = UniApp.RequestOptions & {
query?: Record<string, any>
/** 出错时是否隐藏错误提示 */
hideErrorToast?: boolean
}
// 请求基准地址
const baseUrl = import.meta.env.VITE_SERVER_BASEURL
const proxyPrefix = import.meta.env.VITE_APP_PROXY_PREFIX
// 拦截器配置
const httpInterceptor = {
// 拦截前触发
invoke(options: CustomRequestOptions) {
// 接口请求支持通过 query 参数配置 queryString
if (options.query) {
const queryStr = qs.stringify(options.query)
if (options.url.includes('?')) {
options.url += `&${queryStr}`
} else {
options.url += `?${queryStr}`
}
}
// 非 http 开头需拼接地址
if (!options.url.startsWith('http')) {
// #ifdef H5
// console.log(__VITE_APP_PROXY__)
if (JSON.parse(__VITE_APP_PROXY__)) {
// 啥都不需要做
options.url = proxyPrefix + options.url
} else {
options.url = baseUrl + options.url
}
// #endif
// 非H5正常拼接
// #ifndef H5
options.url = baseUrl + options.url
// #endif
// TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址
}
// 1. 请求超时
options.timeout = 10000 // 10s
// 2. (可选)添加小程序端请求头标识
const localeLanguages = uni.getLocale()
console.log(localeLanguages)
options.header = {
platform, // 可选,与 uniapp 定义的平台一致,告诉后台来源
'content-language': localeLanguages === 'zh-Hans' ? 'zh_CN' : 'en_US',
...options.header,
}
// console.log(options)
// 3. 添加 token 请求头标识
const userStore = useUserStore()
if (userStore.token) {
// options.header.Authorization = `Bearer ${userStore.token}`
options.header.token = userStore.token
}
// 4. 添加当前商户的TOKEN
if (userStore.currentMerchantToken) {
options.header['merchantToken'] = userStore.currentMerchantToken
}
},
}
export const requestInterceptor = {
install() {
// 拦截 request 请求
uni.addInterceptor('request', httpInterceptor)
// 拦截 uploadFile 文件上传
uni.addInterceptor('uploadFile', httpInterceptor)
},
}
+51
View File
@@ -0,0 +1,51 @@
import {useUserStore} from '@/store'
import Config from '@/config'
let loginPathPattern = ['/pages-user', '/pages/order', '/pages/invite', '/pages/scan-code']
let tabbarPaths = ['pages/index/index']
let prevPath = ''
let isDev = import.meta.env.MODE !== 'development'
// 黑登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录)
const navigateToInterceptor = {
// 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
invoke(result: any) {
console.log('navigateToInterceptor', result)
// if (url === prevPath) {
// return false
// }
// prevPath = url
// setTimeout(() => {
// prevPath = ''
// }, 500)
//
// const path = url.split('?')[0]
// console.log('path', path)
//
// const userStore = useUserStore()
//
// console.log(isDev)
// if (loginPathPattern.some((item) => path.includes(item))) {
// return userStore.checkLogin()
// }
// return true
},
}
const navigateBackInterceptor = {
invoke(result: any) {
console.log('navigateBackInterceptor', result)
return true
},
}
export const routeInterceptor = {
install() {
uni.addInterceptor('navigateTo', navigateToInterceptor)
uni.addInterceptor('redirectTo', navigateToInterceptor)
uni.addInterceptor('navigateBack', navigateBackInterceptor)
},
}
+67
View File
@@ -0,0 +1,67 @@
<template>
<wd-config-provider class="h-full" :themeVars="themeVars">
<view class="" v-if="!isConnected&&configStore.showNetworkAnomaly">
<navbar :fixed="false" :title="t('navbar-network-anomaly')"/>
</view>
<slot v-else/>
</wd-config-provider>
<wd-toast/>
<wd-message-box/>
</template>
<script lang="ts" setup>
import type {ConfigProviderThemeVars} from 'wot-design-uni'
import {useConfigStore} from "@/store";
const {t} = useI18n()
const configStore = useConfigStore()
const isConnected = ref(true)
const themeVars: ConfigProviderThemeVars = {
colorTheme: '#333',
buttonPrimaryBgColor: '#14181B',
}
const OnNetworkStatusChange = (res: any) => {
console.log('=>(useNetworkStatusChange)', res)
isConnected.value = !!res.isConnected
}
onLoad(() => {
uni.onNetworkStatusChange(OnNetworkStatusChange)
})
onShow(() => {
uni.getNetworkType({
success: function (res) {
console.log('networkType', res.networkType);
}
});
})
onUnload(() => {
if (configStore.showNetworkAnomaly) {
configStore.showNetworkAnomaly = false
}
uni.offNetworkStatusChange(OnNetworkStatusChange)
})
</script>
<style lang="scss">
/* #ifndef APP-NVUE */
page {
font-family: 'UberMove', sans-serif;
}
.poynter-oldstyle-text {
font-family: 'UberMove', sans-serif;
}
.helvetica-now-text {
font-family: 'UberMove', sans-serif;
}
/* #endif */
</style>
+692
View File
@@ -0,0 +1,692 @@
{
"agreement": {
"codeAgreement": "Redemption Code Release Agreement",
"platformAgreement": "Platform Agreement",
"privacy-policy": "Privacy Policy",
"user-terms-conditions": "User Terms and Conditions",
"withdrawalAgreement": "Withdrawal Agreement"
},
"common": {
"add": "Add",
"agree": "Agree",
"stock": "Stock",
"app-marketplace": "Application Market",
"appleLoginFailed": "Apple sign-in failed. Please try again.",
"auditStatus": {
"pass": "Review passed",
"reject": "Review rejection",
"submitted": "Under review"
},
"balance": "Balance",
"cancel": "Cancel",
"close": "Close",
"comment": "Comment",
"confirm": "Confirm",
"continue": "Continue",
"cuttingImage": "Crop pictures",
"delete": "Delete",
"details": "Details",
"edit": "Edit",
"email": "Email",
"enter": "Enter",
"enterPassword": "Enter Password",
"go-to-settings": "To Settings",
"google-map": "Google Maps",
"googleLoginFailed": "Google sign-in failed. Please try again.",
"gotIt": "Got it",
"inquiry": "Instant inquiry",
"instructions": "Instructions",
"item": "items",
"itemNum": "Total",
"loading": "Loading",
"menus": "Menus",
"minutes": "Minutes",
"no": "No",
"obtain": "Get",
"operation-success": "Operation successful",
"or": "OR",
"photograph": "photo",
"redeemCode": "Redeem Code",
"coupon": "Coupon",
"distributing": "Distributing",
"ended": "Ended",
"placeholder": {
"pleaseEnter": "Please enter",
"pleaseEnterName": "Please enter a name",
"pleaseEnterPhone": "Please enter your phone number",
"pleaseSelect": "Please select"
},
"please-enter-your-payment-password": "Please enter your payment password",
"prompt": {
"download-failed": "Download failed",
"not-setup-payment-password": "You haven't set a payment password yet?",
"picture-wrong-please-try-again": "Please select a file and try again",
"please-carefully-read-and-agree": "Please read carefully and agree",
"replication-successful": "Copy successful",
"request-failed-please-try-again-later": "Network or server error please wait.",
"request-incorrect": "Request failed",
"system-prompt": "System prompts",
"system-prompt-delete": "Are you sure to delete it?",
"system-prompt-reject": "Should we reject the order?",
"up-failed": "Upload timed out, please try again later",
"update-failed": "Update failed",
"update-successfully": "Update successful",
"phone-number-empty": "Phone number is empty"
},
"reEdit": "Re Edit",
"reject": "Reject",
"remove": "Remove",
"reply": "Reply",
"save": "Save",
"saveSuccess": "Save Success",
"select": "Select",
"select-from-album": "Select from mobile phone album",
"select-maps-app": "Select a map application",
"send": "Send",
"skip": "Skip",
"stores": "Stores",
"submit": "Submit",
"uploading": "Uploading",
"yes": "Yes"
},
"navbar-change-payment-password": "Modify payment password",
"navbar-forget-payment-password": "Forgot your payment password",
"navbar-future-order": "Future Order",
"navbar-month-order": "This Month's Order",
"navbar-refund-order": "Refund order",
"navbar-reservation": "Today's ordered dishes",
"navbar-set-payment-password": "Set a payment password",
"navbar-settings": "Settings",
"navbar-today-order": "Today's Order",
"navbar-yesterday-order": "Yesterday's Order",
"navbar-forget-password": "Forgot password",
"pages": {
"home": {
"bookDishesToday": "Book a dish",
"card-desc": "Self developed vegetable stir fry machine on the platform, your life helper by your side.",
"card-title": "CHEFLINK stir fry machine",
"commonTools": "Common tools",
"coupons": "Coupons",
"createStore": "Create a store",
"dataStatistics": "Data statistics",
"futureOrders": "Booking order",
"monthAmount": "The monthly income",
"monthOrder": "Orders for this month",
"myIncome": "My profits",
"recipes": "menu",
"refundOrders": "Refund order",
"scanCode": "Scan",
"todayAmount": "Today's earnings",
"todayOrder": "Order today",
"totalAmount": "Total revenue",
"view": "View details",
"yesterdayOrder": "Yesterday's order"
},
"menu": {
"add": "New Items",
"addMenu": "New Menu",
"introduction": "Introduction",
"items": "Items",
"menu": "Menu",
"menuHours": "Menu hours",
"menus": "Menus",
"overview": "Overview",
"tipsDesc": "They help customers choose what to order.To add photos,go to items and choose menu item",
"tipsTitle": "Photos of menu items can increase sales",
"title": "Items"
},
"mine": {
"complaintsSuggestions": "Complaints And Suggestions",
"language": "Language",
"log-out-successfully": "Logout successful",
"logOut": "Log Out",
"login-out-tip": "Are you sure you are logging out",
"loginTips": "Please log in",
"set": "Set",
"storeAuth": "Store Authentication",
"storeManage": "Store Management"
},
"order": {
"ACPT": "ACPT",
"CONF": "CONF",
"DCOMP": "DCOMP",
"DEL": "DEL",
"OTW": "OTW",
"PU": "PU",
"cancel": "Cancelled",
"refund": "Refunded",
"subtotal": "Subtotal",
"taxesAndFees": "Taxes and other fees",
"total": "Total"
},
"search": {
}
},
"pages-login": {
"and": "and",
"choose-language": {
"confirm": "Confirm language",
"success": "Language set successfully",
"tip": "You can change this setting later in the 'My' section of the app",
"title": "Hello, welcome to log in"
},
"continuing-agree": "I have read it carefully and agreed",
"forget-password": {
"description": "Set a new password for Sign In",
"newPassword": "New password"
},
"guide-page": {
"welcome": {
"description": "Account created successfully.",
"next": "Next",
"title": "Hello"
}
},
"index": {
"apple-login": "Continue with Apple",
"description": "Create an account or log in to book and manage your appointments",
"facebook-login": "Continue with Facebook",
"google-login": "Continue with Google",
"input-placeholder": "Email Address/Phone",
"prompt": {
"email-address": "Please enter email address",
"email-address-verify": "Please enter the correct email address"
},
"title": "Log in or sign up",
"wechat-login": "Continue with Wechat"
},
"login": {
"description": "Enter password to Sign In",
"forgotPassword": "Forgot password",
"title": "Sign In"
},
"login-successfully": "Login successful",
"prompt": {
"confirm-email": "Please enter your confirmation email address",
"email": "Please enter your email address",
"email-verify": "Please enter the correct email address",
"first-name": "Please enter a name",
"last-name": "Please enter your last name",
"password": "Please enter your password",
"password-verify": "Please enter a 6-20 digit password",
"phone-number": "Please enter your mobile phone number",
"phone-number-verify": "Please enter the correct mobile phone number",
"reset-success": "Reset successfully"
},
"sign-up": {
"confirm-email": "Confirm email",
"first-name": "First name",
"last-name": "Last name",
"password": "Password",
"phone-number": "Phone number",
"register-success": "Sign Up Successful",
"title": "Sign Up"
},
"verify-code": {
"changeOne": "Change one",
"description": "For security reasons, please type characters to continue",
"title": "verification code"
}
},
"pages-user": {
"card": {
"add": "+ Add",
"desc": "After binding the card, payment can be made quickly and directly.",
"listCard": "Credit card list",
"title": "Bind the card first and make the payment later"
},
"choosePaymethod": {
"creditCard": "Credit card payment",
"replace": "replace",
"title": "Choose a payment method",
"wallet": "Balance payment"
},
"complaints": {
"contact-information": "Contact information",
"contact-information-tip": "Your contact information helps us communicate and solve problems, only visible to staff",
"description": "You are welcome to give us feedback on the use of our products and suggestions.",
"feedback-content": "Feedback content",
"feedback-content-placeholder": "Please fill in the feedback content",
"image": "Image",
"title": "Complaints and suggestions",
"validation": {
"contact-phone-invalid": "Please fill in the correct contact phone",
"contact-phone-required": "Please fill in the contact phone",
"content-max-length": "Feedback content cannot exceed 500 characters",
"content-min-length": "Feedback content must be at least 10 characters",
"content-required": "Please fill in the feedback content"
}
},
"coupons": {
"addCoupon": "Add Coupon",
"addForFree": "Add for Free",
"agreeText": "I have read it carefully and agreed",
"andReceiveDiscount": "and receive a discount of",
"copyCode": "Copy",
"couponAgreement": "Coupon Publishing Agreement",
"dateFilter": "Date Filter",
"days": "Days",
"discount": "Discount",
"discountRatio": "Discount Ratio",
"fullDiscount": "Full Discount",
"name": "Coupon Name",
"paidPublishing": "Paid Publishing",
"payMethods": "Payment Methods",
"publishNow": "Publish Now",
"selectValidEndDate": "Select Expiry Date",
"selectValidStartDate": "Select Effective Date",
"spendOver": "Spend over",
"title": "Coupons",
"totalQuantity": "Distribution quantity",
"type": "Type",
"validEnd": "Time of failure",
"validDays": "Validity Days",
"validStart": "Effective time",
"validityDays": "Validity Days",
"validation": {
"agreeAgreementFirst": "Please agree to the redemption code publishing agreement first",
"createSuccess": "Created successfully",
"discountValueDecimal": "Discount value can have at most 2 decimal places",
"discountValueInvalid": "Please enter a valid discount value (0-1, max 2 decimal places)",
"discountValueRange": "Please enter an integer between 1-100",
"discountValueRequired": "Discount value is required",
"minAmountRequired": "Please enter the minimum spending threshold",
"nameRequired": "Coupon name is required",
"startDateCannotLaterThanEndDate": "Effective date cannot be later than expiry date",
"totalQuantityPositive": "Distribution quantity must be a positive integer",
"totalQuantityRequired": "Total quantity is required",
"validDaysPositive": "Validity days must be a positive integer",
"validDaysRequired": "Please enter validity days",
"validEndRequired": "Expiry time is required",
"validStartRequired": "Effective time is required"
}
},
"food": {
"add-food": {
"active": "Active",
"add": "Add",
"addMenu": "Add Menu",
"addPicture": "Add Picture",
"description": "Description",
"descriptionTip": "Manually add detailed dish descriptions to help customers decide what to order",
"discountPriceError": "Discount price cannot be higher than original price",
"discountedPrice": "Actual selling price:",
"enterPlaceholder": "Enter",
"frequentlyBoughtTogether": "Frequently Bought Together",
"itemActiveDesc": "Customers can view and order this dish",
"itemIsActive": "Item is Active",
"itemIsNot": "The dish is not activated",
"menu": "Menu:",
"name": "Name:",
"pictures": "Pictures:",
"pound": "lb",
"price": "Price:",
"schema": {
"dishDescription": "Dish description cannot be empty",
"dishImage": "Please upload at least one dish image",
"dishName": "Dish name cannot be empty",
"discountPriceRequired": "Please enter discount price",
"discountPricePositive": "Discount price must be greater than 0",
"menuId": "Please select a menu",
"originalPrice": "Please enter original price"
},
"taxRate": "Tax Rate:",
"tips": "Please add a menu first before adding dishes",
"title": "Add Dish",
"weight": "Weight:",
"stock": "Stock Quantity:",
"stockPlaceholder": "Enter stock quantity",
"stockRequired": "Please enter stock quantity",
"stockMustBeInteger": "Stock quantity must be an integer",
"stockMustBeNonNegative": "Stock quantity must be greater than or equal to 0"
},
"add-item": {
"add": "Add",
"addSideDishes": "+ Add Side Dishes",
"enterPlaceholder": "Enter",
"isRequired": "Is Required",
"name": "Name",
"price": "Price($)",
"sideDish": "Side Dish",
"sideDishesFlavorValue": "Side Dishes/Flavor Value:",
"title": "Add Side Dish",
"typeOfSideDishes": "Type of Side Dishes/Flavor:",
"validation": {
"addAtLeastOne": "Please add at least one side dish",
"completeAllFields": "Please complete all fields for side dish {index}",
"enterSideDishName": "Please enter side dish name for item {index}",
"submittedSuccessfully": "Submitted successfully"
},
"value": "Value"
}
},
"income": {
"index": {
"accountToReceive": "Account to receive:",
"accumulatedIncome": "Accumulated income:",
"addBankCard": "Add bank card",
"auditFailureReason": "Audit Failure Reason:",
"availableWithdrawalAmount": "Available withdrawal amount:",
"detailedList": "Detailed List",
"handlingFee": "Handling Fee:",
"paymentVoucher": "Payment Voucher:",
"thisMonth": "This month's:",
"toBeCredited": "To be credited:",
"today": "Today's:",
"walletBalance": "Wallet balance",
"withdrawal": "Withdrawal",
"withdrawalAccount": "Withdrawal Account:",
"withdrawalAmount": "Withdrawal Amount",
"withdrawalAmountLabel": "Withdrawal Amount:",
"withdrawalFee": "Withdrawal fee:",
"withdrawalRecords": "Withdrawal Records",
"withdrawalTime": "Withdrawal Time:",
"yesterday": "Yesterday's:"
},
"title": "My income",
"toast": {
"pleaseSelectACard": "Please select an arrival card"
},
"withdraw": {
"addCard": "Add Bank Card",
"bankName": "Bank Name",
"bankNameTips": "Please enter bank name",
"card": "Bank Card Number",
"cardListTitle": "Bank card list",
"cardTips": "Please enter bank card number",
"name": "Payee Name",
"nameTips": "Please enter payee name",
"phone": "Contact Information",
"phoneTips": "Please enter contact information",
"title": "Withdraw",
"withdrawImmediately": "Withdraw immediately"
}
},
"member": {
"creditCard": "Add a credit card"
},
"menu": {
"add": "Add",
"addMenu": "Add Menu",
"customersCanOverview": "Customers can overview and order from this menu",
"detail": {
"addTimeSlot": "+ Add Time Slot",
"dateFilter": "Date Filter",
"endTime": "End Time:",
"endTimeError": "End time cannot be earlier than start time",
"open24Hours": "Open 24 Hours",
"pleaseSelectDays": "Please select days for time slot",
"pleaseSetTime": "Please set start and end time for time slot",
"selectDaysAndHours": "Select days and hours:",
"startTime": "Start Time:"
},
"editMenu": "Edit Menu",
"enterPlaceholder": "Enter",
"menuHours": "Menu hours",
"menuIsActive": "Menu is active",
"menuIsNotActive": "Menu is not active",
"name": "name:",
"nameRequired": "Name is required",
"setDefaultDays": "Set the default days and times this menu will be available",
"submit": "submit"
},
"message": {
"readAll": "One-click read",
"title": "Message"
},
"order": {
"agree": "Agreed",
"agreeRefund": "You have agreed to the user's refund application",
"balancePayment": "Balance Payment",
"cancelOrder": "User has applied to cancel the order",
"checkout": {
"priceDetail": {
"desc": "These fees are only due to factors such as the number of shopping carts, and are used to pay for expenses related to your order, including platform services and delivery services.",
"memberDiscount": "Member discount",
"serviceFees": "Service fees and other expenses",
"taxation": "Taxation",
"title": "What content is included"
}
},
"collapse": "Collapse",
"copies": "Copies",
"createTime": "Create Time",
"creditCardPayment": "Credit Card Payment",
"delivered": "Delivered",
"deliveredOrder": {
"uploadPhoto": "Please upload delivery photo"
},
"deliveredTips": "Take and upload delivery photos",
"deliveryAddress": "Delivery Address:",
"deliveryTime": "Delivery Time:",
"details": "Details",
"enterReply": "Enter reply text",
"expand": "Expand",
"items": "items",
"mapNavigation": "Map Navigation",
"mealPickupTime:": "Meal pickup time",
"noReview": "No review yet",
"orderInfo": "Order Information:",
"orderNumber": "Order Number",
"payMethod": "Payment Method",
"payTime": "Payment Time",
"pending": "Pending",
"phoneContact": "Phone Contact",
"pickUp": "Pick Up",
"pleaseEnterReply": "Please enter reply content",
"processed": "Processed",
"productInfo": "Product Information:",
"receiving": "Receiving",
"refundReason": "Reason for refund",
"refuseReason": "Reason for rejection",
"refuseRefund": "You have rejected the user's refund application",
"reject": "Rejected",
"remark": "Remark:",
"reply": "Reply:",
"replySuccess": "Reply successful",
"score": " points",
"share": "Share",
"startDelivery": "Start delivery",
"startDeliveryForm": {
"deliveryAvatar": "Delivery Avatar",
"deliveryFirstName": "Delivery First Name",
"deliveryPhone": "Delivery Phone",
"deliverySurname": "Delivery Surname",
"title": "Add Delivery Person",
"validation": {
"deliveryAvatarRequired": "Please upload delivery avatar",
"deliveryFirstNameRequired": "Please enter delivery first name",
"deliveryPhoneRequired": "Please enter delivery phone number",
"deliverySurnameRequired": "Please enter delivery surname",
"orderIdRequired": "Please enter order ID"
}
},
"status": {
"completed": "Completed",
"completedDesc": "Order completed",
"delivering": "Delivering",
"deliveringDesc": "Waiting for merchant delivery...",
"merchantAccepted": "Order Accepted",
"merchantAcceptedDesc": "Merchant has accepted the order",
"merchantRejected": "Rejected",
"merchantRejectedDesc": "The merchant has rejected the order",
"pendingPayment": "Pending Order",
"waitingForOrder": "Waiting for order..."
},
"toast": {
"receiveSuccess": "Order successfully accepted",
"refuseReasonRequired": "Please enter a reason for rejection",
"refuseSuccess": "Order successfully rejected"
},
"total": "Total",
"viewReview": "View Review",
"writeOff": "Write off"
},
"pay-password": {
"change-payment-password-successfully": "Modify payment password successfully",
"enter-6-digit-password": "Please enter a 6-digit password",
"forget-payment-password-successfully": "Forgot your payment password successfully",
"input-placeholder": {
"enter-new-password": "Please enter a new password",
"enter-new-password-again": "Please enter the new password again",
"enter-old-password": "Please enter the original password",
"enter-phone-number": "Please enter your mobile phone number",
"enter-verification-code": "Please enter the verification code"
},
"set-payment-password-successfully": "Setting the payment password successfully",
"two-passwords-inconsistent": "The password is inconsistent when entered twice"
},
"recipe": {
"addPicture": "Add Picture",
"addRecipe": "Add Recipe",
"collapseReply": "Collapse reply content",
"detail": {
"title": "Recipe Details"
},
"dragTip": "Default first image as cover",
"editRecipe": "Edit recipes",
"enterPlaceholder": "Enter",
"expandReply": "Expand reply",
"index": {
"add": "Add",
"delete": "Delete",
"edit": "Edit",
"title": "Recipe Management"
},
"ingredientsPreparation": "Ingredients preparation:",
"recipeClassification": "Recipe Classification:",
"recipeImage": "Recipe Image:",
"recipeName": "Recipe Name:",
"replyTo": "Reply to",
"schema": {
"ingredients": "Ingredients preparation is required",
"recipeCategory": "Please select recipe category",
"recipeImage": "Please upload at least one recipe image",
"recipeName": "Recipe name is required"
}
},
"sales-record": {
"amt": "Amt",
"dateFilter": "Date Filter",
"dishAmount": "Dish Amount",
"dishQuantity": "Dish Quantity",
"portion": " portions",
"qty": "Qty",
"title": "Sales Record"
},
"setting": {
"language": "Language Switching",
"logOff": "Log out of account",
"logOffConfirm": "Still need to be cancelled",
"logOffDesc": "Clear all user information on this platform, including but not limited to personal information, consumption information, etc. The above information is deleted, and it is unrecoverable, unrecoverable, unrecoverable, unrecoverable, unrecoverable, unrecoverable.",
"logOffRisk": "There are the following risks when canceling the account",
"modification": "Modify password",
"payPwd": "Payment password"
},
"store-management": {
"index": {
"addPicture": "Add Picture",
"businessHours": "Business Hours:",
"deliveryAmount": "Delivery Amount:",
"deliveryFee": "Delivery Fee:",
"deliveryTime": "Delivery Time:",
"hours": {
"businessHours": "Business Hours",
"days": {
"friday": "Fri.",
"monday": "Mon.",
"saturday": "Sat.",
"sunday": "Sun.",
"thursday": "Thu.",
"tuesday": "Tue.",
"wednesday": "Wed."
},
"endTime": "End Time",
"pleaseSelectAtLeastOneDay": "Please select at least one day",
"pleaseSelectStartAndEndTime": "Please select start and end time",
"selectDayPrompt": "Please select the day of the week to start and end.",
"startTime": "Start Time",
"startTimeCannotLaterThanEndTime": "Start time cannot be later than end time"
},
"isDelivery": "Whether to deliver or not?",
"isSelfPickup": "Is it self pickup?",
"logo": "LOGO:",
"logoSizeHint": "(Suggested size 1:1)",
"mainBusinessScope": "Main Business Scope:",
"mealPickupTime": "Meal Pickup Time:",
"off": "OFF",
"on": "ON",
"prompt": {
"businessHours": "Please select business hours",
"deliveryFee": "Please enter delivery fee",
"deliveryTime": "Please enter delivery time",
"logo": "Please upload store LOGO",
"merchantAddress": "Please select store location",
"merchantCategoryIds": "Please select main business scope",
"merchantName": "Please enter store name",
"minOrderPrice": "Please enter minimum order amount",
"minOrderPriceTip": "The starting amount must be a number greater than 0 at most two decimal places",
"phone": "Please enter contact phone",
"pickupTime": "Please enter pickup time",
"shopImages": "Please upload recommended dishes pictures"
},
"recommendedDishPictures": "Recommended Dish Pictures:",
"save": "Save",
"shopLocation": "Shop Location:",
"shopNames": "Shop Names:",
"shopPhone": "Shop Phone:",
"shopState": "Shop State",
"title": "Store Management"
}
},
"store-settle-in": {
"address": "Address",
"audit": {
"pass": "Successful review",
"passDesc": "Congratulations on your successful entry into the platform!",
"passDesc1": "Come and upload your product information to start your money-making journey",
"reject": "Review failed",
"rejectDesc": "Reasons for failure",
"submitted": "Under review",
"submittedDesc": "The platform is under review. Please wait patiently..."
},
"businessLicense": "Business license",
"category": "Type",
"desc": "Just fill in the information and start your money-making journey now",
"detailedAddress": "Detailed address",
"idCardFront": "Picture of ID card",
"introduction": "Introduction",
"schema": {
"address": "Please select the address",
"businessLicense": "Please upload your food handling license",
"category": "Please select your type",
"detailedAddress": "Please enter the detailed address",
"idCardBack": "Please upload your passport",
"idCardFront": "Please upload your ID",
"introduction": "Please enter the introduction",
"storeName": "Please enter the store name"
},
"storeName": "Shop name",
"title": "Shop entry"
}
},
"tabBar": {
"browse": "Browse",
"global": "Global Shopping",
"home": "Home",
"menu": "Menu",
"mine": "Profile",
"order": "History"
},
"toast": {
"commentSuccess": "Comment successful",
"deleteSuccess": "Delete successfully",
"orderWriteOffSuccess": "Order verification successfully",
"pleasePhone": "Please pay attention to answering the phone.",
"pleaseSelectStore": "select a store",
"rejectSuccess": "Rejection successful",
"selectScope": "Please select a store type",
"submitSuccess": "Submission successful",
"submitSuccessTip": "Submission is successful, please wait for review"
}
}
+18
View File
@@ -0,0 +1,18 @@
import {createI18n} from 'vue-i18n'// v9.x
import en from './en.json'
import zhHans from './zh-Hans.json'
import Config from "@/config";
const localeLanguage = uni.getLocale() || Config.defaultLanguage
const i18n = createI18n({
legacy: false,
locale: localeLanguage,// 获取已设置的语言
fallbackLocale: Config.defaultLanguage,
messages:{
en,
'zh-Hans': zhHans,
}
})
export {i18n}
+692
View File
@@ -0,0 +1,692 @@
{
"agreement": {
"codeAgreement": "兑换码发布协议",
"platformAgreement": "平台协议",
"privacy-policy": "隐私政策",
"user-terms-conditions": "用户须知及使用协议",
"withdrawalAgreement": "提现协议"
},
"common": {
"add": "添加",
"agree": "同意",
"app-marketplace": "应用市场",
"stock": "库存",
"auditStatus": {
"pass": "审核通过",
"reject": "审核拒绝",
"submitted": "审核中"
},
"balance": "余额",
"cancel": "取消",
"close": "关闭",
"comment": "评论",
"confirm": "确认",
"continue": "继续",
"cuttingImage": "裁剪图片",
"delete": "删除",
"details": "详情",
"edit": "编辑",
"email": "邮箱",
"enter": "输入",
"enterPassword": "输入密码",
"go-to-settings": "去设置",
"google-map": "谷歌地图",
"gotIt": "知道了",
"inquiry": "立即申请",
"instructions": "描述",
"loading": "加载中",
"menus": "菜单",
"minutes": "分钟",
"no": "否",
"obtain": "获取",
"or": "或",
"photograph": "拍照",
"itemNum": "共",
"item": "件",
"googleLoginFailed": "谷歌登录授权失败,请重试",
"appleLoginFailed": "苹果登录授权失败,请重试",
"redeemCode": "兑换码",
"coupon": "优惠券",
"distributing": "分发中",
"ended": "已过期",
"placeholder": {
"pleaseEnter": "请输入",
"pleaseEnterName": "请输入姓名",
"pleaseEnterPhone": "请输入电话",
"pleaseSelect": "请选择"
},
"please-enter-your-payment-password": "请输入支付密码",
"prompt": {
"not-setup-payment-password": "您尚未设置支付密码?",
"please-carefully-read-and-agree": "请仔细阅读并同意",
"replication-successful": "复制成功",
"system-prompt": "系统提示",
"system-prompt-delete": "是否确认删除?",
"system-prompt-reject": "是否拒绝订单?",
"picture-wrong-please-try-again": "请选择一个文件后重试",
"request-incorrect": "请求失败",
"up-failed": "上传超时,请稍后重试",
"request-failed-please-try-again-later": "网络或服务器错误请稍后",
"update-successfully": "更新成功",
"download-failed": "下载失败",
"update-failed": "更新失败",
"phone-number-empty": "手机号码为空"
},
"operation-success": "操作成功",
"reEdit": "重新编辑",
"reject": "拒绝",
"remove": "删除",
"reply": "回复",
"save": "保存",
"saveSuccess": "保存成功",
"select": "选择",
"select-from-album": "从相册选择",
"select-maps-app": "选择地图应用",
"send": "发送",
"skip": "跳过",
"stores": "店铺",
"submit": "提交",
"uploading": "上传中",
"yes": "是"
},
"navbar-change-payment-password": "修改支付密码",
"navbar-forget-payment-password": "忘记支付密码",
"navbar-future-order": "未来订单",
"navbar-month-order": "本月订单",
"navbar-refund-order": "退款订单",
"navbar-reservation": "今日预定菜品统计",
"navbar-set-payment-password": "设置支付密码",
"navbar-settings": "设置",
"navbar-today-order": "今日订单",
"navbar-yesterday-order": "昨日订单",
"navbar-forget-password": "忘记密码",
"pages": {
"home": {
"bookDishesToday": "预订菜品",
"commonTools": "常用工具",
"coupons": "优惠券",
"createStore": "创建店铺",
"dataStatistics": "数据统计",
"futureOrders": "预定单",
"monthAmount": "本月收益",
"monthOrder": "本月订单",
"myIncome": "我的收益",
"recipes": "菜谱",
"refundOrders": "退款订单",
"scanCode": "扫一扫",
"todayAmount": "今日收益",
"todayOrder": "今日订单",
"totalAmount": "总收益",
"yesterdayOrder": "昨日订单",
"card-title": "CHEFLINK 炒菜机",
"card-desc": "平台自主研发的蔬菜炒菜机,您生活中的好帮手。",
"view": "查看详情"
},
"menu": {
"add": "添加菜品",
"addMenu": "添加菜单",
"introduction": "简介",
"items": "菜品",
"menu": "菜单",
"menuHours": "菜单营业时间",
"menus": "菜单",
"overview": "总览",
"tipsDesc": "他们帮助顾客选择要点什么。要添加照片,请转到项目并选择菜单项",
"tipsTitle": "菜单上的照片可以增加销量",
"title": "菜品"
},
"mine": {
"complaintsSuggestions": "投诉建议",
"language": "语言切换",
"log-out-successfully": "退出登录成功",
"logOut": "退出登录",
"login-out-tip": "确定退出登录吗?",
"loginTips": "请登录",
"set": "设置",
"storeAuth": "店铺认证",
"storeManage": "店铺管理"
},
"order": {
"ACPT": "已接单",
"CONF": "待接单",
"DCOMP": "已完成",
"DEL": "配送",
"OTW": "配送中",
"PU": "自取",
"cancel": "已取消",
"refund": "已退款",
"subtotal": "小计",
"taxesAndFees": "税费和其他费用",
"total": "总计"
},
"search": {
}
},
"pages-login": {
"and": "和",
"continuing-agree": "我已认真阅读并同意",
"forget-password": {
"description": "设置新密码",
"newPassword": "新密码"
},
"guide-page": {
"welcome": {
"description": "账户创建成功。",
"next": "下一步",
"title": "你好"
}
},
"index": {
"apple-login": "苹果登录",
"description": "创建账户或登录以预订和管理您的预约",
"facebook-login": "Facebook登录",
"google-login": "Google登录",
"input-placeholder": "邮箱地址/手机号",
"prompt": {
"email-address": "请输入邮箱地址",
"email-address-verify": "请输入正确的邮箱地址"
},
"title": "登录或注册",
"wechat-login": "微信登录"
},
"login": {
"description": "输入密码以继续",
"forgotPassword": "忘记密码",
"title": "登录"
},
"login-successfully": "登录成功",
"prompt": {
"confirm-email": "请输入确认邮箱",
"email": "请输入邮箱",
"email-verify": "请输入正确的邮箱",
"first-name": "请输入名",
"last-name": "请输入姓",
"password": "请输入密码",
"password-verify": "请输入6-20位密码",
"phone-number": "请输入手机号",
"phone-number-verify": "请输入正确的手机号",
"reset-success": "重置成功"
},
"sign-up": {
"confirm-email": "确认邮箱",
"first-name": "名",
"last-name": "姓",
"password": "密码",
"phone-number": "手机号",
"register-success": "注册成功",
"title": "注册"
},
"verify-code": {
"changeOne": "换一个",
"description": "出于安全原因,请键入验证码继续",
"title": "验证码"
},
"choose-language": {
"success": "语言设置成功",
"title": "您好,欢迎登录",
"tip": "您可以在应用程序的'我的'部分稍后更改此设置",
"confirm": "确认语言"
}
},
"pages-user": {
"card": {
"add": "添加信用卡",
"desc": "卡绑定后,可以快速直接付款",
"listCard": "信用卡列表",
"title": "先绑定卡,再付款"
},
"choosePaymethod": {
"creditCard": "信用卡支付",
"replace": "替换",
"title": "选择支付方式",
"wallet": "余额支付"
},
"complaints": {
"contact-information": "联系电话",
"contact-information-tip": "您的联系方式有助于我们沟通解决问题,仅工作人员可见",
"description": "您好,欢迎您给我们反馈产品的使用感受和建议。",
"feedback-content": "反馈内容",
"feedback-content-placeholder": "请填写意见反馈内容",
"image": "图片",
"title": "投诉建议",
"validation": {
"contact-phone-invalid": "请填写正确的联系电话",
"contact-phone-required": "请填写联系电话",
"content-max-length": "反馈内容不能超过500个字符",
"content-min-length": "反馈内容至少需要10个字符",
"content-required": "请填写反馈内容"
}
},
"coupons": {
"addCoupon": "添加优惠券",
"addForFree": "免费添加",
"agreeText": "我已认真阅读并同意",
"andReceiveDiscount": "减免金额",
"copyCode": "复制兑换码",
"couponAgreement": "优惠券发布协议",
"dateFilter": "日期筛选",
"days": "天",
"discount": "折扣券",
"discountRatio": "折扣比例",
"fullDiscount": "满减券",
"name": "优惠券名称",
"paidPublishing": "付费发布",
"payMethods": "支付方式",
"publishNow": "立即发布",
"selectValidEndDate": "选择失效日期",
"selectValidStartDate": "选择生效日期",
"spendOver": "满减门槛",
"title": "优惠券",
"totalQuantity": "发放数量",
"type": "类型",
"validEnd": "失效时间",
"validDays": "有效天数",
"validStart": "生效时间",
"validityDays": "有效天数",
"validation": {
"agreeAgreementFirst": "请先同意兑换码发布协议",
"createSuccess": "创建成功",
"discountValueRequired": "优惠值不能为空",
"discountValueRange": "请输入1-100之间的整数",
"discountValueDecimal": "折扣值最多保留两位小数",
"discountValueInvalid": "请输入有效的折扣值(0-1之间,最多两位小数)",
"minAmountRequired": "请输入满减门槛",
"nameRequired": "优惠券名称不能为空",
"startDateCannotLaterThanEndDate": "生效日期不能晚于失效日期",
"totalQuantityPositive": "发放数量必须为大于0的整数",
"totalQuantityRequired": "总数量不能为空",
"validDaysPositive": "有效天数必须为大于0的整数",
"validDaysRequired": "请输入有效期天数",
"validEndRequired": "失效时间不能为空",
"validStartRequired": "生效时间不能为空"
}
},
"food": {
"add-food": {
"active": "激活",
"add": "添加",
"addMenu": "添加菜单",
"addPicture": "添加图片",
"description": "描述",
"descriptionTip": "手动添加详细的菜品描述,帮助客户决定订购什么",
"discountPriceError": "折扣价不能高于原价",
"discountedPrice": "实际销售价格:",
"enterPlaceholder": "输入",
"frequentlyBoughtTogether": "经常一起购买",
"itemActiveDesc": "客户可以查看并订购此菜品",
"itemIsActive": "菜品已激活",
"itemIsNot": "菜品未激活",
"menu": "菜单:",
"name": "名称:",
"pictures": "图片:",
"pound": "磅",
"price": "价格:",
"schema": {
"dishDescription": "菜品描述不能为空",
"dishImage": "请至少上传一张菜品图片",
"dishName": "菜品名称不能为空",
"discountPriceRequired": "请输入折扣价",
"discountPricePositive": "折扣价必须大于0",
"menuId": "请选择菜单",
"originalPrice": "请输入原价"
},
"taxRate": "税率:",
"tips": "请先添加菜单之后再添加菜品",
"title": "添加菜品",
"weight": "重量:",
"stock": "库存数量:",
"stockPlaceholder": "请输入库存数量",
"stockRequired": "请输入库存数量",
"stockMustBeInteger": "库存数量必须为整数",
"stockMustBeNonNegative": "库存数量必须大于等于0"
},
"add-item": {
"add": "添加",
"addSideDishes": "+ 添加配菜",
"enterPlaceholder": "输入",
"isRequired": "是否必选",
"name": "名称",
"price": "价格($)",
"sideDish": "配菜",
"sideDishesFlavorValue": "配菜/口味值:",
"title": "添加配菜",
"typeOfSideDishes": "配菜类型/口味:",
"validation": {
"addAtLeastOne": "请至少添加一个配菜",
"completeAllFields": "请完成配菜 {index} 的所有字段",
"enterSideDishName": "请输入配菜 {index} 的配菜名称",
"submittedSuccessfully": "提交成功"
},
"value": "值"
}
},
"income": {
"index": {
"accountToReceive": "到账账户:",
"accumulatedIncome": "累计收入:",
"addBankCard": "添加银行卡",
"auditFailureReason": "审核失败原因:",
"availableWithdrawalAmount": "可提现金额:",
"detailedList": "明细列表",
"handlingFee": "手续费:",
"paymentVoucher": "打款凭证:",
"thisMonth": "本月:",
"toBeCredited": "待入账:",
"today": "今日:",
"walletBalance": "钱包余额",
"withdrawal": "提现",
"withdrawalAccount": "提现账户:",
"withdrawalAmount": "提现金额",
"withdrawalAmountLabel": "提现金额:",
"withdrawalFee": "提现手续费:",
"withdrawalRecords": "提现记录",
"withdrawalTime": "提现时间:",
"yesterday": "昨日:"
},
"title": "我的收益",
"toast": {
"pleaseSelectACard": "请选择一张到账卡"
},
"withdraw": {
"addCard": "新增银行卡",
"bankName": "开户行",
"bankNameTips": "请输入开户行",
"card": "银行卡号",
"cardListTitle": "银行卡列表",
"cardTips": "请输入银行卡号",
"name": "收款人姓名",
"nameTips": "请输入收款人姓名",
"phone": "联系方式",
"phoneTips": "请输入联系方式",
"title": "提现",
"withdrawImmediately": "立即提现"
}
},
"member": {
"creditCard": "添加信用卡"
},
"menu": {
"add": "添加",
"addMenu": "添加菜单",
"customersCanOverview": "顾客可以查看并从此菜单订购",
"detail": {
"addTimeSlot": "+ 添加时间段",
"dateFilter": "日期筛选",
"endTime": "结束时间:",
"endTimeError": "结束时间不能早于开始时间",
"open24Hours": "24小时营业",
"pleaseSelectDays": "请为时间段选择天数",
"pleaseSetTime": "请为时间段设置开始和结束时间",
"selectDaysAndHours": "选择天数和时间:",
"startTime": "开始时间:"
},
"editMenu": "编辑菜单",
"enterPlaceholder": "请输入",
"menuHours": "菜单时间",
"menuIsActive": "菜单已激活",
"menuIsNotActive": "菜单未激活",
"name": "名称:",
"nameRequired": "名称不能为空",
"setDefaultDays": "设置此菜单可用的默认天数和时间",
"submit": "提交"
},
"message": {
"readAll": "一键已读",
"title": "消息"
},
"order": {
"agree": "已同意",
"agreeRefund": "您已同意用户的退款申请",
"balancePayment": "余额支付",
"cancelOrder": "用户已申请取消订单",
"checkout": {
"priceDetail": {
"desc": "这些费用仅基于购物车数量等因素,并用于支付与您的订单相关的费用,包括平台服务和配送服务。",
"memberDiscount": "会员折扣",
"serviceFees": "服务费及其他费用",
"taxation": "税收",
"title": "包含哪些内容"
}
},
"collapse": "收起",
"copies": "预定份数",
"createTime": "创建时间",
"creditCardPayment": "信用卡支付",
"delivered": "确认送达",
"deliveredOrder": {
"uploadPhoto": "请上传配送照片"
},
"deliveredTips": "拍摄并上传送货照片",
"deliveryAddress": "配送地址:",
"deliveryTime": "送达时间:",
"details": "详情",
"enterReply": "输入回复文字",
"expand": "展开",
"items": "个",
"mapNavigation": "地图导航",
"mealPickupTime:": "取餐时间",
"orderInfo": "订单信息:",
"orderNumber": "订单编号",
"payMethod": "支付方式",
"payTime": "付款时间",
"pending": "待处理",
"phoneContact": "电话联系",
"pickUp": "自取信息",
"pleaseEnterReply": "请输入回复内容",
"processed": "已处理",
"productInfo": "商品信息:",
"receiving": "接单",
"refundReason": "退款原因",
"refuseReason": "拒绝原因",
"refuseRefund": "您已拒绝用户的退款申请",
"reject": "已拒绝",
"remark": "备注:",
"reply": "回复:",
"replySuccess": "回复成功",
"score": "分",
"share": "份",
"startDelivery": "开始配送",
"startDeliveryForm": {
"deliveryAvatar": "配送员头像",
"deliveryFirstName": "配送员名字",
"deliveryPhone": "配送员手机号",
"deliverySurname": "配送员姓",
"title": "添加配送员",
"validation": {
"deliveryAvatarRequired": "请上传配送员头像",
"deliveryFirstNameRequired": "请输入配送员名字",
"deliveryPhoneRequired": "请输入配送员手机号",
"deliverySurnameRequired": "请输入配送员姓氏",
"orderIdRequired": "请输入订单ID"
}
},
"status": {
"completed": "已完成",
"completedDesc": "订单已完成",
"delivering": "配送中",
"deliveringDesc": "等待商家配送中...",
"merchantAccepted": "已接单",
"merchantAcceptedDesc": "商家已接单",
"pendingPayment": "待接单",
"waitingForOrder": "等待接单中...",
"merchantRejected": "已拒绝",
"merchantRejectedDesc": "商家已拒绝接单"
},
"toast": {
"receiveSuccess": "接单成功",
"refuseReasonRequired": "请输入拒绝原因",
"refuseSuccess": "拒绝成功"
},
"total": "共",
"viewReview": "查看评价",
"noReview": "暂无评价",
"writeOff": "核销"
},
"pay-password": {
"change-payment-password-successfully": "修改支付密码成功",
"enter-6-digit-password": "请输入6位密码",
"forget-payment-password-successfully": "忘记支付密码成功",
"input-placeholder": {
"enter-new-password": "请输入新密码",
"enter-new-password-again": "请再次输入新密码",
"enter-old-password": "请输入原密码",
"enter-phone-number": "请输入手机号",
"enter-verification-code": "请输入验证码"
},
"set-payment-password-successfully": "设置支付密码成功",
"two-passwords-inconsistent": "两次输入的密码不一致"
},
"recipe": {
"addPicture": "添加图片",
"addRecipe": "添加菜谱",
"collapseReply": "收起回复内容",
"detail": {
"title": "菜谱详情"
},
"dragTip": "默认第一张图片为封面",
"editRecipe": "编辑菜谱",
"enterPlaceholder": "请输入",
"expandReply": "展开回复",
"index": {
"add": "新增",
"delete": "删除",
"edit": "修改",
"title": "菜谱管理"
},
"ingredientsPreparation": "食材准备:",
"recipeClassification": "菜谱分类:",
"recipeImage": "菜谱图片:",
"recipeName": "菜谱名称:",
"replyTo": "回复给",
"schema": {
"ingredients": "食材准备不能为空",
"recipeCategory": "请选择菜谱分类",
"recipeImage": "请至少上传一张菜谱图片",
"recipeName": "菜谱名称不能为空"
}
},
"sales-record": {
"amt": "金额",
"dateFilter": "日期筛选",
"dishAmount": "菜品金额",
"dishQuantity": "菜品份数",
"portion": "份",
"qty": "数量",
"title": "销售记录"
},
"setting": {
"language": "语言切换",
"logOff": "注销账号",
"logOffConfirm": "仍要注销",
"logOffDesc": "清空用户在该平台所有信息,包括但不限于个人资料、消费信息等,以上信息均被删除,不可恢复不可恢复不可恢复不可恢复。",
"logOffRisk": "注销账号存在以下风险",
"modification": "修改密码",
"payPwd": "支付密码"
},
"store-management": {
"index": {
"addPicture": "添加图片",
"businessHours": "营业时间:",
"deliveryAmount": "起送金额:",
"deliveryFee": "配送费:",
"deliveryTime": "配送时间:",
"hours": {
"businessHours": "营业时间",
"days": {
"friday": "周五",
"monday": "周一",
"saturday": "周六",
"sunday": "周日",
"thursday": "周四",
"tuesday": "周二",
"wednesday": "周三"
},
"endTime": "结束时间",
"pleaseSelectAtLeastOneDay": "请选择至少一天",
"pleaseSelectStartAndEndTime": "请选择开始时间和结束时间",
"selectDayPrompt": "请选择开始和结束的星期几。",
"startTime": "开始时间",
"startTimeCannotLaterThanEndTime": "开始时间不能晚于结束时间"
},
"isDelivery": "是否配送?",
"isSelfPickup": "是否支持自取?",
"logo": "LOGO:",
"logoSizeHint": "(建议尺寸1:1)",
"mainBusinessScope": "主营业务范围:",
"mealPickupTime": "取餐时间:",
"off": "关闭",
"on": "开启",
"prompt": {
"businessHours": "请选择营业时间",
"deliveryFee": "请输入配送费",
"deliveryTime": "请输入配送时长",
"logo": "请上传店铺LOGO",
"merchantAddress": "请选择店铺位置",
"merchantCategoryIds": "请选择主营业务范围",
"merchantName": "请输入店铺名称",
"minOrderPrice": "请输入最小订单金额",
"minOrderPriceTip": "起送金额必须为大于0的数字最多两位小数",
"phone": "请输入联系电话",
"pickupTime": "请输入自取时长",
"shopImages": "请上传推荐菜品图片"
},
"recommendedDishPictures": "推荐菜品图片:",
"save": "保存",
"shopLocation": "店铺位置:",
"shopNames": "店铺名称:",
"shopPhone": "联系电话:",
"title": "店铺管理",
"shopState": "所在州"
}
},
"store-settle-in": {
"address": "地址",
"audit": {
"pass": "审核成功",
"passDesc": "恭喜您成功入驻平台!",
"passDesc1": "快来上传商品信息,开启赚钱之旅吧~",
"reject": "审核失败",
"rejectDesc": "失败原因",
"submitted": "审核中",
"submittedDesc": "平台审核中,耐心等待..."
},
"businessLicense": "食品操作处理证",
"category": "所属类型",
"desc": "简单填写信息,快来开启赚钱之旅",
"detailedAddress": "详细地址",
"idCardFront": "ID/护照",
"introduction": "介绍",
"schema": {
"address": "请选择地址",
"businessLicense": "请上传食品操作处理证",
"category": "请选择所属类型",
"detailedAddress": "请输入详细地址",
"idCardBack": "请上传护照",
"idCardFront": "请上传ID",
"introduction": "请输入介绍",
"storeName": "请输入店铺名称"
},
"storeName": "店铺名称",
"title": "店铺入驻"
}
},
"tabBar": {
"browse": "浏览",
"global": "全球购",
"home": "首页",
"menu": "菜单",
"mine": "我的",
"order": "订单"
},
"toast": {
"commentSuccess": "评论成功",
"deleteSuccess": "删除成功",
"orderWriteOffSuccess": "订单核销成功",
"pleasePhone": "请注意接听电话。",
"pleaseSelectStore": "请选择店铺",
"selectScope": "请选择店铺类型",
"submitSuccess": "提交成功",
"submitSuccessTip": "提交成功,请等待审核",
"rejectSuccess": "拒绝成功"
}
}
+24
View File
@@ -0,0 +1,24 @@
import {createSSRApp} from "vue";
import App from "./App.vue";
import {Pinia, store} from '@/store'
import {installAppPlugins} from '@/plugin'
import {requestInterceptor, routeInterceptor} from "@/interceptor";
import {i18n} from "@/locale";
// #ifndef APP-NVUE
import 'virtual:uno.css'
// #endif
export function createApp() {
const app = createSSRApp(App);
app.use(store)
app.use(requestInterceptor)
app.use(routeInterceptor)
app.use(i18n)
installAppPlugins(app)
return {
app,
Pinia,
};
}
+187
View File
@@ -0,0 +1,187 @@
{
"name" : "CHEFLINK Merchant",
"appid" : "__UNI__BB8E3C9",
"description" : "美国外卖商户端",
"versionName" : "1.0.13",
"versionCode" : 113,
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "Merchant",
"compilerVersion" : 3,
"checkPermissionDenied" : true,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : false,
"autoclose" : true,
"delay" : 0
},
"safearea" : {
"background" : "#fff",
"bottom" : {
"offset" : "auto"
}
},
"optimization" : {
"subPackages" : true
},
"runmode" : "liberate",
//
"compatible" : {
"ignoreVersion" : true
},
"screenOrientation" : [ "portrait-primary", "landscape-primary" ],
/* */
"modules" : {
"Camera" : {},
"OAuth" : {},
"Barcode" : {}
},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>"
],
"minSdkVersion" : 28,
"targetSdkVersion" : 35,
"schemes" : "cheflinkmerchant",
"abiFilters" : [ "armeabi-v7a", "arm64-v8a" ],
"excludePermissions" : [
"<uses-permission android:name=\"android.permission.READ_MEDIA_IMAGES\" />",
"<uses-permission android:name=\"android.permission.READ_MEDIA_VIDEO\"/>"
]
},
/* ios */
"ios" : {
"urltypes" : "cheflinkmerchant",
"idfa" : false,
"dSYMs" : false,
"privacyDescription" : {
"NSPhotoLibraryUsageDescription" : "上传用户头像等",
"NSPhotoLibraryAddUsageDescription" : "保存图片",
"NSCameraUsageDescription" : "核销用户订单"
}
},
/* SDK */
"sdkConfigs" : {
"geolocation" : {
"system" : {
"__platform__" : [ "ios", "android" ]
}
},
"maps" : {},
"payment" : {},
"ad" : {},
"oauth" : {
"apple" : {},
"google" : {
"clientid" : "455840300142-2d46cgso94qgb41o9feo0njq2dgne9p6.apps.googleusercontent.com"
}
},
"share" : {}
},
"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"
}
}
},
"splashscreen" : {
"androidStyle" : "default",
"iosStyle" : "storyboard",
"ios" : {
"storyboard" : "CustomStoryboard.zip"
},
"android" : {
"hdpi" : "starter/drawable-hdpi/outline.9.png",
"xhdpi" : "starter/drawable-xhdpi/outline.9.png",
"xxhdpi" : "starter/drawable-xxhdpi/outline.9.png"
}
}
},
"locales" : {
"en" : {
//
"ios" : {
"privacyDescription" : {
"NSCameraUsageDescription" : "Allow access to your camera to upload your avatar and verify user orders.",
"NSPhotoLibraryUsageDescription" : "Allow access to your photo library to select or save images.",
"NSPhotoLibraryAddUsageDescription" : "Allow saving images to your photo library, such as invitation posters."
}
}
},
"zh" : {
//
"ios" : {
"privacyDescription" : {
"NSCameraUsageDescription" : "允许访问相机以上传头像。核销用户订单",
"NSPhotoLibraryUsageDescription" : "允许访问相册以选择或保存图片。",
"NSPhotoLibraryAddUsageDescription" : "允许保存图片到相册,例如邀请海报。"
}
}
}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3",
"locale" : "en"
}
//
+20
View File
@@ -0,0 +1,20 @@
{
"id": "ca-switch",
"name": "CaSwitch开关组件,支持个性化定义",
"displayName": "CaSwitch开关组件,支持个性化定义",
"version": "1.0.0",
"description": "能够支持文字配置,联网切换,渐变颜色切换,滑块大小。",
"keywords": [
"switch",
"滑块",
"开关",
"滑动开关",
"切换开关"
],
"dcloudext": {
"category": [
"前端组件",
"通用组件"
]
}
}
@@ -0,0 +1,106 @@
<script lang="ts" setup>
import {getCaptcha} from "@/pages-login/service";
const {t} = useI18n();
const emit = defineEmits(["submit"]);
const show = ref(false);
const code = ref("");
const captchaData = ref({
img: "",
uuid: "",
code: "",
});
function onOpen() {
getCode();
show.value = true;
}
function getCode() {
code.value = "";
getCaptcha().then((res: any) => {
captchaData.value = res.data;
});
}
function handleClose() {
show.value = false;
}
function handleSubmit() {
emit("submit", {
code: code.value,
uuid: captchaData.value.uuid,
});
handleClose();
}
defineExpose({
onOpen,
});
</script>
<template>
<wd-popup
v-model="show"
custom-style="border-radius:20rpx;"
position="center"
@close="handleClose"
>
<view class="relative w-630rpx box-border px-36rpx py-40rpx">
<image
class="absolute top-34rpx right-36rpx w-32rpx h-32rpx"
src="@img/chef/100404.png"
@click="handleClose"
></image>
<view class="text-36rpx text-#333333 text-center lh-33rpx mb-22rpx">{{
t("pages-login.verify-code.title")
}}
</view>
<view class="text-28rpx text-#999999 lh-28rpx text-center mb-40rpx">{{
t("pages-login.verify-code.description")
}}
</view>
<view
class="mt-24rpx overflow-hidden flex pl-30rpx items-center justify-between h-90rpx border-2rpx border-solid border-#D1D1D1 rounded-20rpx"
>
<wd-input
v-model.trim="code"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="20"
:placeholder="t('common.enter')"
clearable
custom-class="flex-1"
no-border
placeholderStyle="font-size: 28rpx;color: #999;line-height: 42rpx;"
type="number"
>
</wd-input>
<image
:src="`data:image/gif;base64,${captchaData.img}`"
class="w-120rpx h-88rpx"
></image>
</view>
<view
class="text-24rpx text-#FF6106 lh-24rpx text-right mt-16rpx"
@click="getCode"
>{{ t("pages-login.verify-code.changeOne") }}
</view
>
<view class="mt-54rpx">
<wd-button
block
custom-class="!h-90rpx !bg-#14181B !text-36rpx text-#fff font-bold !rounded-16rpx"
@click="handleSubmit"
>
{{ t("common.continue") }}
</wd-button>
</view>
</view>
</wd-popup>
</template>
<style lang="scss" scoped></style>
@@ -0,0 +1,242 @@
<script lang="ts" setup>
import {debounce} from "throttle-debounce"
import Config from "@/config"
import {useConfigStore} from "@/store"
import {setDayjsLocale, setWotDesignLocale} from "@/plugin"
const {t} = useI18n()
const {locale} = useI18n()
const configStore = useConfigStore()
// -
const languageOptions = ref([
{
name: "English",
systemValue: "en",
selected: false
},
{
name: "中文",
systemValue: "zh-Hans",
selected: false
}
])
//
const initLanguageSelection = () => {
const currentLocale = uni.getLocale()
languageOptions.value.forEach(option => {
option.selected = option.systemValue === currentLocale
})
}
//
const selectLanguage = (selectedValue: string) => {
languageOptions.value.forEach(option => {
option.selected = option.systemValue === selectedValue
})
}
// -
const confirmLanguage = () => {
const selectedLanguage = languageOptions.value.find(option => option.selected)
if (selectedLanguage) {
const localeLanguages = uni.getLocale()
console.log(localeLanguages)
if (selectedLanguage.systemValue === uni.getLocale()) {
//
uni.redirectTo({
url: '/pages-login/pages/index'
})
return
}
// - 使
uni.setLocale(selectedLanguage.systemValue)
locale.value = selectedLanguage.systemValue
setWotDesignLocale()
setDayjsLocale()
// #ifdef APP-PLUS
if (configStore.isIos) {
setTimeout(() => {
plus.runtime.restart()
}, 1000)
}
// #endif
//
uni.showToast({
title: t('pages-login.choose-language.success'),
icon: 'none'
})
//
setTimeout(() => {
uni.switchTab({
url: '/pages/home/index'
})
}, 1500)
}
}
// 使
const handleConfirm = debounce(Config.debounceShortTime, confirmLanguage, {
atBegin: true,
})
//
onMounted(() => {
initLanguageSelection()
})
</script>
<template>
<view class="language-container">
<!-- 欢迎文字 -->
<view class="welcome-section">
<text class="welcome-text">{{ t('pages-login.choose-language.title') }}</text>
</view>
<!-- 语言选择列表 -->
<view class="language-list">
<view
v-for="option in languageOptions"
:key="option.systemValue"
class="language-item"
@click="selectLanguage(option.systemValue)"
>
<view class="language-card">
<view v-show="option.selected" class="i-carbon:checkmark-filled text-primary"></view>
<view v-show="!option.selected" class="i-carbon:circle-outline text-#CCCCCC"></view>
<text :class="{ 'selected': option.selected }" class="language-name">{{ option.name }}</text>
</view>
</view>
</view>
<!-- 提示文字 -->
<view class="tip-section">
<text class="tip-text">{{ t('pages-login.choose-language.tip') }}</text>
</view>
<!-- 确认按钮 -->
<view class="confirm-section">
<view class="confirm-button" @click="handleConfirm">
<text class="confirm-text">{{ t('pages-login.choose-language.confirm') }}</text>
</view>
</view>
</view>
</template>
<style>
page {
background-color: white;
}
</style>
<style lang="scss" scoped>
.welcome-section {
padding: 286rpx 32rpx 0;
.welcome-text {
font-size: 52rpx;
font-weight: 500;
color: #333333;
line-height: 60rpx;
}
}
.language-list {
padding: 104rpx 32rpx 0;
.language-item {
margin-bottom: 28rpx;
&:last-child {
margin-bottom: 0;
}
.language-card {
width: 686rpx;
height: 90rpx;
background-color: #FFFFFF;
border: 2rpx solid #E8E8E8;
border-radius: 46rpx;
padding: 0 32rpx;
display: flex;
align-items: center;
gap: 24rpx;
.radio-button {
.radio-outer {
width: 32rpx;
height: 32rpx;
border: 2rpx solid #666666;
border-radius: 50%;
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
&.selected {
border-color: #000;
}
.radio-inner {
width: 20rpx;
height: 20rpx;
background-color: #000;
border-radius: 50%;
}
}
}
.language-name {
font-size: 26rpx;
color: #666666;
line-height: 36rpx;
&.selected {
color: #000;
font-weight: 500;
}
}
}
}
}
.tip-section {
padding: 324rpx 76rpx 0;
.tip-text {
font-size: 26rpx;
color: #666666;
line-height: 36rpx;
text-align: center;
display: block;
}
}
.confirm-section {
padding: 54rpx 32rpx 0;
.confirm-button {
width: 686rpx;
height: 92rpx;
background-color: #000;
border-radius: 46rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0px 8rpx 20rpx 0px rgba(0, 0, 0, 0.3);
.confirm-text {
font-size: 32rpx;
font-weight: 500;
color: #FFFFFF;
line-height: 44rpx;
}
}
}
</style>
@@ -0,0 +1,176 @@
<script lang="ts" setup>
import * as R from 'ramda'
import {z} from "zod";
import {useLogicStore} from "@/pages-login/store/module/logic";
import {debounce} from "throttle-debounce";
import Config from "@/config";
import {useUserStore} from "@/store";
import {appUserForgetPwdNotLoginPost, appUserGetByEmailPost, appUserGetByPhonePost} from "@/service";
const {t} = useI18n()
const logicStore = useLogicStore()
const userStore = useUserStore();
const verificationCodeRef = ref();
const FormSchema = z.string()
.min(1, {message: t('pages-login.prompt.password')})
function checkForm(): boolean {
const validateFormField = FormSchema.safeParse(logicStore.forgetPasswordForm.loginPwd)
console.log('validateFormField', validateFormField)
if (!validateFormField.success) {
const formatted = validateFormField.error.format()
R.path(['_errors', 0], formatted) && uni.showToast({
title: R.path(['_errors', 0], formatted),
icon: 'none',
})
}
return validateFormField.success;
}
function submit() {
// verificationCodeRef.value.onOpen()
codeSubmit()
}
function codeSubmit() {
// forgetPwdNotLogin
appUserForgetPwdNotLoginPost({
body: {
email: logicStore.forgetPasswordForm.email,
phone: logicStore.forgetPasswordForm.phone,
// captcha: data.code,
// uuid: data.uuid,
loginPwd: logicStore.forgetPasswordForm.loginPwd,
userPort: 2, // 2
}
}).then((res) => {
uni.showToast({
title: t('pages-login.prompt.reset-success'),
icon: 'none',
})
logicStore.reset()
uni.redirectTo({
url: '/pages-login/pages/index',
})
})
}
//
const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit, {
atBegin: true
}))
function navigateTo(url: string) {
uni.navigateTo({url});
}
// onLoad(R.pipe(() => getUserInfoByEmail(R.pick(['email'], logicStore.registerForm)), R.andThen((res) => {
// console.log(res)
// logicStore.forgetPasswordForm = {
// ...R.pick(['phone', 'areaCode'], res.data),
// email: logicStore.registerForm.email,
// loginPwd: '',
// captcha: "",
// }
// })))
onLoad(() => {
//
if (logicStore.loginForm.type === 'email' && logicStore.loginForm.email) {
//
appUserGetByEmailPost({
body: {
email: logicStore.loginForm.email,
userPort: 2, // 2
}
}).then((res: any) => {
console.log('根据邮箱获取用户信息', res)
if (res.data) {
logicStore.forgetPasswordForm.email = res.data.email
logicStore.forgetPasswordForm.phone = res.data.phone
}
})
} else if (logicStore.loginForm.type === 'phone' && logicStore.loginForm.phone) {
//
appUserGetByPhonePost({
body: {
phone: logicStore.loginForm.phone,
userPort: 2, // 2
}
}).then((res: any) => {
console.log('根据手机号获取用户信息', res)
if (res.data) {
logicStore.forgetPasswordForm.email = res.data.email
logicStore.forgetPasswordForm.phone = res.data.phone
}
})
}
})
</script>
<template>
<view>
<navbar/>
<view class="pt-64rpx px-27rpx">
<view class="text-50rpx lh-50rpx text-#14181B font-bold">
{{ t('pages-login.login.forgotPassword') }}
</view>
<view class="mt-30rpx text-28rpx lh-28rpx text-#999 font-bold">
{{ t('pages-login.forget-password.description') }}
</view>
<view class="mt-78rpx">
<view class="">
<view class="text-32rpx lh-32rpx text-#14181B">{{ t("common.email") }}</view>
<view
class="mt-24rpx flex px-30rpx items-center h-100rpx bg-#EFEFEF rounded-20rpx">
<image class="shrink-0 mr-12rpx w-30rpx h-30rpx" src="@img-login/103.png"></image>
<text class="text-28-bold">{{ logicStore.forgetPasswordForm.email }}</text>
</view>
</view>
<view class="mt-36rpx">
<view class="text-32rpx lh-32rpx text-#14181B">{{ t("pages-login.sign-up.phone-number") }}</view>
<view
class="mt-24rpx flex px-30rpx items-center h-100rpx bg-#EFEFEF rounded-20rpx">
<image class="shrink-0 mr-12rpx w-30rpx h-30rpx" src="@img-login/102.png"></image>
<text class="text-28-bold">{{ logicStore.forgetPasswordForm.phone }}</text>
</view>
</view>
<view class="mt-36rpx">
<view class="text-32rpx lh-32rpx text-#14181B">{{ t("pages-login.forget-password.newPassword") }}</view>
<view
class="mt-24rpx flex px-30rpx items-center h-100rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx">
<wd-input
v-model.trim="logicStore.forgetPasswordForm.loginPwd"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="20"
:placeholder="t('common.enter')"
clearable
custom-class="flex-1"
no-border
placeholderStyle="font-size: 28rpx;color: #999;font-weight: bold;line-height: 42rpx;"
show-password
>
</wd-input>
</view>
</view>
<view class="mt-80rpx">
<wd-button block custom-class="!h-108rpx !text-36rpx font-bold !rounded-16rpx" @click="handleSubmit">
{{ t('common.continue') }}
</wd-button>
</view>
</view>
</view>
</view>
<!-- <verification-code ref="verificationCodeRef" @submit="codeSubmit"/>-->
</template>
<style lang="scss">
page {
background-color: #fff;
}
</style>
@@ -0,0 +1,61 @@
<script lang="ts" setup>
import {useConfigStore, useUserStore} from "@/store";
const userStore = useUserStore();
const configStore = useConfigStore()
const {t} = useI18n();
const next = () => {
configStore.isShowedGuidePage = true
uni.switchTab({
url: '/pages/home/index'
})
};
</script>
<template>
<view class="bg-white flex flex-col">
<!-- 主要内容区域 -->
<view
class="flex-1 flex flex-col items-center justify-center px-40rpx pt-216rpx pb-120rpx"
>
<!-- 图标 -->
<image
class="w-256rpx h-256rpx rounded-full"
src="@img-login/100220.png"
/>
<!-- 标题 -->
<view class="text-center mt-84rpx">
<text class="text-50-bold mr-4rpx">{{ t('pages-login.guide-page.welcome.title') }},</text>
<text>{{ userStore.userInfo.firstName }} {{ userStore.userInfo.surname }}</text>
</view>
<!-- 描述文本 -->
<view class="text-center mt-36rpx mb-113rpx">
<text class="text-30rpx lh-30rpx text-#999 leading-30rpx">
{{ t('pages-login.guide-page.welcome.description') }}
</text>
</view>
<!-- 主要按钮 -->
<view class="w-full">
<button
class="w-full h-108rpx bg-#14181B rounded-16rpx border-none"
@click="next"
>
<text class="text-white text-36rpx lh-108rpx"
>{{ t('pages-login.guide-page.welcome.next') }}
</text
>
</button>
</view>
</view>
</view>
</template>
<style lang="scss">
page {
background-color: #ffffff;
}
</style>
+219
View File
@@ -0,0 +1,219 @@
<script lang="ts" setup>
import * as R from 'ramda'
import {useLogicStore} from "@/pages-login/store/module/logic";
import {debounce} from "throttle-debounce";
import Config from "@/config";
import {useConfigStore} from "@/store";
import {isEmail} from '@/utils'
import {appUserCheckEmailUniquePost, appUserCheckPhoneUniquePost} from '@/service'
interface LoginMethod {
id: number;
method: string;
title: string;
icon: string;
}
const {t} = useI18n()
const logicStore = useLogicStore()
const configStore = useConfigStore()
const loginMethods = computed<LoginMethod[]>(() => {
const methods = [
{
id: 3,
method: 'Google',
title: t('pages-login.index.google-login'),
icon: '/static/app/images/100204.png',
},
// {
// id: 2,
// method: 'Facebook',
// title: t('pages-login.index.facebook-login'),
// icon: '/static/app/images/100207.png',
// },
{
id: 1,
method: 'Apple',
title: t('pages-login.index.apple-login'),
icon: '/static/app/images/100208.png',
},
// {
// id: 4,
// method: 'Wechat',
// title: t('pages-login.index.wechat-login'),
// icon: '/static/app/images/100209.png',
// },
]
if (R.and(configStore.isApp, configStore.isIos)) {
return methods
}
return methods.filter(item => item.method !== 'Apple')
})
const btnLoading = ref(false)
function handleSubmit() {
const {email} = logicStore.registerForm
let type = '';
console.log('email', email)
console.log('email', isEmail(email))
if (!email) return
if (isEmail(email)) {
btnLoading.value = true
appUserCheckEmailUniquePost({
body: {
email,
userPort: 2, // 2
},
options: {}
}).then(res => {
console.log('email', res)
if (res.data) {
logicStore.loginForm.email = email;
logicStore.loginForm.type = 'email';
//
uni.navigateTo({url: '/pages-login/pages/login/index'})
} else {
//
type = 'email';
//
logicStore.registerForm.type = type;
logicStore.registerForm.confirmEmail = email;
uni.navigateTo({url: '/pages-login/pages/sign-up/index'})
}
}).finally(() => {
btnLoading.value = false
})
} else {
btnLoading.value = true
//
appUserCheckPhoneUniquePost({
body: {
phone: email,
userPort: 2, // 2
},
options: {}
}).then(res => {
type = 'phone';
if (res.data) {
logicStore.loginForm.phone = email;
logicStore.loginForm.type = type;
//
uni.navigateTo({url: '/pages-login/pages/login/index'})
} else {
//
logicStore.registerForm.type = type;
logicStore.registerForm.phone = email;
logicStore.registerForm.email = '';
logicStore.registerForm.confirmEmail = '';
uni.navigateTo({url: '/pages-login/pages/sign-up/index'})
}
}).finally(() => {
btnLoading.value = false
})
}
}
function loginByMethod(item: LoginMethod) {
switch (item.method) {
case 'Apple': {
logicStore.appleLogin()
break
}
case 'Facebook': {
logicStore.facebookLogin()
break
}
case 'Google': {
logicStore.googleLogin()
break
}
case 'Wechat': {
logicStore.wechatLogin()
break
}
}
}
const handleLoginByMethod = debounce(Config.debounceLongTime, loginByMethod, {
atBegin: true
})
function handleGoBack() {
uni.navigateBack()
}
onUnload(() => {
logicStore.reset()
})
</script>
<template>
<view>
<status-bar/>
<view class="pl-30rpx pt-72rpx mb-74rpx" @click="handleGoBack">
<image class="w-34rpx h-30rpx shrink-0" src="@img/chef/2.png"></image>
</view>
<view class="px-27rpx">
<view class="text-50rpx lh-50rpx text-#14181B font-bold">
{{ t('pages-login.index.title') }}
</view>
<view class="mt-30rpx text-28rpx lh-28rpx text-#999 font-bold">
{{ t('pages-login.index.description') }}
</view>
<view class="mt-80rpx">
<view class="">
<view v-for="(item,index) in loginMethods"
:key="item.id" :class="[index>0&&'mt-22rpx']"
class="center h-88rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx"
@click="handleLoginByMethod(item)"
>
<image :src="item.icon" class="shrink-0 mr-16rpx w-40rpx h-40rpx"/>
<text class="text-28-bold">{{ item.title }}</text>
</view>
</view>
<view class="my-38rpx flex items-center justify-between">
<view class="w-308rpx h-0rpx border-bottom"></view>
<text class="text-28-bold">{{ t('common.or') }}</text>
<view class="w-308rpx h-0rpx border-bottom"></view>
</view>
<view class="">
<view class="flex px-24rpx items-center h-88rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx">
<wd-input
v-model.trim="logicStore.registerForm.email"
:focus-when-clear="false"
:maxlength="255"
:placeholder="t('pages-login.index.input-placeholder')"
clearable
custom-class="flex-1"
no-border
placeholderStyle="font-size: 32rpx;color: #999;line-height: 32rpx;"
use-prefix-slot
>
</wd-input>
</view>
</view>
<view class="mt-42rpx">
<wd-button :loading="btnLoading" block custom-class="!h-108rpx !text-36rpx leading-36rpx !rounded-16rpx"
loading-color="#000" @click="handleSubmit"
>
{{ t('common.continue') }}
</wd-button>
</view>
</view>
</view>
</view>
</template>
<style lang="scss">
page {
background-color: #fff;
}
</style>
+142
View File
@@ -0,0 +1,142 @@
<script lang="ts" setup>
import * as R from 'ramda'
import {z} from "zod";
import {useLogicStore} from "@/pages-login/store/module/logic";
import {LoginType} from "@/constant/enums";
import {appLoginPost} from "@/service";
import {debounce} from "throttle-debounce";
import Config from "@/config";
import {useConfigStore, useUserStore} from "@/store";
const {t} = useI18n()
const userStore = useUserStore();
const configStore = useConfigStore()
const logicStore = useLogicStore()
const FormSchema = z.string()
.min(1, {message: t('pages-login.prompt.password')})
function checkForm(): boolean {
const validateFormField = FormSchema.safeParse(logicStore.loginForm.password)
console.log('validateFormField', validateFormField)
if (!validateFormField.success) {
const formatted = validateFormField.error.format()
R.path(['_errors', 0], formatted) && uni.showToast({
title: R.path(['_errors', 0], formatted),
icon: 'none',
})
}
return validateFormField.success;
}
const btnLoading = ref(false)
async function submit() {
try {
const params = {
email: '',
phone: '',
password: logicStore.loginForm.password,
type: logicStore.loginForm.type === 'email' ? LoginType.EMAIL : LoginType.ACCOUNT,
userPort: 2, //
}
logicStore.loginForm.type === 'email' ? params.email = logicStore.loginForm.email : params.phone = logicStore.loginForm.phone
console.log('params42', params)
btnLoading.value = true
const res = await appLoginPost({
body: params,
})
btnLoading.value = false
console.log('res', res)
await uni.showToast({title: t('pages-login.login-successfully'), icon: "none"});
userStore.token = res.data.token;
nextTick(() => {
userStore.getUserInfo();
})
const pages = getCurrentPages()
console.log('pages.length', pages.length)
setTimeout(() => {
uni.switchTab(
{
url: Config.indexPath
})
}, 1000);
} catch (e) {
btnLoading.value = false
}
}
//
const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit, {
atBegin: true
}))
function navigateTo(url: string) {
logicStore.resetRegisterForm()
uni.navigateTo({url});
}
</script>
<template>
<view>
<navbar/>
<view class="pt-60rpx px-27rpx">
<view class="text-50rpx lh-50rpx text-#14181B font-bold">
{{ t('pages-login.login.title') }}
</view>
<view class="mt-30rpx text-28rpx lh-28rpx text-#999 font-bold">
{{ t('pages-login.login.description') }}
</view>
<view class="mt-80rpx">
<view class="">
<view class="text-32rpx lh-32rpx text-#14181B">{{ t("pages-login.sign-up.password") }}</view>
<view
class="mt-24rpx flex px-30rpx items-center h-98rpx border-2rpx border-solid border-#D4D4D4 rounded-20rpx">
<wd-input
v-model.trim="logicStore.loginForm.password"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="20"
:placeholder="t('common.enterPassword')"
clearable
custom-class="flex-1"
no-border
placeholderStyle="font-size: 32rpx;color: #999;"
show-password
>
</wd-input>
</view>
</view>
<view class="mt-40rpx">
<wd-button :loading="btnLoading" block custom-class="!h-108rpx !text-36rpx font-bold !rounded-16rpx"
loading-color="#000" @click="handleSubmit">
{{ t('pages-login.login.title') }}
</wd-button>
</view>
<view class="flex-center-sb mt-60rpx">
<text class="text-32rpx text-#333 underline" @click="navigateTo('/pages-login/pages/sign-up/index')">
{{ t('pages-login.sign-up.title') }}
</text>
<text class="text-32rpx text-#CE7138 lh-34rpx underline"
@click="navigateTo('/pages-login/pages/forget-password/index')">
{{ t('pages-login.login.forgotPassword') }}
</text>
</view>
</view>
</view>
</view>
</template>
<style lang="scss">
page {
background-color: #fff;
}
</style>
+308
View File
@@ -0,0 +1,308 @@
<script lang="ts" setup>
import * as R from 'ramda'
import {z} from "zod";
import {useLogicStore} from "@/pages-login/store/module/logic";
import {Agreement} from "@/constant/enums";
import {debounce} from "throttle-debounce";
import Config from "@/config";
import VerificationCode from "../../components/verification-code.vue";
import {appUserRegisterPost} from "@/service";
import {useUserStore} from "@/store";
const areaCode = ref<string>('+1');
const columns = ref<string[]>(Config.phoneCodeList);
const {t} = useI18n()
const userStore = useUserStore();
const logicStore = useLogicStore()
const isAgreed = ref(false)
const FormSchema = z.object({
firstName: z.string().min(1, {message: t('pages-login.prompt.first-name')}),
surname: z.string().min(1, {message: t('pages-login.prompt.last-name')}),
phone: z.string().min(1, {message: t('pages-login.prompt.phone-number')}).regex(/^\d{6,11}$/, {message: t('pages-login.prompt.phone-number-verify')}),
loginPwd: z.string().min(1, {message: t('pages-login.prompt.password')}),
email: z.string().email({message: t('pages-login.prompt.email-verify')}),
confirmEmail: z.string().email({message: t('pages-login.prompt.email-verify')}),
})
function checkForm(): boolean {
const validateFormField = FormSchema.safeParse(logicStore.registerForm)
if (!validateFormField.success) {
const fieldErrorMessage = validateFormField.error.flatten().fieldErrors
const errorMessage: string | undefined = R.path([0, 0], R.values(fieldErrorMessage))
errorMessage &&
uni.showToast({
title: errorMessage,
icon: 'none',
})
} else if (!isAgreed.value) {
uni.showToast({
title: `${t('common.prompt.please-carefully-read-and-agree')} ${t('agreement.user-terms-conditions')}${t('pages-login.and')}${t('agreement.privacy-policy')}`,
icon: 'none'
})
}
return validateFormField.success && isAgreed.value
}
const verificationCodeRef = ref()
const submit = () => {
// verificationCodeRef.value.onOpen()
codeSubmit()
}
const btnLoading = ref(false)
function codeSubmit() {
// console.log(data)
btnLoading.value = true
appUserRegisterPost({
body: {
...logicStore.registerForm,
phone: logicStore.registerForm.phone,
areaCode: areaCode.value,
// captcha: data.code,
// uuid: data.uuid,
userPort: 2, // 2
}
}).then((res) => {
userStore.token = res.data.token;
logicStore.reset()
uni.showToast({
title: t('pages-login.sign-up.register-success'),
icon: 'none',
})
nextTick(() => {
userStore.getUserInfo();
uni.redirectTo({
url: '/pages-login/pages/guide-page/welcome'
})
})
}).finally(() => {
btnLoading.value = false
})
}
//
const handleSubmit = R.when(checkForm, debounce(Config.debounceLongTime, submit, {
atBegin: true
}))
function navigateTo(url: string) {
uni.navigateTo({url});
}
const emailIsReadonly = ref(false)
const phoneIsReadonly = ref(false)
onMounted(() => {
const {email, type} = logicStore.registerForm
if (type === 'phone') {
// phoneIsReadonly.value = true
}
if (type === 'email') {
// emailIsReadonly.value = true
}
})
</script>
<template>
<view>
<navbar/>
<view class="pt-60rpx px-27rpx pb-80rpx">
<view class="mb-60rpx text-50rpx leading-50rpx font-bold text-#14181B">
{{ t('pages-login.sign-up.title') }}
</view>
<view class="">
<!-- 邮箱 -->
<view class="">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("common.email") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.email"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
:readonly="emailIsReadonly"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Confirm email -->
<view class="mt-36rpx">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{
t("pages-login.sign-up.confirm-email")
}}
</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.confirmEmail"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
:readonly="emailIsReadonly"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Password -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.password") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.loginPwd"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enterPassword')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
showPassword
>
</wd-input>
</view>
</view>
<!-- First name -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.first-name") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.firstName"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Last name -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{ t("pages-login.sign-up.last-name") }}</view>
<view class="border-color px-30rpx flex items-center bg-#EFEFEF">
<wd-input
v-model.trim="logicStore.registerForm.surname"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<!-- Phone number -->
<view class="mt-36rpx ">
<view class="text-32rpx leading-32rpx text-#14181B mb-24rpx">{{
t("pages-login.sign-up.phone-number")
}}
</view>
<view class="border-color pr-30rpx flex items-center bg-#EFEFEF">
<view class="pr-14rpx text-28rpx">
<wd-picker v-model="areaCode" :columns="columns"/>
</view>
<wd-input
v-model.trim="logicStore.registerForm.phone"
:cursorSpacing="20"
:focus-when-clear="false"
:maxlength="40"
:placeholder="t('common.enter')"
:readonly="phoneIsReadonly"
clearable
custom-class="flex-1 !bg-transparent"
no-border
placeholderClass="!text-#999 !text-32rpx"
>
</wd-input>
</view>
</view>
<view class="mt-54rpx">
<wd-button :loading="btnLoading" block
custom-class="!h-108rpx !text-36rpx text-#fff font-bold !rounded-16rpx" loading-color="#000" @click="handleSubmit">
{{ t('pages-login.sign-up.title') }}
</wd-button>
</view>
<view class="mt-36rpx flex items-start text-28rpx text-primary lh-42rpx" @click="isAgreed = !isAgreed">
<view class="shrink-0 center py-5rpx px-10rpx">
<image v-show="isAgreed" class=" w-28rpx h-28rpx"
src="@img-login/101.png"></image>
<image v-show="!isAgreed" class=" w-28rpx h-28rpx"
src="@img-login/100.png"></image>
</view>
<view class="">
<text class="">{{ t('pages-login.continuing-agree') }}</text>
<text class="text-#00A76D"
@click.stop="navigateTo(`/pages/agreement/index?code=${Agreement.USER_AGREEMENT}`)">
{{ t('agreement.user-terms-conditions') }}
</text>
<text class="text-#00A76D"
@click.stop="navigateTo(`/pages/agreement/index?code=${Agreement.PRIVACY_POLICY}`)">
{{ t('agreement.privacy-policy') }}
</text>
</view>
</view>
</view>
</view>
<verification-code ref="verificationCodeRef" @submit="codeSubmit"/>
</view>
</template>
<style lang="scss">
page {
background-color: #fff;
}
.border-color {
height: 98rpx;
border-radius: 16rpx;
border: 2rpx solid #D4D4D4;
}
:deep(.wd-input__clear) {
background-color: transparent !important;
}
:deep(.wd-input__icon) {
background-color: transparent !important;
}
:deep(.wd-picker__cell) {
background-color: transparent !important;
.wd-picker__value {
margin-right: 8rpx !important;
}
}
:deep(.wd-picker-view-column__item) {
line-height: 94rpx !important;
}
:deep(.uni-picker-view-indicator) {
height: 94rpx !important;
}
</style>
View File
+12
View File
@@ -0,0 +1,12 @@
import {http} from '@/utils/http'
// 获取图片验证码
export const getCaptcha = () => http.get<Record<string, any>>('/auth/code')
// 登录
export const login = (data: Record<string, any>) =>
http.post<{
token: string
}>('/app/login', data)
Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File
+313
View File
@@ -0,0 +1,313 @@
import {defineStore} from "pinia";
import {login} from "@/pages-login/service";
import {useUserStore} from "@/store";
import * as R from "ramda";
import Config from "@/config";
import {LoginType} from "@/constant/enums";
export const useLogicStore = defineStore('login-logic', () => {
const {defaultAreaCode} = useAreaCode()
const registerForm = ref({
type: "",
email: '',
confirmEmail: '',
firstName: '',
surname: '',
phone: '',
loginPwd: '',
areaCode: defaultAreaCode.value,
captcha: "",
})
const loginForm = ref({
type: "",
email: '',
areaCode: defaultAreaCode.value,
phone: '',
code: '',
password: '',
tripartiteLoginIdentify: '',
})
const forgetPasswordForm = ref({
email: '',
loginPwd: '',
captcha: "",
phone: '',
areaCode: defaultAreaCode.value,
})
function navigateTo(url: string) {
uni.navigateTo({
url,
})
}
function reset() {
registerForm.value = {
type: "",
email: '',
confirmEmail: '',
firstName: '',
surname: '',
phone: '',
loginPwd: '',
areaCode: defaultAreaCode.value,
captcha: "",
}
loginForm.value = {
type: "",
email: '',
areaCode: defaultAreaCode.value,
phone: '',
code: '',
password: '',
tripartiteLoginIdentify: '',
}
forgetPasswordForm.value = {
email: '',
loginPwd: '',
captcha: "",
phone: '',
areaCode: defaultAreaCode.value,
}
}
function resetRegisterForm() {
registerForm.value = {
type: "",
email: '',
confirmEmail: '',
firstName: '',
surname: '',
phone: '',
loginPwd: '',
areaCode: defaultAreaCode.value,
captcha: "",
}
}
const userStore = useUserStore()
const {t} = useI18n()
function appleLogin() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'apple',
success: (res) => {
uni.getUserInfo({
provider: 'apple',
success: async (info) => {
// 获取用户信息成功, info.authResult保存用户信息
console.log('info', info)
const params = {
type: LoginType.APPLE,
email: info.userInfo?.email || '',
tripartiteLoginIdentify: info?.userInfo?.openId ?? '',
}
try {
const loginRes = await login(params)
console.log('loginRes', loginRes)
userStore.token = loginRes.data.token
nextTick(() => {
userStore.getUserInfo();
})
await uni.showToast({title: t('pages-login.login-successfully'), icon: "none"});
const pages = getCurrentPages()
setTimeout(R.ifElse(() => pages.length > 1, () => uni.navigateBack({delta: 1}), () => uni.switchTab(
{
url: Config.indexPath
}
)), 1000);
resolve(true)
} catch (err: any) {
console.log('err', err)
if (R.equals(+err.code, 711)) {
registerForm.value = {
email: info.userInfo?.email || '',
areaCode: '',
phone: '',
code: '',
password: '',
type: LoginType.APPLE,
tripartiteLoginIdentify: info?.userInfo?.openId ?? '',
}
navigateTo('/pages-login/pages/sign-up/index')
}
}
},
fail: (err: any) => {
console.log('err', err)
},
})
},
fail: (err: any) => {
console.log('err', err)
// 登录授权失败
uni.showToast({title: t('common.appleLoginFailed'), icon: 'none'})
},
})
})
}
function facebookLogin() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'apple',
success: (res) => {
uni.getUserInfo({
provider: 'apple',
success: async (info) => {
// 获取用户信息成功, info.authResult保存用户信息
console.log('info', info)
const params = {
userType: 1,
// 1 验证码 2 密码 3 一键登录 4 微信登录 5 小程序登录 6 苹果登录
type: 6,
openid: info?.userInfo?.openId ?? '',
}
try {
const loginRes = await login(params)
console.log('loginRes', loginRes)
userStore.token = loginRes.data.token
resolve(true)
} catch (err: any) {
console.log('err', err)
}
},
fail: (err: any) => {
console.log('err', err)
},
})
},
fail: (err: any) => {
console.log('err', err)
// 登录授权失败
uni.showToast({title: '登录授权失败', icon: 'none'})
},
})
})
}
function googleLogin() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'google',
success: (res) => {
uni.getUserInfo({
provider: 'google',
success: async (info) => {
// 获取用户信息成功, info.authResult保存用户信息
console.log('info', info)
const params = {
userPort: 2,
type: LoginType.GOOGLE,
email: info.userInfo.email,
tripartiteLoginIdentify: info.userInfo.openid,
}
try {
const loginRes = await login(params)
console.log('loginRes', loginRes)
userStore.token = loginRes.data.token
nextTick(() => {
userStore.getUserInfo();
})
await uni.showToast({title: t('pages-login.login-successfully'), icon: "none"});
const pages = getCurrentPages()
setTimeout(R.ifElse(() => pages.length > 1, () => uni.navigateBack({delta: 1}), () => uni.switchTab(
{
url: Config.indexPath
}
)), 1000);
resolve(true)
} catch (err: any) {
if (R.equals(+err.code, 711)) {
registerForm.value = {
email: info.userInfo.email,
areaCode: '+1',
phone: '',
code: '',
password: '',
type: LoginType.GOOGLE,
tripartiteLoginIdentify: info.userInfo.openid,
userPort: 2,
}
navigateTo('/pages-login/pages/sign-up/index')
}
}
},
fail: (err: any) => {
console.log('err', err)
},
})
},
fail: (err: any) => {
console.log('err', err)
// 登录授权失败
uni.showToast({title: t('common.googleLoginFailed'), icon: 'none'})
}
})
})
}
function wechatLogin() {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (res) => {
const {code} = res
uni.getUserInfo({
provider: 'weixin',
success: async (info) => {
// 获取用户信息成功, info.authResult保存用户信息
console.log('info', info)
const params = {
userType: 1,
// 1 验证码 2 密码 3 一键登录 4 微信登录 5 小程序登录 6 苹果登录
type: 6,
openid: info?.userInfo?.openId ?? '',
}
try {
const loginRes = await login(params)
console.log('loginRes', loginRes)
userStore.token = loginRes.data.token
resolve(true)
} catch (err: any) {
console.log('err', err)
}
},
fail: (err: any) => {
console.log('err', err)
},
})
},
fail: (err: any) => {
console.log('err', err)
// 登录授权失败
uni.showToast({title: '微信登录授权失败', icon: 'none'})
}
})
})
}
return {
registerForm,
loginForm,
forgetPasswordForm,
reset,
appleLogin,
facebookLogin,
googleLogin,
wechatLogin,
resetRegisterForm,
}
}
)
+166
View File
@@ -0,0 +1,166 @@
<script setup>
import {debounce} from "throttle-debounce";
import Config from "@/config";
const {t} = useI18n();
const isSubmit = ref(false)
function submit() {
isSubmit.value = true
setTimeout(() => {
isSubmit.value = false
}, 3000)
}
//
const handleSubmit = debounce(Config.debounceLongTime, submit, {
atBegin: true
})
</script>
<script>
// @ts-ignore
import {debounce} from "throttle-debounce";
// @ts-ignore
import Config from "@/config";
import {appUserCardSavePost} from "@/service";
export default {
data() {
return {
isSubmit: false
}
},
onLoad() {
console.log('onLoad')
// @ts-ignore
this.handleSuccess = debounce(Config.debounceLongTime, this.handleSuccess, {
atBegin: true
})
},
methods: {
handleError() {
uni.showToast({
icon: 'none',
title: 'Add credit card failed'
})
},
async handleSuccess(data) {
try {
const params = {
cardNumber: '************' + data.card.last4,
cardId: data.id,
}
const res = await appUserCardSavePost({
body: params
})
console.log('handleSuccess', res)
await uni.showToast({
icon: 'none',
title: 'The credit card was added successfully.'
})
// const eventChannel = this.getOpenerEventChannel();
// const id = res.data
// eventChannel.emit('acceptDataFromOpenedPage', {...params, id});
setTimeout(uni.navigateBack, 1000)
} catch (e) {
}
},
}
}
</script>
<script lang="renderjs" module="addRenderjs">
import {loadStripe} from '@stripe/stripe-js/pure';
import Config from '@/config/index'
// @ts-ignore
export default {
data() {
return {
isCompleted: false,
stripe: null,
elements: null,
}
},
mounted() {
console.log('mounted')
this.init()
},
methods: {
async init() {
console.log('本次初始化使用的key', Config.stripeKey)
loadStripe.setLoadParameters({advancedFraudSignals: false});
const stripe = await loadStripe(Config.stripeKey);
this.stripe = stripe
console.log(stripe)
const options = {
appearance: {/*...*/},
};
this.elements = stripe.elements(options);
const paymentElement = this.elements.create('card', {disableLink: true});
paymentElement.mount('#payment-form');
this.elements.get
paymentElement.on('change', (event) => {
console.log(event)
if (event.complete) {
this.isCompleted = true
// enable payment button
}
});
},
async handleSubmit(isSubmit) {
console.log('handleSubmit', isSubmit, this.isCompleted)
console.log('handleSubmit', this.stripe)
if (!isSubmit || !this.isCompleted || !this.stripe) {
return
}
const {error, paymentMethod} = await this.stripe.createPaymentMethod({
elements: this.elements,
});
console.log(error)
console.log(paymentMethod)
if (error) {
// Handle error
this.$ownerInstance.callMethod('handleError')
} else {
// Send paymentMethod.id to your server
this.$ownerInstance.callMethod('handleSuccess', paymentMethod)
}
}
}
}
</script>
<template>
<view class="">
<navbar customClass="!bg-transparent"/>
<view class="text-center px-30rpx mt-98rpx">
<view class="text-46rpx lh-46rpx text-#333 font-bold">{{ Config.appName }}</view>
<view class="text-32rpx text-#333 font-500 mt-70rpx mb-12rpx">{{ t('pages-user.card.title') }}</view>
<view class="text-28rpx lh-28rpx text-#999">{{ t('pages-user.card.desc') }}</view>
</view>
<view id="payment-form" :change:prop="addRenderjs.handleSubmit"
:prop="isSubmit"
class="mt-188rpx px-30rpx py-40rpx bg-#fff"
></view>
<!-- 底部确认按钮 -->
<fixed-bottom-large-btn
:text="t('common.save')"
class="z-100"
fixed
@click="handleSubmit"
/>
</view>
</template>
<style lang="scss" scoped>
page {
background-color: #F5F5F5;
}
</style>
@@ -0,0 +1,143 @@
<script lang="ts" setup>
import {useUserStore} from "@/store";
import {EventEnum} from "@/constant/enums";
import {appUserCardSelectDefaultPost} from "@/service";
const {t} = useI18n();
const userStore = useUserStore();
//
const selectedPayment = ref<1 | 2>(1)
function changePayment(payment: 1 | 2) {
payMethodOptions.value.payMethod = payment
}
onLoad(() => {
//
appUserCardSelectDefault()
})
//
const payMethodOptions = ref({
cardNumber: '',
cardId: '',
payMethod: 1, // 1 2
})
function appUserCardSelectDefault() {
appUserCardSelectDefaultPost({}).then(res => {
console.log('查询用户默认信用卡', res)
payMethodOptions.value.cardId = res.data?.cardId || ''
payMethodOptions.value.cardNumber = res.data?.cardNumber || ''
})
}
const confirmPayment = () => {
if (payMethodOptions.value.payMethod === 1) {
if (!payMethodOptions.value.cardId) {
chooseCard()
} else {
uni.$emit(EventEnum.CHOOSE_PAYMENT_METHOD, payMethodOptions.value)
uni.navigateBack()
}
} else {
uni.$emit(EventEnum.CHOOSE_PAYMENT_METHOD, payMethodOptions.value)
uni.navigateBack()
}
}
function chooseCard() {
uni.navigateTo({
url: '/pages-user/pages/select-credit-card/index',
events: {
//
selectedCard: function (data) {
if (data) {
payMethodOptions.value.cardId = data.cardId
payMethodOptions.value.cardNumber = data.cardNumber
}
},
},
})
}
function navigateTo(url: string) {
uni.navigateTo({url})
}
</script>
<template>
<view class="">
<navbar/>
<!-- 页面标题 -->
<view class="px-30rpx pt-20rpx mb-88rpx">
<text class="text-46rpx font-bold text-#333 leading-46rpx">{{ t('pages-user.choosePaymethod.title') }}</text>
</view>
<view class="px-30rpx">
<view class="flex-center-sb mb-66rpx" @click="changePayment(2)">
<view class="flex items-center">
<image
class="w-44rpx h-44rpx shrink-0 mr-28rpx"
mode="aspectFill"
src="@img/chef/156.png"
/>
<text class="text-32rpx lh-32rpx text-#333 font-500">{{
t('pages-user.choosePaymethod.wallet')
}}(${{ userStore.currenMerchantInfo?.balance || 0 }})
</text>
</view>
<!-- 单选按钮 -->
<image
:src="
payMethodOptions.payMethod === 2
? '/static/images/chef/133.png'
: '/static/images/chef/134.png'
"
class="w-48rpx h-48rpx shrink-0"
mode="aspectFit"
/>
</view>
<view class="flex-center-sb" @click="changePayment(1)">
<view class="flex items-center">
<image
class="w-44rpx h-44rpx shrink-0 mr-28rpx"
mode="aspectFill"
src="@img/chef/138.png"
/>
<text class="text-32rpx lh-32rpx text-#333 font-500">{{ t('pages-user.member.creditCard') }}</text>
</view>
<!-- 单选按钮 -->
<image
:src="
payMethodOptions.payMethod === 1
? '/static/images/chef/133.png'
: '/static/images/chef/134.png'
"
class="w-48rpx h-48rpx shrink-0"
mode="aspectFit"
/>
</view>
<view v-if="payMethodOptions.cardId"
class="h-98rpx rounded-16rpx bg-#F2F2F2 ml-72rpx mt-30rpx flex-center-sb px-28rpx">
<text class="text-30rpx lh-30rpx text-#9E9E9E">{{ payMethodOptions.cardNumber }}</text>
<view class="h-50rpx rounded-25rpx bg-white center px-20rpx text-24rpx lh-24rpx text-#333" @click="chooseCard">
{{ t('pages-user.choosePaymethod.replace') }}
</view>
</view>
</view>
<!-- 底部确认按钮 -->
<fixed-bottom-large-btn
:text="t('common.confirm')"
class="z-100"
fixed
@click="confirmPayment"
/>
</view>
</template>
<style>
page {
background-color: #fff;
}
</style>
+168
View File
@@ -0,0 +1,168 @@
<script lang="ts" setup>
import ChooseImage from "@/components/choose-image/choose-image.vue";
import {appFeedbackAddPost} from "@/service";
import {z} from 'zod';
const {t} = useI18n()
const form = ref({
content: '',
contactPhone: '',
images: '',
})
// zodschema
const createValidationSchema = () => {
return z.object({
content: z.string()
.min(1, t('pages-user.complaints.validation.content-required'))
.min(10, t('pages-user.complaints.validation.content-min-length'))
.max(500, t('pages-user.complaints.validation.content-max-length')),
contactPhone: z.string()
.min(1, t('pages-user.complaints.validation.contact-phone-required'))
.regex(/^[\d\s\-\+\(\)]+$/, t('pages-user.complaints.validation.contact-phone-invalid'))
})
}
const chooseImageRef = ref()
//
const validateField = (field: keyof typeof form.value) => {
try {
const schema = createValidationSchema()
const fieldSchema = schema.shape[field]
fieldSchema.parse(form.value[field])
return true
} catch (error) {
if (error instanceof z.ZodError) {
uni.showToast({
title: error.errors[0].message,
icon: 'none'
})
}
return false
}
}
//
const validateForm = () => {
try {
const schema = createValidationSchema()
schema.parse(form.value)
return true
} catch (error) {
if (error instanceof z.ZodError) {
uni.showToast({
title: error.errors[0].message,
icon: 'none'
})
}
return false
}
}
const handleSubmit = () => {
if (!validateForm()) {
return
}
appFeedbackAddPost({
body: {
...form.value
}
}).then(res => {
uni.showToast({
title: t('toast.submitSuccess'),
icon: 'none'
})
form.value = {
content: '',
contactPhone: '',
images: '',
}
})
}
const handleChooseImage = () => {
chooseImageRef.value.init()
}
function onImageChange(files: string[]) {
form.value.images = files[0]
}
</script>
<template>
<navbar :title="t('pages-user.complaints.title')"/>
<view class="px-18rpx">
<view class="mb-27rpx mt-32rpx text-28rpx text-#333">
{{ t('pages-user.complaints.description') }}
</view>
<view class="bg-white rounded-14rpx px-18rpx pt-30rpx pb-24rpx">
<view class="text-28rpx text-#333 mb-28rpx">{{ t('pages-user.complaints.feedback-content') }}</view>
<view
class="min-h-234rpx box-border bg-#F6F6F6 rounded-14rpx overflow-hidden px-18rpx py-10rpx"
>
<wd-textarea
v-model="form.content"
:maxlength="500"
:placeholder="t('pages-user.complaints.feedback-content-placeholder')"
auto-height
custom-class="!bg-#F6F6F6"
custom-textarea-container-class="!bg-#F6F6F6"
no-border
@blur="validateField('content')"
/>
</view>
<view>
<view class="text-28rpx text-#333 mt-32rpx mb-24rpx">{{ t('pages-user.complaints.image') }}:</view>
<view class="relative w-210rpx h-210rpx" @click="handleChooseImage">
<image
v-if="form.images"
class="absolute top--10rpx right--10rpx z-10 w-36rpx h-36rpx"
src="@img/chef/190.png"
></image>
<image
v-if="!form.images"
class="w-210rpx h-210rpx"
src="@img/chef/119.png"
></image>
<image
v-else
:src="form.images"
class="w-210rpx h-210rpx rounded-16rpx"
mode="aspectFill"
></image>
</view>
</view>
</view>
<view class="mt-14rpx flex-center-sb bg-white h-86rpx rounded-14rpx px-18rpx">
<view class="text-28rpx text-#333 ">{{ t('pages-user.complaints.contact-information') }}:</view>
<wd-input
v-model.trim="form.contactPhone"
:maxlength="50"
:placeholder="t('common.enter')"
custom-class="!p-[12rpx+20rpx] rounded-10rpx"
custom-input-class="text-right"
no-border
placeholderStyle="font-size: 28rpx;line-height: 40rpx;color: #B1B1B1; text-align: right;"
@blur="validateField('contactPhone')"
/>
</view>
<view class="mt-38rpx text-28rpx text-#999">
{{ t('pages-user.complaints.contact-information-tip') }}
</view>
<fixed-bottom-large-btn
:text="`${t('common.submit')}`"
class="z-100"
fixed
@click="handleSubmit"
/>
<ChooseImage ref="chooseImageRef" @change="onImageChange"/>
</view>
</template>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,110 @@
<script lang="ts" setup>
import {appAutomaticCookingMachineRegistrationPost} from "@/service";
import {useConfigStore} from "@/store";
import {z} from 'zod';
const {t} = useI18n();
const configStore = useConfigStore();
const emit = defineEmits(['submit']);
const formData = ref({
name: '',
phone: '',
})
// Zod schema
const formSchema = z.object({
name: z.string().min(1, t('common.placeholder.pleaseEnterName')),
phone: z.string().min(1, t('common.placeholder.pleaseEnterPhone'))
})
const show = ref(false);
function onOpen() {
show.value = true;
}
function handleClose() {
show.value = false;
}
function handleSubmit() {
// Zod
const result = formSchema.safeParse(formData.value)
if (!result.success) {
const firstError = result.error.errors[0]
uni.showToast({
title: firstError.message,
icon: 'none'
})
return
}
//
appAutomaticCookingMachineRegistrationPost({
body: {
...formData.value
}
}).then(res => {
uni.showToast({
title: t('toast.submitSuccess'),
icon: 'none'
})
handleClose()
emit('submit')
})
}
defineExpose({
onOpen,
});
</script>
<template>
<wd-popup
v-model="show"
position="bottom"
@close="handleClose"
>
<view class="px-30rpx pt-52rpx">
<view class="text-40rpx lh-40rpx font-500 text-#333 text-center mb-52rpx">{{ t('common.inquiry') }}</view>
<view class="h-98rpx rounded-16rpx px-28rpx flex items-center bg-#F2F2F2 mb-40rpx">
<wd-input
v-model="formData.name"
:focus-when-clear="false"
:placeholder="t('common.placeholder.pleaseEnterName')"
clearable
custom-class="flex items-center !text-30rpx !bg-transparent flex-1"
maxlength="20"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
></wd-input>
</view>
<view class="h-98rpx rounded-16rpx px-28rpx flex items-center bg-#F2F2F2">
<wd-input
v-model="formData.phone"
:focus-when-clear="false"
:placeholder="t('common.placeholder.pleaseEnterPhone')"
clearable
custom-class="flex items-center !text-30rpx !bg-transparent flex-1"
maxlength="20"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
type="number"
></wd-input>
</view>
<wd-button block class="!h-108rpx !rounded-16rpx !text-36rpx text-#fff font-500 mt-68rpx" @click="handleSubmit">
{{ t('common.submit') }}
</wd-button>
<view :style="[configStore.iosSafeBottomPlaceholder]"></view>
</view>
</wd-popup>
</template>
<style lang="scss" scoped>
:deep(.wd-input__clear) {
background-color: transparent !important;
}
</style>
@@ -0,0 +1,65 @@
<template>
<view class="cooking-machine-skeleton bg-#fff">
<!-- 主图骨架屏 -->
<view class="w-750rpx h-750rpx skeleton-item"></view>
<!-- 固定头部骨架屏 -->
<view class="fixed top-0 left-0 z-9 w-full px-30rpx pt-16rpx">
<!-- 状态栏占位 -->
<view class="w-full h-44rpx"></view>
<!-- 返回按钮骨架屏 -->
<view class="w-48rpx h-48rpx rounded-8rpx skeleton-item"></view>
</view>
<!-- 内容区域骨架屏 -->
<view class="px-30rpx py-28rpx">
<!-- 产品标题骨架屏 -->
<view class="w-500rpx h-40rpx mb-28rpx skeleton-item"></view>
<!-- 销量骨架屏 -->
<view class="w-300rpx h-28rpx mb-64rpx skeleton-item"></view>
<!-- 分段控制器骨架屏 -->
<view class="w-374rpx h-60rpx rounded-30rpx mb-36rpx skeleton-item"></view>
<!-- 详细描述骨架屏 -->
<view class="space-y-16rpx">
<!-- 概述标题 -->
<view class="w-120rpx h-30rpx skeleton-item"></view>
<!-- 描述文本行 -->
<template v-for="item in 5" :key="item">
<view
class="skeleton-item"
:class="[
item % 4 === 0 ? 'w-200rpx' : item % 3 === 0 ? 'w-600rpx' : 'w-680rpx',
'h-30rpx'
]"
></view>
</template>
<!-- 主要类型与形态标题 -->
<view class="w-280rpx h-30rpx mt-32rpx skeleton-item"></view>
</view>
</view>
<!-- 底部按钮骨架屏 -->
<view class="fixed bottom-0 left-0 w-full px-30rpx pb-30rpx">
<view class="w-full h-88rpx rounded-44rpx skeleton-item"></view>
</view>
<!-- 底部安全区域 -->
<view class="h-200rpx"></view>
</view>
</template>
<script setup lang="ts">
//
</script>
<style scoped lang="scss">
.cooking-machine-skeleton {
min-height: 100vh;
position: relative;
}
</style>
@@ -0,0 +1,45 @@
<script lang="ts" setup>
const {t} = useI18n();
const show = ref(false);
function onOpen() {
show.value = true;
}
function handleClose() {
show.value = false;
}
defineExpose({
onOpen,
});
</script>
<template>
<wd-popup
v-model="show"
custom-style="border-radius:20rpx;"
position="center"
@close="handleClose"
>
<view class="w-490rpx rounded-20rpx relative">
<view class="flex flex-col items-center mb-34rpx py-36rpx">
<image class="w-190rpx h-190rpx mb-32rpx" src="@img/chef/100.png"></image>
<view class="text-40rpx lh-40rpx text-#333 font-500 mb-22rpx">{{ t('toast.submitSuccess') }}</view>
<view class="text-28rpx text-#999 text-center px-45rpx tracking-[.04em]">
{{ t('toast.pleasePhone') }}
</view>
</view>
<!-- #ifndef APP-PLUS -->
<view class="h-98rpx"></view>
<!-- #endif -->
<view class="absolute bottom-0 left-0 w-full border-top h-98rpx center text-30rpx text-#333" @click="handleClose">
{{ t('common.gotIt') }}
</view>
</view>
</wd-popup>
</template>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,139 @@
<script lang="ts" setup>
import {useScrollThreshold} from "@/hooks/useScrollThreshold";
import Application from "./components/application.vue";
import TipPopup from "./components/tip-popup.vue";
import CookingMachineSkeleton from "./components/cooking-machine-skeleton.vue";
import {appAutomaticCookingMachineDetailGet} from "@/service";
const {t} = useI18n()
const applicationRef = ref();
function handleInputUpdateModelValue() {
applicationRef.value.onOpen()
}
function applicationSubmit() {
handleTipPopupOpen()
}
const tipPopupRef = ref();
function handleTipPopupOpen() {
tipPopupRef.value.onOpen()
}
const currentType = ref(0);
const typeList = [t('common.details'), t('common.instructions')];
const showStatusBar = useScrollThreshold();
//
const loading = ref(true);
const detailData = ref({})
const swiperList = computed(() => {
if (!detailData.value) return [];
return detailData.value?.showImage.split(',');
})
onMounted(() => {
loading.value = true;
appAutomaticCookingMachineDetailGet({}).then(res => {
console.log('res', res)
detailData.value = res.data
}).finally(() => {
loading.value = false;
})
});
onPageScroll((e) => {
uni.$emit("page-scroll", e);
});
function goBack() {
uni.navigateBack({
delta: 1
})
}
</script>
<template>
<view>
<!-- 骨架屏 -->
<cooking-machine-skeleton v-if="loading"/>
<!-- 主要内容 -->
<template v-else>
<!-- 主图 -->
<wd-swiper :list="swiperList" autoplay height="750rpx"></wd-swiper>
<view
:class="[showStatusBar ? 'bg-#fff' : '']"
class="fixed top-0 left-0 z-9 w-full px-30rpx transition-all pt-16rpx"
>
<!-- 状态栏 -->
<status-bar/>
<!-- 返回按钮 -->
<image
class="w-48rpx h-48rpx relative z-1"
mode="aspectFill"
src="@img/chef/1327.png"
@click="goBack"
/>
</view>
<view class="px-30rpx py-28rpx">
<view class="text-40rpx lh-40rpx font-500 text-#333">{{ detailData.title }}</view>
<view class="text-28rpx lh-28rpx text-#333 mt-28rpx">sales volume{{ detailData.sales }}</view>
<view class="w-374rpx mt-64rpx">
<l-segmented v-model="currentType" :options="typeList" active-color="#333" bg-color="#F2F2F2" shape="round"/>
</view>
<view v-show="currentType === 0" class="text-30rpx lh-44rpx text-#333 mt-36rpx">
<mp-html
:content="(detailData.detail)"
:preview-img="false"
:show-img-menu="false"
:tag-style="{
div: 'white-space: pre-wrap;',
p: 'white-space: pre-wrap;',
img: 'width:100%;max-width: 100%;height:auto;',
}"
selectable
></mp-html>
</view>
<view v-show="currentType === 1" class="text-30rpx lh-44rpx text-#333 mt-36rpx">
<mp-html
:content="(detailData.instructions)"
:preview-img="false"
:show-img-menu="false"
:tag-style="{
div: 'white-space: pre-wrap;',
p: 'white-space: pre-wrap;',
img: 'width:100%;max-width: 100%;height:auto;',
}"
selectable
></mp-html>
</view>
</view>
<fixed-bottom-large-btn
:text="t('common.inquiry')"
class="z-100"
fixed
@click="handleInputUpdateModelValue"
/>
<application ref="applicationRef" @submit="applicationSubmit"/>
<tip-popup ref="tipPopupRef"/>
</template>
</view>
</template>
<style>
page {
background-color: #fff;
}
</style>
<style lang="scss" scoped>
:deep(.wd-swiper__track) {
border-radius: 0 !important;
}
</style>
@@ -0,0 +1,399 @@
<script lang="ts" setup>
import {z} from 'zod'
import dayjs from 'dayjs'
import {appCouponAddCouponPost, appCouponCouponCostPost, appUserCardSelectDefaultPost} from "@/service";
import {Agreement, EventEnum} from "@/constant/enums";
const {t} = useI18n()
const formData = ref({
nameZh: '',
couponType: 1,// 1-, 2-
minAmount: 0, //
discountValue: '', //
issuerType: 2, // 1-, 2-
distributionType: 2, // 1-, 2-
validStart: '', //
validEnd: '', //
totalQuantity: '', //
})
// Zod schema
const formSchema = z.object({
nameZh: z.string().min(1, t('pages-user.coupons.validation.nameRequired')),
// couponType: z.number().min(1, ''),
// minAmount: z.string().min(1, ''),
discountValue: z.string()
.min(1, t('pages-user.coupons.validation.discountValueRequired'))
.refine((val) => {
//
if (!val || val.trim() === '') return false
//
if (!/^\d+$/.test(val.trim())) return false
const num = Number(val)
// 1-100
return Number.isInteger(num) && num >= 1 && num <= 100
}, {message: t('pages-user.coupons.validation.discountValueRange') ?? '请输入1-100之间的整数'}),
validStart: z.union([z.string().min(1, t('pages-user.coupons.validation.validStartRequired')), z.number().min(1, t('pages-user.coupons.validation.validStartRequired'))]),
validEnd: z.union([z.string().min(1, t('pages-user.coupons.validation.validEndRequired')), z.number().min(1, t('pages-user.coupons.validation.validEndRequired'))]),
totalQuantity: z.string().min(1, t('pages-user.coupons.validation.totalQuantityRequired')),
})
const isAgree = ref(false)
const dateType = ref('') // 'start' , 'end'
function changeCouponType(type: number) {
formData.value.couponType = type
}
function openHours(type: string) {
console.log(type)
dateType.value = type
show.value = true
}
function navigateTo(url: string) {
uni.navigateTo({url})
}
const show = ref(false)
function handleClose() {
show.value = false
}
const timeValue = ref(Date.now())
const maxDate = dayjs().valueOf();
function handleSubmit() {
console.log(timeValue.value);
//
if (dateType.value === 'start') {
formData.value.validStart = timeValue.value
} else if (dateType.value === 'end') {
formData.value.validEnd = timeValue.value
}
//
show.value = false
}
//
function submitForm() {
try {
//
if (!isAgree.value) {
uni.showToast({
title: t('pages-user.coupons.validation.agreeAgreementFirst'),
icon: 'none',
duration: 2000
})
return
}
// 使 zod
formSchema.parse(formData.value)
//
if (formData.value.validStart && formData.value.validEnd) {
const startTime = dayjs(formData.value.validStart)
const endTime = dayjs(formData.value.validEnd)
if (startTime.isAfter(endTime)) {
uni.showToast({
title: t('pages-user.coupons.validation.startDateCannotLaterThanEndDate'),
icon: 'none',
duration: 2000
})
return
}
}
// ,
if (payMethodOptions.value.payMethod === 2) {
passwordInputRef.value?.showPasswordInput()
} else {
appCouponAddCoupon()
}
} catch (error) {
//
if (error instanceof z.ZodError) {
const firstError = error.errors[0]
uni.showToast({
title: firstError.message,
icon: 'none',
duration: 2000
})
}
}
}
function appCouponAddCoupon() {
appCouponAddCouponPost({
body: {
...payMethodOptions.value,
...formData.value,
nameEn: formData.value.nameZh,
// 1-100 1 - (x/100)90 -> 0.1
discountValue: Number((1 - (Number(formData.value.discountValue) / 100)).toFixed(2)),
//
totalQuantity: Number(formData.value.totalQuantity),
validStart: Number(formData.value.validStart),
validEnd: Number(formData.value.validEnd),
}
}).then(res => {
uni.showToast({
title: t('pages-user.coupons.validation.createSuccess'),
icon: 'none'
})
uni.navigateBack()
})
}
//
const payMethodOptions = ref({
orderId: '',
cardId: '',
payMethod: 1, // 1 2
payPassword: '',
})
const passwordInputRef = ref()
useEventEmit(EventEnum.CHOOSE_PAYMENT_METHOD, (data) => {
if (data) {
if (data.payMethod === 1) {
payMethodOptions.value.cardId = data.cardId
payMethodOptions.value.cardNumber = data.cardNumber
payMethodOptions.value.payMethod = 1
} else {
payMethodOptions.value.payMethod = 2
}
}
})
function payPawSuccess(payPassword: string) {
payMethodOptions.value.payPassword = payPassword
appCouponAddCoupon()
}
//
onLoad((options: any) => {
//
appCouponCouponCost()
//
appUserCardSelectDefault()
})
function appUserCardSelectDefault() {
appUserCardSelectDefaultPost({}).then(res => {
console.log('查询用户默认信用卡', res)
payMethodOptions.value.cardId = res.data?.cardId || ''
payMethodOptions.value.cardNumber = res.data?.cardNumber || ''
})
}
const feeNum = ref(0)
function appCouponCouponCost() {
appCouponCouponCostPost({}).then(res => {
console.log('是否免费添加优惠券', res)
if (res.data) {
feeNum.value = Number(res.data) || 0
}
})
}
</script>
<template>
<view>
<navbar title="Add for free"></navbar>
<view class="px-30rpx">
<!--优惠券名称-->
<view class="mt-44rpx">
<view class="text-36rpx lh-36rpx text-#333 font-500 mb-20rpx">{{ t('pages-user.coupons.name') }}:</view>
<view class="flex-center-sb h-98rpx px-24rpx rounded-16rpx bg-#F6F6F6">
<wd-input
v-model="formData.nameZh"
:focus-when-clear="false"
:maxlength="20"
:placeholder="t('common.placeholder.pleaseEnter')"
clearable
confirm-type="search"
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
use-prefix-slot
>
</wd-input>
</view>
</view>
<!--优惠值-->
<view class="mt-44rpx">
<view class="text-36rpx lh-36rpx text-#333 font-500 mb-20rpx">{{ t('pages-user.coupons.discount') }}:</view>
<view class="flex-center-sb h-98rpx px-24rpx rounded-16rpx bg-#F6F6F6">
<wd-input
v-model="formData.discountValue"
:focus-when-clear="false"
:max="100"
:min="1"
:placeholder="t('common.placeholder.pleaseEnter')"
clearable
confirm-type="search"
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
type="number"
use-prefix-slot
>
</wd-input>
</view>
</view>
<!--Distribution quantity-->
<view class="mt-44rpx">
<view class="text-36rpx lh-36rpx text-#333 font-500 mb-20rpx">{{
t('pages-user.coupons.totalQuantity')
}}:
</view>
<view class="flex-center-sb h-98rpx px-24rpx rounded-16rpx bg-#F6F6F6">
<wd-input
v-model="formData.totalQuantity"
:focus-when-clear="false"
:placeholder="t('common.placeholder.pleaseEnter')"
clearable
confirm-type="search"
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
type="number"
use-prefix-slot
>
</wd-input>
</view>
</view>
<!--生效日期:-->
<view class="mt-44rpx" @click="openHours('start')">
<view class="text-36rpx lh-36rpx text-#333 font-500 mb-20rpx">{{ t('pages-user.coupons.validStart') }}:</view>
<view class="flex-center-sb h-98rpx px-24rpx rounded-16rpx bg-#F6F6F6">
<text :class="formData.validStart ? 'text-#333' : 'text-#999'" class="text-30rpx lh-30rpx">
{{
formData.validStart ? dayjs(formData.validStart).format(`DD/MM/YYYY`) : t('common.placeholder.pleaseSelect')
}}
</text>
<image class="w-32rpx h-32rpx" src="@img/chef/115.png"></image>
</view>
</view>
<!--失效日期:-->
<view class="mt-44rpx" @click="openHours('end')">
<view class="text-36rpx lh-36rpx text-#333 font-500 mb-20rpx">{{ t('pages-user.coupons.validEnd') }}:</view>
<view class="flex-center-sb h-98rpx px-24rpx rounded-16rpx bg-#F6F6F6">
<text :class="formData.validEnd ? 'text-#333' : 'text-#999'" class="text-30rpx lh-30rpx">
{{
formData.validEnd ? dayjs(formData.validEnd).format(`DD/MM/YYYY`) : t('common.placeholder.pleaseSelect')
}}
</text>
<image class="w-32rpx h-32rpx" src="@img/chef/115.png"></image>
</view>
</view>
<view v-if="feeNum > 0" class="mt-44rpx" @click="navigateTo('/pages-user/pages/choose-paymethod/index')">
<view class="text-36rpx lh-36rpx text-#333 font-500 mb-20rpx">{{ t('pages-user.coupons.payMethods') }}:</view>
<view class="h-98rpx px-24rpx rounded-16rpx bg-#F6F6F6 flex items-center w-full">
<view class="w-full flex-center-sb text-32rpx lh-32rpx font-500 text-#333">
<view class="flex items-center">
<image
class="w-44rpx h-44rpx mr-28rpx shrink-0"
mode="aspectFit"
src="@img/chef/138.png"
/>
<text class="">
<template v-if="payMethodOptions.payMethod === 1">{{
t('pages-user.choosePaymethod.creditCard')
}}
</template>
<template v-else>{{ t('pages-user.choosePaymethod.wallet') }}</template>
</text>
</view>
<view class="flex items-center">
<view class="mr-12px">
<template v-if="payMethodOptions.payMethod === 1">
<!--判断用户是否有信用卡 {{ t("pages-user.member.creditCard") }}-->
<text v-if="!payMethodOptions.cardId"></text>
<text v-else>{{ payMethodOptions.cardNumber }}</text>
</template>
<template v-else-if="payMethodOptions.payMethod === 2">
<!-- {{ t('pages-user.choosePaymethod.wallet') }}-->
<text></text>
</template>
</view>
<image
class="w-32rpx h-32rpx shrink-0"
mode="aspectFit"
src="@img/chef/142.png"
/>
</view>
</view>
</view>
</view>
</view>
<view class="px-90rpx flex items-center mt-104rpx" @click="isAgree = !isAgree">
<image
:src="
isAgree
? '/static/images/chef/152.png'
: '/static/images/chef/134.png'
"
class="w-28rpx h-28rpx shrink-0 mr-10rpx"
mode="aspectFit"
/>
<view class="text-28rpx lh-28rpx text-#333">
{{ t('pages-login.continuing-agree') }}
<text class="text-#00A76D"
@click.stop="navigateTo('/pages/agreement/index?code=' + Agreement.CHEF_M_CODE_AGREEMENT)">
{{ t('agreement.codeAgreement') }}
</text>
</view>
</view>
<view class="h-100rpx"></view>
<fixed-bottom-large-btn
:text="feeNum > 0 ? `${t('pages-user.coupons.paidPublishing')}($${feeNum})` : t('pages-user.coupons.publishNow')"
class="z-100"
fixed
@click="submitForm"
/>
<wd-popup v-model="show" position="bottom" @close="handleClose">
<view>
<view class="flex-center-sb h-102rpx bg-#F7F7F7 px-30rpx">
<view class="text-30rpx text-#999" @click="handleClose">{{ t('common.cancel') }}</view>
<view class="text-34rpx text-#333">
{{
dateType === 'start' ? t('pages-user.coupons.selectValidStartDate') : dateType === 'end' ? t('pages-user.coupons.selectValidEndDate') : t('pages-user.coupons.dateFilter')
}}
</view>
<view class="text-30rpx text-#FF6106" @click="handleSubmit">{{ t('common.confirm') }}</view>
</view>
<view class="px-60rpx pt-60rpx">
<wd-datetime-picker-view v-model="timeValue" :minDate="maxDate" type="date"/>
</view>
</view>
</wd-popup>
<password-container ref="passwordInputRef" @success="payPawSuccess"/>
</view>
</template>
<style>
page {
background-color: #fff;
}
</style>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,335 @@
<script lang="ts" setup>
// @ts-nocheck
import {computed, ref} from 'vue'
import {useI18n} from 'vue-i18n'
import {z} from 'zod'
import {appCouponAddCouponPost} from '@/service'
import {Agreement} from '@/constant/enums'
const {t} = useI18n()
const couponTypes = computed(() => [
{label: t('pages-user.coupons.discount'), value: 1},
{label: t('pages-user.coupons.fullDiscount'), value: 2},
])
const formData = ref({
nameZh: '',
couponType: 1, // 1-, 2-
totalQuantity: '',
discountValue: '', //
minAmount: '', //
validDays: '', //
distributionType: 1, // 1-, 2-
issuerType: 2, // 1-, 2-
})
const isAgree = ref(false)
const submitting = ref(false)
const discountSchema = z.object({
nameZh: z.string().min(1, t('pages-user.coupons.validation.nameRequired')),
totalQuantity: z.string()
.min(1, t('pages-user.coupons.validation.totalQuantityRequired'))
.refine(
(val) => /^\d+$/.test(val.trim()) && Number(val) > 0,
{message: t('pages-user.coupons.validation.totalQuantityPositive') ?? '发放数量必须为大于0的整数'},
),
validDays: z.string()
.min(1, t('pages-user.coupons.validation.validDaysRequired'))
.refine(
(val) => /^\d+$/.test(val.trim()) && Number(val) > 0,
{message: t('pages-user.coupons.validation.validDaysPositive') ?? '有效天数必须为大于0的整数'},
),
discountValue: z.string()
.min(1, t('pages-user.coupons.validation.discountValueRequired'))
.refine((val) => {
//
if (!val || val.trim() === '') return false
//
if (!/^\d+$/.test(val.trim())) return false
const num = Number(val)
// 1-100
return Number.isInteger(num) && num >= 1 && num <= 100
}, {message: t('pages-user.coupons.validation.discountValueRange') ?? '请输入1-100之间的整数'})
})
const fullDiscountSchema = z.object({
nameZh: z.string().min(1, t('pages-user.coupons.validation.nameRequired')),
totalQuantity: z.string()
.min(1, t('pages-user.coupons.validation.totalQuantityRequired'))
.refine(
(val) => /^\d+$/.test(val.trim()) && Number(val) > 0,
{message: t('pages-user.coupons.validation.totalQuantityPositive') ?? '发放数量必须为大于0的整数'},
),
validDays: z.string()
.min(1, t('pages-user.coupons.validation.validDaysRequired') ?? '请输入有效期天数')
.refine(
(val) => /^\d+$/.test(val.trim()) && Number(val) > 0,
{message: t('pages-user.coupons.validation.validDaysPositive') ?? '有效天数必须为大于0的整数'},
),
minAmount: z.string()
.min(1, t('pages-user.coupons.validation.minAmountRequired') ?? '请输入满减门槛')
.refine((val) => Number(val) > 0, {message: t('pages-user.coupons.validation.minAmountRequired') ?? '请输入正确的满减门槛'}),
discountValue: z.string()
.min(1, t('pages-user.coupons.validation.discountValueRequired'))
.refine((val) => Number(val) > 0, {message: t('pages-user.coupons.validation.discountValueRange') ?? '请输入正确的减免金额'})
})
const schema = computed(() => formData.value.couponType === 1 ? discountSchema : fullDiscountSchema)
function showToast(title: string) {
uni.showToast({title, icon: 'none', duration: 2000})
}
function toggleType(type: number) {
formData.value.couponType = type
//
formData.value.discountValue = ''
formData.value.minAmount = ''
}
async function handleSubmit() {
console.log(formData.value)
if (submitting.value) return
if (!isAgree.value) {
showToast(t('pages-user.coupons.validation.agreeAgreementFirst'))
return
}
try {
schema.value.parse(formData.value)
} catch (error) {
if (error instanceof z.ZodError) {
const firstError = error.errors[0]
showToast(firstError.message)
return
}
}
submitting.value = true
try {
const payload = {
...formData.value,
//
distributionType: 1,
issuerType: 2,
isValid: 2,
// 0
minAmount: formData.value.couponType === 1 ? 0 : Number(formData.value.minAmount),
// 1-100 1 - (x/100)90 -> 0.1
// 使
discountValue: formData.value.couponType === 1
? Number((1 - (Number(formData.value.discountValue) / 100)).toFixed(2))
: Number(formData.value.discountValue),
totalQuantity: Number(formData.value.totalQuantity),
validDays: Number(formData.value.validDays),
}
const res = await appCouponAddCouponPost({body: payload as any})
if (res?.code === 200) {
showToast(t('pages-user.coupons.validation.createSuccess'))
uni.navigateBack()
} else {
showToast(res?.msg || t('common.prompt.request-incorrect'))
}
} catch (error) {
showToast(t('common.prompt.request-failed-please-try-again-later'))
} finally {
submitting.value = false
}
}
</script>
<template>
<view>
<navbar :title="t('pages-user.coupons.addCoupon')"/>
<view class="px-30rpx">
<!-- 优惠券名称 -->
<view class="mt-44rpx">
<view class="field-label">{{ t('pages-user.coupons.name') }}:</view>
<view class="field">
<wd-input
v-model="formData.nameZh"
:focus-when-clear="false"
:maxlength="20"
:placeholder="t('common.placeholder.pleaseEnter')"
clearable
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
/>
</view>
</view>
<!-- 发放数量 -->
<view class="mt-44rpx">
<view class="field-label">{{ t('pages-user.coupons.totalQuantity') }}:</view>
<view class="field">
<wd-input
v-model="formData.totalQuantity"
:focus-when-clear="false"
:placeholder="t('common.placeholder.pleaseEnter')"
clearable
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
type="number"
/>
</view>
</view>
<!-- 类型切换 -->
<view class="mt-44rpx flex-center-sb">
<view class="field-label">{{ t('pages-user.coupons.type') }}:</view>
<view class="flex items-center gap-40rpx">
<view
v-for="item in couponTypes"
:key="item.value"
class="flex items-center gap-16rpx"
@click="toggleType(item.value)"
>
<view v-if="formData.couponType !== item.value" class="i-carbon:checkbox"></view>
<view v-else class="i-carbon:checkbox-checked-filled"></view>
<text class="text-30rpx lh-30rpx text-#333">{{ item.label }}</text>
</view>
</view>
</view>
<!-- 折扣券折扣比例 -->
<view v-if="formData.couponType === 1" class="mt-44rpx">
<view class="field-label">{{ t('pages-user.coupons.discountRatio') }}:</view>
<view class="field">
<wd-input
v-model="formData.discountValue"
:focus-when-clear="false"
:max="100"
:min="1"
:placeholder="t('common.placeholder.pleaseEnter')"
clearable
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
type="number"
/>
</view>
</view>
<!-- 满减券门槛与减免 -->
<template v-else>
<view class="mt-44rpx row-field">
<view class="field-label !mb-0 shrink-0 mr-30rpx">{{ t('pages-user.coupons.spendOver') }}</view>
<view class="field field-narrow">
<wd-input
v-model="formData.minAmount"
:focus-when-clear="false"
:placeholder="t('common.placeholder.pleaseEnter')"
clearable
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
type="digit"
/>
</view>
</view>
<view class="mt-32rpx row-field">
<view class="field-label !mb-0 shrink-0">{{ t('pages-user.coupons.andReceiveDiscount') }}</view>
<view class="field field-narrow">
<wd-input
v-model="formData.discountValue"
:focus-when-clear="false"
:placeholder="t('common.placeholder.pleaseEnter')"
clearable
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
type="digit"
/>
</view>
</view>
</template>
<!-- 有效天数 -->
<view class="mt-44rpx">
<view class="field-label">{{ t('pages-user.coupons.validityDays') }}:</view>
<view class="field flex-center-sb">
<wd-input
v-model="formData.validDays"
:focus-when-clear="false"
:placeholder="t('common.placeholder.pleaseEnter')"
clearable
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
type="number"
/>
<text class="text-30rpx lh-30rpx text-#333 shrink-0 ml-12rpx">{{ t('pages-user.coupons.days') }}</text>
</view>
</view>
<!-- 协议 -->
<view class="px-10rpx flex items-center mt-80rpx" @click="isAgree = !isAgree">
<image
:src="isAgree ? '/static/images/chef/152.png' : '/static/images/chef/134.png'"
class="w-28rpx h-28rpx shrink-0 mr-10rpx"
mode="aspectFit"
/>
<view class="text-28rpx lh-28rpx text-#333">
{{ t('pages-user.coupons.agreeText') }}
<text
class="text-#00A76D"
@click.stop="uni.navigateTo({url: '/pages/agreement/index?code=' + Agreement.CHEF_M_CODE_AGREEMENT})"
>
{{ t('pages-user.coupons.couponAgreement') }}
</text>
</view>
</view>
<view class="h-120rpx"/>
</view>
<fixed-bottom-large-btn
:loading="submitting"
:text="t('pages-user.coupons.publishNow')"
class="z-100"
fixed
@click="handleSubmit"
/>
</view>
</template>
<style>
page {
background-color: #fff;
}
</style>
<style lang="scss" scoped>
.field-label {
font-size: 36rpx;
line-height: 36rpx;
color: #333;
font-weight: 500;
margin-bottom: 20rpx;
}
.field {
height: 98rpx;
padding: 0 24rpx;
border-radius: 16rpx;
background: #F6F6F6;
display: flex;
align-items: center;
}
.row-field {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.field-narrow {
width: 480rpx;
}
</style>
+237
View File
@@ -0,0 +1,237 @@
<script lang="ts" setup>
import dayjs from "dayjs";
import {appCouponCopyCouponCodePost, appCouponMerchantCouponListPost} from "@/service";
import usePage from "@/hooks/usePage";
import {setClipboardData} from "@/utils/utils";
import {useUserStore} from "@/store";
import {useMessage} from "wot-design-uni";
import {deleteCouponApi} from "@/pages-user/service";
const message = useMessage();
const userStore = useUserStore();
const {t} = useI18n()
function appShopSettlementList(pageNum: number, pageSize: number) {
return new Promise(resolve => {
appCouponMerchantCouponListPost({
params: {
pageNum,
pageSize,
}
}).then(res => {
resolve(res)
})
})
}
const {paging, loading, firstLoaded, dataList, queryList} = usePage(appShopSettlementList)
onShow(() => {
nextTick(() => {
paging.value?.refresh()
})
})
function navigateTo(url: string) {
uni.navigateTo({url})
}
//
function appCouponCopyCouponCode(item: any) {
if (item.distributionType === 2) {
appCouponCopyCouponCodePost({
body: {
id: item.id,
}
}).then((res: any) => {
console.log('生成的', res)
if (res.data && res.data.length > 0) {
setClipboardData(res.data.join(','))
}
})
}
}
//
function appCouponDeleteCoupon(item: any) {
message
.confirm({
title: t("common.prompt.system-prompt"),
msg: `${t("common.prompt.system-prompt-delete")}`,
confirmButtonText: t("common.yes"),
cancelButtonText: t("common.no"),
cancelButtonProps: {
customClass:
"!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !border-#666666 !rounded-20rpx",
},
confirmButtonProps: {
customClass:
"!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !bg-primary !rounded-20rpx",
},
})
.then(async () => {
deleteCouponApi(item.id).then(() => {
uni.showToast({
title: t('toast.deleteSuccess'),
icon: 'none'
})
paging.value?.refresh()
})
})
.catch(() => {
});
}
//
const isMerchant = computed(() => {
return userStore.currentMerchantToken.length > 0
})
const show = ref(false)
const pickerValue = ref(0)
const columns = [
{
label: t('common.redeemCode'),
value: 0
},
{
label: t('common.coupon'),
value: 1
}
]
function handleClose() {
show.value = false
}
function handleSubmit() {
show.value = false
pickerValue.value ? navigateTo('/pages-user/pages/coupons/add-new-coupons') : navigateTo('/pages-user/pages/coupons/add-coupons')
}
function openCoupons() {
show.value = true
}
</script>
<template>
<view>
<z-paging ref="paging" v-model="dataList" :auto="false" @query="queryList">
<template #top>
<navbar :title="t('pages-user.coupons.title')"></navbar>
</template>
<view class="p-30rpx">
<template v-for="item in dataList">
<wd-swipe-action>
<view class="w-full h-178rpx relative mb-30rpx last:mb-0">
<image
class="absolute top-0 left-0 w-full h-full"
src="@img/chef/198.png"
></image>
<view class="absolute top-0 left-0 w-full h-full relative z-2 px-40rpx py-42rpx">
<view class="flex-center-sb gap-20rpx">
<text class="text-36rpx lh-36rpx text-#333 font-500 line-clamp-1">
{{ item.name || item.nameZh }}
</text>
<view class="shrink-0 ml-30rpx">
<template v-if="item.usedQuantity !== item.totalQuantity">
<text class="text-28rpx lh-28rpx text-#CE7138 font-500">{{ t('common.distributing') }}</text>
</template>
<template v-else>
<text class="text-28rpx lh-28rpx text-#00A76D font-500">{{ t('common.ended') }}</text>
</template>
</view>
</view>
<view class="flex-center-sb text-28rpx lh-28rpx mt-30rpx">
<text v-if="item.distributionType === 1" class="text-#999">
{{ dayjs(Number(item.createTime)).format('YYYY.MM.DD') }}-
{{ dayjs(Number(item.createTime)).add(Number(item.validDays), 'day').format('YYYY.MM.DD') }}
</text>
<text v-if="item.distributionType === 2" class="text-#999">
{{ dayjs(Number(item.validStart)).format('YYYY.MM.DD') }}-
{{ dayjs(Number(item.validEnd)).format('YYYY.MM.DD') }}
</text>
<text class="text-#333 font-500">{{ item.usedQuantity }}/{{ item.totalQuantity }}</text>
</view>
</view>
</view>
<template #right>
<view class="action flex items-center text-30rpx lh-30rpx text-#fff">
<view class="w-180rpx h-178rpx center text-#333"
@click="appCouponCopyCouponCode(item)">
{{ t('pages-user.coupons.copyCode') }}
</view>
<view class="w-80rpx h-178rpx center"
@click="appCouponDeleteCoupon(item)">
<view class="i-carbon-trash-can text-36rpx text-#f00"></view>
</view>
</view>
</template>
</wd-swipe-action>
</template>
</view>
</z-paging>
<view v-if="isMerchant" class="w-118rpx h-118rpx rounded-50% fixed bottom-104rpx right-30rpx"
@click="openCoupons">
<image
class="absolute top-0 left-0 w-118rpx h-118rpx"
src="@img/chef/195.png"
></image>
<view class="absolute top-0 left-0 w-118rpx h-118rpx center flex-col relative z-2">
<wd-icon color="#fff" name="add" size="18px"></wd-icon>
<text class="text-26rpx text-#fff mt-10rpx">{{ t('pages-user.recipe.index.add') }}</text>
</view>
</view>
</view>
<wd-popup
v-model="show"
position="bottom"
@close="handleClose"
>
<view>
<view class="flex-center-sb h-102rpx bg-#F7F7F7 px-30rpx">
<view class="text-30rpx text-#999" @click="handleClose">{{ t('common.cancel') }}</view>
<view class="text-34rpx text-#333">{{ t('common.select') }} {{ t('common.stores') }}</view>
<view class="text-30rpx text-#FF6106" @click="handleSubmit">{{ t('common.confirm') }}</view>
</view>
<view class="bg-#fff px-54rpx py-56rpx">
<wd-picker-view v-if="show" v-model="pickerValue" :columns="columns"/>
</view>
</view>
</wd-popup>
</template>
<style>
page {
background-color: #F6F6F6;
}
</style>
<style lang="scss" scoped>
:deep(.uni-picker-view-wrapper) {
& > uni-picker-view-column:first-of-type .uni-picker-view-group {
.uni-picker-view-indicator {
border-radius: 20rpx !important;
&:after {
border: none;
}
&:before {
border: none;
}
}
}
}
:deep(.wd-picker-view-column__item) {
line-height: 94rpx !important;
}
:deep(.uni-picker-view-indicator) {
height: 94rpx !important;
}
</style>
+636
View File
@@ -0,0 +1,636 @@
<script lang="ts" setup>
import {ref} from 'vue'
import {z} from "zod";
import ChooseImage from "@/components/choose-image/choose-image.vue";
import {
appMerchantDishAddDishPost,
appMerchantDishDetailGet,
appMerchantDishEditDishPost,
appMerchantDishRemovePost,
appMerchantMenuMenuListByMerchantPost
} from "@/service";
import {useMessage} from "wot-design-uni";
const message = useMessage();
const {t} = useI18n()
//
const formData = ref({
id: '',
dishName: '',
dishDescription: '',
menu: '',
menuId: '',
originalPrice: '', //
discountPrice: '', //
weight: '', // ()
tax: '', //
dishImage: [] as string[],
merchantSideDishVoList: [] as any[], //
isActive: true, //
stock: '', //
})
// Zod schema
const FormSchema = z.object({
dishName: z.string().min(1, t('pages-user.food.add-food.schema.dishName')),
dishDescription: z.string().min(1, t('pages-user.food.add-food.schema.dishDescription')),
menuId: z.string().min(1, t('pages-user.food.add-food.schema.menuId')),
originalPrice: z.string().min(1, t('pages-user.food.add-food.schema.originalPrice')),
discountPrice: z.string()
.min(1, t('pages-user.food.add-food.schema.discountPriceRequired'))
.refine(
(val) => {
const num = parseFloat(val.trim())
return !isNaN(num) && num > 0
},
{message: t('pages-user.food.add-food.schema.discountPricePositive') ?? '折扣价必须大于0'},
),
// dishImage: z.array(z.string()).min(1, t('pages-user.food.add-food.schema.dishImage')),
stock: z.string()
.min(1, t('pages-user.food.add-food.stockRequired'))
.refine(
(val) => {
//
const trimmed = val.trim()
if (trimmed === '') return false
//
const num = Number(trimmed)
return Number.isInteger(num)
},
{message: t('pages-user.food.add-food.stockMustBeInteger')}
)
.refine(
(val) => {
const num = Number(val.trim())
return num >= 0
},
{message: t('pages-user.food.add-food.stockMustBeNonNegative')}
)
}).refine((data) => {
//
if (data.discountPrice && data.discountPrice.trim() !== '') {
const originalPrice = parseFloat(data.originalPrice)
const discountPrice = parseFloat(data.discountPrice)
//
if (!isNaN(originalPrice) && !isNaN(discountPrice)) {
return discountPrice <= originalPrice
}
}
return true
}, {
message: t('pages-user.food.add-food.discountPriceError'),
path: ['discountPrice']
})
//
const chooseImageRef = ref()
const handleChooseImage = () => {
chooseImageRef.value.init()
}
function onImageChange(files: string[]) {
formData.value.dishImage.push(files[0])
}
//
const removeImage = (index: number) => {
formData.value.dishImage.splice(index, 1)
}
//
const merchantSideDishVoList = ref([])
function navigateTo(url: string) {
uni.navigateTo({
url,
events: {
acceptDataFromOpenedPage: function (data) {
console.log(data)
if (data) {
merchantSideDishVoList.value = data
}
},
},
success: function (res) {
// eventChannel
res.eventChannel.emit('acceptDataFromOpenerPage', {data: merchantSideDishVoList.value.merchantSideDishBoList || []})
}
})
}
onLoad((options) => {
if (options && options.id) {
formData.value.id = options.id
getDishDetail()
}
//
getMenuListByMerchant()
})
const menuList = ref([])
function getMenuListByMerchant() {
appMerchantMenuMenuListByMerchantPost({
params: {
pageSize: 1000,
pageNum: 1
}
}).then(res => {
menuList.value = res.rows
selectedMenu.value = menuList.value[0].id
if (res.rows.length === 0) {
uni.showToast({
title: t('pages-user.food.add-food.tips'),
icon: 'none'
})
} else {
// menu
formData.value.menu = menuList.value.find(item => item.id === formData.value.menuId)?.menuName
}
})
}
//
function getDishDetail() {
if (!formData.value.id) {
return
}
appMerchantDishDetailGet({
params: {
dishId: formData.value.id
}
}).then(res => {
console.log('获取菜品详情', res)
const transformedData = res.data.merchantSideDishVoList.map(item => {
//
if ('merchantSideDishItemVoList' in item) {
const {merchantSideDishItemVoList, ...rest} = item;
return {
...rest,
merchantSideDishItemBoList: merchantSideDishItemVoList
};
}
return {...item};
});
console.log('获取菜品详情', transformedData)
merchantSideDishVoList.value.merchantSideDishBoList = transformedData
//
let stockValue = ''
if (res.data.stock !== null && res.data.stock !== undefined) {
const stock = Number(res.data.stock)
//
if (Number.isInteger(stock) && stock >= 0) {
stockValue = String(stock)
}
}
formData.value = {
...res.data,
dishImage: res.data.dishImage ? res.data.dishImage.split(',') : [],
merchantSideDishBoList: transformedData,
isActive: +res.data.delFlag === 1,
stock: stockValue
}
console.log('获取菜品详情', formData.value)
})
}
const show = ref(false)
const selectedMenu = ref('')
function chooseMenu() {
if (menuList.value.length === 0) {
uni.showToast({
title: t('pages-user.food.add-food.tips'),
icon: 'none'
})
return
}
show.value = true
}
function handleClose() {
show.value = false
}
//
function deleteDish() {
message
.confirm({
title: t("common.prompt.system-prompt"),
msg: `${t("common.prompt.system-prompt-delete")}`,
confirmButtonText: t("common.yes"),
cancelButtonText: t("common.no"),
cancelButtonProps: {
customClass:
"!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !border-#666666 !rounded-20rpx",
},
confirmButtonProps: {
customClass:
"!h-88rpx !w-258rpx !min-w-auto !text-30rpx !lh-42rpx !font-bold !bg-primary !rounded-20rpx",
},
})
.then(async () => {
appMerchantDishRemovePost({
body: [formData.value.id]
}).then((res) => {
console.log('删除菜品', res)
uni.showToast({
title: t('toast.deleteSuccess'),
icon: 'none'
})
uni.navigateBack()
})
})
.catch(() => {
});
}
function handleSubmit() {
let data = menuList.value.find(item => item.id === selectedMenu.value)
if (data) {
formData.value.menu = data.menuName
formData.value.menuId = data.id
}
handleClose()
}
function saveDish() {
// Zod
const validationResult = FormSchema.safeParse(formData.value)
if (!validationResult.success) {
const firstError = validationResult.error.errors[0]
uni.showToast({
title: firstError.message,
icon: 'none'
})
return
}
//
const stockValue = formData.value.stock ? Number(formData.value.stock.trim()) : 0
if (formData.value.id) {
appMerchantDishEditDishPost({
body: {
...formData.value,
tax: Number(formData.value.tax) / 100 || 0,
stock: stockValue,
merchantSideDishBoList: merchantSideDishVoList.value.merchantSideDishBoList,
dishImage: formData.value.dishImage.join(','),
delFlag: formData.value.isActive ? 1 : 3
}
}).then(() => {
uni.showToast({
title: t('toast.submitSuccess'),
icon: 'none'
})
uni.navigateBack()
})
} else {
appMerchantDishAddDishPost({
body: {
...formData.value,
tax: Number(formData.value.tax) / 100 || 0,
stock: stockValue,
merchantSideDishBoList: merchantSideDishVoList.value.merchantSideDishBoList,
dishImage: formData.value.dishImage.join(','),
delFlag: formData.value.isActive ? 1 : 3
}
}).then(() => {
uni.showToast({
title: t('toast.submitSuccess'),
icon: 'none'
})
uni.navigateBack()
})
}
}
</script>
<template>
<view class="add-food-page">
<!-- 状态栏 -->
<navbar/>
<!-- 页面内容 -->
<view class="px-30rpx">
<!-- 标题 -->
<view class="mb-80rpx flex-center-sb mt-20rpx">
<text class="text-46rpx lh-46rpx text-#333 font-500">{{ t('pages-user.food.add-food.title') }}</text>
<view>
<template v-if="formData.id">
<view class="bg-#F2F2F2 rounded-32rpx w-134rpx h-64rpx center font-500 text-26rpx text-#333"
@click="deleteDish">
{{ t('common.delete') }}
</view>
</template>
</view>
</view>
<!-- 菜品名称 -->
<view class="form-item mb-40rpx">
<text class="label text-36rpx lh-36rpx text-#333 mb-20rpx block font-500">{{
t('pages-user.food.add-food.name')
}}
</text>
<view class="bg-#F6F6F6 rounded-16rpx h-98rpx px-24rpx flex items-center">
<wd-input
v-model="formData.dishName"
:focus-when-clear="false"
:placeholder="t('pages-user.food.add-food.enterPlaceholder')"
clearable
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
use-prefix-slot
>
</wd-input>
</view>
</view>
<!-- 菜品描述 -->
<view class="form-item mb-52rpx">
<view class="flex-center-sb text-36rpx lh-36rpx text-#333 font-500 mb-24rpx">
<text class="">{{ t('pages-user.food.add-food.description') }}</text>
<text class="">{{ formData.dishDescription.length }}/4000</text>
</view>
<text class="text-28rpx lh-28rpx text-#999 mb-36rpx block">{{
t('pages-user.food.add-food.descriptionTip')
}}
</text>
<view class="bg-#F6F6F6 rounded-16rpx min-h-352rpx px-24rpx">
<wd-textarea
v-model="formData.dishDescription"
:maxlength="4000"
:placeholder="t('pages-user.food.add-food.enterPlaceholder')"
auto-height
custom-class="!bg-#F6F6F6"
custom-textarea-container-class="!bg-#F6F6F6"
no-border
/>
</view>
</view>
<!-- 图片上传 -->
<view class="form-item mb-52rpx">
<view class="flex items-center mb-20rpx">
<text class="text-36rpx lh-36rpx text-#333 mr-10rpx font-500">{{
t('pages-user.food.add-food.pictures')
}}
</text>
<text class="text-28rpx text-#999">({{ formData.dishImage.length }}/9)</text>
</view>
<view class="grid grid-cols-3 gap-20rpx">
<template v-for="(item, index) in formData.dishImage">
<view class="relative w-210rpx h-210rpx">
<image
v-if="item"
class="absolute top-0rpx right-0rpx z-1 w-36rpx h-36rpx"
src="@img/chef/190.png"
@click="removeImage(index)"
></image>
<image
:src="item"
class="w-210rpx h-210rpx rounded-16rpx"
mode="aspectFill"
></image>
</view>
</template>
<view v-if="formData.dishImage.length < 9" class="w-210rpx h-210rpx center flex-col bg-#F6F6F6 rounded-20rpx"
@click="handleChooseImage">
<wd-icon color="#3D3D3D" name="add" size="32px"></wd-icon>
<text class="text-30rpx lh-30rpx text-#333 mt-20rpx">{{ t('pages-user.food.add-food.addPicture') }}</text>
</view>
</view>
</view>
<!-- 菜单选择 -->
<view class="form-item mb-52rpx" @click="chooseMenu">
<text class="text-36rpx lh-36rpx text-#333 mb-20rpx block font-500">{{
t('pages-user.food.add-food.menu')
}}
</text>
<view
class=" bg-#F6F6F6 rounded-32rpx h-98rpx px-24rpx flex-center-sb"
>
<view class="flex items-center">
<image class="w-28rpx h-28rpx shrink-0 mr-16rpx" src="@img/chef/106.png"></image>
<text :class="formData.menu ? 'text-#333' : 'text-#999'" class="text-30rpx">
{{ formData.menu || t('pages-user.food.add-food.addMenu') }}
</text>
</view>
<image class="w-32rpx h-32rpx shrink-0" src="@img/chef/115.png"></image>
</view>
</view>
<!-- 价格输入 -->
<view class="form-item mb-52rpx">
<text class="label text-36rpx lh-36rpx text-#333 mb-20rpx block font-500">{{
t('pages-user.food.add-food.price')
}}
</text>
<view class="bg-#F6F6F6 rounded-16rpx h-98rpx px-24rpx flex items-center">
<text class="text-30rpx text-#333 mr-20rpx">$</text>
<wd-input
v-model="formData.originalPrice"
:focus-when-clear="false"
clearable
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholder="0.00"
placeholderStyle="font-size: 30rpx;color: #999;"
use-prefix-slot
>
</wd-input>
</view>
</view>
<!-- 折扣价输入 -->
<view class="form-item mb-52rpx">
<text class="label text-36rpx lh-36rpx text-#333 mb-20rpx block font-500">
{{ t('pages-user.food.add-food.discountedPrice') }}
</text>
<view class="bg-#F6F6F6 rounded-16rpx h-98rpx px-24rpx flex items-center">
<text class="text-30rpx text-#333 mr-20rpx">$</text>
<wd-input
v-model="formData.discountPrice"
:focus-when-clear="false"
clearable
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholder="0.00"
placeholderStyle="font-size: 30rpx;color: #999;"
use-prefix-slot
>
</wd-input>
</view>
</view>
<!-- 重量: -->
<view class="form-item mb-52rpx">
<text class="label text-36rpx lh-36rpx text-#333 mb-20rpx block font-500">
{{ t('pages-user.food.add-food.weight') }}
</text>
<view class="bg-#F6F6F6 rounded-16rpx h-98rpx px-24rpx flex-center-sb">
<wd-input
v-model="formData.weight"
:focus-when-clear="false"
clearable
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholder="0.00"
placeholderStyle="font-size: 30rpx;color: #999;"
use-prefix-slot
>
</wd-input>
<text class="text-30rpx text-#333 ml-20rpx">{{ t('pages-user.food.add-food.pound') }}</text>
</view>
</view>
<!-- 税费: -->
<view class="form-item mb-52rpx">
<text class="label text-36rpx lh-36rpx text-#333 mb-20rpx block font-500">
{{ t('pages-user.food.add-food.taxRate') }}
</text>
<view class="bg-#F6F6F6 rounded-16rpx h-98rpx px-24rpx flex-center-sb">
<wd-input
v-model="formData.tax"
:focus-when-clear="false"
clearable
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholder="0.00"
placeholderStyle="font-size: 30rpx;color: #999;"
use-prefix-slot
>
</wd-input>
<text class="text-30rpx text-#333 ml-20rpx">%</text>
</view>
</view>
<!-- 库存数量 -->
<view class="form-item mb-52rpx">
<text class="label text-36rpx lh-36rpx text-#333 mb-20rpx block font-500">
{{ t('pages-user.food.add-food.stock') }}
</text>
<view class="bg-#F6F6F6 rounded-16rpx h-98rpx px-24rpx flex items-center">
<wd-input
v-model="formData.stock"
:focus-when-clear="false"
:maxlength="5"
:placeholder="t('pages-user.food.add-food.stockPlaceholder')"
clearable
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
type="number"
use-prefix-slot
>
</wd-input>
</view>
</view>
<!-- Item is active -->
<view class="form-item mb-52rpx">
<view class="flex-center-sb">
<text class="text-36rpx lh-36rpx text-#333 mb-20rpx block font-500">
{{
formData.isActive ? t('pages-user.food.add-food.itemIsActive') : t('pages-user.food.add-food.itemIsNot')
}}:
</text>
<wd-switch v-model="formData.isActive" active-color="#00A76D" size="20px"/>
</view>
<view class="text-28rpx lh-28rpx text-#999 mt-10rpx">
{{ t('pages-user.food.add-food.itemActiveDesc') }}
</view>
</view>
<view class="form-item mb-52rpx">
<view class="flex-center-sb">
<text class="text-36rpx lh-36rpx text-#333 mb-20rpx block font-500">
{{ t('pages-user.food.add-food.frequentlyBoughtTogether') }}
</text>
<view class="bg-#F2F2F2 rounded-32rpx w-158rpx h-64rpx center font-500 text-26rpx text-#333"
@click="navigateTo('/pages-user/pages/food/add-item')">
<image class="w-28rpx h-28rpx shrink-0 mr-10rpx" src="@img/chef/107.png"></image>
{{ t('pages-user.food.add-food.add') }}
</view>
</view>
</view>
</view>
<!-- 底部按钮 -->
<fixed-bottom-large-btn
:text="t('common.save')"
class="z-100"
fixed
@click="saveDish"
/>
<ChooseImage ref="chooseImageRef" @change="onImageChange"/>
<wd-popup
v-model="show"
position="bottom"
@close="handleClose"
>
<view>
<view class="flex-center-sb h-102rpx bg-#F7F7F7 px-30rpx">
<view class="text-30rpx text-#999">{{ t('common.cancel') }}</view>
<view class="text-34rpx text-#333"></view>
<view class="text-30rpx text-#FF6106" @click="handleSubmit">{{ t('common.confirm') }}</view>
</view>
<view class="bg-#fff px-54rpx py-56rpx">
<wd-picker-view v-model="selectedMenu" :columns="menuList" label-key="menuName" value-key="id"/>
</view>
</view>
</wd-popup>
</view>
</template>
<style>
page {
background-color: #fff;
}
</style>
<style lang="scss" scoped>
:deep(.uni-picker-view-wrapper) {
& > uni-picker-view-column:first-of-type .uni-picker-view-group {
.uni-picker-view-indicator {
border-radius: 20rpx 0 0 20rpx !important;
&:after {
border: none;
}
&:before {
border: none;
}
}
}
}
:deep(.wd-picker-view-column__item) {
line-height: 94rpx !important;
}
:deep(.uni-picker-view-indicator) {
height: 94rpx !important;
}
:deep(.wd-input__clear) {
background-color: transparent !important;
}
</style>
+301
View File
@@ -0,0 +1,301 @@
<script lang="ts" setup>
const {t} = useI18n()
//
interface SideDishValue {
name: string
price: string
}
//
interface SideDishGroup {
sideDishName: string
isRequired: 1 | 2, // 1 2
merchantSideDishItemBoList: SideDishValue[]
}
//
const formData = reactive({
merchantSideDishBoList: [] as SideDishGroup[]
})
//
const addSideDishGroup = () => {
const newGroup: SideDishGroup = {
sideDishName: '',
isRequired: 1,
merchantSideDishItemBoList: [
{
name: '',
price: ''
}
]
}
formData.merchantSideDishBoList.push(newGroup)
}
//
const removeSideDishGroup = (groupIndex: number) => {
formData.merchantSideDishBoList.splice(groupIndex, 1)
}
//
const addSideDishValue = (groupIndex: number) => {
const newValue: SideDishValue = {
name: '',
price: ''
}
formData.merchantSideDishBoList[groupIndex].merchantSideDishItemBoList.push(newValue)
}
//
const removeSideDishValue = (groupIndex: number, valueIndex: number) => {
if (formData.merchantSideDishBoList[groupIndex].merchantSideDishItemBoList.length > 1) {
formData.merchantSideDishBoList[groupIndex].merchantSideDishItemBoList.splice(valueIndex, 1)
}
}
//
const submitForm = () => {
//
for (let i = 0; i < formData.merchantSideDishBoList.length; i++) {
const group = formData.merchantSideDishBoList[i]
if (!group.sideDishName.trim()) {
uni.showToast({
title: t('pages-user.food.add-item.validation.enterSideDishName', {index: i + 1}),
icon: 'none'
})
return
}
for (let j = 0; j < group.merchantSideDishItemBoList.length; j++) {
const value = group.merchantSideDishItemBoList[j]
if (!value.name.trim() || !value.price.trim()) {
uni.showToast({
title: t('pages-user.food.add-item.validation.completeAllFields', {index: i + 1}),
icon: 'none'
})
return
}
}
}
if (formData.merchantSideDishBoList.length === 0) {
uni.showToast({
title: t('pages-user.food.add-item.validation.addAtLeastOne'),
icon: 'none'
})
return
}
// API
console.log('Submit form:', formData)
uni.showToast({
title: t('pages-user.food.add-item.validation.submittedSuccessfully'),
icon: 'none'
})
eventChannel.emit('acceptDataFromOpenedPage', formData);
uni.navigateBack()
}
let instance = null
let eventChannel = null
onMounted(() => {
instance = getCurrentInstance().proxy
eventChannel = instance.getOpenerEventChannel();
eventChannel.on('acceptDataFromOpenerPage', function (data) {
console.log('acceptDataFromOpenerPage', data.data)
if (data.data && data.data.length > 0) {
formData.merchantSideDishBoList = data.data
}
})
})
onUnload(() => {
instance = null
eventChannel = null
})
//
addSideDishGroup()
function handleClickItem(group: SideDishGroup) {
group.isRequired = group.isRequired === 1 ? 2 : 1
}
</script>
<template>
<view class="add-item-page bg-#F6F6F6 min-h-100vh">
<!-- 状态栏 -->
<navbar :title="t('pages-user.food.add-item.title')"/>
<!-- 页面内容 -->
<view class="content px-30rpx pt-40rpx pb-200rpx">
<!-- 配菜组列表 -->
<view
v-for="(group, groupIndex) in formData.merchantSideDishBoList"
:key="groupIndex"
class="side-dish-group mb-40rpx"
>
<!-- 配菜组容器 -->
<view class="flex-center-sb mb-26rpx">
<!-- 配菜组标题 -->
<text class="text-28rpx text-#999 block">{{ t('pages-user.food.add-item.sideDish') }} {{
groupIndex + 1
}}:
</text>
<!-- 删除配菜组按钮 -->
<view
class="w-48rpx h-48rpx"
@tap="removeSideDishGroup(groupIndex)"
>
<image class="w-48rpx h-48rpx" src="@img/chef/190.png"></image>
</view>
</view>
<view class="bg-white rounded-20rpx px-20rpx py-30rpx relative">
<!-- 配菜类型输入 -->
<view class="mb-20rpx">
<text class="text-36rpx text-#333 font-500 mb-20rpx block">{{
t('pages-user.food.add-item.typeOfSideDishes')
}}
</text>
<view class="bg-#F6F6F6 rounded-16rpx h-98rpx px-24rpx flex items-center">
<wd-input
v-model="group.sideDishName"
:focus-when-clear="false"
:placeholder="t('pages-user.food.add-item.enterPlaceholder')"
clearable
confirm-type="search"
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
use-prefix-slot
>
</wd-input>
</view>
</view>
<!-- 配菜组必选状态 -->
<view class="mb-44rpx flex-center-sb">
<text class="text-36rpx text-#333 font-500 mb-20rpx block">
{{ t('pages-user.food.add-item.isRequired') }}
</text>
<view class="shrink-0" @click="handleClickItem(group)">
<!-- 单选按钮 -->
<image
:src="
group.isRequired === 1
? '/static/images/chef/133.png'
: '/static/images/chef/134.png'
"
class="w-48rpx h-48rpx shrink-0"
mode="aspectFit"
/>
</view>
</view>
<!-- 配菜值标题 -->
<text class="text-36rpx text-#333 font-500 mb-30rpx block">
{{ t('pages-user.food.add-item.sideDishesFlavorValue') }}
</text>
<!-- 配菜值列表 -->
<view class="values-list">
<view
v-for="(value, valueIndex) in group.merchantSideDishItemBoList"
:key="valueIndex"
class="mb-10rpx last:mb-0"
>
<!-- 配菜值容器 -->
<view class="bg-white py-28rpx border-2rpx border-solid border-#DFDFDF rounded-16rpx px-20rpx">
<view class="flex-center-sb mb-26rpx">
<!-- 配菜组标题 -->
<text class="text-26rpx text-#333 font-500 block">{{ t('pages-user.food.add-item.value') }}
{{ valueIndex + 1 }}:
</text>
<!-- 删除配菜组按钮 -->
<view
class="w-48rpx h-48rpx"
@click="removeSideDishValue(groupIndex, valueIndex)"
>
<image class="w-48rpx h-48rpx" src="@img/chef/190.png"></image>
</view>
</view>
<!-- 名称输入 -->
<view class="form-row flex items-center mb-24rpx">
<text class="label text-28rpx text-#333 w-120rpx">{{ t('pages-user.food.add-item.name') }}</text>
<view class="bg-#F6F6F6 rounded-16rpx h-88rpx px-20rpx flex-1 flex items-center">
<wd-input
v-model="value.name"
:focus-when-clear="false"
:placeholder="t('pages-user.food.add-item.enterPlaceholder')"
clearable
confirm-type="search"
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
use-prefix-slot
>
</wd-input>
</view>
</view>
<!-- 价格输入 -->
<view class="form-row flex items-center">
<text class="label text-28rpx text-#333 w-120rpx">{{ t('pages-user.food.add-item.price') }}</text>
<view class="bg-#F6F6F6 rounded-16rpx h-88rpx px-20rpx flex-1 flex items-center">
<wd-input
v-model="value.price"
:focus-when-clear="false"
:placeholder="t('pages-user.food.add-item.enterPlaceholder')"
clearable
confirm-type="search"
custom-class="!text-30rpx !bg-transparent flex-1"
no-border
placeholderStyle="font-size: 30rpx;color: #999;"
use-prefix-slot
>
</wd-input>
</view>
</view>
</view>
</view>
</view>
<!-- 添加配菜值按钮 -->
<view
class="bg-#F2F2F2 rounded-20rpx h-88rpx center mt-24rpx"
@click="addSideDishValue(groupIndex)"
>
<text class="text-30rpx text-#333 font-500">+ {{ t('pages-user.food.add-item.add') }}</text>
</view>
</view>
</view>
<!-- 添加配菜组按钮 -->
<view
class="bg-#F6F6F6 border-2rpx border-solid border-#333 rounded-16rpx h-88rpx flex items-center justify-center mb-40rpx"
@click="addSideDishGroup"
>
<text class="text-30rpx text-#333 font-500">{{ t('pages-user.food.add-item.addSideDishes') }}</text>
</view>
</view>
<!-- 底部提交按钮 -->
<fixed-bottom-large-btn
:text="t('common.submit')"
class="z-100"
fixed
@click="submitForm"
/>
</view>
</template>
<style>
page {
background-color: #F6F6F6;
}
</style>
+106
View File
@@ -0,0 +1,106 @@
<script lang="ts" setup>
import {appMerchantOrderOrderListPost} from "@/service";
import dayjs from "dayjs";
const {t} = useI18n()
const timeData = ref({
createBeginTime: dayjs().add(1, 'day').startOf('day').valueOf(),
createEndTime: dayjs().add(1, 'day').endOf('day').valueOf(),
})
const {paging, loading, dataList, queryList, firstLoaded} = usePage((pageNum, pageSize) => {
return appMerchantOrderOrderListPost({
params: {
pageNum,
pageSize,
},
body: {
orderStatusList: [],
...timeData.value
}
})
})
// ,
const recentDays = computed(() => {
const days = []
for (let i = 1; i < 6; i++) {
const date = new Date()
date.setDate(date.getDate() + i)
// /
const month = date.getMonth() + 1
const day = date.getDate()
days.push(`${month}/${day}`)
}
return days
})
//
const selectedDate = ref(0)
// timeData
watch(selectedDate, (newIndex) => {
const targetDate = dayjs().add(newIndex + 1, 'day')
timeData.value = {
createBeginTime: targetDate.startOf('day').valueOf(),
createEndTime: targetDate.endOf('day').valueOf(),
}
//
paging.value?.refresh()
})
</script>
<template>
<z-paging ref="paging" v-model="dataList" @query="queryList">
<template #top>
<navbar :title="t('navbar-future-order')"></navbar>
<wd-tabs v-model="selectedDate" auto-line-width color="#14181B" inactiveColor="#666666" sticky>
<template v-for="(item, index) in recentDays" :key="item">
<wd-tab :title="item"></wd-tab>
</template>
</wd-tabs>
</template>
<view
v-show="loading"
class="animate-in fade-in animate-ease-out animate-duration-300"
>
<view class="bg-#F6F6F6 px-20rpx py-28rpx rounded-16rpx">
<template v-for="item in 5" :key="item">
<view class="w-full p-20rpx rounded-16rpx bg-white mb-20rpx last:mb-0">
<view class="flex items-center justify-between mb-20rpx">
<view class="w-150rpx h-24rpx skeleton-item"></view>
<view class="w-80rpx h-24rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between mb-20rpx">
<view class="w-200rpx h-20rpx skeleton-item"></view>
<view class="w-100rpx h-20rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between mb-20rpx">
<view class="w-180rpx h-20rpx skeleton-item"></view>
<view class="w-120rpx h-20rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between">
<view class="w-160rpx h-20rpx skeleton-item"></view>
<view class="w-120rpx h-56rpx rounded-56rpx skeleton-item"></view>
</view>
</view>
</template>
</view>
</view>
<view
v-show="!loading"
class="animate-in fade-in animate-ease-in animate-duration-300"
>
<view class="px-30rpx py-20rpx">
<template v-for="item in dataList">
<view class="w-full p-20rpx rounded-16rpx bg-white mb-20rpx last:mb-0">
<order-item :item="item"/>
</view>
</template>
</view>
</view>
</z-paging>
</template>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,71 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import {appMerchantOrderOrderListPost} from "@/service";
const {t} = useI18n()
const {paging, dataList, loading, queryList, firstLoaded} = usePage((pageNum, pageSize) => {
return appMerchantOrderOrderListPost({
params: {
pageNum,
pageSize,
},
body: {
orderStatusList: [],
createBeginTime: dayjs().startOf('month').valueOf(),
createEndTime: dayjs().endOf('month').valueOf(),
}
})
})
</script>
<template>
<z-paging ref="paging" v-model="dataList" @query="queryList">
<template #top>
<navbar :title="t('navbar-month-order')"></navbar>
</template>
<view
v-show="loading"
class="animate-in fade-in animate-ease-out animate-duration-300"
>
<view class="bg-#F6F6F6 px-20rpx py-28rpx rounded-16rpx">
<template v-for="item in 6" :key="item">
<view class="w-full p-20rpx rounded-16rpx bg-white mb-20rpx last:mb-0">
<view class="flex items-center justify-between mb-20rpx">
<view class="w-150rpx h-24rpx skeleton-item"></view>
<view class="w-80rpx h-24rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between mb-20rpx">
<view class="w-200rpx h-20rpx skeleton-item"></view>
<view class="w-100rpx h-20rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between mb-20rpx">
<view class="w-180rpx h-20rpx skeleton-item"></view>
<view class="w-120rpx h-20rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between">
<view class="w-160rpx h-20rpx skeleton-item"></view>
<view class="w-120rpx h-56rpx rounded-56rpx skeleton-item"></view>
</view>
</view>
</template>
</view>
</view>
<view
v-show="!loading"
class="animate-in fade-in animate-ease-in animate-duration-300"
>
<view class="px-30rpx py-20rpx">
<template v-for="item in dataList">
<view class="w-full p-20rpx rounded-16rpx bg-white mb-20rpx last:mb-0">
<order-item :item="item"/>
</view>
</template>
</view>
</view>
</z-paging>
</template>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,112 @@
<script lang="ts" setup>
import {appMerchantOrderOrderListPost} from "@/service";
const {t} = useI18n()
const {paging, dataList, loading, queryList, firstLoaded} = usePage((pageNum, pageSize) => {
return appMerchantOrderOrderListPost({
params: {
pageNum,
pageSize,
},
body: {
orderStatusList: [],
refundStatusList: currentTabs.value === 0 ? [currentTabs.value + 1] : [currentTabs.value + 1, 3],
}
})
})
const currentTabs = ref(0)
const tabsList = ref([
{
title: t('pages-user.order.pending'),
value: '1',
},
{
title: t('pages-user.order.processed'),
value: '3',
},
])
function tabsChange(e: any) {
currentTabs.value = e.index
paging.value.refresh()
}
onShow(() => {
nextTick(() => {
paging.value.refresh()
})
})
</script>
<template>
<z-paging ref="paging" v-model="dataList" :auto="false" @query="queryList">
<template #top>
<navbar :title="t('navbar-refund-order')"></navbar>
<wd-tabs v-model="currentTabs" :lineWidth="67" animated color="#14181B" inactiveColor="#666666"
sticky @change="tabsChange">
<template v-for="(item, index) in tabsList" :key="item">
<wd-tab :title="item.title"></wd-tab>
</template>
</wd-tabs>
</template>
<view
v-show="loading"
class="animate-in fade-in animate-ease-out animate-duration-300"
>
<view class="bg-#F6F6F6 px-20rpx py-28rpx rounded-16rpx">
<template v-for="item in 6" :key="item">
<view class="w-full p-20rpx rounded-16rpx bg-white mb-20rpx last:mb-0">
<view class="flex items-center justify-between mb-20rpx">
<view class="w-150rpx h-24rpx skeleton-item"></view>
<view class="w-80rpx h-24rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between mb-20rpx">
<view class="w-200rpx h-20rpx skeleton-item"></view>
<view class="w-100rpx h-20rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between mb-20rpx">
<view class="w-180rpx h-20rpx skeleton-item"></view>
<view class="w-120rpx h-20rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between">
<view class="w-160rpx h-20rpx skeleton-item"></view>
<view class="w-120rpx h-56rpx rounded-56rpx skeleton-item"></view>
</view>
</view>
</template>
</view>
</view>
<view
v-show="!loading"
class="animate-in fade-in animate-ease-in animate-duration-300"
>
<view class="px-30rpx py-20rpx">
<template v-for="item in dataList">
<view class="w-full p-20rpx rounded-16rpx bg-white mb-20rpx last:mb-0">
<order-item :item="item">
<template #rightBtn></template>
</order-item>
</view>
</template>
</view>
</view>
</z-paging>
</template>
<style lang="scss" scoped>
:deep(.wd-tabs__nav-item) {
font-size: 30rpx !important;
&.is-active {
font-size: 34rpx !important;
}
}
:deep(.wd-tabs__line) {
border-radius: 0 !important;
height: 10rpx !important;
bottom: 0 !important;
}
</style>
@@ -0,0 +1,36 @@
<script lang="ts" setup>
import {appMerchantOrderDishListPost} from "@/service";
import dayjs from "dayjs";
const {t} = useI18n()
const {paging, dataList, loading, queryList, firstLoaded} = usePage((pageNum, pageSize) => {
return appMerchantOrderDishListPost({
body: {
pageNum,
pageSize,
createBeginTime: dayjs().startOf('day').valueOf(),
createEndTime: dayjs().endOf('day').valueOf(),
}
})
})
</script>
<template>
<z-paging ref="paging" v-model="dataList" @query="queryList">
<template #top>
<navbar :title="t('navbar-reservation')"></navbar>
</template>
<view class="px-30rpx py-20rpx">
<template v-for="item in dataList">
<view class="mb-20rpx last:mb-0">
<reservation-item :item="item" class="mb-16rpx last:mb-0rpx"></reservation-item>
</view>
</template>
</view>
</z-paging>
</template>
<style lang="scss" scoped>
</style>
+71
View File
@@ -0,0 +1,71 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import {appMerchantOrderOrderListPost} from "@/service";
const {t} = useI18n()
const {paging, dataList, loading, queryList, firstLoaded} = usePage((pageNum, pageSize) => {
return appMerchantOrderOrderListPost({
params: {
pageNum,
pageSize,
},
body: {
orderStatusList: [],
createBeginTime: dayjs().startOf('day').valueOf(),
createEndTime: dayjs().endOf('day').valueOf(),
}
})
})
</script>
<template>
<z-paging ref="paging" v-model="dataList" @query="queryList">
<template #top>
<navbar :title="t('navbar-today-order')"></navbar>
</template>
<view
v-show="loading"
class="animate-in fade-in animate-ease-out animate-duration-300"
>
<view class="bg-#F6F6F6 px-20rpx py-28rpx rounded-16rpx">
<template v-for="item in 6" :key="item">
<view class="w-full p-20rpx rounded-16rpx bg-white mb-20rpx last:mb-0">
<view class="flex items-center justify-between mb-20rpx">
<view class="w-150rpx h-24rpx skeleton-item"></view>
<view class="w-80rpx h-24rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between mb-20rpx">
<view class="w-200rpx h-20rpx skeleton-item"></view>
<view class="w-100rpx h-20rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between mb-20rpx">
<view class="w-180rpx h-20rpx skeleton-item"></view>
<view class="w-120rpx h-20rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between">
<view class="w-160rpx h-20rpx skeleton-item"></view>
<view class="w-120rpx h-56rpx rounded-56rpx skeleton-item"></view>
</view>
</view>
</template>
</view>
</view>
<view
v-show="!loading"
class="animate-in fade-in animate-ease-in animate-duration-300"
>
<view class="px-30rpx py-20rpx">
<template v-for="item in dataList">
<view class="w-full p-20rpx rounded-16rpx bg-white mb-20rpx last:mb-0">
<order-item :item="item"/>
</view>
</template>
</view>
</view>
</z-paging>
</template>
<style lang="scss" scoped>
</style>
@@ -0,0 +1,71 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import {appMerchantOrderOrderListPost} from "@/service";
const {t} = useI18n()
const {paging, dataList, loading, queryList, firstLoaded} = usePage((pageNum, pageSize) => {
return appMerchantOrderOrderListPost({
params: {
pageNum,
pageSize,
},
body: {
orderStatusList: [],
createBeginTime: dayjs().subtract(1, 'day').startOf('day').valueOf(),
createEndTime: dayjs().subtract(1, 'day').endOf('day').valueOf(),
}
})
})
</script>
<template>
<z-paging ref="paging" v-model="dataList" @query="queryList">
<template #top>
<navbar :title="t('navbar-yesterday-order')"></navbar>
</template>
<view
v-show="loading"
class="animate-in fade-in animate-ease-out animate-duration-300"
>
<view class="bg-#F6F6F6 px-20rpx py-28rpx rounded-16rpx">
<template v-for="item in 6" :key="item">
<view class="w-full p-20rpx rounded-16rpx bg-white mb-20rpx last:mb-0">
<view class="flex items-center justify-between mb-20rpx">
<view class="w-150rpx h-24rpx skeleton-item"></view>
<view class="w-80rpx h-24rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between mb-20rpx">
<view class="w-200rpx h-20rpx skeleton-item"></view>
<view class="w-100rpx h-20rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between mb-20rpx">
<view class="w-180rpx h-20rpx skeleton-item"></view>
<view class="w-120rpx h-20rpx skeleton-item"></view>
</view>
<view class="flex items-center justify-between">
<view class="w-160rpx h-20rpx skeleton-item"></view>
<view class="w-120rpx h-56rpx rounded-56rpx skeleton-item"></view>
</view>
</view>
</template>
</view>
</view>
<view
v-show="!loading"
class="animate-in fade-in animate-ease-in animate-duration-300"
>
<view class="px-30rpx py-20rpx">
<template v-for="item in dataList">
<view class="w-full p-20rpx rounded-16rpx bg-white mb-20rpx last:mb-0">
<order-item :item="item"/>
</view>
</template>
</view>
</view>
</z-paging>
</template>
<style lang="scss" scoped>
</style>

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