Commit 0b74decd by chunhong.mu

refactor(qqqf-mp): 初始化小程序基础工程并新增核心页面

1.  重构App.vue,简化模板结构并引入全局样式
2.  替换全量环境变量配置,适配多环境打包
3.  新增登录、设置、个人资料等核心业务页面
4.  搭建通用UI组件库与工具类样式系统
parent ab9ee8ee
VITE_API_BASE_URL=https://dev-api.example.com
VITE_ENV=development
APP_ENV=dev
NODE_ENV=development
VITE_APP_ENV=dev
VITE_APP_ID=dev_app_id
VITE_DEBUG=true
VITE_APP_PAY_URL="https://payment-api.teammix.com"
VITE_APP_WWW_WORK_APP_URL = "http://106.120.107.150:5000"
VITE_APP_API_WORK_APP_URL = "http://106.120.107.150:8090"
VITE_APP_API_WORK_ORG_URL = "http://106.120.107.150:7771"
VITE_APP_USER_CENTER = "http://106.120.107.150:8080"
VITE_APP_UNIPLAT = "http://hro.test-api.qqxb.jinsehuaqin.com:8800"
VITE_APP_QQXB = "http://test-qqxb-h5.hrs100.com"
VITE_APP_H5_URL = "https://static.qinqinxiaobao.com/flb-mp"
VITE_APP_LAND_PAGE = "https://static.qinqinxiaobao.com/flb-mp"
VITE_APP_BJRSY_COLLECTION = "https://bjcjtest.e-tecsun.com"
VITE_APP_XB_URL="http://test-qqxb-h5.hrs100.com"
VITE_APP_CLIENT_ID = "hrs100_wechat_mini_app"
VITE_APP_UNIPLAT_WEBSOCKET_URI = "ws://hro.channel.jinsehuaqin.com:8080/ws"
VITE_APP_CLIENT_SECRET = "123456"
VITE_APP_LOG_ENV = 0
VITE_API_BASE_URL=https://api.example.com
VITE_ENV=production
APP_ENV=production
NODE_ENV=production
VITE_APP_ENV=prod
VITE_DEBUG=false
VITE_APP_PAY_URL="https://payment-api.teammix.com"
VITE_APP_WWW_WORK_APP_URL = "https://passport.teammix.com"
VITE_APP_API_WORK_APP_URL = "https://userapi.teammix.com"
VITE_APP_USER_CENTER = "http://tmxlogin.teammix.com"
VITE_APP_UNIPLAT = "https://api-hro.qinqinxiaobao.com"
VITE_APP_QQXB = "http://test-qqxb-h5.hrs100.com"
VITE_APP_H5_URL = "https://static.qinqinxiaobao.com/flb-mp"
VITE_APP_LAND_PAGE = "https://static.qinqinxiaobao.com/flb-mp"
VITE_APP_UNIPLAT_WEBSOCKET_URI = "wss://channel.qinqinxiaobao.com/ws"
VITE_APP_XB_URL="https://qqxb-h5.qinqinxiaobao.com"
VITE_APP_CLIENT_ID = "hrs100_wechat_mini_app"
VITE_APP_CLIENT_SECRET = "qqxb#teammix#2019"
VITE_APP_LOG_ENV = 1
VITE_API_BASE_URL=https://staging-api.example.com
VITE_ENV=staging
APP_ENV=staging
NODE_ENV=production
VITE_APP_ENV=pre
VITE_APP_ID=pre_app_id
VITE_DEBUG=true
VITE_APP_PAY_URL="https://payment-api.teammix.com"
VITE_APP_WWW_WORK_APP_URL = "https://pre-passport.teammix.com"
VITE_APP_API_WORK_APP_URL = "https://pre-userapi.teammix.com"
VITE_APP_API_WORK_ORG_URL = "http://106.120.107.150:7771"
VITE_APP_USER_CENTER = "https://pre-user.teammix.com"
VITE_APP_UNIPLAT = "https://pre-api-hro.qinqinxiaobao.com"
VITE_APP_QQXB = "http://test-qqxb-h5.hrs100.com"
VITE_APP_H5_URL = "https://static.qinqinxiaobao.com/flb-mp"
VITE_APP_LAND_PAGE = "https://static.qinqinxiaobao.com/flb-mp"
VITE_APP_XB_URL="https://pre-qqxb-h5.hrs100.com"
VITE_APP_FLB_URL = "http://pre-flb-h5.hrs100.com"
VITE_APP_CLIENT_ID = "qqxb_mini_app"
VITE_APP_UNIPLAT_WEBSOCKET_URI = "wss://pre-channel.qinqinxiaobao.com/ws"
VITE_APP_CLIENT_SECRET = "qqxb#teammix#2019"
VITE_APP_LOG_ENV = 3
VITE_API_BASE_URL=https://test-api.example.com
VITE_ENV=test
APP_ENV=test
NODE_ENV=production
VITE_APP_ENV=test
VITE_APP_ID=test_app_id
VITE_DEBUG=true
VITE_APP_PAY_URL="https://payment-api.teammix.com"
VITE_APP_WWW_WORK_APP_URL = "http://106.120.107.150:5000"
VITE_APP_API_WORK_APP_URL = "http://106.120.107.150:8090"
VITE_APP_API_WORK_ORG_URL = "http://106.120.107.150:7771"
VITE_APP_USER_CENTER = "http://106.120.107.150:8080"
VITE_APP_UNIPLAT = "http://hro.test-api.qqxb.jinsehuaqin.com:8800"
VITE_APP_QQXB = "http://test-qqxb-h5.hrs100.com"
VITE_APP_H5_URL = "https://static.qinqinxiaobao.com/flb-mp"
VITE_APP_LAND_PAGE = "https://static.qinqinxiaobao.com/flb-mp"
VITE_APP_BJRSY_COLLECTION = "https://bjcjtest.e-tecsun.com"
VITE_APP_XB_URL="http://test-qqxb-h5.hrs100.com"
VITE_APP_CLIENT_ID = "hrs100_wechat_mini_app"
VITE_APP_UNIPLAT_WEBSOCKET_URI = "ws://hro.channel.jinsehuaqin.com:8080/ws"
VITE_APP_CLIENT_SECRET = "123456"
VITE_APP_LOG_ENV = 0
<template>
<view class="app-container">
<slot />
</view>
</template>
<script setup lang="ts">
<script lang="ts" setup>
import { onLaunch } from "@dcloudio/uni-app"
onLaunch(() => {
......@@ -13,9 +7,10 @@ onLaunch(() => {
</script>
<style lang="scss">
.app-container {
width: 100%;
min-height: 100vh;
background-color: #f5f7fa;
@import "@/assets/styles/common.scss";
@import "@/assets/styles/style.scss";
view {
background-repeat: no-repeat;
}
</style>
// SCSS 变量定义
$radius-size: 10rpx !default;
$u-bg-color: #f5f7fa !default;
$bg-white: #ffffff !default;
$u-content-color: #666666 !default;
.w-100 {
min-width: 100%;
}
.h-100 {
min-height: 100%;
}
.w-h-100 {
width: 100%;
height: 100%;
}
view,
scroll-view,
swiper,
button,
input,
textarea,
label,
navigator,
image {
box-sizing: border-box;
}
.radius {
border-radius: $radius-size;
}
.common-bg-color {
background-color: $u-bg-color;
}
.bg-white {
background-color: $bg-white;
}
.shadow {
background: $bg-white;
box-shadow: 0rpx 0rpx 6rpx 0rpx rgba(0, 0, 0, 0.1);
}
.shadow2 {
background: $bg-white;
box-shadow: 0rpx 0rpx 16rpx 0rpx rgba(0, 0, 0, 0.03);
}
.font-weight-4 {
font-weight: 400;
}
.font-weight-5 {
font-weight: 500;
}
.font-weight-6 {
font-weight: 600;
}
.common-btn-width {
width: 400rpx;
}
.modal-btn-width {
width: 240rpx;
}
.modal-content {
text-align: center;
color: $u-content-color;
font-size: 30rpx;
padding: 0 30rpx 50rpx 30rpx;
}
.section-title {
padding-top: 38rpx;
padding-bottom: 30rpx;
font-size: 36rpx;
font-weight: 600;
}
.item-title {
font-size: 32rpx;
font-weight: 600;
}
.section-form-title {
font-size: 36rpx;
font-weight: 600;
padding: 38rpx 40rpx;
margin: 0;
background-color: $u-bg-color;
}
/* ==================
轮播 重写
==================== */
swiper .wx-swiper-dots,
swiper .a-swiper-dots,
swiper .uni-swiper-dots {
bottom: 40rpx;
}
swiper .wx-swiper-dot,
swiper .a-swiper-dot,
swiper .uni-swiper-dot {
width: 20rpx;
height: 20rpx;
margin: 0 10rpx;
}
swiper.left-dots .wx-swiper-dots,
swiper.left-dots .a-swiper-dots,
swiper.left-dots .uni-swiper-dots {
left: 30rpx;
transform: none;
}
/* ==================
轮播 end
==================== */
.common-arrow {
margin-left: 6rpx;
transition: transform 0.3s;
align-items: center;
&.rotate {
transform: rotate(180deg);
}
}
.chat-item-width {
max-width: 500rpx;
}
/* ==================
弹框
==================== */
.common-dialog {
.common-dialog-title {
font-size: 34rpx;
font-weight: 500;
color: #333333;
line-height: 48rpx;
padding: 40rpx 40rpx 0 40rpx;
}
.common-dialog-content {
padding: 20rpx 40rpx;
font-size: 30rpx;
text-align: center;
color: #333333;
line-height: 40rpx;
&.left {
text-align: left;
}
}
.common-dialog-confirm {
text-align: center;
font-size: 34rpx;
line-height: 34rpx;
border-top: 1rpx solid #e1e1e1;
padding: 32rpx;
margin-top: 20rpx;
min-width: 300rpx;
width: 100%;
&.close {
color: #333333;
border-right: 1rpx solid #e1e1e1;
}
&.sure {
color: #22bd7a;
}
&.disable {
color: #dddddd;
}
}
}
/* 适配 iPhoneX */
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
.safe-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}
// title
.slot-wrap {
display: flex;
align-items: center;
flex: 1;
.nav-left {
padding: 0 16rpx;
height: 88rpx;
}
}
.w-100 {
min-width: 100%;
}
.h-100 {
min-height: 100%;
}
.w-h-100 {
width: 100%;
height: 100%;
}
/* start--文本行数限制--start */
.u-line-1 {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.u-line-2 {
-webkit-line-clamp: 2;
}
.u-line-3 {
-webkit-line-clamp: 3;
}
.u-line-4 {
-webkit-line-clamp: 4;
}
.u-line-5 {
-webkit-line-clamp: 5;
}
.u-line-2,
.u-line-3,
.u-line-4,
.u-line-5 {
overflow: hidden;
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box; // 弹性伸缩盒
-webkit-box-orient: vertical; // 设置伸缩盒子元素排列方式
}
/* end--文本行数限制--end */
.u-relative,
.u-rela {
position: relative;
}
.u-absolute,
.u-abso {
position: absolute;
}
// nvue不能用标签命名样式,不能放在微信组件中,否则微信开发工具会报警告,无法使用标签名当做选择器
/* #ifndef APP-NVUE */
image {
display: inline-block;
}
// 在weex,也即nvue中,所有元素默认为border-box
view,
text {
box-sizing: border-box;
}
/* #endif */
.u-font-xs {
font-size: 22rpx;
}
.u-font-sm {
font-size: 26rpx;
}
.u-font-md {
font-size: 28rpx;
}
.u-font-lg {
font-size: 30rpx;
}
.u-font-xl {
font-size: 34rpx;
}
.u-flex {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
}
.u-flex-default {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
}
.u-shrink-0 {
flex-shrink: 0;
}
.u-flex-wrap {
flex-wrap: wrap;
}
.u-flex-nowrap {
flex-wrap: nowrap;
}
.u-flex-none {
flex: none;
}
.u-col-center {
align-items: center;
}
.u-col-top {
align-items: flex-start;
}
.u-col-bottom {
align-items: flex-end;
}
.u-col-stretch {
align-items: stretch;
}
.u-row-center {
justify-content: center;
}
.u-row-left {
justify-content: flex-start;
}
.u-row-right {
justify-content: flex-end;
}
.u-row-between {
justify-content: space-between;
}
.u-row-around {
justify-content: space-around;
}
.u-row-evenly {
justify-content: space-evenly;
}
.u-text-left {
text-align: left;
}
.u-text-center {
text-align: center;
}
.u-text-right {
text-align: right;
}
.u-flex-col {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
}
// 定义flex等分
@for $i from 0 through 12 {
.u-flex-#{$i} {
flex: $i;
}
}
// 定义字体(px)单位,小于20都为px单位字体
@for $i from 9 to 20 {
.u-font-#{$i} {
font-size: $i + px;
}
}
// 定义字体(rpx)单位,大于或等于20的都为rpx单位字体
@for $i from 20 through 40 {
.u-font-#{$i} {
font-size: $i + rpx;
}
}
// 定义内外边距,历遍1-80
@for $i from 0 through 80 {
.u-p-x-#{$i} {
padding-left: $i + rpx !important;
padding-right: $i + rpx !important;
}
.u-p-y-#{$i} {
padding-top: $i + rpx !important;
padding-bottom: $i + rpx !important;
}
.u-m-x-#{$i} {
margin-left: $i + rpx !important;
margin-right: $i + rpx !important;
}
.u-m-y-#{$i} {
margin-top: $i + rpx !important;
margin-bottom: $i + rpx !important;
}
.u-gap-#{$i} {
gap: $i + rpx !important;
}
// 只要双数和能被5除尽的数
@if $i % 2==0 or $i % 5==0 {
// 得出:u-margin-30或者u-m-30
.u-m-#{$i} {
margin: $i + rpx !important;
}
// 得出:u-padding-30或者u-p-30
.u-p-#{$i} {
padding: $i + rpx !important;
}
@each $short, $long in l left, t top, r right, b bottom {
// 缩写版,结果如: u-m-l-30
// 定义外边距
.u-m-#{$short}-#{$i} {
margin-#{$long}: $i + rpx !important;
}
// 定义内边距
.u-p-#{$short}-#{$i} {
padding-#{$long}: $i + rpx !important;
}
}
}
}
// 重置nvue的默认关于flex的样式
.u-reset-nvue {
flex-direction: row;
align-items: center;
}
// 重置drop-down标题样式
.u-dropdown__menu__item.u-dropdown__menu__item__gap {
border: none !important;
position: relative;
}
.u-dropdown__menu__item:not(:last-child) .after {
width: 1rpx;
height: 40rpx;
position: absolute;
right: 0;
}
// 下拉框背景色
.u-dropdown-item {
background-color: #fff;
}
.c-page {
min-height: 100vh;
background-color: #f5f7fa;
.footer {
position: fixed;
bottom: env(safe-area-inset-bottom);
left: 0;
width: 750rpx;
}
}
.c-btn {
display: flex;
align-items: center;
justify-content: center;
border-radius: 100rpx;
background: var(--Primary-6, #2f54eb);
color: #fff;
font-size: 34rpx;
cursor: pointer;
&.plain {
border-radius: 100rpx;
border: 1px solid var(--Primary-6, #2f54eb);
background: var(--Primary-6, #fff);
color: var(--Primary-6, #2f54eb);
}
&.disabled {
opacity: 0.5;
}
}
.c-form {
border-radius: 10px;
background: #fff;
padding-left: 24rpx;
padding-right: 24rpx;
.c-form-item {
display: flex;
align-items: flex-start;
min-height: 104rpx;
line-height: 104rpx;
.label {
color: var(--Gray-Gray, #15151a);
font-size: 30rpx;
line-height: 100rpx;
min-height: 100rpx;
}
.c-input {
font-weight: 600;
color: var(--Gray-Gray, #15151a);
}
.c-placeholder {
font-weight: 400;
color: #bdbdc7;
}
border-top: 2rpx solid rgba(#ebebf0, 0.8);
&:nth-of-type(1) {
border-top: 2rpx solid transparent;
}
}
}
......@@ -5,6 +5,47 @@
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/settings/index",
"style": {
"navigationBarTitleText": "设置",
"navigationBarBackgroundColor": "#f5f7fa"
}
},
{
"path": "pages/profile/index",
"style": {
"navigationBarTitleText": "个人资料",
"navigationBarBackgroundColor": "#f5f7fa"
}
},
{
"path": "pages/verify/index",
"style": {
"navigationBarTitleText": "实名验证",
"navigationBarBackgroundColor": "#f5f7fa"
}
},
{
"path": "pages/password/index",
"style": {
"navigationBarTitleText": "修改密码",
"navigationBarBackgroundColor": "#f5f7fa"
}
}
],
"globalStyle": {
......
<template>
<view class="container h-100">
<view class="bg-gradient"></view>
<view class="content">
<view class="service u-flex u-row-center">
<view class="agree" @click="agree = !agree">
<view class="radio-box" :class="{ checked: agree }">
<view v-if="agree" class="radio-inner"></view>
</view>
<view class="agree-text">
我已阅读并同意
<text class="link" @click.stop="showAgreement('service')">
用户使用协议
</text>
<text class="link" @click.stop="showAgreement('privacy')">
用户隐私政策
</text>
</view>
</view>
</view>
<view class="login-btn">
<button
:disabled="!agree"
class="btn u-flex u-row-center"
open-type="getPhoneNumber"
@getphonenumber="doWxLogin"
>
<image class="wx-icon" src="/static/login/wx.svg"></image>
<view>微信用户一键登录</view>
</button>
</view>
<view class="other-login u-flex u-row-center" @click="toMobileLogin">
其他登录方式
</view>
<view class="u-flex" @click="back">
<view class="not-login">暂不登录</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { onLoad } from "@dcloudio/uni-app"
import { ref } from "vue"
const agree = ref(false)
onLoad((e: { backUrl?: string }) => {
if (e.backUrl) {
uni.setStorageSync("loginRedirect", e.backUrl)
}
})
function toMobileLogin() {
uni.navigateTo({ url: "/pages/login/login" })
}
function showAgreement(type: "service" | "privacy") {
uni.showToast({
title: type === "service" ? "用户协议" : "隐私政策",
icon: "none",
})
}
function doWxLogin(e: any) {
if (!agree.value) {
uni.showToast({ title: "需同意《服务协议》《隐私政策》", icon: "none" })
return
}
if (e.detail.errMsg.indexOf("user deny") > -1) {
uni.showToast({ title: "请点击允许", icon: "none" })
return
}
if (e.detail.errMsg.indexOf("fail") > -1) {
uni.showToast({ title: "获取手机号失败,请重试", icon: "none" })
return
}
if (!e || !e.detail || !e.detail.encryptedData) {
uni.showToast({ title: "登录失败,请重试", icon: "none" })
return
}
uni.showLoading({ title: "登录中...", mask: true })
// TODO: 调用微信登录接口
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: "登录成功", icon: "success" })
setTimeout(() => {
const redirect = uni.getStorageSync("loginRedirect") || "/pages/home/index"
uni.reLaunch({ url: redirect })
}, 1500)
}, 1500)
}
function back() {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.switchTab({ url: "/pages/home/index" })
}
}
</script>
<style lang="scss" scoped>
.container {
position: relative;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 200rpx;
overflow: hidden;
min-height: 1320rpx;
.bg-gradient {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 500rpx;
background:
linear-gradient(167deg, rgba(132, 243, 255, 0.2) -0.4%, rgba(132, 243, 255, 0) 19.03%),
radial-gradient(70.76% 50.44% at 98.67% -0.49%, rgba(201, 153, 255, 0.2) 0%, rgba(201, 153, 255, 0) 100%),
linear-gradient(180deg, #dbe7ff 0%, #ebf1ff 46.15%, #d6e4ff 100%);
}
.content {
position: absolute;
width: 750rpx;
left: 0;
bottom: env(safe-area-inset-bottom);
}
.login-btn {
.btn {
border-radius: 60rpx;
width: 327 * 2rpx;
height: 48 * 2rpx;
color: #fff;
background: var(--Primary-6, #2f54eb);
margin: auto;
font-size: 32rpx;
.wx-icon {
width: 36rpx;
height: 36rpx;
margin-right: 10rpx;
}
&::after {
border: none;
}
&[disabled] {
background: var(--Colors-Bue-4, #598bff);
}
}
}
.other-login {
border-radius: 60rpx;
width: 327 * 2rpx;
height: 48 * 2rpx;
color: var(--Gray-Gray, #15151a);
background: #fff;
margin: auto;
font-size: 32rpx;
margin-top: 30rpx;
}
.not-login {
margin: auto;
margin-top: 132rpx;
border-radius: 30px;
background: rgba(255, 255, 255, 0.6);
padding: 0 24rpx;
line-height: 60rpx;
color: var(--Gray-Gray-2, #676770);
font-size: 26rpx;
width: auto;
margin-bottom: 20rpx;
}
.service {
height: 61 * 2rpx;
.agree {
display: flex;
align-items: flex-start;
.radio-box {
flex-shrink: 0;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
margin-right: 12rpx;
margin-top: 4rpx;
display: flex;
align-items: center;
justify-content: center;
&.checked {
border-color: #598bff;
background-color: #598bff;
}
.radio-inner {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: #fff;
}
}
.agree-text {
font-size: 24rpx;
color: #999;
.link {
color: #15151a;
}
}
}
}
}
</style>
<template>
<view class="h-100 bg-white">
<view class="content">
<view class="tabs">
<view
class="tab-item"
:class="{ active: activeTab === 'sms' }"
@click="activeTab = 'sms'"
>
验证码登录
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'password' }"
@click="activeTab = 'password'"
>
密码登录
</view>
<view class="tab-line" :style="tabLineStyle"></view>
</view>
<!-- 验证码登录 -->
<view v-if="activeTab === 'sms'" class="form">
<view class="form-item">
<input
v-model="smsModel.mobile"
class="u-input"
placeholder="请输入手机号"
placeholder-style="color: #bdbdc7;"
type="number"
/>
</view>
<view class="form-item form-item-row">
<input
v-model="smsModel.imgCode"
class="u-input flex-1"
placeholder="请输入图形验证码"
placeholder-style="color: #bdbdc7;"
type="text"
/>
<view class="code-placeholder" @click="getVerifyCodeImg">
图形验证码
</view>
</view>
<view class="form-item form-item-row">
<input
v-model="smsModel.code"
class="u-input flex-1"
placeholder="请输入短信验证码"
placeholder-style="color: #bdbdc7;"
type="number"
/>
<text
class="code-text"
:class="{ active: !codeCounting }"
@click="getCode"
>
{{ codeTips }}
</text>
</view>
<view class="agree" @click="smsAgree = !smsAgree">
<view class="radio-box" :class="{ checked: smsAgree }">
<view v-if="smsAgree" class="radio-inner"></view>
</view>
<view class="agree-text">
我已阅读并同意
<text class="link" @click.stop="showAgreement('service')">
用户使用协议
</text>
<text class="link" @click.stop="showAgreement('privacy')">
用户隐私政策
</text>
</view>
</view>
<view class="login-btn">
<button class="btn" :loading="smsLoading" @click="smsLogin">
登录
</button>
</view>
</view>
<!-- 密码登录 -->
<view v-if="activeTab === 'password'" class="form">
<view class="form-item">
<input
v-model="passwordModel.username"
class="u-input"
placeholder="请输入用户名或手机号"
placeholder-style="color: #bdbdc7;"
type="text"
/>
</view>
<view class="form-item">
<input
v-model="passwordModel.password"
class="u-input"
placeholder="请输入密码"
placeholder-style="color: #bdbdc7;"
type="password"
/>
</view>
<view class="agree" @click="pwdAgree = !pwdAgree">
<view class="radio-box" :class="{ checked: pwdAgree }">
<view v-if="pwdAgree" class="radio-inner"></view>
</view>
<view class="agree-text">
我已阅读并同意
<text class="link" @click.stop="showAgreement('service')">
用户使用协议
</text>
<text class="link" @click.stop="showAgreement('privacy')">
用户隐私政策
</text>
</view>
</view>
<view class="login-btn">
<button
class="btn"
:loading="pwdLoading"
@click="passwordLogin"
>
登录
</button>
</view>
<view class="forget" @click="goForget">忘记密码</view>
</view>
</view>
<view class="u-flex mt80" @click="back">
<view class="not-login">暂不登录</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { onLoad } from "@dcloudio/uni-app"
import { computed, reactive, ref } from "vue"
const activeTab = ref<"sms" | "password">("sms")
const tabLineStyle = computed(() => {
return activeTab.value === "sms"
? { left: "0", transform: "translateX(0)" }
: { left: "230rpx", width: "128rpx" }
})
const smsModel = reactive({
mobile: "",
imgCode: "",
code: "",
})
const smsLoading = ref(false)
const smsAgree = ref(false)
const passwordModel = reactive({
username: "",
password: "",
})
const pwdLoading = ref(false)
const pwdAgree = ref(false)
const codeTips = ref("获取验证码")
const codeCounting = ref(false)
const codeCountdown = ref(60)
let codeTimer: ReturnType<typeof setInterval> | null = null
function getVerifyCodeImg() {
// TODO: 调用图形验证码接口
uni.showToast({ title: "图形验证码功能待实现", icon: "none" })
}
function startCodeCountdown() {
codeCounting.value = true
codeCountdown.value = 60
codeTips.value = `重新获取(${codeCountdown.value}s)`
codeTimer = setInterval(() => {
codeCountdown.value--
if (codeCountdown.value <= 0) {
stopCodeCountdown()
} else {
codeTips.value = `重新获取(${codeCountdown.value}s)`
}
}, 1000)
}
function stopCodeCountdown() {
if (codeTimer) {
clearInterval(codeTimer)
codeTimer = null
}
codeCounting.value = false
codeTips.value = "获取验证码"
}
function isPhone(val: string) {
return /^1[3-9]\d{9}$/.test(val)
}
function getCode() {
if (!smsModel.mobile) {
uni.showToast({ title: "请输入手机号", icon: "none" })
return
}
if (!isPhone(smsModel.mobile)) {
uni.showToast({ title: "手机号码不正确", icon: "none" })
return
}
if (!smsModel.imgCode) {
uni.showToast({ title: "请输入图形验证码", icon: "none" })
return
}
if (codeCounting.value) {
uni.showToast({ title: "倒计时结束后再发送", icon: "none" })
return
}
uni.showLoading({ title: "正在获取验证码", mask: true })
// TODO: 调用发送验证码接口
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: "验证码已发送", icon: "success" })
startCodeCountdown()
}, 1000)
}
function smsLogin() {
if (!smsAgree.value) {
uni.showToast({ title: "需同意《服务协议》《隐私政策》", icon: "none" })
return
}
if (!smsModel.mobile) {
return uni.showToast({ title: "请输入手机号", icon: "none" })
}
if (!isPhone(smsModel.mobile)) {
return uni.showToast({ title: "手机号码不正确", icon: "none" })
}
if (!smsModel.code) {
return uni.showToast({ title: "请输入验证码", icon: "none" })
}
smsLoading.value = true
// TODO: 调用验证码登录接口
setTimeout(() => {
smsLoading.value = false
uni.showToast({ title: "登录成功", icon: "success" })
setTimeout(() => {
const redirect = uni.getStorageSync("loginRedirect") || "/pages/home/index"
uni.reLaunch({ url: redirect })
}, 1500)
}, 1500)
}
function passwordLogin() {
if (!pwdAgree.value) {
uni.showToast({ title: "需同意《服务协议》《隐私政策》", icon: "none" })
return
}
if (!passwordModel.username) {
return uni.showToast({ title: "请输入用户名或手机号", icon: "none" })
}
if (!passwordModel.password) {
return uni.showToast({ title: "请输入密码", icon: "none" })
}
pwdLoading.value = true
// TODO: 调用密码登录接口
setTimeout(() => {
pwdLoading.value = false
uni.showToast({ title: "登录成功", icon: "success" })
setTimeout(() => {
const redirect = uni.getStorageSync("loginRedirect") || "/pages/home/index"
uni.reLaunch({ url: redirect })
}, 1500)
}, 1500)
}
function goForget() {
uni.navigateTo({ url: "/pages/password/index" })
}
function showAgreement(type: "service" | "privacy") {
uni.showToast({
title: type === "service" ? "用户协议" : "隐私政策",
icon: "none",
})
}
function back() {
const pages = getCurrentPages()
if (pages.length > 1) {
uni.navigateBack()
} else {
uni.switchTab({ url: "/pages/home/index" })
}
}
onLoad(() => {
uni.setNavigationBarTitle({ title: "验证码登录" })
})
</script>
<style lang="scss" scoped>
.content {
padding: 80rpx 48rpx 0rpx;
.tabs {
position: relative;
display: flex;
margin-bottom: 80rpx;
gap: 70rpx;
.tab-item {
flex: none;
text-align: center;
padding: 20rpx 0;
font-size: 32rpx;
color: var(--Gray-Gray-3, #93939e);
&.active {
color: #15151a;
font-weight: 500;
}
}
.tab-line {
position: absolute;
bottom: 0;
left: 0;
width: 160rpx;
height: 4rpx;
background-color: #2f54eb;
transition: all 0.3s;
}
}
.form {
.form-item {
height: 58 * 2rpx;
border-bottom: 1rpx solid #eee;
.u-input {
height: 58 * 2rpx;
font-size: 32rpx;
padding: 0 10rpx;
}
&.form-item-row {
display: flex;
align-items: center;
.flex-1 {
flex: 1;
}
.code-placeholder {
width: 160rpx;
height: 60rpx;
margin-left: 20rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
border-radius: 8rpx;
font-size: 24rpx;
color: #93939e;
}
.code-text {
font-size: 28rpx;
white-space: nowrap;
margin-left: 20rpx;
flex-shrink: 0;
border-radius: 34rpx;
border: 2rpx solid var(--Gray-Gray-4, #bdbdc7);
color: var(--Gray-Gray-4, #bdbdc7);
padding: 0 18rpx;
line-height: 50rpx;
&.active {
color: #2f54eb;
border: 2rpx solid #2f54eb;
}
}
}
}
}
.login-btn {
margin-top: 60rpx;
.btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #2f54eb;
color: #fff;
border-radius: 44rpx;
font-size: 32rpx;
border: none;
&::after {
border: none;
}
}
}
.agree {
margin-top: 100rpx;
display: flex;
align-items: flex-start;
.radio-box {
flex-shrink: 0;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #ccc;
margin-right: 12rpx;
margin-top: 4rpx;
display: flex;
align-items: center;
justify-content: center;
&.checked {
border-color: #2f54eb;
background-color: #2f54eb;
}
.radio-inner {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: #fff;
}
}
.agree-text {
font-size: 24rpx;
color: #999;
.link {
color: #2f54eb;
}
}
}
.forget {
text-align: right;
margin-top: 30rpx;
color: var(--Gray-Gray-2, #676770);
font-size: 28rpx;
margin-right: 16rpx;
}
}
.mt80 {
margin-top: 80rpx;
}
.not-login {
margin: auto;
border-radius: 30px;
background: rgba(255, 255, 255, 0.6);
padding: 0 24rpx;
line-height: 60rpx;
color: var(--Gray-Gray-2, #676770);
font-size: 26rpx;
width: auto;
margin-bottom: 20rpx;
border: 1px solid var(--Gray-Gray-5, #e3e3e8);
}
</style>
<template>
<view class="c-page">
<view class="content">
<view class="c-form">
<view class="c-form-item vc-item">
<view class="u-flex">
<view class="label"> 手机号码 </view>
<view v-if="sdkLogin" class="value mobile">
{{ desensitization(userInfo.mobile) }}
</view>
<view v-else class="value mobile2 u-flex">
<view class="u-flex">
<input
v-model="phone"
class="c-input"
placeholder="请输入手机号"
placeholder-class="c-placeholder"
/>
</view>
</view>
</view>
<view v-show="warn.phone" class="warn">
请输入正确的手机号
</view>
</view>
<view class="c-form-item vc-item">
<view class="u-flex">
<view class="label"> 图形验证码</view>
<view class="value u-flex u-row-between u-flex-1">
<view>
<input
v-model="imgVerifyCode"
class="c-input"
placeholder="请输入图形验证码"
placeholder-class="c-placeholder"
/>
</view>
<view class="vc u-flex-none u-flex">
<image
:src="imageCodeModel && imageCodeModel.img"
mode="aspectFit"
class="code-img"
@click="getVerifyCodeImg"
/>
</view>
</view>
</view>
<view v-show="warn.img" class="warn">
请输入正确的图形验证码
</view>
</view>
<view class="c-form-item vc-item">
<view class="u-flex">
<view class="label"> 验证码</view>
<view class="value u-flex u-row-between u-flex-1">
<view>
<input
v-model="verifyCode"
class="c-input"
maxlength="6"
type="numeric"
placeholder="请输入验证码"
placeholder-class="c-placeholder"
/>
</view>
<view
class="vc u-flex-none"
:class="{ sended }"
@click="getCode"
>
{{ vc }}
</view>
</view>
</view>
<view v-show="warn.code" class="warn">
请输入正确的6位数字验证码
</view>
</view>
<view class="c-form-item vc-item">
<view class="u-flex">
<view class="label"> 新密码</view>
<view class="value u-flex u-row-between u-flex-1">
<view>
<input
v-model="pwd.pwd1.value"
class="c-input"
placeholder="请设置新密码"
placeholder-class="c-placeholder"
:password="!pwd.pwd1.show"
/>
</view>
<view
class="u-flex-none"
@click="pwd.pwd1.show = !pwd.pwd1.show"
>
<image
:src="pwd.pwd1.show ? eyeOpenIcon : eyeCloseIcon"
class="eye"
></image>
</view>
</view>
</view>
<view v-show="warn.pwd1" class="warn">
密码需超过8位,且包含数字、大小写字母、特殊字符中至少三种
</view>
</view>
<view class="c-form-item vc-item">
<view class="u-flex">
<view class="label"> 确认密码 </view>
<view class="value u-flex u-row-between u-flex-1">
<view>
<input
v-model="pwd.pwd2.value"
class="c-input"
placeholder="再次确认新密码"
placeholder-class="c-placeholder"
:password="!pwd.pwd2.show"
/>
</view>
<view
class="u-flex-none"
@click="pwd.pwd2.show = !pwd.pwd2.show"
>
<image
:src="pwd.pwd2.show ? eyeOpenIcon : eyeCloseIcon"
class="eye"
></image>
</view>
</view>
</view>
<view v-show="warn.pwd2" class="warn">
两次密码输入不同
</view>
</view>
</view>
</view>
<view class="footer u-flex u-row-center" @click="confirm">
<view class="c-btn" :class="{ disabled: !isFormValid }"> 确认修改 </view>
</view>
</view>
</template>
<script lang="ts" setup>
import { onLoad } from "@dcloudio/uni-app"
import { computed, ref } from "vue"
// 模拟数据
const userInfo = ref({ mobile: "13800138000" })
const sdkLogin = ref(true)
const phone = ref("")
const imgVerifyCode = ref("")
const verifyCode = ref("")
const imageCodeModel = ref<{ seed: number; img: string } | null>(null)
const sended = ref(false)
const warn = ref({
phone: false,
img: false,
code: false,
pwd1: false,
pwd2: false,
})
const eyeOpenIcon = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTAgMTJDMTEuMTA0NiAxMiAxMiAxMS4xMDQ2IDEyIDEwQzEyIDguODk1NDMgMTEuMTA0NiA4IDEwIDhDOC44OTU0MyA4IDggOC44OTU0MyA4IDEwQzggMTEuMTA0NiA4Ljg5NTQzIDEyIDEwIDEyWiIgZmlsbD0iIzY3Njc3MCIvPjxwYXRoIGQ9Ik0xMCAxNUMxMi4yMDkxIDE1IDE0IDEzLjIwOTEgMTQgMTFDMTQgOC43OTA4NiAxMi4yMDkxIDcgMTAgN0M3Ljc5MDg2IDcgNiA4Ljc5MDg2IDYgMTFDNiAxMy4yMDkxIDcuNzkwODYgMTUgMTAgMTVaIiBzdHJva2U9IiM2NzY3NzAiIHN0cm9rZS13aWR0aD0iMS41Ii8+PC9zdmc+"
const eyeCloseIcon = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMyAzTDE3IDE3IiBzdHJva2U9IiM2NzY3NzAiIHN0cm9rZS13aWR0aD0iMS41Ii8+PHBhdGggZD0iTTEwIDEyQzExLjEwNDYgMTIgMTIgMTEuMTA0NiAxMiAxMEMxMiA4Ljg5NTQzIDExLjEwNDYgOCAxMCA4QzguODk1NDMgOCA4IDguODk1NDMgOCAxMEM4IDExLjEwNDYgOC44OTU0MyAxMiAxMCAxMloiIGZpbGw9IiM2NzY3NzAiLz48cGF0aCBkPSJNMTAgMTVDMTIuMjA5MSAxNSAxNCAxMy4yMDkxIDE0IDExQzE0IDguNzkwODYgMTIuMjA5MSA3IDEwIDdDNy43OTA4NiA3IDYgOC43OTA4NiA2IDExQzYgMTMuMjA5MSA3Ljc5MDg2IDE1IDEwIDE1WiIgc3Ryb2tlPSIjNjc2NzcwIiBzdHJva2Utd2lkdGg9IjEuNSIvPjwvc3ZnPg=="
function getVerifyCodeImg() {
// TODO: 调用图形验证码接口
imageCodeModel.value = { seed: 123, img: "" }
}
getVerifyCodeImg()
const pwd = ref({
pwd1: { value: "", show: false },
pwd2: { value: "", show: false },
})
const codeCounting = ref(false)
const codeCountdown = ref(60)
let codeTimer: ReturnType<typeof setInterval> | null = null
function getCode() {
if (!imgVerifyCode.value) {
uni.showToast({ title: "请输入图形验证码", icon: "none" })
return
}
if (codeCounting.value) {
uni.showToast({ title: "倒计时结束后再发送", icon: "none" })
return
}
uni.showLoading({ title: "正在获取验证码", mask: true })
// TODO: 调用发送验证码接口
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: "验证码已发送", icon: "success" })
warn.value.img = false
startCodeCountdown()
}, 1000)
}
function startCodeCountdown() {
codeCounting.value = true
codeCountdown.value = 60
sended.value = true
codeTimer = setInterval(() => {
codeCountdown.value--
if (codeCountdown.value <= 0) {
stopCodeCountdown()
}
}, 1000)
}
function stopCodeCountdown() {
if (codeTimer) {
clearInterval(codeTimer)
codeTimer = null
}
codeCounting.value = false
sended.value = false
}
const vc = computed(() => {
if (!sended.value) {
return "获取验证码"
}
return `${codeCountdown.value}s后重新获取`
})
const reg4Password = /^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_]+$)(?![a-z0-9]+$)(?![a-z\W_]+$)(?![0-9\W_]+$)[\s\S]+$/
const isFormValid = computed(() => {
const phoneValid = sdkLogin.value || phone.value
return phoneValid && imgVerifyCode.value && verifyCode.value && pwd.value.pwd1.value && pwd.value.pwd2.value
})
function isPhone(val: string) {
return /^1[3-9]\d{9}$/.test(val)
}
function desensitization(str: string) {
if (!str) return ""
if (str.length <= 6) return str
return str.slice(0, 3) + "****" + str.slice(-4)
}
function confirm() {
if (!sdkLogin.value && !isPhone(phone.value)) {
warn.value.phone = true
return
}
warn.value.phone = false
if (!imgVerifyCode.value) {
warn.value.img = true
return
}
warn.value.img = false
if (!verifyCode.value) {
warn.value.code = true
return
}
warn.value.code = false
if (!reg4Password.test(pwd.value.pwd1.value) || pwd.value.pwd1.value.length < 8) {
warn.value.pwd1 = true
return
}
warn.value.pwd1 = false
if (pwd.value.pwd1.value !== pwd.value.pwd2.value) {
warn.value.pwd2 = true
return
}
warn.value.pwd2 = false
uni.showLoading({ title: "修改中..." })
// TODO: 调用重置密码接口
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: "密码重置成功", icon: "success" })
setTimeout(() => {
uni.navigateTo({ url: "/pages/home/index" })
}, 2000)
}, 1500)
}
onLoad(() => {
if (!sdkLogin.value) {
uni.setNavigationBarTitle({ title: "忘记密码" })
}
})
</script>
<style lang="scss" scoped>
.content {
padding: 24rpx;
.c-form {
overflow: hidden;
}
.eye {
width: 32rpx;
height: 32rpx;
}
.code-img {
width: 160rpx;
height: 60rpx;
margin-left: 20rpx;
flex-shrink: 0;
}
.vc-item {
display: block;
}
.label {
width: 200rpx;
flex: none;
}
.value {
width: 100%;
color: var(--Gray-Gray, #15151a);
font-family: "PingFang SC";
font-size: 30rpx;
&.mobile {
opacity: 0.8;
font-weight: 500;
}
&.mobile2 {
height: 100rpx;
}
.vc {
color: var(--Primary-6, #2f54eb);
&.sended {
color: var(--Gray-Gray-3, #93939e);
}
}
}
.warn {
line-height: 56rpx;
border-radius: 4px;
background: #fef2f4;
color: #f3362c;
font-size: 24rpx;
padding-left: 20rpx;
margin-bottom: 30rpx;
}
}
.footer {
padding: 28rpx 0;
.c-btn {
height: 88rpx;
width: 700rpx;
}
}
</style>
<template>
<view class="c-page">
<view v-if="userInfo" class="content">
<view class="c-form u-p-b-24">
<view
v-for="item in form"
:key="item.prop"
class="c-form-item u-flex u-row-between u-col-top"
>
<view class="label"> {{ item.label }} </view>
<view class="u-flex-1">
<view v-if="item.disabled" class="value">
{{
item.desensitization
? desensitization(getInfo(item.prop))
: getInfo(item.prop)
}}
</view>
<view
v-else-if="item.prop === 'identityType'"
class="identity-type"
>
<view
v-for="i in idType"
:key="i.value"
class="id-type"
:class="{ active: curIdType === i.value }"
@click="curIdType = i.value"
>
{{ i.label }}
</view>
</view>
</view>
</view>
</view>
</view>
<view class="footer u-flex u-row-center" @click="confirm">
<view class="c-btn"> 确认 </view>
</view>
</view>
</template>
<script setup lang="ts">
import { onLoad } from "@dcloudio/uni-app"
import { get } from "lodash"
import { ref } from "vue"
// 模拟用户信息(实际应从 store 获取)
const userInfo = ref<any>({
realname: "张三",
mobile: "13800138000",
user_info: {
idCard: "110101199001011234",
},
})
const form = ref([
{
label: "姓名",
prop: "realname",
disabled: true,
},
{
label: "手机号",
prop: "mobile",
disabled: true,
desensitization: true,
},
{
label: "身份证号",
prop: "user_info.idCard",
disabled: true,
desensitization: true,
},
{
label: "所在城市",
prop: "city",
},
{
label: "身份类别",
prop: "identityType",
},
])
const idType = ref([
{ label: "HR", value: 1 },
{ label: "财务", value: 2 },
{ label: "行政", value: 3 },
{ label: "法务", value: 4 },
{ label: "其他", value: 5 },
])
function getInfo(prop: string) {
return get(userInfo.value, prop) || ""
}
function desensitization(str: string) {
if (!str) return ""
if (str.length <= 6) return str
return str.slice(0, 3) + "****" + str.slice(-4)
}
const curIdType = ref<number | null>(null)
function confirm() {
console.log("confirm", curIdType.value)
uni.showToast({ title: "保存成功", icon: "success" })
}
onLoad(() => {
console.log("userInfo", userInfo.value)
})
</script>
<style scoped lang="scss">
.content {
padding: 24rpx;
.label {
width: 200rpx;
flex: none;
}
.value {
color: var(--Gray-Gray-2, #676770);
font-size: 30rpx;
}
.identity-type {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30rpx;
margin-top: 30rpx;
.id-type {
text-align: center;
padding: 0 32rpx;
line-height: 64rpx;
border-radius: 12px;
border: 1.6rpx solid var(--Gray-Gray-6, #ebebf0);
background: var(--Gray-Gray-7, #f5f7fa);
font-size: 28rpx;
color: rgba(#15151a, 0.9);
&.active {
background: var(--Primary-6, #2f54eb);
color: #fff;
border-color: var(--Primary-6, #2f54eb);
}
}
}
}
.footer {
padding: 28rpx 0;
.c-btn {
height: 88rpx;
width: 700rpx;
}
}
</style>
<template>
<view class="c-page">
<view class="content">
<view class="c-form">
<view
v-for="item in form"
:key="item.prop"
class="c-form-item u-flex u-row-between"
@click="toPage(item.route)"
>
<view class="label"> {{ item.label }} </view>
<uni-icons
type="right"
size="16"
color="#BDBDC7"
></uni-icons>
</view>
</view>
</view>
<view class="footer u-flex u-row-center" @click="exit">
<view class="c-btn plain"> 退出登录 </view>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed } from "vue"
const form = computed(() => [
{
label: "实名验证",
prop: "verify",
route: "/pages/verify/index",
},
{
label: "修改密码",
prop: "password",
route: "/pages/password/index",
},
{
label: "加盟协议",
prop: "agreement",
route: "/pages/agreement/index",
},
])
function toPage(route: string) {
uni.navigateTo({ url: route })
}
function exit() {
uni.showLoading({ title: "退出中..." })
// TODO: 调用登出逻辑
uni.hideLoading()
uni.reLaunch({ url: "/pages/home/index" })
}
</script>
<style lang="scss" scoped>
.content {
padding: 24rpx;
}
.footer {
padding: 28rpx 0;
.c-btn {
height: 88rpx;
width: 700rpx;
}
}
</style>
<template>
<view class="c-page">
<view v-if="isVerified" class="verified">
<view class="verified-container">
<view class="verified-content">
<image
class="bg"
src="https://static.qinqinxiaobao.com/flb-mp/img/auth/auth-detail-bg.png"
></image>
<view class="u-flex text u-row-between">
<view class="">您已实名认证</view>
<view class="">
<image
class="icon"
src="https://static.qinqinxiaobao.com/flb-mp/img/auth/auth-status-logo.png"
></image>
</view>
</view>
</view>
<view class="info-list">
<view class="info u-flex">
<view>姓名</view>
<view>{{ userInfo.realname }}</view>
</view>
<view class="info u-flex">
<view>身份证号</view>
<view>{{ desensitization(userInfo.user_info.idCard) }}</view>
</view>
<view class="info u-flex">
<view>认证方式</view>
<view>手机号验证</view>
</view>
</view>
</view>
<view class="tip">实名认证已通过,无法更改认证信息</view>
</view>
<view v-else class="">
<view class="content">
<view class="c-form">
<view class="c-form-item vc-item">
<view class="u-flex">
<view class="label"> 姓名 </view>
<view class="value mobile2 u-flex">
<view class="u-flex">
<input
v-model="name"
class="c-input"
placeholder="请输入姓名"
placeholder-class="c-placeholder"
/>
</view>
</view>
</view>
<view v-show="warn.name" class="warn">
请输入正确的姓名
</view>
</view>
<view class="c-form-item vc-item">
<view class="u-flex">
<view class="label"> 手机号码 </view>
<view v-if="sdkLogin" class="value mobile">
{{ desensitization(userInfo.mobile) }}
</view>
<view v-else class="value mobile2 u-flex">
<view class="u-flex">
<input
v-model="phone"
class="c-input"
placeholder="请输入手机号"
placeholder-class="c-placeholder"
/>
</view>
</view>
</view>
<view v-show="warn.phone" class="warn">
请输入正确的手机号
</view>
</view>
<view class="c-form-item vc-item">
<view class="u-flex">
<view class="label"> 身份证号 </view>
<view class="value u-flex u-row-between u-flex-1">
<view>
<input
v-model="id_card"
class="c-input"
placeholder="请输入身份证号"
placeholder-class="c-placeholder"
@blur="blurIdCard"
/>
</view>
</view>
</view>
<view v-show="warn.id_card" class="warn">
请输入正确的身份证号
</view>
<view v-show="warn.id_card_exist" class="warn">
该身份证号已被{{ existPhone }}绑定
</view>
</view>
<view class="c-form-item vc-item">
<view class="u-flex">
<view class="label"> 图形验证码</view>
<view class="value u-flex u-row-between u-flex-1">
<view>
<input
v-model="imgVerifyCode"
class="c-input"
placeholder="请输入图形验证码"
placeholder-class="c-placeholder"
/>
</view>
<view class="vc u-flex-none u-flex">
<image
:src="imageCodeModel && imageCodeModel.img"
mode="aspectFit"
class="code-img"
@click="getVerifyCodeImg"
/>
</view>
</view>
</view>
<view v-show="warn.img" class="warn">
请输入正确的图形验证码
</view>
</view>
<view class="c-form-item vc-item">
<view class="u-flex">
<view class="label"> 验证码</view>
<view class="value u-flex u-row-between u-flex-1">
<view>
<input
v-model="verifyCode"
class="c-input"
maxlength="6"
type="numeric"
placeholder="请输入验证码"
placeholder-class="c-placeholder"
/>
</view>
<view
class="vc u-flex-none"
:class="{ sended }"
@click="getCode"
>
{{ vc }}
</view>
</view>
</view>
<view v-show="warn.code" class="warn">
请输入正确的6位数字验证码
</view>
</view>
</view>
</view>
<view class="footer u-flex u-row-center" @click="confirm">
<view class="c-btn" :class="{ disabled: !isFormValid }"> 实名认证 </view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { onLoad } from "@dcloudio/uni-app"
import { computed, ref } from "vue"
// 模拟数据(实际应从 SDK/API 获取)
const userInfo = ref({
realname: "张三",
mobile: "13800138000",
user_info: { idCard: "" },
})
const isVerified = ref(false)
const sdkLogin = ref(true)
const phone = ref("")
const imgVerifyCode = ref("")
const verifyCode = ref("")
const name = ref("")
const imageCodeModel = ref<{ seed: number; img: string } | null>(null)
const sended = ref(false)
const warn = ref({
name: false,
phone: false,
img: false,
code: false,
id_card: false,
id_card_exist: false,
})
function getVerifyCodeImg() {
// TODO: 调用图形验证码接口
imageCodeModel.value = { seed: 123, img: "" }
}
getVerifyCodeImg()
const id_card = ref("")
const idCardLoading = ref(false)
const codeCounting = ref(false)
const codeCountdown = ref(60)
let codeTimer: ReturnType<typeof setInterval> | null = null
function getCode() {
if (!imgVerifyCode.value) {
uni.showToast({ title: "请输入图形验证码", icon: "none" })
return
}
if (codeCounting.value) {
uni.showToast({ title: "倒计时结束后再发送", icon: "none" })
return
}
uni.showLoading({ title: "正在获取验证码", mask: true })
// TODO: 调用发送验证码接口
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: "验证码已发送", icon: "success" })
warn.value.img = false
startCodeCountdown()
}, 1000)
}
function startCodeCountdown() {
codeCounting.value = true
codeCountdown.value = 60
sended.value = true
codeTimer = setInterval(() => {
codeCountdown.value--
if (codeCountdown.value <= 0) {
stopCodeCountdown()
}
}, 1000)
}
function stopCodeCountdown() {
if (codeTimer) {
clearInterval(codeTimer)
codeTimer = null
}
codeCounting.value = false
sended.value = false
}
const vc = computed(() => {
if (!sended.value) {
return "获取验证码"
}
return `${codeCountdown.value}s后重新获取`
})
const redirectUrl = ref("")
const isFormValid = computed(() => {
const phoneValid = sdkLogin.value || phone.value
return name.value && phoneValid && id_card.value && imgVerifyCode.value && verifyCode.value
})
function isPhone(val: string) {
return /^1[3-9]\d{9}$/.test(val)
}
function isIdentityNumber(val: string) {
return /(^\d{15}$)|(^\d{18}$)|(^\d{17}([\dXx])$)/.test(val)
}
function desensitization(str: string) {
if (!str) return ""
if (str.length <= 6) return str
return str.slice(0, 3) + "****" + str.slice(-4)
}
function confirm() {
if (idCardLoading.value) return
if (!name.value) {
warn.value.name = true
return
}
warn.value.name = false
if (!sdkLogin.value && !isPhone(phone.value)) {
warn.value.phone = true
return
}
warn.value.phone = false
if (!isIdentityNumber(id_card.value)) {
warn.value.id_card = true
return
}
warn.value.id_card = false
if (warn.value.id_card_exist) return
if (!imgVerifyCode.value) {
warn.value.img = true
return
}
warn.value.img = false
if (!verifyCode.value) {
warn.value.code = true
return
}
warn.value.code = false
uni.showLoading({ title: "认证中..." })
// TODO: 调用实名认证接口
setTimeout(() => {
uni.hideLoading()
uni.showToast({ title: "实名认证成功", icon: "success" })
setTimeout(() => {
if (redirectUrl.value) {
uni.redirectTo({ url: redirectUrl.value })
} else {
uni.navigateBack()
}
}, 2000)
}, 1500)
}
const existPhone = ref("")
function blurIdCard() {
if (!isIdentityNumber(id_card.value)) return
warn.value.id_card = false
idCardLoading.value = true
// TODO: 调用身份证唯一性检查接口
setTimeout(() => {
idCardLoading.value = false
}, 500)
}
onLoad((options) => {
if (options?.redirectUrl) {
redirectUrl.value = decodeURIComponent(options.redirectUrl)
}
})
</script>
<style lang="scss" scoped>
.c-page {
overflow: hidden;
}
.verified-container {
background-color: #fff;
width: 345 * 2rpx;
border-radius: 16rpx;
overflow: hidden;
margin: auto;
margin-top: 80rpx;
.verified-content {
width: 345 * 2rpx;
margin: auto;
position: relative;
.text {
position: absolute;
width: 345 * 2rpx;
height: 120 * 2rpx;
top: 0;
left: 0;
font-size: 40rpx;
color: #fff;
padding: 0 60rpx;
.icon {
width: 64 * 2rpx;
height: 64 * 2rpx;
}
}
.bg {
width: 345 * 2rpx;
height: 120 * 2rpx;
}
}
.info-list {
padding-bottom: 20rpx;
.info {
justify-content: space-between;
padding: 0 30rpx;
line-height: 80rpx;
}
}
}
.tip {
position: fixed;
font-size: 24rpx;
bottom: 60rpx;
color: #95989e;
line-height: 36rpx;
width: 100%;
text-align: center;
}
.content {
padding: 24rpx;
.c-form {
overflow: hidden;
}
.eye {
width: 32rpx;
height: 32rpx;
}
.code-img {
width: 160rpx;
height: 60rpx;
margin-left: 20rpx;
flex-shrink: 0;
}
.vc-item {
display: block;
}
.label {
width: 200rpx;
flex: none;
}
.value {
width: 100%;
color: var(--Gray-Gray, #15151a);
font-family: "PingFang SC";
font-size: 30rpx;
&.mobile {
opacity: 0.9;
font-weight: 400;
}
&.mobile2 {
height: 100rpx;
}
.vc {
color: var(--Primary-6, #2f54eb);
&.sended {
color: var(--Gray-Gray-3, #93939e);
}
}
}
.warn {
line-height: 56rpx;
border-radius: 4px;
background: #fef2f4;
color: #f3362c;
font-size: 24rpx;
padding-left: 20rpx;
margin-bottom: 30rpx;
}
}
.footer {
padding: 28rpx 0;
.c-btn {
height: 88rpx;
width: 700rpx;
}
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment