feat: 商家入驻&员工注册增加微信授权获取手机号,手动填号需验证码;按钮移至输入框右侧

This commit is contained in:
MaDaLei 2026-04-16 23:09:16 +08:00
parent 1e10ebc759
commit b387f6d944
7 changed files with 390 additions and 272 deletions

View File

@ -58,6 +58,57 @@ page {
} }
.van-field::placeholder { color: var(--c-text-4) !important; } .van-field::placeholder { color: var(--c-text-4) !important; }
/* ---- 通用表单(业务页复用) ---- */
.app-form-label {
display: block;
font-size: 13px;
font-weight: 600;
color: #666660;
margin-bottom: 6px;
}
.app-form-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;
}
.app-form-input:focus {
border-color: #2db96d;
background: #ffffff;
}
.app-form-picker {
display: flex;
align-items: center;
justify-content: space-between;
}
.app-form-placeholder { color: #bbbbb5; }
.app-form-arrow {
color: #bbbbb5;
font-size: 18px;
font-weight: 300;
}
.app-form-textarea {
background: #f5f5f2;
border: 1.5px solid transparent;
border-radius: 12px;
padding: 12px 14px;
font-size: 14px;
color: #1a1a1a;
width: 100%;
box-sizing: border-box;
min-height: 100px;
line-height: 1.6;
}
.app-form-textarea:focus {
border-color: #2db96d;
background: #fff;
}
/* ---- 按钮 ---- /* ---- 按钮 ----
* 微信小程序原生 button 默认白底会盖住 class 背景且仅写 var(--c-brand) 在部分基础库下可能不生效 * 微信小程序原生 button 默认白底会盖住 class 背景且仅写 var(--c-brand) 在部分基础库下可能不生效
* 导致白底 + 白字此处用实色 + button 元素选择器强制覆盖 */ * 导致白底 + 白字此处用实色 + button 元素选择器强制覆盖 */
@ -119,6 +170,132 @@ button::after {
border: none !important; border: none !important;
} }
/* ---- 通用操作按钮(业务页复用) ---- */
.app-btn-sm,
button.app-btn-sm {
height: 34px !important;
min-height: 34px !important;
line-height: 34px !important;
padding: 0 16px !important;
margin: 0 !important;
border-radius: 10px !important;
font-size: 13px !important;
font-weight: 600 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
border: none !important;
box-sizing: border-box !important;
flex-shrink: 0;
transition: opacity 0.15s, transform 0.15s, box-shadow 0.15s;
}
.app-btn-sm:active {
opacity: 0.9;
transform: translateY(1px);
}
.app-btn-primary,
button.app-btn-primary {
background: linear-gradient(135deg, #32c877 0%, #2db96d 100%) !important;
color: #ffffff !important;
box-shadow: 0 4px 12px rgba(45, 185, 109, 0.28);
}
.app-btn-outline,
button.app-btn-outline {
background: transparent !important;
color: #999993 !important;
border: 1.5px solid #ebebea !important;
}
.app-btn-danger-outline,
button.app-btn-danger-outline {
background: #ffffff !important;
color: #e5534b !important;
border: 1.5px solid #f0e0de !important;
}
.app-btn-sm[disabled],
button.app-btn-sm[disabled] {
background: #ddddd8 !important;
color: #999993 !important;
box-shadow: none !important;
border: none !important;
}
.app-btn-main,
button.app-btn-main {
width: 100% !important;
height: 48px !important;
min-height: 48px !important;
line-height: 48px !important;
padding: 0 18px !important;
margin: 0 !important;
border-radius: 14px !important;
font-size: 16px !important;
font-weight: 700 !important;
letter-spacing: 0.4px;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
border: none !important;
box-sizing: border-box !important;
transition: opacity 0.15s, transform 0.15s, box-shadow 0.15s;
}
.app-btn-main:active {
opacity: 0.9;
transform: translateY(1px);
}
.app-btn-main[disabled],
button.app-btn-main[disabled] {
background: #ddddd8 !important;
color: #999993 !important;
box-shadow: none !important;
border: none !important;
}
.app-btn-soft,
button.app-btn-soft {
background: #f5f5f2 !important;
color: #1a1a1a !important;
border: none !important;
box-shadow: none !important;
}
.app-btn-code,
button.app-btn-code {
height: 48px !important;
min-height: 48px !important;
line-height: 48px !important;
padding: 0 16px !important;
margin: 0 !important;
border-radius: 12px !important;
font-size: 14px !important;
font-weight: 600 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
white-space: nowrap;
box-sizing: border-box !important;
border: none !important;
transition: opacity 0.15s, transform 0.15s;
}
.app-btn-code:active {
opacity: 0.9;
transform: translateY(1px);
}
.app-btn-code--neutral,
button.app-btn-code--neutral {
background: #f5f5f2 !important;
color: #2db96d !important;
border: none !important;
}
.app-btn-code--brand,
button.app-btn-code--brand {
background: #f0fcf5 !important;
color: #2db96d !important;
border: 1.5px solid #c0f0d8 !important;
}
.app-btn-code--neutral[disabled],
button.app-btn-code--neutral[disabled] {
background: #f5f5f2 !important;
color: #bbbbb5 !important;
border: none !important;
}
/* ---- 单元格组 ---- */ /* ---- 单元格组 ---- */
.van-cell-group { .van-cell-group {
background: var(--c-surface) !important; background: var(--c-surface) !important;

View File

@ -13,7 +13,7 @@
<view class="c-form-card"> <view class="c-form-card">
<view class="c-field"> <view class="c-field">
<text class="c-label">预约门店</text> <text class="app-form-label">预约门店</text>
<picker <picker
mode="selector" mode="selector"
:range="orderedStores" :range="orderedStores"
@ -22,50 +22,50 @@
:disabled="!orderedStores.length" :disabled="!orderedStores.length"
@change="onStorePickerChange" @change="onStorePickerChange"
> >
<view class="c-input c-picker"> <view class="app-form-input app-form-picker">
<text :class="{ 'c-placeholder': !orderedStores.length }">{{ storePickerDisplay }}</text> <text :class="{ 'app-form-placeholder': !orderedStores.length }">{{ storePickerDisplay }}</text>
<text class="c-arrow"></text> <text class="app-form-arrow"></text>
</view> </view>
</picker> </picker>
<text v-if="storeHintLine" class="c-store-hint">{{ storeHintLine }}</text> <text v-if="storeHintLine" class="c-store-hint">{{ storeHintLine }}</text>
</view> </view>
<view v-if="myPets.length" class="c-field"> <view v-if="myPets.length" class="c-field">
<text class="c-label">使用档案可选</text> <text class="app-form-label">使用档案可选</text>
<picker mode="selector" :range="petPickerOptions" range-key="label" @change="onPickMyPet"> <picker mode="selector" :range="petPickerOptions" range-key="label" @change="onPickMyPet">
<view class="c-input c-picker"> <view class="app-form-input app-form-picker">
<text :class="{ 'c-placeholder': selectedPetIndex === 0 }">{{ petPickerLabel }}</text> <text :class="{ 'app-form-placeholder': selectedPetIndex === 0 }">{{ petPickerLabel }}</text>
<text class="c-arrow"></text> <text class="app-form-arrow"></text>
</view> </view>
</picker> </picker>
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="c-label">宠物名字</text> <text class="app-form-label">宠物名字</text>
<input v-model="newAppt.petName" class="c-input" placeholder="请输入宠物名字" /> <input v-model="newAppt.petName" class="app-form-input" placeholder="请输入宠物名字" />
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="c-label">宠物类型</text> <text class="app-form-label">宠物类型</text>
<picker mode="selector" :range="petTypes" range-key="label" @change="e => newAppt.petType = petTypes[e.detail.value].value"> <picker mode="selector" :range="petTypes" range-key="label" @change="e => newAppt.petType = petTypes[e.detail.value].value">
<view class="c-input c-picker"> <view class="app-form-input app-form-picker">
<text :class="{ 'c-placeholder': !newAppt.petType }">{{ newAppt.petType || '请选择' }}</text> <text :class="{ 'app-form-placeholder': !newAppt.petType }">{{ newAppt.petType || '请选择' }}</text>
<text class="c-arrow"></text> <text class="app-form-arrow"></text>
</view> </view>
</picker> </picker>
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="c-label">服务类型</text> <text class="app-form-label">服务类型</text>
<picker mode="selector" :range="serviceTypes" range-key="label" @change="e => newAppt.serviceType = serviceTypes[e.detail.value].value"> <picker mode="selector" :range="serviceTypes" range-key="label" @change="e => newAppt.serviceType = serviceTypes[e.detail.value].value">
<view class="c-input c-picker"> <view class="app-form-input app-form-picker">
<text :class="{ 'c-placeholder': !newAppt.serviceType }">{{ newAppt.serviceType || '请选择' }}</text> <text :class="{ 'app-form-placeholder': !newAppt.serviceType }">{{ newAppt.serviceType || '请选择' }}</text>
<text class="c-arrow"></text> <text class="app-form-arrow"></text>
</view> </view>
</picker> </picker>
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="c-label">预约时间</text> <text class="app-form-label">预约时间</text>
<view class="c-time-row"> <view class="c-time-row">
<picker mode="date" :value="appointmentDate" @change="onAppointmentDateChange" class="c-time-half"> <picker mode="date" :value="appointmentDate" @change="onAppointmentDateChange" class="c-time-half">
<view class="c-input c-picker"> <view class="app-form-input app-form-picker">
<text :class="{ 'c-placeholder': !appointmentDate }">{{ appointmentDate || '日期' }}</text> <text :class="{ 'app-form-placeholder': !appointmentDate }">{{ appointmentDate || '日期' }}</text>
</view> </view>
</picker> </picker>
<picker <picker
@ -75,17 +75,17 @@
@change="onAppointmentTimeOnlyChange" @change="onAppointmentTimeOnlyChange"
class="c-time-half" class="c-time-half"
> >
<view class="c-input c-picker"> <view class="app-form-input app-form-picker">
<text :class="{ 'c-placeholder': !appointmentTime }">{{ appointmentTime || '时间' }}</text> <text :class="{ 'app-form-placeholder': !appointmentTime }">{{ appointmentTime || '时间' }}</text>
</view> </view>
</picker> </picker>
</view> </view>
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="c-label">备注可选</text> <text class="app-form-label">备注可选</text>
<input v-model="newAppt.remark" class="c-input" placeholder="可选" /> <input v-model="newAppt.remark" class="app-form-input" placeholder="可选" />
</view> </view>
<button class="c-submit" :loading="creatingAppt" @click="confirmNewAppt">提交预约</button> <button class="app-btn-main app-btn-primary" style="margin-top:20px" :loading="creatingAppt" @click="confirmNewAppt">提交预约</button>
</view> </view>
</view> </view>
</template> </template>
@ -370,41 +370,8 @@ onMounted(async () => {
box-shadow: 0 2px 8px rgba(0,0,0,0.04); box-shadow: 0 2px 8px rgba(0,0,0,0.04);
} }
.c-field { margin-bottom: 16px; } .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-row { display: flex; gap: 10px; }
.c-time-half { flex: 1; } .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;
}
.c-store-hint { .c-store-hint {
display: block; display: block;
margin-top: 6px; margin-top: 6px;

View File

@ -46,7 +46,7 @@
</view> </view>
<view class="order-time">{{ item.time }}</view> <view class="order-time">{{ item.time }}</view>
<view v-if="item.status === 'new'" class="order-actions"> <view v-if="item.status === 'new'" class="order-actions">
<button class="btn-sm btn-outline" @click.stop="cancelService(item)">取消预约</button> <button class="app-btn-sm app-btn-outline" @click.stop="cancelService(item)">取消预约</button>
</view> </view>
</view> </view>
<view v-if="filteredOrders.length === 0" class="empty"><text>暂无预约</text></view> <view v-if="filteredOrders.length === 0" class="empty"><text>暂无预约</text></view>
@ -96,10 +96,12 @@
<view class="order-time">{{ item.time }}</view> <view class="order-time">{{ item.time }}</view>
<view class="order-actions"> <view class="order-actions">
<view v-if="item.status === 'new'" class="order-btns"> <view v-if="item.status === 'new'" class="order-btns">
<button class="btn-sm btn-fill" @click.stop="startService(item)">开始服务</button> <button class="app-btn-sm app-btn-primary" @click.stop="startService(item)">开始服务</button>
<button class="btn-sm btn-outline" @click.stop="cancelService(item)">取消</button> <button class="app-btn-sm app-btn-outline" @click.stop="cancelService(item)">取消</button>
</view>
<view v-else-if="item.status === 'doing'" class="order-btns">
<button class="app-btn-sm app-btn-primary" @click.stop="goReport(item)">填写报告</button>
</view> </view>
<button v-else-if="item.status === 'doing'" class="btn-sm btn-fill" @click.stop="goReport(item)">填写报告</button>
</view> </view>
</view> </view>
<view v-if="filteredOrders.length === 0" class="empty"><text>暂无数据</text></view> <view v-if="filteredOrders.length === 0" class="empty"><text>暂无数据</text></view>
@ -167,8 +169,8 @@
</view> </view>
<view class="popup-footer"> <view class="popup-footer">
<view class="popup-actions"> <view class="popup-actions">
<button class="btn-sm btn-outline" style="flex:0 0 80px" @click="showNewAppt = false">取消</button> <button class="app-btn-sm app-btn-outline" style="flex:0 0 80px" @click="showNewAppt = false">取消</button>
<button class="c-submit" style="flex:1;margin:0" :loading="creatingAppt" @click="confirmNewAppt">确认创建</button> <button class="app-btn-main app-btn-primary" style="flex:1;margin:0" :loading="creatingAppt" @click="confirmNewAppt">确认创建</button>
</view> </view>
</view> </view>
</view> </view>
@ -493,20 +495,6 @@ onShow(() => {
.c-time-half { flex: 1; } .c-time-half { flex: 1; }
.c-textarea { min-height: 80px; line-height: 1.5; } .c-textarea { min-height: 80px; line-height: 1.5; }
.c-submit {
width: 100%;
height: 48px;
border-radius: 14px;
background: #1a1a1a;
color: #fff;
font-size: 16px;
font-weight: 700;
border: none;
margin-top: 20px;
letter-spacing: 0.3px;
}
.c-submit:active { opacity: 0.85; }
/* B端 Tab */ /* B端 Tab */
.tabs-wrap { .tabs-wrap {
display: flex; display: flex;
@ -599,25 +587,6 @@ onShow(() => {
.order-actions { display: flex; justify-content: flex-end; } .order-actions { display: flex; justify-content: flex-end; }
.order-btns { display: flex; gap: 8px; } .order-btns { display: flex; gap: 8px; }
.btn-sm {
height: 34px;
padding: 0 16px;
border-radius: 10px;
font-size: 13px;
font-weight: 600;
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
}
.btn-fill { background: #1a1a1a; color: #fff; }
.btn-fill:active { opacity: 0.85; }
.btn-outline {
background: transparent;
color: #999993;
border: 1.5px solid #ebebea;
}
/* 弹窗 */ /* 弹窗 */
.popup-mask { .popup-mask {
position: fixed; position: fixed;

View File

@ -11,9 +11,9 @@
<input v-model="loginForm.phone" type="tel" class="field" placeholder="手机号" maxlength="11" /> <input v-model="loginForm.phone" type="tel" class="field" placeholder="手机号" maxlength="11" />
<view class="sms-row"> <view class="sms-row">
<input v-model="loginForm.code" type="digit" class="field sms-input" placeholder="验证码" maxlength="6" /> <input v-model="loginForm.code" type="digit" class="field sms-input" placeholder="验证码" maxlength="6" />
<button class="sms-btn" :disabled="smsCountdown > 0" @click="handleSendSms">{{ smsCountdown > 0 ? smsCountdown + 's' : '获取验证码' }}</button> <button class="app-btn-code app-btn-code--neutral sms-code-btn" :disabled="smsCountdown > 0" @click="handleSendSms">{{ smsCountdown > 0 ? smsCountdown + 's' : '获取验证码' }}</button>
</view> </view>
<button class="primary-btn" :disabled="loginLoading" @click="handleLogin">{{ loginLoading ? '登录中' : '登录' }}</button> <button class="app-btn-main app-btn-primary login-main-btn" :disabled="loginLoading" @click="handleLogin">{{ loginLoading ? '登录中' : '登录' }}</button>
<view class="divider"> <view class="divider">
<view class="divider-line"></view> <view class="divider-line"></view>
@ -23,14 +23,14 @@
<!-- #ifdef MP-WEIXIN --> <!-- #ifdef MP-WEIXIN -->
<button <button
class="ghost-btn" class="app-btn-main app-btn-soft"
open-type="getPhoneNumber" open-type="getPhoneNumber"
:disabled="loginLoading" :disabled="loginLoading"
@getphonenumber="onWxGetPhoneNumber" @getphonenumber="onWxGetPhoneNumber"
>{{ loginLoading ? '登录中…' : '微信授权登录' }}</button> >{{ loginLoading ? '登录中…' : '微信授权登录' }}</button>
<!-- #endif --> <!-- #endif -->
<!-- #ifndef MP-WEIXIN --> <!-- #ifndef MP-WEIXIN -->
<button class="ghost-btn" @click="onWechatLoginNotInMp">微信授权登录</button> <button class="app-btn-main app-btn-soft" @click="onWechatLoginNotInMp">微信授权登录</button>
<!-- #endif --> <!-- #endif -->
<view class="footer-links"> <view class="footer-links">
@ -45,9 +45,23 @@
<text class="card-title">商家入驻</text> <text class="card-title">商家入驻</text>
<input v-model="bossForm.storeName" class="field" placeholder="店铺名称" /> <input v-model="bossForm.storeName" class="field" placeholder="店铺名称" />
<input v-model="bossForm.bossName" class="field" placeholder="您的姓名" /> <input v-model="bossForm.bossName" class="field" placeholder="您的姓名" />
<!-- #ifdef MP-WEIXIN -->
<view class="phone-row">
<input v-model="bossForm.phone" type="tel" class="field phone-input" placeholder="手机号" maxlength="11" />
<button class="app-btn-code app-btn-code--brand phone-auth-btn" open-type="getPhoneNumber" @getphonenumber="onWxGetPhoneNumberBoss">授权手机号</button>
</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<input v-model="bossForm.phone" type="tel" class="field" placeholder="手机号" maxlength="11" /> <input v-model="bossForm.phone" type="tel" class="field" placeholder="手机号" maxlength="11" />
<!-- #endif -->
<!-- 手机号手动填写时需要验证码 -->
<view v-if="!bossForm.wxPhoneAuto" class="sms-row">
<input v-model="bossForm.smsCode" type="digit" class="field sms-input" placeholder="验证码" maxlength="6" />
<button class="app-btn-code app-btn-code--neutral sms-code-btn" :disabled="bossSmsCountdown > 0" @click="handleSendSmsBoss">{{ bossSmsCountdown > 0 ? bossSmsCountdown + 's' : '获取验证码' }}</button>
</view>
<view v-else class="wx-verified-hint"> 微信已认证手机号</view>
<input v-model="bossForm.password" type="password" class="field" placeholder="登录密码至少6位" /> <input v-model="bossForm.password" type="password" class="field" placeholder="登录密码至少6位" />
<button class="primary-btn" :disabled="regLoading" @click="handleRegisterBoss">{{ regLoading ? '提交中' : '提交申请' }}</button> <button class="app-btn-main app-btn-primary login-main-btn" :disabled="regLoading" @click="handleRegisterBoss">{{ regLoading ? '提交中' : '提交申请' }}</button>
<text class="back-link" @click="backToLoginForm"> 返回登录</text> <text class="back-link" @click="backToLoginForm"> 返回登录</text>
</view> </view>
@ -57,9 +71,23 @@
<view class="hint-box">请输入店长提供的邀请码加入团队</view> <view class="hint-box">请输入店长提供的邀请码加入团队</view>
<input v-model="staffForm.inviteCode" class="field" placeholder="8位邀请码" maxlength="8" /> <input v-model="staffForm.inviteCode" class="field" placeholder="8位邀请码" maxlength="8" />
<input v-model="staffForm.name" class="field" placeholder="您的姓名" /> <input v-model="staffForm.name" class="field" placeholder="您的姓名" />
<!-- #ifdef MP-WEIXIN -->
<view class="phone-row">
<input v-model="staffForm.phone" type="tel" class="field phone-input" placeholder="手机号" maxlength="11" />
<button class="app-btn-code app-btn-code--brand phone-auth-btn" open-type="getPhoneNumber" @getphonenumber="onWxGetPhoneNumberStaff">授权手机号</button>
</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<input v-model="staffForm.phone" type="tel" class="field" placeholder="手机号" maxlength="11" /> <input v-model="staffForm.phone" type="tel" class="field" placeholder="手机号" maxlength="11" />
<!-- #endif -->
<!-- 手机号手动填写时需要验证码 -->
<view v-if="!staffForm.wxPhoneAuto" class="sms-row">
<input v-model="staffForm.smsCode" type="digit" class="field sms-input" placeholder="验证码" maxlength="6" />
<button class="app-btn-code app-btn-code--neutral sms-code-btn" :disabled="staffSmsCountdown > 0" @click="handleSendSmsStaff">{{ staffSmsCountdown > 0 ? staffSmsCountdown + 's' : '获取验证码' }}</button>
</view>
<view v-else class="wx-verified-hint"> 微信已认证手机号</view>
<input v-model="staffForm.password" type="password" class="field" placeholder="登录密码至少6位" /> <input v-model="staffForm.password" type="password" class="field" placeholder="登录密码至少6位" />
<button class="primary-btn" :disabled="regLoading" @click="handleRegisterStaff">{{ regLoading ? '提交中' : '注册' }}</button> <button class="app-btn-main app-btn-primary login-main-btn" :disabled="regLoading" @click="handleRegisterStaff">{{ regLoading ? '提交中' : '注册' }}</button>
<text class="back-link" @click="backToLoginForm"> 返回登录</text> <text class="back-link" @click="backToLoginForm"> 返回登录</text>
</view> </view>
</view> </view>
@ -74,11 +102,15 @@ import { openAppPage } from '../../utils/globalState.js'
const view = ref('login') const view = ref('login')
const loginForm = reactive({ phone: '13800138001', code: '123456' }) const loginForm = reactive({ phone: '13800138001', code: '123456' })
const bossForm = reactive({ storeName: '', bossName: '', phone: '', password: '' }) const bossForm = reactive({ storeName: '', bossName: '', phone: '', password: '', smsCode: '', wxPhoneAuto: false })
const staffForm = reactive({ inviteCode: '', name: '', phone: '', password: '' }) const staffForm = reactive({ inviteCode: '', name: '', phone: '', password: '', smsCode: '', wxPhoneAuto: false })
const loginLoading = ref(false) const loginLoading = ref(false)
const regLoading = ref(false) const regLoading = ref(false)
const smsCountdown = ref(0) const smsCountdown = ref(0)
const bossSmsCountdown = ref(0)
const staffSmsCountdown = ref(0)
let bossSmsTimer = null
let staffSmsTimer = null
let smsTimer = null let smsTimer = null
@ -143,6 +175,36 @@ const handleSendSms = async () => {
} }
} }
const handleSendSmsBoss = async () => {
if (!bossForm.phone || bossForm.phone.length !== 11) return showToast('请输入正确的手机号')
const res = await sendSms(bossForm.phone)
if (res.code === 200) {
showToast('验证码已发送')
bossSmsCountdown.value = 60
bossSmsTimer = setInterval(() => {
bossSmsCountdown.value--
if (bossSmsCountdown.value <= 0) clearInterval(bossSmsTimer)
}, 1000)
} else {
showToast(res.message || '发送失败')
}
}
const handleSendSmsStaff = async () => {
if (!staffForm.phone || staffForm.phone.length !== 11) return showToast('请输入正确的手机号')
const res = await sendSms(staffForm.phone)
if (res.code === 200) {
showToast('验证码已发送')
staffSmsCountdown.value = 60
staffSmsTimer = setInterval(() => {
staffSmsCountdown.value--
if (staffSmsCountdown.value <= 0) clearInterval(staffSmsTimer)
}, 1000)
} else {
showToast(res.message || '发送失败')
}
}
const handleLogin = async () => { const handleLogin = async () => {
if (!loginForm.phone || loginForm.phone.length !== 11) return showToast('请输入正确的手机号') if (!loginForm.phone || loginForm.phone.length !== 11) return showToast('请输入正确的手机号')
if (!loginForm.code || loginForm.code.length !== 6) return showToast('请输入6位验证码') if (!loginForm.code || loginForm.code.length !== 6) return showToast('请输入6位验证码')
@ -201,14 +263,65 @@ const onWxGetPhoneNumber = async (e) => {
} }
} }
/** 商家入驻:微信获取手机号并填入 */
const onWxGetPhoneNumberBoss = async (e) => {
const d = e.detail || {}
const errMsg = d.errMsg || ''
if (errMsg.includes('deny') || errMsg.includes('cancel')) return
const code = d.code
if (!code) { showToast('未获取到授权,请手动输入'); return }
loginLoading.value = true
try {
const res = await wxPhoneLogin(code)
if (res.code === 200 && res.data?.phone) {
bossForm.phone = res.data.phone
bossForm.wxPhoneAuto = true
showToast('手机号已获取')
} else {
showToast('未获取到手机号,请手动输入')
}
} catch {
showToast('网络异常')
} finally {
loginLoading.value = false
}
}
/** 员工注册:微信获取手机号并填入 */
const onWxGetPhoneNumberStaff = async (e) => {
const d = e.detail || {}
const errMsg = d.errMsg || ''
if (errMsg.includes('deny') || errMsg.includes('cancel')) return
const code = d.code
if (!code) { showToast('未获取到授权,请手动输入'); return }
loginLoading.value = true
try {
const res = await wxPhoneLogin(code)
if (res.code === 200 && res.data?.phone) {
staffForm.phone = res.data.phone
staffForm.wxPhoneAuto = true
showToast('手机号已获取')
} else {
showToast('未获取到手机号,请手动输入')
}
} catch {
showToast('网络异常')
} finally {
loginLoading.value = false
}
}
const handleRegisterBoss = async () => { const handleRegisterBoss = async () => {
const f = bossForm const f = bossForm
if (!f.storeName) return showToast('请输入店铺名称') if (!f.storeName) return showToast('请输入店铺名称')
if (!f.bossName) return showToast('请输入您的姓名') if (!f.bossName) return showToast('请输入您的姓名')
if (!f.phone || f.phone.length !== 11) return showToast('请输入正确的手机号') if (!f.phone || f.phone.length !== 11) return showToast('请输入正确的手机号')
if (!f.wxPhoneAuto && (!f.smsCode || f.smsCode.length !== 6)) return showToast('请输入6位验证码')
if (!f.password || f.password.length < 6) return showToast('密码至少6位') if (!f.password || f.password.length < 6) return showToast('密码至少6位')
const payload = { ...f }
if (f.wxPhoneAuto) delete payload.smsCode
regLoading.value = true regLoading.value = true
const res = await registerBoss(f) const res = await registerBoss(payload)
regLoading.value = false regLoading.value = false
if (isHttpOk(res)) { if (isHttpOk(res)) {
const parsed = parseBossRegData(res.data) const parsed = parseBossRegData(res.data)
@ -243,9 +356,12 @@ const handleRegisterStaff = async () => {
if (!f.inviteCode || f.inviteCode.length !== 8) return showToast('请输入8位邀请码') if (!f.inviteCode || f.inviteCode.length !== 8) return showToast('请输入8位邀请码')
if (!f.name) return showToast('请输入您的姓名') if (!f.name) return showToast('请输入您的姓名')
if (!f.phone || f.phone.length !== 11) return showToast('请输入正确的手机号') if (!f.phone || f.phone.length !== 11) return showToast('请输入正确的手机号')
if (!f.wxPhoneAuto && (!f.smsCode || f.smsCode.length !== 6)) return showToast('请输入6位验证码')
if (!f.password || f.password.length < 6) return showToast('密码至少6位') if (!f.password || f.password.length < 6) return showToast('密码至少6位')
const payload = { ...f }
if (f.wxPhoneAuto) delete payload.smsCode
regLoading.value = true regLoading.value = true
const res = await registerStaff(f) const res = await registerStaff(payload)
regLoading.value = false regLoading.value = false
if (isHttpOk(res)) { if (isHttpOk(res)) {
const parsed = parseStaffRegData(res.data) const parsed = parseStaffRegData(res.data)
@ -267,7 +383,11 @@ const handleRegisterStaff = async () => {
} }
} }
onUnmounted(() => { if (smsTimer) clearInterval(smsTimer) }) onUnmounted(() => {
if (smsTimer) clearInterval(smsTimer)
if (bossSmsTimer) clearInterval(bossSmsTimer)
if (staffSmsTimer) clearInterval(staffSmsTimer)
})
</script> </script>
<style scoped> <style scoped>
@ -344,36 +464,33 @@ onUnmounted(() => { if (smsTimer) clearInterval(smsTimer) })
margin-bottom: 0; margin-bottom: 0;
min-width: 0; min-width: 0;
} }
.sms-btn { .sms-code-btn {
flex-shrink: 0; flex-shrink: 0;
align-self: center; align-self: center;
height: 48px;
line-height: 48px;
padding: 0 16px;
border-radius: 12px;
background: #f5f5f2;
color: #2db96d;
font-size: 14px;
font-weight: 600;
border: none;
white-space: nowrap;
} }
.sms-btn[disabled] { color: #bbbbb5; }
/* 主按钮 */ /* 微信获取手机号按钮 */
.primary-btn { .phone-row {
width: 100%; display: flex;
height: 48px; align-items: center;
border-radius: 14px; gap: 8px;
background: #1a1a1a; margin-bottom: 12px;
color: #fff;
font-size: 16px;
font-weight: 700;
border: none;
margin-top: 4px;
} }
.primary-btn:active { opacity: 0.85; } .phone-input { flex: 1; margin-bottom: 0; }
.primary-btn[disabled] { background: #ddddd8; color: #999993; } .phone-auth-btn {
flex-shrink: 0;
}
.wx-verified-hint {
font-size: 13px;
color: #2db96d;
font-weight: 600;
margin-bottom: 12px;
padding: 8px 12px;
background: #eaf8f0;
border-radius: 10px;
}
.login-main-btn { margin-top: 4px !important; }
/* 分割线 */ /* 分割线 */
.divider { .divider {
@ -385,19 +502,6 @@ onUnmounted(() => { if (smsTimer) clearInterval(smsTimer) })
.divider-line { flex: 1; height: 1px; background: #ebebea; } .divider-line { flex: 1; height: 1px; background: #ebebea; }
.divider-text { font-size: 12px; color: #bbbbb5; } .divider-text { font-size: 12px; color: #bbbbb5; }
/* 幽灵按钮 */
.ghost-btn {
width: 100%;
height: 48px;
border-radius: 14px;
background: #f5f5f2;
color: #1a1a1a;
font-size: 15px;
font-weight: 600;
border: none;
}
.ghost-btn:active { opacity: 0.85; }
/* 底部链接 */ /* 底部链接 */
.footer-links { .footer-links {
display: flex; display: flex;

View File

@ -60,7 +60,7 @@
</view> </view>
<view class="logout-wrap"> <view class="logout-wrap">
<button class="logout-btn" @click="logout">退出登录</button> <button class="app-btn-main app-btn-danger-outline" @click="logout">退出登录</button>
</view> </view>
<TabBar current-page="mine" @change="goPage" /> <TabBar current-page="mine" @change="goPage" />
</view> </view>
@ -235,18 +235,4 @@ const logout = () => {
.logout-wrap { .logout-wrap {
padding: 24px 16px; padding: 24px 16px;
} }
.logout-btn {
width: 100%;
height: 46px;
border-radius: 14px;
background: #fff;
color: #e5534b;
font-size: 15px;
font-weight: 600;
border: 1.5px solid #f0e0de;
display: flex;
align-items: center;
justify-content: center;
}
.logout-btn:active { opacity: 0.85; }
</style> </style>

View File

@ -77,28 +77,28 @@
</view> </view>
<scroll-view scroll-y class="pet-form-body"> <scroll-view scroll-y class="pet-form-body">
<view class="c-field"> <view class="c-field">
<text class="c-label">名字</text> <text class="app-form-label">名字</text>
<input v-model="form.name" class="c-input" placeholder="宠物名字" maxlength="32" /> <input v-model="form.name" class="app-form-input" placeholder="宠物名字" maxlength="32" />
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="c-label">类型</text> <text class="app-form-label">类型</text>
<picker mode="selector" :range="petTypes" range-key="label" @change="onPetTypeChange"> <picker mode="selector" :range="petTypes" range-key="label" @change="onPetTypeChange">
<view class="c-input c-picker"> <view class="app-form-input app-form-picker">
<text :class="{ 'c-placeholder': !form.petType }">{{ form.petType || '请选择' }}</text> <text :class="{ 'app-form-placeholder': !form.petType }">{{ form.petType || '请选择' }}</text>
<text class="c-arrow"></text> <text class="app-form-arrow"></text>
</view> </view>
</picker> </picker>
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="c-label">品种可选</text> <text class="app-form-label">品种可选</text>
<input v-model="form.breed" class="c-input" placeholder="如:金毛" maxlength="32" /> <input v-model="form.breed" class="app-form-input" placeholder="如:金毛" maxlength="32" />
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="c-label">备注可选</text> <text class="app-form-label">备注可选</text>
<input v-model="form.remark" class="c-input" placeholder="习性、注意事项等" maxlength="200" /> <input v-model="form.remark" class="app-form-input" placeholder="习性、注意事项等" maxlength="200" />
</view> </view>
<view class="c-field"> <view class="c-field">
<text class="c-label">头像</text> <text class="app-form-label">头像</text>
<view class="avatar-tap" @click="pickAvatar"> <view class="avatar-tap" @click="pickAvatar">
<image v-if="form.avatarDisplay" :src="form.avatarDisplay" class="avatar-prev" mode="aspectFill" /> <image v-if="form.avatarDisplay" :src="form.avatarDisplay" class="avatar-prev" mode="aspectFill" />
<text v-else class="avatar-tap-text">点击上传</text> <text v-else class="avatar-tap-text">点击上传</text>
@ -106,7 +106,7 @@
</view> </view>
</scroll-view> </scroll-view>
<view class="popup-footer"> <view class="popup-footer">
<button class="c-submit" :loading="saving" @click="savePet">{{ saving ? '保存中' : '保存' }}</button> <button class="app-btn-main app-btn-primary" :loading="saving" @click="savePet">{{ saving ? '保存中' : '保存' }}</button>
</view> </view>
</view> </view>
</view> </view>
@ -515,29 +515,6 @@ onShow(() => loadPets())
.popup-footer { padding: 12px 20px max(16px, env(safe-area-inset-bottom)); border-top: 1px solid #f0f0f0; } .popup-footer { padding: 12px 20px max(16px, env(safe-area-inset-bottom)); border-top: 1px solid #f0f0f0; }
.c-field { margin-bottom: 14px; } .c-field { margin-bottom: 14px; }
.c-label { font-size: 13px; font-weight: 600; color: #666660; display: block; margin-bottom: 6px; }
.c-input {
background: #f5f5f2;
border-radius: 12px;
padding: 11px 14px;
font-size: 15px;
width: 100%;
box-sizing: border-box;
min-height: 44px;
}
.c-picker { display: flex; justify-content: space-between; align-items: center; }
.c-placeholder { color: #bbbbb5; }
.c-arrow { color: #bbbbb5; }
.c-submit {
width: 100%;
height: 48px;
border-radius: 14px;
background: #1a1a1a;
color: #fff;
font-size: 16px;
font-weight: 700;
border: none;
}
.avatar-tap { .avatar-tap {
width: 72px; width: 72px;
height: 72px; height: 72px;

View File

@ -15,24 +15,24 @@
</view> </view>
<view v-show="!basicInfoCollapsed"> <view v-show="!basicInfoCollapsed">
<view class="form-row"> <view class="form-row">
<text class="form-label">宠物名字</text> <text class="app-form-label">宠物名字</text>
<input v-model="report.petName" class="form-input" placeholder="请输入宠物名字" /> <input v-model="report.petName" class="app-form-input" placeholder="请输入宠物名字" />
</view> </view>
<view class="form-row"> <view class="form-row">
<text class="form-label">服务类型</text> <text class="app-form-label">服务类型</text>
<picker mode="selector" :range="serviceColumns" range-key="label" @change="onServiceConfirm"> <picker mode="selector" :range="serviceColumns" range-key="label" @change="onServiceConfirm">
<view class="form-input form-picker"> <view class="app-form-input app-form-picker">
<text :class="{ placeholder: !report.serviceType }">{{ report.serviceType || '请选择' }}</text> <text :class="{ 'app-form-placeholder': !report.serviceType }">{{ report.serviceType || '请选择' }}</text>
<text class="picker-arrow"></text> <text class="app-form-arrow"></text>
</view> </view>
</picker> </picker>
</view> </view>
<view class="form-row"> <view class="form-row">
<text class="form-label">服务时间</text> <text class="app-form-label">服务时间</text>
<view class="time-row"> <view class="time-row">
<picker mode="date" :value="serviceDate" @change="onServiceDateChange" class="time-picker"> <picker mode="date" :value="serviceDate" @change="onServiceDateChange" class="time-picker">
<view class="form-input form-picker"> <view class="app-form-input app-form-picker">
<text :class="{ placeholder: !serviceDate }">{{ serviceDate || '日期' }}</text> <text :class="{ 'app-form-placeholder': !serviceDate }">{{ serviceDate || '日期' }}</text>
</view> </view>
</picker> </picker>
<picker <picker
@ -42,8 +42,8 @@
@change="onServiceTimeChange" @change="onServiceTimeChange"
class="time-picker" class="time-picker"
> >
<view class="form-input form-picker"> <view class="app-form-input app-form-picker">
<text :class="{ placeholder: !serviceTimePart }">{{ serviceTimePart || '时间' }}</text> <text :class="{ 'app-form-placeholder': !serviceTimePart }">{{ serviceTimePart || '时间' }}</text>
</view> </view>
</picker> </picker>
</view> </view>
@ -135,7 +135,7 @@
</view> </view>
<textarea <textarea
v-model="report.remark" v-model="report.remark"
class="form-textarea" class="app-form-textarea"
placeholder="服务细节、注意事项(可选)" placeholder="服务细节、注意事项(可选)"
@focus="onRemarkFocus" @focus="onRemarkFocus"
@blur="onRemarkBlur" @blur="onRemarkBlur"
@ -149,7 +149,7 @@
:class="{ 'submit-fixed--scrolled': hasScrolled }" :class="{ 'submit-fixed--scrolled': hasScrolled }"
:style="submitFixedStyle" :style="submitFixedStyle"
> >
<button class="submit-btn submit-btn--fixed" :disabled="submitting" @click="submitReport"> <button class="app-btn-main app-btn-primary submit-btn--fixed" :disabled="submitting" @click="submitReport">
{{ submitting ? '提交中…' : '生成报告' }} {{ submitting ? '提交中…' : '生成报告' }}
</button> </button>
</view> </view>
@ -546,35 +546,6 @@ const onRemarkBlur = () => {
margin-bottom: 14px; margin-bottom: 14px;
} }
.form-row:last-child { margin-bottom: 0; } .form-row:last-child { margin-bottom: 0; }
.form-label {
font-size: 13px;
font-weight: 600;
color: #666660;
margin-bottom: 6px;
display: block;
}
.form-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;
}
.form-input:focus {
border-color: #2db96d;
background: #fff;
}
.form-picker {
display: flex;
align-items: center;
justify-content: space-between;
}
.placeholder { color: #bbbbb5; }
.picker-arrow { color: #bbbbb5; font-size: 18px; font-weight: 300; }
.time-row { .time-row {
display: flex; display: flex;
gap: 10px; gap: 10px;
@ -701,37 +672,6 @@ const onRemarkBlur = () => {
} }
/* 备注 */ /* 备注 */
.form-textarea {
background: #f5f5f2;
border: 1.5px solid transparent;
border-radius: 12px;
padding: 12px 14px;
font-size: 14px;
color: #1a1a1a;
width: 100%;
box-sizing: border-box;
min-height: 100px;
line-height: 1.6;
}
.form-textarea:focus {
border-color: #2db96d;
background: #fff;
}
/* 提交按钮 */
.submit-btn {
width: 100%;
height: 48px;
border-radius: 14px;
background: #1a1a1a;
color: #fff;
font-size: 16px;
font-weight: 700;
border: none;
margin-top: 0;
letter-spacing: 0.5px;
transition: opacity 0.15s;
}
.submit-fixed { .submit-fixed {
position: fixed; position: fixed;
left: 0; left: 0;
@ -750,8 +690,6 @@ const onRemarkBlur = () => {
.submit-btn--fixed { .submit-btn--fixed {
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.1); box-shadow: 0 5px 16px rgba(0, 0, 0, 0.1);
} }
.submit-btn:active { opacity: 0.85; }
.submit-btn[disabled] { background: #ddddd8; color: #999993; }
/* Loading */ /* Loading */
.loading-mask { .loading-mask {