feat: 预约组件及多项页面更新
This commit is contained in:
parent
6640f734bf
commit
eda1932d03
@ -19,8 +19,8 @@
|
||||
import AppIcon from './AppIcon.vue'
|
||||
|
||||
const tabs = [
|
||||
{ key: 'home', icon: 'home', label: '预约' },
|
||||
{ key: 'report', icon: 'report', label: '报告' },
|
||||
{ key: 'home', icon: 'home', label: '服务预约' },
|
||||
{ key: 'report', icon: 'report', label: '洗美报告' },
|
||||
{ key: 'mine', icon: 'mine', label: '我的' }
|
||||
]
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
{"path": "pages/login/Login"},
|
||||
{"path": "pages/home/Home"},
|
||||
{"path": "pages/appointment/AppointmentDetail"},
|
||||
{"path": "pages/appointment/CustAppointmentCreate"},
|
||||
{"path": "pages/report/Report"},
|
||||
{"path": "pages/mine/Mine"},
|
||||
{"path": "pages/mine/Staff"},
|
||||
|
||||
227
src/pages/appointment/CustAppointmentCreate.vue
Normal file
227
src/pages/appointment/CustAppointmentCreate.vue
Normal file
@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<view class="page-shell cust-appt-create">
|
||||
<view class="create-nav nav-gradient" :style="navSafeStyle">
|
||||
<view class="nav-back" @click="goBack"><AppIcon name="back" :size="18" color="#ffffff" /></view>
|
||||
<text class="nav-title">创建预约</text>
|
||||
<view style="width:32px"></view>
|
||||
</view>
|
||||
|
||||
<view class="c-hero">
|
||||
<text class="c-hero-title">预约宠物服务</text>
|
||||
<text class="c-hero-sub">在线预约,专业服务</text>
|
||||
</view>
|
||||
|
||||
<view class="c-form-card">
|
||||
<view class="c-field">
|
||||
<text class="c-label">宠物名字</text>
|
||||
<input v-model="newAppt.petName" class="c-input" placeholder="请输入宠物名字" />
|
||||
</view>
|
||||
<view class="c-field">
|
||||
<text class="c-label">宠物类型</text>
|
||||
<picker mode="selector" :range="petTypes" range-key="label" @change="e => newAppt.petType = petTypes[e.detail.value].value">
|
||||
<view class="c-input c-picker">
|
||||
<text :class="{ 'c-placeholder': !newAppt.petType }">{{ newAppt.petType || '请选择' }}</text>
|
||||
<text class="c-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="c-field">
|
||||
<text class="c-label">服务类型</text>
|
||||
<picker mode="selector" :range="serviceTypes" range-key="label" @change="e => newAppt.serviceType = serviceTypes[e.detail.value].value">
|
||||
<view class="c-input c-picker">
|
||||
<text :class="{ 'c-placeholder': !newAppt.serviceType }">{{ newAppt.serviceType || '请选择' }}</text>
|
||||
<text class="c-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="c-field">
|
||||
<text class="c-label">预约时间</text>
|
||||
<view class="c-time-row">
|
||||
<picker mode="date" :value="appointmentDate" @change="onAppointmentDateChange" class="c-time-half">
|
||||
<view class="c-input c-picker">
|
||||
<text :class="{ 'c-placeholder': !appointmentDate }">{{ appointmentDate || '日期' }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<picker mode="time" :value="appointmentTime" @change="onAppointmentTimeOnlyChange" class="c-time-half">
|
||||
<view class="c-input c-picker">
|
||||
<text :class="{ 'c-placeholder': !appointmentTime }">{{ appointmentTime || '时间' }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<view class="c-field">
|
||||
<text class="c-label">备注(可选)</text>
|
||||
<input v-model="newAppt.remark" class="c-input" placeholder="可选" />
|
||||
</view>
|
||||
<button class="c-submit" :loading="creatingAppt" @click="confirmNewAppt">提交预约</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { createAppointment, getServiceTypeList } from '../../api/index.js'
|
||||
import AppIcon from '../../components/AppIcon.vue'
|
||||
import { getUserSession, getStoreSession } from '../../utils/session.js'
|
||||
|
||||
const userInfo = getUserSession()
|
||||
const storeInfo = getStoreSession()
|
||||
|
||||
const navSafeStyle = (() => {
|
||||
const statusBarHeight = uni.getSystemInfoSync?.().statusBarHeight || 20
|
||||
let navHeight = statusBarHeight + 44
|
||||
const menuRect = uni.getMenuButtonBoundingClientRect?.()
|
||||
if (menuRect && menuRect.top && menuRect.height) {
|
||||
const verticalGap = Math.max(menuRect.top - statusBarHeight, 4)
|
||||
navHeight = statusBarHeight + verticalGap * 2 + menuRect.height
|
||||
}
|
||||
return `padding-top:${statusBarHeight}px;height:${navHeight}px;`
|
||||
})()
|
||||
|
||||
const petTypes = [
|
||||
{ label: '猫', value: '猫' },
|
||||
{ label: '狗', value: '狗' },
|
||||
{ label: '其他', value: '其他' }
|
||||
]
|
||||
|
||||
const serviceTypes = ref([])
|
||||
const newAppt = ref({ petName: '', petType: '', serviceType: '', appointmentTime: '', remark: '' })
|
||||
const creatingAppt = ref(false)
|
||||
|
||||
const appointmentDate = computed(() => {
|
||||
const raw = newAppt.value.appointmentTime || ''
|
||||
return raw.includes('T') ? raw.split('T')[0] : ''
|
||||
})
|
||||
const appointmentTime = computed(() => {
|
||||
const raw = newAppt.value.appointmentTime || ''
|
||||
if (!raw.includes('T')) return ''
|
||||
return (raw.split('T')[1] || '').slice(0, 5)
|
||||
})
|
||||
|
||||
const onAppointmentDateChange = (e) => {
|
||||
const date = e?.detail?.value || ''
|
||||
const time = appointmentTime.value || '00:00'
|
||||
newAppt.value.appointmentTime = date ? `${date}T${time}` : ''
|
||||
}
|
||||
|
||||
const onAppointmentTimeOnlyChange = (e) => {
|
||||
const time = e?.detail?.value || ''
|
||||
const date = appointmentDate.value
|
||||
if (!date) { uni.showToast({ title: '请先选择日期', icon: 'none' }); return }
|
||||
newAppt.value.appointmentTime = `${date}T${time}`
|
||||
}
|
||||
|
||||
const loadServiceTypes = async () => {
|
||||
if (!storeInfo?.id) return
|
||||
const res = await getServiceTypeList(storeInfo.id)
|
||||
if (res.code === 200) {
|
||||
serviceTypes.value = res.data.map(s => ({ label: s.name, value: s.name }))
|
||||
}
|
||||
}
|
||||
|
||||
const confirmNewAppt = async () => {
|
||||
const a = newAppt.value
|
||||
if (!a.petName) { uni.showToast({ title: '请输入宠物名字', icon: 'none' }); return }
|
||||
if (!a.petType) { uni.showToast({ title: '请选择宠物类型', icon: 'none' }); return }
|
||||
if (!a.serviceType) { uni.showToast({ title: '请选择服务类型', icon: 'none' }); return }
|
||||
if (!a.appointmentTime) { uni.showToast({ title: '请选择预约时间', icon: 'none' }); return }
|
||||
if (!storeInfo?.id) {
|
||||
uni.showToast({ title: '缺少门店信息,请重新登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
creatingAppt.value = true
|
||||
const res = await createAppointment({ ...a, storeId: storeInfo.id, userId: userInfo.id })
|
||||
creatingAppt.value = false
|
||||
if (res.code === 200) {
|
||||
uni.showToast({ title: '预约成功', icon: 'success' })
|
||||
setTimeout(() => uni.navigateBack(), 600)
|
||||
} else {
|
||||
uni.showToast({ title: res.message || '创建失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) uni.navigateBack()
|
||||
else uni.reLaunch({ url: '/pages/home/Home' })
|
||||
}
|
||||
|
||||
onMounted(() => loadServiceTypes())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cust-appt-create {
|
||||
background: #fafaf8;
|
||||
min-height: 100vh;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
.create-nav {
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.nav-back { font-size: 20px; color: #fff; }
|
||||
.nav-title { font-size: 18px; font-weight: 700; color: #fff; }
|
||||
|
||||
.c-hero { padding: 24px 20px 20px; }
|
||||
.c-hero-title {
|
||||
display: block;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
color: #1a1a1a;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
.c-hero-sub {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #999993;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.c-form-card {
|
||||
margin: 0 16px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 24px 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
}
|
||||
.c-field { margin-bottom: 16px; }
|
||||
.c-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #666660;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.c-input {
|
||||
background: #f5f5f2;
|
||||
border: 1.5px solid transparent;
|
||||
border-radius: 12px;
|
||||
padding: 11px 14px;
|
||||
font-size: 15px;
|
||||
color: #1a1a1a;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 44px;
|
||||
}
|
||||
.c-input:focus { border-color: #2db96d; background: #fff; }
|
||||
.c-picker { display: flex; align-items: center; justify-content: space-between; }
|
||||
.c-placeholder { color: #bbbbb5; }
|
||||
.c-arrow { color: #bbbbb5; font-size: 18px; }
|
||||
.c-time-row { display: flex; gap: 10px; }
|
||||
.c-time-half { flex: 1; }
|
||||
.c-submit {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
border-radius: 14px;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -1,59 +1,51 @@
|
||||
<template>
|
||||
<view class="page-shell home-page">
|
||||
<!-- C端:预约 -->
|
||||
<!-- C端:服务预约列表 + 加号新建 -->
|
||||
<view v-if="userInfo.role === 'customer'" class="c-home">
|
||||
<view class="home-nav" :style="navSafeStyle">
|
||||
<text class="c-nav-title">{{ storeInfo.name || '宠伴生活馆' }}</text>
|
||||
<view class="home-nav home-nav--center nav-gradient" :style="navSafeStyle">
|
||||
<text class="nav-title">{{ storeInfo.name || '宠伴生活馆' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="c-hero">
|
||||
<text class="c-hero-title">预约宠物服务</text>
|
||||
<text class="c-hero-sub">在线预约,专业服务</text>
|
||||
<text class="c-hero-title">服务预约</text>
|
||||
<text class="c-hero-sub">查看预约进度,点击右下角加号新建预约</text>
|
||||
</view>
|
||||
|
||||
<view class="c-form-card">
|
||||
<view class="c-field">
|
||||
<text class="c-label">宠物名字</text>
|
||||
<input v-model="newAppt.petName" class="c-input" placeholder="请输入宠物名字" />
|
||||
</view>
|
||||
<view class="c-field">
|
||||
<text class="c-label">宠物类型</text>
|
||||
<picker mode="selector" :range="petTypes" range-key="label" @change="e => newAppt.petType = petTypes[e.detail.value].value">
|
||||
<view class="c-input c-picker">
|
||||
<text :class="{ 'c-placeholder': !newAppt.petType }">{{ newAppt.petType || '请选择' }}</text>
|
||||
<text class="c-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="c-field">
|
||||
<text class="c-label">服务类型</text>
|
||||
<picker mode="selector" :range="serviceTypes" range-key="label" @change="e => newAppt.serviceType = serviceTypes[e.detail.value].value">
|
||||
<view class="c-input c-picker">
|
||||
<text :class="{ 'c-placeholder': !newAppt.serviceType }">{{ newAppt.serviceType || '请选择' }}</text>
|
||||
<text class="c-arrow">›</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="c-field">
|
||||
<text class="c-label">预约时间</text>
|
||||
<view class="c-time-row">
|
||||
<picker mode="date" :value="appointmentDate" @change="onAppointmentDateChange" class="c-time-half">
|
||||
<view class="c-input c-picker">
|
||||
<text :class="{ 'c-placeholder': !appointmentDate }">{{ appointmentDate || '日期' }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<picker mode="time" :value="appointmentTime" @change="onAppointmentTimeOnlyChange" class="c-time-half">
|
||||
<view class="c-input c-picker">
|
||||
<text :class="{ 'c-placeholder': !appointmentTime }">{{ appointmentTime || '时间' }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
<view class="tabs-wrap">
|
||||
<view
|
||||
v-for="tab in statusTabs"
|
||||
:key="tab.name"
|
||||
:class="['tab', { 'tab-active': currentStatus === tab.name }]"
|
||||
@click="currentStatus = tab.name"
|
||||
>
|
||||
<text class="tab-text">{{ tab.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="c-field">
|
||||
<text class="c-label">备注(可选)</text>
|
||||
<input v-model="newAppt.remark" class="c-input" placeholder="可选" />
|
||||
|
||||
<view class="order-list">
|
||||
<view
|
||||
v-for="item in filteredOrders"
|
||||
:key="item.id"
|
||||
class="order-card"
|
||||
@click="goAppointmentDetail(item)"
|
||||
>
|
||||
<view class="order-top">
|
||||
<view class="order-pet">
|
||||
<text class="order-pet-name">{{ item.petName }}</text>
|
||||
<text class="order-svc">{{ item.serviceType }}</text>
|
||||
</view>
|
||||
<button class="c-submit" :loading="creatingAppt" @click="confirmNewAppt">提交预约</button>
|
||||
<view :class="['order-status', `status-${item.status}`]">{{ item.statusText }}</view>
|
||||
</view>
|
||||
<view class="order-time">{{ item.time }}</view>
|
||||
<view v-if="item.status === 'new'" class="order-actions">
|
||||
<button class="btn-sm btn-outline" @click.stop="cancelService(item)">取消预约</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="filteredOrders.length === 0" class="empty"><text>暂无预约</text></view>
|
||||
</view>
|
||||
|
||||
<view class="fab-appt" @click="openCustCreateAppt" hover-class="fab-appt--hover">
|
||||
<text class="fab-appt-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -168,17 +160,17 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { getAppointmentList, createAppointment, startAppointment, cancelAppointment, getServiceTypeList } from '../../api/index.js'
|
||||
import TabBar from '../../components/TabBar.vue'
|
||||
import AppIcon from '../../components/AppIcon.vue'
|
||||
import { getUserSession, getStoreSession } from '../../utils/session.js'
|
||||
import { useNavigator } from '../../composables/useNavigator.js'
|
||||
import { formatDateTimeCN } from '../../utils/datetime.js'
|
||||
import { getAppointmentStatusText, getAppointmentTagClass } from '../../utils/appointment.js'
|
||||
import { getAppointmentStatusText } from '../../utils/appointment.js'
|
||||
|
||||
const userInfo = getUserSession()
|
||||
const storeInfo = getStoreSession()
|
||||
const { goPage, navigateTo } = useNavigator()
|
||||
const { goPage } = useNavigator()
|
||||
const navSafeStyle = (() => {
|
||||
const statusBarHeight = uni.getSystemInfoSync?.().statusBarHeight || 20
|
||||
let navHeight = statusBarHeight + 44
|
||||
@ -190,7 +182,7 @@ const navSafeStyle = (() => {
|
||||
return `padding-top:${statusBarHeight}px;height:${navHeight}px;`
|
||||
})()
|
||||
|
||||
const currentStatus = ref('doing')
|
||||
const currentStatus = ref(userInfo.role === 'customer' ? 'new' : 'doing')
|
||||
const statusTabs = [
|
||||
{ title: '待确认', name: 'new' },
|
||||
{ title: '进行中', name: 'doing' },
|
||||
@ -241,6 +233,25 @@ const onAppointmentTimeOnlyChange = (e) => {
|
||||
}
|
||||
|
||||
const fetchAppointments = async () => {
|
||||
if (userInfo.role === 'customer') {
|
||||
if (!userInfo.id) return
|
||||
const res = await getAppointmentList(userInfo.id)
|
||||
if (res.code === 200) {
|
||||
orders.value = res.data.map(appt => ({
|
||||
id: appt.id,
|
||||
title: appt.serviceType || '洗澡美容预约',
|
||||
desc: `${appt.petType || ''} - ${appt.petName || ''}`,
|
||||
time: formatDateTimeCN(appt.appointmentTime),
|
||||
status: appt.status || 'new',
|
||||
statusText: getAppointmentStatusText(appt.status),
|
||||
petName: appt.petName,
|
||||
petType: appt.petType,
|
||||
serviceType: appt.serviceType,
|
||||
appointmentTime: appt.appointmentTime
|
||||
}))
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!storeInfo.id) return
|
||||
const res = await getAppointmentList(null, storeInfo.id)
|
||||
if (res.code === 200) {
|
||||
@ -261,6 +272,10 @@ const fetchAppointments = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const openCustCreateAppt = () => {
|
||||
uni.navigateTo({ url: '/pages/appointment/CustAppointmentCreate' })
|
||||
}
|
||||
|
||||
const loadServiceTypes = async () => {
|
||||
if (!storeInfo.id) return
|
||||
const res = await getServiceTypeList(storeInfo.id)
|
||||
@ -294,13 +309,13 @@ const goAppointmentDetail = (item) => {
|
||||
}
|
||||
|
||||
const goReport = (item) => {
|
||||
navigateTo('report')
|
||||
uni.setStorageSync('petstore_report_prefill', JSON.stringify({
|
||||
appointmentId: item.id,
|
||||
petName: item.petName,
|
||||
serviceType: item.serviceType,
|
||||
appointmentTime: item.appointmentTime
|
||||
}))
|
||||
uni.navigateTo({ url: '/pages/report/Report' })
|
||||
}
|
||||
|
||||
const confirmNewAppt = async () => {
|
||||
@ -322,7 +337,13 @@ const confirmNewAppt = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => { fetchAppointments(); loadServiceTypes() })
|
||||
onMounted(() => {
|
||||
fetchAppointments()
|
||||
if (userInfo.role !== 'customer') loadServiceTypes()
|
||||
})
|
||||
onShow(() => {
|
||||
if (userInfo.role === 'customer') fetchAppointments()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -380,10 +401,10 @@ onMounted(() => { fetchAppointments(); loadServiceTypes() })
|
||||
|
||||
/* C端 */
|
||||
.c-home { background: #fafaf8; min-height: 100vh; }
|
||||
.c-nav-title {
|
||||
.c-home .home-nav.nav-gradient .nav-title {
|
||||
color: #fff;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
.c-hero {
|
||||
padding: 24px 20px 32px;
|
||||
|
||||
@ -46,11 +46,6 @@
|
||||
<text class="menu-text">个人信息</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view v-if="userInfo.role !== 'customer'" class="menu-item" @click="goMyReportsPage">
|
||||
<view class="menu-icon-wrap"><AppIcon name="report" :size="16" color="#666660" /></view>
|
||||
<text class="menu-text">我的报告</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goMyOrdersPage">
|
||||
<view class="menu-icon-wrap"><AppIcon name="orders" :size="16" color="#666660" /></view>
|
||||
<text class="menu-text">我的订单</text>
|
||||
@ -80,7 +75,6 @@ const goStaffPage = () => uni.navigateTo({ url: '/pages/mine/Staff' })
|
||||
const goServiceTypePage = () => uni.navigateTo({ url: '/pages/mine/ServiceType' })
|
||||
const goStorePage = () => uni.navigateTo({ url: '/pages/mine/Store' })
|
||||
const openProfile = () => uni.navigateTo({ url: '/pages/mine/Profile' })
|
||||
const goMyReportsPage = () => uni.navigateTo({ url: '/pages/mine/MyReports' })
|
||||
const goMyOrdersPage = () => uni.navigateTo({ url: '/pages/mine/MyOrders' })
|
||||
|
||||
const userCardSafeStyle = (() => {
|
||||
|
||||
@ -164,7 +164,7 @@ const goReport = (item) => {
|
||||
uni.setStorageSync('petstore_report_prefill', JSON.stringify({
|
||||
appointmentId: item.id, petName: item.petName, serviceType: item.serviceType, appointmentTime: item.appointmentTime
|
||||
}))
|
||||
navigateTo('report')
|
||||
uni.navigateTo({ url: '/pages/report/Report' })
|
||||
}
|
||||
|
||||
onMounted(() => fetchOrders())
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="page-shell my-reports-page">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="report-nav nav-gradient" :style="navSafeStyle">
|
||||
<view class="nav-back" @click="goBackToMine"><AppIcon name="back" :size="18" color="#ffffff" /></view>
|
||||
<text class="nav-title">我的报告</text>
|
||||
<view class="report-nav report-nav--tab-root nav-gradient" :style="navSafeStyle">
|
||||
<view class="nav-placeholder"></view>
|
||||
<text class="nav-title">洗护美容报告</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
|
||||
@ -44,16 +44,32 @@
|
||||
</div>
|
||||
|
||||
<view v-if="!loading && reportList.length === 0" class="empty"><text>暂无报告</text></view>
|
||||
|
||||
<!-- 仅商家/员工可新建报告;客户仅查看列表 -->
|
||||
<view
|
||||
v-if="userInfo.role !== 'customer'"
|
||||
class="fab-new-report"
|
||||
@click="openFillReport"
|
||||
hover-class="fab-new-report--hover"
|
||||
>
|
||||
<text class="fab-new-report-icon">+</text>
|
||||
</view>
|
||||
|
||||
<TabBar current-page="report" @change="goPage" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import AppIcon from '../../components/AppIcon.vue'
|
||||
import { navigateTo } from '../../utils/globalState.js'
|
||||
import TabBar from '../../components/TabBar.vue'
|
||||
import { useNavigator } from '../../composables/useNavigator.js'
|
||||
import { getReportList, imgUrl, API_ORIGIN } from '../../api/index.js'
|
||||
import { getStoreSession, getUserSession } from '../../utils/session.js'
|
||||
|
||||
const { goPage } = useNavigator()
|
||||
|
||||
const emit = defineEmits(['change-page'])
|
||||
const userInfo = getUserSession()
|
||||
const storeInfo = getStoreSession()
|
||||
@ -80,13 +96,8 @@ const loadReports = async () => {
|
||||
if (res.code === 200) reportList.value = res.data
|
||||
}
|
||||
|
||||
const goBackToMine = () => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack()
|
||||
return
|
||||
}
|
||||
navigateTo('mine')
|
||||
const openFillReport = () => {
|
||||
uni.navigateTo({ url: '/pages/report/Report' })
|
||||
}
|
||||
|
||||
const viewReport = (r) => {
|
||||
@ -104,10 +115,14 @@ const viewReport = (r) => {
|
||||
}
|
||||
|
||||
onMounted(() => loadReports())
|
||||
onShow(() => loadReports())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-reports-page { padding: 0 0 120rpx; }
|
||||
.my-reports-page {
|
||||
padding: 0 0 calc(76px + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.nav-placeholder { width: 32px; }
|
||||
.reports-hero {
|
||||
margin-top: 12px;
|
||||
@ -130,11 +145,24 @@ onMounted(() => loadReports())
|
||||
}
|
||||
.report-nav {
|
||||
padding: 14px 16px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
position: sticky; top: 0; z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.report-nav--tab-root .nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
max-width: 72%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
.nav-back { font-size: 20px; color: #fff; }
|
||||
.nav-title { font-size: 18px; font-weight: 700; color: #fff; }
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@ -187,4 +215,31 @@ onMounted(() => loadReports())
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* 新建报告(与首页新建预约 FAB 对齐,z-index 高于 TabBar) */
|
||||
.fab-new-report {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: calc(56px + env(safe-area-inset-bottom) + 20px);
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 28px;
|
||||
background: #2db96d;
|
||||
box-shadow: 0 6px 20px rgba(45, 185, 109, 0.42);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1002;
|
||||
}
|
||||
.fab-new-report--hover {
|
||||
opacity: 0.92;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
.fab-new-report-icon {
|
||||
color: #fff;
|
||||
font-size: 32px;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
margin-top: -2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
<div class="hero-phone text-sub">{{ userInfo.phone || '未绑定手机号' }}</div>
|
||||
<div class="hero-tags">
|
||||
<span v-if="userInfo.role === 'boss'" class="role-pill hero-role">店长</span>
|
||||
<span v-else-if="userInfo.role === 'customer'" class="role-pill hero-role role-customer">客户</span>
|
||||
<span v-else class="role-pill hero-role role-staff">员工</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -33,7 +34,7 @@
|
||||
<div class="menu-section">
|
||||
<div class="menu-card">
|
||||
<div class="menu-title">资料与账号</div>
|
||||
<div class="menu-item" @click="showEditName = true">
|
||||
<div class="menu-item" @click="openEditName">
|
||||
<div class="menu-left">
|
||||
<span class="menu-icon"><AppIcon name="profile" :size="15" /></span>
|
||||
<span class="menu-text">姓名</span>
|
||||
@ -43,7 +44,7 @@
|
||||
<span class="menu-arrow">›</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-item" @click="showEditPhone = true">
|
||||
<div class="menu-item" @click="openEditPhone">
|
||||
<div class="menu-left">
|
||||
<span class="menu-icon"><AppIcon name="orders" :size="15" /></span>
|
||||
<span class="menu-text">手机号</span>
|
||||
@ -60,6 +61,7 @@
|
||||
</div>
|
||||
<div class="menu-right">
|
||||
<span v-if="userInfo.role === 'boss'" class="role-pill role-pill-compact">店长</span>
|
||||
<span v-else-if="userInfo.role === 'customer'" class="role-pill role-pill-compact role-customer">客户</span>
|
||||
<span v-else class="role-pill role-pill-compact role-staff">员工</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -67,7 +69,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<div class="footer-tip">宠伴生活馆 · {{ userInfo.role === 'boss' ? '商家版' : '员工版' }}</div>
|
||||
<div class="footer-tip">宠伴生活馆 · {{ editionLabel }}</div>
|
||||
|
||||
<!-- 修改姓名弹窗 -->
|
||||
<view v-if="showEditName" class="popup-mask profile-popup-mask" @click="showEditName = false">
|
||||
@ -78,7 +80,16 @@
|
||||
</view>
|
||||
<view class="popup-body">
|
||||
<view class="field-label">姓名</view>
|
||||
<input v-model="editForm.name" class="van-field" placeholder="请输入姓名" />
|
||||
<!-- 小程序原生 input 需外包一层可见边框,否则易与弹窗底色融为一体 -->
|
||||
<view class="profile-input-shell">
|
||||
<input
|
||||
v-model="editForm.name"
|
||||
class="profile-input-native"
|
||||
type="nickname"
|
||||
placeholder="请输入姓名"
|
||||
maxlength="32"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="popup-footer profile-popup-footer">
|
||||
<button class="van-button van-button--block van-button--primary" :loading="saving" @click="confirmEditName">保存</button>
|
||||
@ -95,10 +106,14 @@
|
||||
</view>
|
||||
<view class="popup-body">
|
||||
<view class="field-label">新手机号</view>
|
||||
<input v-model="editForm.phone" type="tel" class="van-field" placeholder="请输入手机号" maxlength="11" />
|
||||
<view class="profile-input-shell">
|
||||
<input v-model="editForm.phone" class="profile-input-native" type="number" placeholder="请输入手机号" maxlength="11" />
|
||||
</view>
|
||||
<view class="field-label field-label-gap">验证码</view>
|
||||
<view class="sms-row">
|
||||
<input v-model="editForm.code" type="digit" class="van-field sms-input" placeholder="请输入验证码" maxlength="6" />
|
||||
<view class="profile-input-shell profile-sms-code-shell">
|
||||
<input v-model="editForm.code" class="profile-input-native" type="digit" placeholder="请输入验证码" maxlength="6" />
|
||||
</view>
|
||||
<button class="van-button van-button--small van-button--primary" :disabled="smsCountdown > 0" @click="sendSms">
|
||||
{{ smsCountdown > 0 ? smsCountdown + 's' : '获取验证码' }}
|
||||
</button>
|
||||
@ -151,6 +166,13 @@ let smsTimer = null
|
||||
|
||||
const editForm = ref({ name: '', phone: '', code: '' })
|
||||
|
||||
const editionLabel = computed(() => {
|
||||
const r = userInfo.value.role
|
||||
if (r === 'boss') return '商家版'
|
||||
if (r === 'customer') return '客户版'
|
||||
return '员工版'
|
||||
})
|
||||
|
||||
const initials = computed(() => {
|
||||
if (!userInfo.value.name) return '?'
|
||||
return userInfo.value.name.slice(0, 1).toUpperCase()
|
||||
@ -162,6 +184,18 @@ const avatarBg = computed(() => {
|
||||
return { background: COLORS[idx] }
|
||||
})
|
||||
|
||||
/** 打开弹窗前同步当前资料,避免仅 onMounted 一次导致输入框空白或与展示不一致(尤其 C 端微信登录后) */
|
||||
const openEditName = () => {
|
||||
editForm.value.name = userInfo.value.name || ''
|
||||
showEditName.value = true
|
||||
}
|
||||
|
||||
const openEditPhone = () => {
|
||||
editForm.value.phone = userInfo.value.phone || ''
|
||||
editForm.value.code = ''
|
||||
showEditPhone.value = true
|
||||
}
|
||||
|
||||
const sendSms = async () => {
|
||||
const phone = editForm.value.phone
|
||||
if (!phone || phone.length !== 11) {
|
||||
@ -255,6 +289,7 @@ const changeAvatar = () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
userInfo.value = getUserSession()
|
||||
editForm.value.name = userInfo.value.name || ''
|
||||
editForm.value.phone = userInfo.value.phone || ''
|
||||
})
|
||||
@ -369,6 +404,11 @@ onUnmounted(() => {
|
||||
background: #ecfdf3;
|
||||
color: #166534;
|
||||
}
|
||||
.role-pill.hero-role.role-customer,
|
||||
.role-pill.role-pill-compact.role-customer {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.role-pill-compact {
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
@ -521,6 +561,11 @@ onUnmounted(() => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sms-row .profile-sms-code-shell {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.footer-tip {
|
||||
text-align: center;
|
||||
margin-top: 28px;
|
||||
@ -528,3 +573,39 @@ onUnmounted(() => {
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 非 scoped:小程序原生 input 在部分机型上 scoped 样式穿透不稳定 -->
|
||||
<style>
|
||||
.profile-popup-sheet .profile-input-shell {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
padding: 0 14px;
|
||||
background: #f1f5f9;
|
||||
border: 2px solid #94a3b8;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.profile-popup-sheet .profile-input-native {
|
||||
display: block;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 46px;
|
||||
min-height: 46px;
|
||||
line-height: 46px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none !important;
|
||||
outline: none;
|
||||
background: transparent !important;
|
||||
font-size: 16px;
|
||||
color: #0f172a;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
.profile-popup-sheet .profile-input-native::placeholder {
|
||||
color: #64748b;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -242,7 +242,14 @@ const submitReport = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => navigateTo('home')
|
||||
const goBack = () => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack()
|
||||
return
|
||||
}
|
||||
navigateTo('report')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadServiceTypes()
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
const ROOT_ROUTE_MAP = {
|
||||
login: '/pages/login/Login',
|
||||
home: '/pages/home/Home',
|
||||
report: '/pages/report/Report',
|
||||
/** 底部「洗美报告」统一进列表页,填写报告从列表页加号进入 */
|
||||
report: '/pages/mine/MyReports',
|
||||
mine: '/pages/mine/Mine'
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user