feat:登录页面
262
docs/src-directory-convention.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# src 目录结构规范
|
||||
|
||||
本文档定义 `src` 目录下的文件组织方式与命名规范。**后续新增与修改的代码必须严格遵循本规范。**
|
||||
|
||||
## 目录总览
|
||||
|
||||
```
|
||||
src/
|
||||
├── pages/ # 页面组件
|
||||
├── components/ # 全局公共组件
|
||||
├── business-cmp/ # 业务组件(页面专用)
|
||||
├── static/ # 静态资源
|
||||
├── api/ # 网络请求接口
|
||||
├── stores/ # 状态管理(Pinia)
|
||||
├── utils/ # 工具函数
|
||||
├── types/ # TypeScript 类型定义
|
||||
├── hooks/ # Vue Composition API Hooks
|
||||
└── styles/ # 全局样式
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 各目录说明
|
||||
|
||||
### pages — 页面组件
|
||||
|
||||
存放应用页面级组件,每个页面独立一个文件夹。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 文件夹命名 | **kebab-case**(小写 + 连字符) |
|
||||
| 示例 | `user-login/`、`home-index/` |
|
||||
|
||||
**推荐结构:**
|
||||
|
||||
```
|
||||
pages/
|
||||
├── user-login/
|
||||
│ ├── index.vue # 页面入口
|
||||
│ └── components/ # 仅该页面使用的组件(可选)
|
||||
│ └── LoginForm.vue
|
||||
└── home-index/
|
||||
└── index.vue
|
||||
```
|
||||
|
||||
> 页面专属组件优先放在 `pages/<page-name>/components/` 下;若业务组件较多、需跨页面复用但又不属于全局公共组件,可使用 `business-cmp/`。
|
||||
|
||||
---
|
||||
|
||||
### components — 全局公共组件
|
||||
|
||||
存放可在多个页面复用的通用 UI 组件。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 文件命名 | **PascalCase**(大驼峰) |
|
||||
| 文件扩展名 | `.vue` |
|
||||
| 示例 | `AppFooter.vue`、`InputEntry.vue` |
|
||||
|
||||
**适用场景:**
|
||||
|
||||
- 按钮、输入框、弹窗等基础 UI 封装
|
||||
- 布局类组件(Header、Footer、Sidebar 等)
|
||||
- 与具体业务无关、可在任意页面引用的组件
|
||||
|
||||
---
|
||||
|
||||
### business-cmp — 业务组件
|
||||
|
||||
存放与特定业务逻辑绑定、**不对外全局复用**的组件。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 组织方式 | 按业务模块或页面分子目录 |
|
||||
| 文件命名 | **PascalCase** |
|
||||
| 示例 | `business-cmp/order/OrderCard.vue` |
|
||||
|
||||
**与 `components` 的区别:**
|
||||
|
||||
| 目录 | 复用范围 | 示例 |
|
||||
|------|----------|------|
|
||||
| `components/` | 全局、跨业务复用 | `AppFooter.vue` |
|
||||
| `business-cmp/` | 特定业务/页面 | `OrderCard.vue` |
|
||||
| `pages/xxx/components/` | 仅单个页面 | `LoginForm.vue` |
|
||||
|
||||
**选择原则:**
|
||||
|
||||
1. 仅一个页面使用 → `pages/<page>/components/`
|
||||
2. 同一业务模块内多页面复用 → `business-cmp/<module>/`
|
||||
3. 跨业务、通用 UI → `components/`
|
||||
|
||||
---
|
||||
|
||||
### static — 静态资源
|
||||
|
||||
存放图片、字体、图标等静态文件。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 文件/文件夹命名 | **小写**(可用连字符分隔) |
|
||||
| 示例 | `logo.png`、`github.svg`、`icon-arrow.png` |
|
||||
|
||||
**推荐结构:**
|
||||
|
||||
```
|
||||
static/
|
||||
├── images/
|
||||
│ └── banner.png
|
||||
├── icons/
|
||||
│ └── home.svg
|
||||
└── fonts/
|
||||
└── custom-font.woff2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### api — 网络请求接口
|
||||
|
||||
封装所有后端 API 调用,按业务模块拆分文件。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 文件命名 | **kebab-case** 或按模块命名 |
|
||||
| 文件扩展名 | `.ts` |
|
||||
| 示例 | `user.ts`、`order.ts` |
|
||||
|
||||
**示例:**
|
||||
|
||||
```ts
|
||||
// api/user.ts
|
||||
export function login(data: LoginParams) {
|
||||
return request.post<LoginResult>('/user/login', data)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### stores — 状态管理
|
||||
|
||||
使用 **Pinia** 管理全局状态,按功能模块拆分 Store。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 文件命名 | **kebab-case** 或 `useXxxStore` 风格 |
|
||||
| 文件扩展名 | `.ts` |
|
||||
| 示例 | `user.ts`、`cart.ts` |
|
||||
|
||||
**示例:**
|
||||
|
||||
```ts
|
||||
// stores/user.ts
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### utils — 工具函数
|
||||
|
||||
存放与业务无关的纯函数工具。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 文件命名 | **kebab-case** |
|
||||
| 文件扩展名 | `.ts` |
|
||||
| 示例 | `format-date.ts`、`storage.ts` |
|
||||
|
||||
**适用场景:**
|
||||
|
||||
- 日期/金额格式化
|
||||
- 本地存储封装
|
||||
- 通用校验、防抖节流等
|
||||
|
||||
---
|
||||
|
||||
### types — TypeScript 类型定义
|
||||
|
||||
集中管理项目中的 TypeScript 类型与接口。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 文件命名 | **kebab-case** |
|
||||
| 文件扩展名 | **`.ts`**(必须) |
|
||||
| 示例 | `user.ts`、`api-response.ts` |
|
||||
|
||||
**示例:**
|
||||
|
||||
```ts
|
||||
// types/user.ts
|
||||
export interface UserInfo {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### hooks — Composition API Hooks
|
||||
|
||||
封装可复用的 Vue Composition API 逻辑。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 文件命名 | **kebab-case**,以 `use` 开头 |
|
||||
| 文件扩展名 | `.ts` |
|
||||
| 示例 | `use-auth.ts`、`use-pagination.ts` |
|
||||
|
||||
**示例:**
|
||||
|
||||
```ts
|
||||
// hooks/use-auth.ts
|
||||
export function useAuth() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### styles — 全局样式
|
||||
|
||||
存放全局样式、变量、混入等。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 文件命名 | **kebab-case** |
|
||||
| 文件扩展名 | `.scss` / `.css` |
|
||||
| 示例 | `variables.scss`、`reset.scss` |
|
||||
|
||||
---
|
||||
|
||||
## 命名规范速查表
|
||||
|
||||
| 类型 | 命名风格 | 示例 |
|
||||
|------|----------|------|
|
||||
| 页面文件夹 | kebab-case | `user-login/` |
|
||||
| 全局组件文件 | PascalCase | `AppFooter.vue` |
|
||||
| 业务/页面组件文件 | PascalCase | `OrderCard.vue` |
|
||||
| 静态资源 | 小写 | `logo.png` |
|
||||
| API / Store / Utils / Types / Hooks | kebab-case | `user.ts`、`use-auth.ts` |
|
||||
| 样式文件 | kebab-case | `variables.scss` |
|
||||
|
||||
---
|
||||
|
||||
## 导入路径约定
|
||||
|
||||
- 全局组件:从 `@/components/` 引入
|
||||
- 业务组件:从 `@/business-cmp/` 或页面相对路径引入
|
||||
- API:从 `@/api/` 引入
|
||||
- 类型:从 `@/types/` 引入
|
||||
- 工具:从 `@/utils/` 引入
|
||||
- Hooks:从 `@/hooks/` 引入
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **禁止**在 `components/` 中放置与具体业务强耦合的组件。
|
||||
2. **禁止**在页面目录外散落页面专属组件。
|
||||
3. 新增目录或调整规范前,需同步更新本文档。
|
||||
4. 所有 TypeScript 类型定义统一放在 `types/`,避免在业务文件中重复定义相同类型。
|
||||
5. 网络请求逻辑统一通过 `api/` 层调用,页面与组件中不直接写请求 URL。
|
||||
7
package-lock.json
generated
@@ -24,6 +24,7 @@
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-5000720260410001",
|
||||
"@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
|
||||
"@dcloudio/uni-quickapp-webview": "3.0.0-5000720260410001",
|
||||
"@fontpkg/alimama-fang-yuan-ti-vf": "^1.0.5",
|
||||
"vue": "3.4.21",
|
||||
"vue-i18n": "9.6.2",
|
||||
"vue-router": "4.5.1"
|
||||
@@ -3807,6 +3808,12 @@
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontpkg/alimama-fang-yuan-ti-vf": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/@fontpkg/alimama-fang-yuan-ti-vf/-/alimama-fang-yuan-ti-vf-1.0.5.tgz",
|
||||
"integrity": "sha512-lwNA/hF0tlJuqEvCZc7Y6sXLtO5IKYOmvEQkJlvRBMlTLO9jVojnkqDbLiTXCnrHQRUUgYOJTjbX1DKnbhudSg==",
|
||||
"license": "SEE LICENSE IN README.md"
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-5000720260410001",
|
||||
"@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
|
||||
"@dcloudio/uni-quickapp-webview": "3.0.0-5000720260410001",
|
||||
"@fontpkg/alimama-fang-yuan-ti-vf": "^1.0.5",
|
||||
"vue": "3.4.21",
|
||||
"vue-i18n": "9.6.2",
|
||||
"vue-router": "4.5.1"
|
||||
@@ -53,4 +54,4 @@
|
||||
"vite": "5.2.8",
|
||||
"vue-tsc": "^3.2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,11 @@ import { onLaunch } from '@dcloudio/uni-app'
|
||||
onLaunch(() => {})
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style lang="scss">
|
||||
@import '@/styles/fonts.scss';
|
||||
|
||||
page {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,57 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index",
|
||||
"type": "home"
|
||||
"path": "pages/Login/splash/index",
|
||||
"type": "home",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/Login/user-login/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/Login/user-forgot-password/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/Login/user-register/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/Login/user-register-success/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/Login/user-sms-login/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/Login/user-agreement/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"backgroundColor": "@bgColor",
|
||||
"backgroundColorBottom": "@bgColorBottom",
|
||||
"backgroundColorTop": "@bgColorTop",
|
||||
"backgroundTextStyle": "@bgTxtStyle",
|
||||
"navigationBarBackgroundColor": "#000000",
|
||||
"navigationBarTextStyle": "@navTxtStyle",
|
||||
"navigationBarTitleText": "Uni Creator",
|
||||
"backgroundColor": "#ffffff",
|
||||
"backgroundColorBottom": "#ffffff",
|
||||
"backgroundColorTop": "#ffffff",
|
||||
"backgroundTextStyle": "dark",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
},
|
||||
"subPackages": []
|
||||
|
||||
89
src/pages/Login/splash/index.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const SPLASH_DURATION = 3000
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: '/pages/Login/user-login/index',
|
||||
})
|
||||
}, SPLASH_DURATION)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="splash">
|
||||
<view class="splash__bg-circle" />
|
||||
|
||||
<view class="splash__content">
|
||||
<image
|
||||
class="splash__logo"
|
||||
src="/static/icons/logo.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="splash__title">
|
||||
虫子智造
|
||||
</text>
|
||||
<text class="splash__subtitle">
|
||||
智造研发一体化平台
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.splash {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, $color-splash-gradient-start 0%, $color-splash-gradient-end 100%);
|
||||
}
|
||||
|
||||
.splash__bg-circle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -120rpx;
|
||||
width: 520rpx;
|
||||
height: 520rpx;
|
||||
border-radius: 50%;
|
||||
background: $color-splash-circle;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.splash__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 38vh;
|
||||
}
|
||||
|
||||
.splash__logo {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
}
|
||||
|
||||
.splash__title {
|
||||
margin-top: 40rpx;
|
||||
font-family: 'Alimama FangYuanTi VF-Bold', 'Alimama FangYuanTi VF', sans-serif;
|
||||
font-size: 61.07rpx;
|
||||
font-weight: 700;
|
||||
font-variation-settings: 'wght' 700, 'BEVL' 100;
|
||||
color: $color-text-primary;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.splash__subtitle {
|
||||
margin-top: 16rpx;
|
||||
font-family: 'PingFangSC-Regular', 'PingFang SC', sans-serif;
|
||||
font-size: 22.9rpx;
|
||||
font-weight: 400;
|
||||
color: $color-text-secondary;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
</style>
|
||||
159
src/pages/Login/user-agreement/index.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
function handleBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="user-agreement-page">
|
||||
<view class="user-agreement-page__navbar">
|
||||
<view
|
||||
class="user-agreement-page__back"
|
||||
@tap="handleBack"
|
||||
>
|
||||
<image
|
||||
class="user-agreement-page__back-icon"
|
||||
src="/static/icons/icon-back.svg"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<text class="user-agreement-page__navbar-title">
|
||||
帮助中心
|
||||
</text>
|
||||
<view class="user-agreement-page__navbar-placeholder" />
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="user-agreement-page__scroll"
|
||||
scroll-y
|
||||
>
|
||||
<view class="user-agreement-page__content">
|
||||
<text class="user-agreement-page__doc-title">
|
||||
【平台名称】用户协议
|
||||
</text>
|
||||
|
||||
<text class="user-agreement-page__date">
|
||||
更新日期:【YYYY】年【MM】月【DD】日
|
||||
</text>
|
||||
<text class="user-agreement-page__date">
|
||||
生效日期:【YYYY】年【MM】月【DD】日
|
||||
</text>
|
||||
|
||||
<text class="user-agreement-page__section-title">
|
||||
导言
|
||||
</text>
|
||||
<text class="user-agreement-page__paragraph">
|
||||
欢迎您使用【平台名称】!本平台由【公司全称】(以下简称"我们")运营,为您提供硬件电路设计、物联网全案服务、软件设计等一站式研发服务。在您注册成为本平台用户前,请您务必审慎阅读、充分理解本协议各条款内容,特别是以加粗或下划线标识的免除或限制责任的条款、知识产权条款、法律适用和争议解决条款。当您按照注册页面提示填写信息、阅读并同意本协议且完成全部注册程序后,即表示您已充分阅读、理解并接受本协议的全部内容。
|
||||
</text>
|
||||
|
||||
<text class="user-agreement-page__section-title">
|
||||
第一条 定义
|
||||
</text>
|
||||
<text class="user-agreement-page__paragraph">
|
||||
1.1 本平台:指由【公司全称】运营的网站、移动应用程序及相关服务,包括但不限于【平台名称】官方网站及客户端。
|
||||
</text>
|
||||
<text class="user-agreement-page__paragraph">
|
||||
1.2 用户:指完成本平台注册程序,使用本平台服务的自然人、法人或其他组织。
|
||||
</text>
|
||||
<text class="user-agreement-page__paragraph">
|
||||
1.3 企业认证:指用户向本平台提交营业执照等企业资质材料,经平台审核通过的企业身份认证流程。
|
||||
</text>
|
||||
<text class="user-agreement-page__paragraph">
|
||||
1.4 项目:指用户在本平台发起的服务需求,包括但不限于硬件设计、软件开发、物联网方案等研发类任务。
|
||||
</text>
|
||||
<text class="user-agreement-page__paragraph">
|
||||
1.5 电子合同:指用户通过本平台在线签署的电子协议,与纸质合同具有同等法律效力。
|
||||
</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.user-agreement-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.user-agreement-page__navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: calc(env(safe-area-inset-top) + 20rpx) 32rpx 20rpx;
|
||||
background: $color-bg-page;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.user-agreement-page__back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.user-agreement-page__back-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.user-agreement-page__navbar-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.user-agreement-page__navbar-placeholder {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.user-agreement-page__scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.user-agreement-page__content {
|
||||
padding: 32rpx 40rpx calc(40rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.user-agreement-page__doc-title {
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.user-agreement-page__date {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.user-agreement-page__section-title {
|
||||
display: block;
|
||||
margin-top: 40rpx;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
color: $color-text-primary;
|
||||
}
|
||||
|
||||
.user-agreement-page__paragraph {
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.8;
|
||||
color: $color-text-primary;
|
||||
text-align: justify;
|
||||
}
|
||||
</style>
|
||||
111
src/pages/Login/user-forgot-password/components/CodeInput.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
options: {
|
||||
virtualHost: true,
|
||||
},
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const countdown = ref(0)
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
function onInput(e: { detail: { value: string } }) {
|
||||
emit('update:modelValue', e.detail.value)
|
||||
}
|
||||
|
||||
function handleGetCode() {
|
||||
if (countdown.value > 0)
|
||||
return
|
||||
|
||||
uni.showToast({ title: '验证码已发送', icon: 'none' })
|
||||
countdown.value = 60
|
||||
timer = setInterval(() => {
|
||||
countdown.value -= 1
|
||||
if (countdown.value <= 0 && timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="code-input">
|
||||
<input
|
||||
class="code-input__field"
|
||||
type="number"
|
||||
placeholder="请输入验证码"
|
||||
placeholder-class="code-input__placeholder"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
>
|
||||
<view
|
||||
class="code-input__btn"
|
||||
:class="{ 'code-input__btn--disabled': countdown > 0 }"
|
||||
@tap="handleGetCode"
|
||||
>
|
||||
<text class="code-input__btn-text">
|
||||
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.code-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
padding: 0 16rpx 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
background: $color-bg-input;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.code-input__field {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
font-size: 30rpx;
|
||||
color: $color-text-primary;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.code-input__placeholder {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
|
||||
.code-input__btn {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 64rpx;
|
||||
padding: 0 24rpx;
|
||||
background: $color-primary;
|
||||
border-radius: 32rpx;
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.code-input__btn-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
options: {
|
||||
virtualHost: true,
|
||||
},
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
placeholder: string
|
||||
inputType?: 'text' | 'number'
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
function onInput(e: { detail: { value: string } }) {
|
||||
emit('update:modelValue', e.detail.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="forgot-input">
|
||||
<input
|
||||
class="forgot-input__field"
|
||||
:type="inputType || 'text'"
|
||||
:placeholder="placeholder"
|
||||
placeholder-class="forgot-input__placeholder"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.forgot-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
padding: 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
background: $color-bg-input;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.forgot-input__field {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
font-size: 30rpx;
|
||||
color: $color-text-primary;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.forgot-input__placeholder {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
options: {
|
||||
virtualHost: true,
|
||||
},
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
placeholder: string
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
|
||||
function onInput(e: { detail: { value: string } }) {
|
||||
emit('update:modelValue', e.detail.value)
|
||||
}
|
||||
|
||||
function toggleVisible() {
|
||||
visible.value = !visible.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="password-field">
|
||||
<input
|
||||
class="password-field__field"
|
||||
:password="!visible"
|
||||
:placeholder="placeholder"
|
||||
placeholder-class="password-field__placeholder"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
>
|
||||
<image
|
||||
class="password-field__eye"
|
||||
:src="visible ? '/static/icons/icon-eye-open.svg' : '/static/icons/icon-eye-close.svg'"
|
||||
mode="aspectFit"
|
||||
@tap="toggleVisible"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.password-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
padding: 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
background: $color-bg-input;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.password-field__field {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
font-size: 30rpx;
|
||||
color: $color-text-primary;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.password-field__placeholder {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
|
||||
.password-field__eye {
|
||||
flex-shrink: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
</style>
|
||||
273
src/pages/Login/user-forgot-password/index.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { navigateToResultPage } from '@/utils/navigate-result'
|
||||
import CodeInput from './components/CodeInput.vue'
|
||||
import ForgotInput from './components/ForgotInput.vue'
|
||||
import PasswordField from './components/PasswordField.vue'
|
||||
|
||||
const phone = ref('')
|
||||
const code = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const agreed = ref(false)
|
||||
|
||||
function toggleAgreed() {
|
||||
agreed.value = !agreed.value
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (!agreed.value) {
|
||||
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!phone.value || !code.value || !newPassword.value || !confirmPassword.value) {
|
||||
uni.showToast({ title: '请填写完整信息', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
uni.showToast({ title: '两次密码输入不一致', icon: 'none' })
|
||||
return
|
||||
}
|
||||
navigateToResultPage('resetPassword')
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function handleUserAgreement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/Login/user-agreement/index',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="forgot-page">
|
||||
<view class="forgot-page__header">
|
||||
<text class="forgot-page__title">
|
||||
找回密码
|
||||
</text>
|
||||
<text class="forgot-page__subtitle">
|
||||
使用手机号重新设置您的密码
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="forgot-page__form">
|
||||
<view class="forgot-page__field">
|
||||
<ForgotInput
|
||||
v-model="phone"
|
||||
placeholder="请输入手机号"
|
||||
input-type="number"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="forgot-page__field">
|
||||
<CodeInput v-model="code" />
|
||||
</view>
|
||||
|
||||
<view class="forgot-page__field">
|
||||
<PasswordField
|
||||
v-model="newPassword"
|
||||
placeholder="请输入新密码"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="forgot-page__field">
|
||||
<PasswordField
|
||||
v-model="confirmPassword"
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="forgot-page__agreement"
|
||||
@tap="toggleAgreed"
|
||||
>
|
||||
<view
|
||||
class="forgot-page__checkbox"
|
||||
:class="{ 'forgot-page__checkbox--checked': agreed }"
|
||||
>
|
||||
<text
|
||||
v-if="agreed"
|
||||
class="forgot-page__checkmark"
|
||||
>
|
||||
✓
|
||||
</text>
|
||||
</view>
|
||||
<view class="forgot-page__agreement-text">
|
||||
<text class="forgot-page__agreement-normal">
|
||||
我已阅读并同意
|
||||
</text>
|
||||
<text
|
||||
class="forgot-page__agreement-link"
|
||||
@tap.stop="handleUserAgreement"
|
||||
>
|
||||
用户协议
|
||||
</text>
|
||||
<text class="forgot-page__agreement-normal">
|
||||
和
|
||||
</text>
|
||||
<text class="forgot-page__agreement-link">
|
||||
隐私政策
|
||||
</text>
|
||||
<text class="forgot-page__agreement-normal">
|
||||
以及
|
||||
</text>
|
||||
<text class="forgot-page__agreement-link">
|
||||
定制条款
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button
|
||||
class="forgot-page__submit"
|
||||
@tap="handleSubmit"
|
||||
>
|
||||
确认修改
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="forgot-page__footer">
|
||||
<text class="forgot-page__footer-normal">
|
||||
已有账号?
|
||||
</text>
|
||||
<text
|
||||
class="forgot-page__footer-link"
|
||||
@tap="handleLogin"
|
||||
>
|
||||
立即登录
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.forgot-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
padding: 0 64rpx;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.forgot-page__header {
|
||||
padding-top: 120rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.forgot-page__title {
|
||||
display: block;
|
||||
font-size: 56rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-primary;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.forgot-page__subtitle {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 400;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.forgot-page__form {
|
||||
width: 100%;
|
||||
margin-top: 80rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.forgot-page__field {
|
||||
width: 100%;
|
||||
margin-top: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.forgot-page__agreement {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.forgot-page__checkbox {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-top: 4rpx;
|
||||
margin-right: 12rpx;
|
||||
border: 2rpx solid #cccccc;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
|
||||
&--checked {
|
||||
background: $color-primary;
|
||||
border-color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-page__checkmark {
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.forgot-page__agreement-text {
|
||||
flex: 1;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.forgot-page__agreement-normal {
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.forgot-page__agreement-link {
|
||||
color: $color-text-link;
|
||||
}
|
||||
|
||||
.forgot-page__submit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
margin-top: 48rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
background: $color-primary;
|
||||
border: none;
|
||||
border-radius: 48rpx;
|
||||
line-height: 96rpx;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-page__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: auto;
|
||||
padding: 60rpx 0 calc(60rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.forgot-page__footer-normal {
|
||||
font-size: 28rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.forgot-page__footer-link {
|
||||
margin-left: 4rpx;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-link;
|
||||
}
|
||||
</style>
|
||||
77
src/pages/Login/user-login/components/LoginInput.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
options: {
|
||||
virtualHost: true,
|
||||
},
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
icon: string
|
||||
placeholder: string
|
||||
inputType?: 'text' | 'number'
|
||||
password?: boolean
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
function onInput(e: { detail: { value: string } }) {
|
||||
emit('update:modelValue', e.detail.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="login-input">
|
||||
<image
|
||||
class="login-input__icon"
|
||||
:src="icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<input
|
||||
class="login-input__field"
|
||||
:type="inputType || 'text'"
|
||||
:password="password"
|
||||
:placeholder="placeholder"
|
||||
placeholder-class="login-input__placeholder"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.login-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
padding: 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
background: $color-bg-input;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.login-input__icon {
|
||||
flex-shrink: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.login-input__field {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
font-size: 30rpx;
|
||||
color: $color-text-primary;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.login-input__placeholder {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
</style>
|
||||
364
src/pages/Login/user-login/index.vue
Normal file
@@ -0,0 +1,364 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { navigateToResultPage } from '@/utils/navigate-result'
|
||||
import LoginInput from './components/LoginInput.vue'
|
||||
|
||||
const phone = ref('')
|
||||
const password = ref('')
|
||||
const agreed = ref(false)
|
||||
|
||||
function toggleAgreed() {
|
||||
agreed.value = !agreed.value
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
if (!agreed.value) {
|
||||
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!phone.value || !password.value) {
|
||||
uni.showToast({ title: '请输入手机号和密码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 接入登录接口后,在接口返回失败时调用
|
||||
navigateToResultPage('loginFailed')
|
||||
}
|
||||
|
||||
function handleForgotPassword() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/Login/user-forgot-password/index',
|
||||
})
|
||||
}
|
||||
|
||||
function handleSmsLogin() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/Login/user-sms-login/index',
|
||||
})
|
||||
}
|
||||
|
||||
function handleRegister() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/Login/user-register/index',
|
||||
})
|
||||
}
|
||||
|
||||
function handleUserAgreement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/Login/user-agreement/index',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<view class="login-page__header">
|
||||
<text class="login-page__title">
|
||||
欢迎回来
|
||||
</text>
|
||||
<text class="login-page__subtitle">
|
||||
使用密码登录您的账号
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="login-page__form">
|
||||
<LoginInput
|
||||
v-model="phone"
|
||||
icon="/static/icons/icon-phone.png"
|
||||
placeholder="请输入手机号"
|
||||
input-type="number"
|
||||
/>
|
||||
|
||||
<view class="login-page__password-group">
|
||||
<LoginInput
|
||||
v-model="password"
|
||||
icon="/static/icons/icon-shield-check.png"
|
||||
placeholder="请输入密码"
|
||||
password
|
||||
/>
|
||||
<text
|
||||
class="login-page__forgot"
|
||||
@tap="handleForgotPassword"
|
||||
>
|
||||
忘记密码?
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="login-page__agreement"
|
||||
@tap="toggleAgreed"
|
||||
>
|
||||
<view
|
||||
class="login-page__checkbox"
|
||||
:class="{ 'login-page__checkbox--checked': agreed }"
|
||||
>
|
||||
<text
|
||||
v-if="agreed"
|
||||
class="login-page__checkmark"
|
||||
>
|
||||
✓
|
||||
</text>
|
||||
</view>
|
||||
<view class="login-page__agreement-text">
|
||||
<text class="login-page__agreement-normal">
|
||||
我已阅读并同意
|
||||
</text>
|
||||
<text
|
||||
class="login-page__agreement-link"
|
||||
@tap.stop="handleUserAgreement"
|
||||
>
|
||||
用户协议
|
||||
</text>
|
||||
<text class="login-page__agreement-normal">
|
||||
和
|
||||
</text>
|
||||
<text class="login-page__agreement-link">
|
||||
隐私政策
|
||||
</text>
|
||||
<text class="login-page__agreement-normal">
|
||||
以及
|
||||
</text>
|
||||
<text class="login-page__agreement-link">
|
||||
定制条款
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button
|
||||
class="login-page__submit"
|
||||
@tap="handleLogin"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="login-page__other">
|
||||
<view class="login-page__divider">
|
||||
<view class="login-page__divider-line" />
|
||||
<text class="login-page__divider-text">
|
||||
其他登录方式
|
||||
</text>
|
||||
<view class="login-page__divider-line" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="login-page__sms"
|
||||
@tap="handleSmsLogin"
|
||||
>
|
||||
<view class="login-page__sms-icon-wrap">
|
||||
<image
|
||||
class="login-page__sms-icon"
|
||||
src="/static/icons/icon-sms-login.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<text class="login-page__sms-text">
|
||||
验证码登录
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="login-page__footer">
|
||||
<text class="login-page__footer-normal">
|
||||
没有账号?
|
||||
</text>
|
||||
<text
|
||||
class="login-page__footer-link"
|
||||
@tap="handleRegister"
|
||||
>
|
||||
立即注册
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.login-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
padding: 0 64rpx;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.login-page__header {
|
||||
padding-top: 120rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-page__title {
|
||||
display: block;
|
||||
font-size: 56rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-primary;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.login-page__subtitle {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 400;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.login-page__form {
|
||||
width: 100%;
|
||||
margin-top: 80rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.login-page__password-group {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.login-page__forgot {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.login-page__agreement {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.login-page__checkbox {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-top: 4rpx;
|
||||
margin-right: 12rpx;
|
||||
border: 2rpx solid #cccccc;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
|
||||
&--checked {
|
||||
background: $color-primary;
|
||||
border-color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.login-page__checkmark {
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.login-page__agreement-text {
|
||||
flex: 1;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.login-page__agreement-normal {
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.login-page__agreement-link {
|
||||
color: $color-text-link;
|
||||
}
|
||||
|
||||
.login-page__submit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
margin-top: 48rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
background: $color-primary;
|
||||
border: none;
|
||||
border-radius: 48rpx;
|
||||
line-height: 96rpx;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.login-page__other {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.login-page__divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-page__divider-line {
|
||||
flex: 1;
|
||||
height: 1rpx;
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.login-page__divider-text {
|
||||
padding: 0 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-secondary;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.login-page__sms {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.login-page__sms-icon-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
background: #B6CDFF;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.login-page__sms-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
.login-page__sms-text {
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.login-page__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: calc(60rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.login-page__footer-normal {
|
||||
font-size: 28rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.login-page__footer-link {
|
||||
margin-left: 4rpx;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-link;
|
||||
}
|
||||
</style>
|
||||
109
src/pages/Login/user-register-success/index.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import {
|
||||
RESULT_PAGE_CONFIG,
|
||||
RESULT_REDIRECT_DELAY,
|
||||
type ResultPageType,
|
||||
} from '@/types/result-page'
|
||||
|
||||
const pageType = ref<ResultPageType>('register')
|
||||
const countdown = ref(RESULT_REDIRECT_DELAY / 1000)
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const pageConfig = computed(() => RESULT_PAGE_CONFIG[pageType.value])
|
||||
|
||||
const title = computed(() => pageConfig.value.title)
|
||||
|
||||
const subtitle = computed(() => `${countdown.value}秒后自动跳转至${pageConfig.value.subtitleTarget}`)
|
||||
|
||||
onLoad((options) => {
|
||||
const type = options?.type
|
||||
if (type === 'resetPassword' || type === 'reset-password') {
|
||||
pageType.value = 'resetPassword'
|
||||
return
|
||||
}
|
||||
if (type === 'loginFailed') {
|
||||
pageType.value = 'loginFailed'
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
timer = setInterval(() => {
|
||||
countdown.value -= 1
|
||||
if (countdown.value <= 0) {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
uni.redirectTo({
|
||||
url: pageConfig.value.redirectUrl,
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="result-page">
|
||||
<view class="result-page__content">
|
||||
<image
|
||||
class="result-page__icon"
|
||||
:src="pageConfig.icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text class="result-page__title">
|
||||
{{ title }}
|
||||
</text>
|
||||
<text class="result-page__subtitle">
|
||||
{{ subtitle }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.result-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.result-page__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 64rpx;
|
||||
}
|
||||
|
||||
.result-page__icon {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
}
|
||||
|
||||
.result-page__title {
|
||||
margin-top: 48rpx;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-primary;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.result-page__subtitle {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 400;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
</style>
|
||||
123
src/pages/Login/user-register/components/CodeInput.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
options: {
|
||||
virtualHost: true,
|
||||
},
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const countdown = ref(0)
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
function onInput(e: { detail: { value: string } }) {
|
||||
emit('update:modelValue', e.detail.value)
|
||||
}
|
||||
|
||||
function handleGetCode() {
|
||||
if (countdown.value > 0)
|
||||
return
|
||||
|
||||
uni.showToast({ title: '验证码已发送', icon: 'none' })
|
||||
countdown.value = 60
|
||||
timer = setInterval(() => {
|
||||
countdown.value -= 1
|
||||
if (countdown.value <= 0 && timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="code-input">
|
||||
<image
|
||||
class="code-input__icon"
|
||||
src="/static/icons/icon-shield-check.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<input
|
||||
class="code-input__field"
|
||||
type="number"
|
||||
placeholder="请输入验证码"
|
||||
placeholder-class="code-input__placeholder"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
>
|
||||
<view
|
||||
class="code-input__btn"
|
||||
:class="{ 'code-input__btn--disabled': countdown > 0 }"
|
||||
@tap="handleGetCode"
|
||||
>
|
||||
<text class="code-input__btn-text">
|
||||
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.code-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
padding: 0 16rpx 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
background: $color-bg-input;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.code-input__icon {
|
||||
flex-shrink: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.code-input__field {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
font-size: 30rpx;
|
||||
color: $color-text-primary;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.code-input__placeholder {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
|
||||
.code-input__btn {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 64rpx;
|
||||
padding: 0 24rpx;
|
||||
background: $color-primary;
|
||||
border-radius: 32rpx;
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.code-input__btn-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
94
src/pages/Login/user-register/components/PasswordField.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
options: {
|
||||
virtualHost: true,
|
||||
},
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
placeholder: string
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
|
||||
function onInput(e: { detail: { value: string } }) {
|
||||
emit('update:modelValue', e.detail.value)
|
||||
}
|
||||
|
||||
function toggleVisible() {
|
||||
visible.value = !visible.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="password-field">
|
||||
<image
|
||||
class="password-field__icon"
|
||||
src="/static/icons/icon-lock.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<input
|
||||
class="password-field__field"
|
||||
:password="!visible"
|
||||
:placeholder="placeholder"
|
||||
placeholder-class="password-field__placeholder"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
>
|
||||
<image
|
||||
class="password-field__eye"
|
||||
:src="visible ? '/static/icons/icon-eye-open.svg' : '/static/icons/icon-eye-close.svg'"
|
||||
mode="aspectFit"
|
||||
@tap="toggleVisible"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.password-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
padding: 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
background: $color-bg-input;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.password-field__icon {
|
||||
flex-shrink: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.password-field__field {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
font-size: 30rpx;
|
||||
color: $color-text-primary;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.password-field__placeholder {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
|
||||
.password-field__eye {
|
||||
flex-shrink: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
</style>
|
||||
119
src/pages/Login/user-register/components/PasswordStrength.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { getPasswordStrength } from '@/utils/password-strength'
|
||||
|
||||
defineOptions({
|
||||
options: {
|
||||
virtualHost: true,
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
password: string
|
||||
}>()
|
||||
|
||||
const strength = computed(() => getPasswordStrength(props.password))
|
||||
|
||||
const showIndicator = computed(() => props.password.length > 0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
v-if="showIndicator"
|
||||
class="password-strength"
|
||||
>
|
||||
<view class="password-strength__row">
|
||||
<view class="password-strength__bars">
|
||||
<view
|
||||
v-for="index in 4"
|
||||
:key="index"
|
||||
class="password-strength__bar"
|
||||
:class="{ 'password-strength__bar--active': index <= strength.level }"
|
||||
:style="index <= strength.level ? { background: strength.color } : undefined"
|
||||
/>
|
||||
</view>
|
||||
<text
|
||||
class="password-strength__label"
|
||||
:style="{ color: strength.color }"
|
||||
>
|
||||
{{ strength.label }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-if="strength.isTooWeak"
|
||||
class="password-strength__error"
|
||||
>
|
||||
<view class="password-strength__error-icon">
|
||||
<text class="password-strength__error-mark">
|
||||
<img src="/static/icons/icon-mark.png" alt="error" />
|
||||
</text>
|
||||
</view>
|
||||
<text class="password-strength__error-text">
|
||||
密码等级过低,请重新输入
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.password-strength {
|
||||
width: 100%;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.password-strength__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.password-strength__bars {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.password-strength__bar {
|
||||
flex: 1;
|
||||
height: 8rpx;
|
||||
background: #e8e8e8;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.password-strength__label {
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.password-strength__error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.password-strength__error-icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
margin-right: 8rpx;
|
||||
// background: #ff4d4f;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.password-strength__error-mark {
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.password-strength__error-text {
|
||||
font-size: 24rpx;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
</style>
|
||||
77
src/pages/Login/user-register/components/RegisterInput.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
options: {
|
||||
virtualHost: true,
|
||||
},
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
icon: string
|
||||
placeholder: string
|
||||
inputType?: 'text' | 'number'
|
||||
password?: boolean
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
function onInput(e: { detail: { value: string } }) {
|
||||
emit('update:modelValue', e.detail.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="register-input">
|
||||
<image
|
||||
class="register-input__icon"
|
||||
:src="icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<input
|
||||
class="register-input__field"
|
||||
:type="inputType || 'text'"
|
||||
:password="password"
|
||||
:placeholder="placeholder"
|
||||
placeholder-class="register-input__placeholder"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.register-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
padding: 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
background: $color-bg-input;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.register-input__icon {
|
||||
flex-shrink: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.register-input__field {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
font-size: 30rpx;
|
||||
color: $color-text-primary;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.register-input__placeholder {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
</style>
|
||||
285
src/pages/Login/user-register/index.vue
Normal file
@@ -0,0 +1,285 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { getPasswordStrength } from '@/utils/password-strength'
|
||||
import { navigateToResultPage } from '@/utils/navigate-result'
|
||||
import CodeInput from './components/CodeInput.vue'
|
||||
import PasswordField from './components/PasswordField.vue'
|
||||
import PasswordStrength from './components/PasswordStrength.vue'
|
||||
import RegisterInput from './components/RegisterInput.vue'
|
||||
|
||||
const phone = ref('')
|
||||
const code = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const agreed = ref(false)
|
||||
|
||||
function toggleAgreed() {
|
||||
agreed.value = !agreed.value
|
||||
}
|
||||
|
||||
function handleRegister() {
|
||||
if (!agreed.value) {
|
||||
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!phone.value || !code.value || !password.value || !confirmPassword.value) {
|
||||
uni.showToast({ title: '请填写完整信息', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (password.value !== confirmPassword.value) {
|
||||
uni.showToast({ title: '两次密码输入不一致', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (getPasswordStrength(password.value).isTooWeak) {
|
||||
uni.showToast({ title: '密码等级过低,请重新输入', icon: 'none' })
|
||||
return
|
||||
}
|
||||
navigateToResultPage('register')
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function handleUserAgreement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/Login/user-agreement/index',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="register-page">
|
||||
<view class="register-page__header">
|
||||
<text class="register-page__title">
|
||||
创建账号
|
||||
</text>
|
||||
<text class="register-page__subtitle">
|
||||
注册后即可开始使用全部功能
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="register-page__form">
|
||||
<view class="register-page__field">
|
||||
<RegisterInput
|
||||
v-model="phone"
|
||||
icon="/static/icons/icon-phone.png"
|
||||
placeholder="请输入手机号"
|
||||
input-type="number"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="register-page__field">
|
||||
<CodeInput v-model="code" />
|
||||
</view>
|
||||
|
||||
<view class="register-page__field">
|
||||
<PasswordField
|
||||
v-model="password"
|
||||
placeholder="设置密码"
|
||||
/>
|
||||
<PasswordStrength :password="password" />
|
||||
</view>
|
||||
|
||||
<view class="register-page__field">
|
||||
<PasswordField
|
||||
v-model="confirmPassword"
|
||||
placeholder="再次确认密码"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="register-page__agreement"
|
||||
@tap="toggleAgreed"
|
||||
>
|
||||
<view
|
||||
class="register-page__checkbox"
|
||||
:class="{ 'register-page__checkbox--checked': agreed }"
|
||||
>
|
||||
<text
|
||||
v-if="agreed"
|
||||
class="register-page__checkmark"
|
||||
>
|
||||
✓
|
||||
</text>
|
||||
</view>
|
||||
<view class="register-page__agreement-text">
|
||||
<text class="register-page__agreement-normal">
|
||||
我已阅读并同意
|
||||
</text>
|
||||
<text
|
||||
class="register-page__agreement-link"
|
||||
@tap.stop="handleUserAgreement"
|
||||
>
|
||||
用户协议
|
||||
</text>
|
||||
<text class="register-page__agreement-normal">
|
||||
和
|
||||
</text>
|
||||
<text class="register-page__agreement-link">
|
||||
隐私政策
|
||||
</text>
|
||||
<text class="register-page__agreement-normal">
|
||||
以及
|
||||
</text>
|
||||
<text class="register-page__agreement-link">
|
||||
定制条款
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button
|
||||
class="register-page__submit"
|
||||
@tap="handleRegister"
|
||||
>
|
||||
注册
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="register-page__footer">
|
||||
<text class="register-page__footer-normal">
|
||||
已有账号?
|
||||
</text>
|
||||
<text
|
||||
class="register-page__footer-link"
|
||||
@tap="handleLogin"
|
||||
>
|
||||
立即登录
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.register-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
padding: 0 64rpx;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.register-page__header {
|
||||
padding-top: 120rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.register-page__title {
|
||||
display: block;
|
||||
font-size: 56rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-primary;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.register-page__subtitle {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 400;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.register-page__form {
|
||||
width: 100%;
|
||||
margin-top: 80rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.register-page__field {
|
||||
width: 100%;
|
||||
margin-top: 24rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.register-page__agreement {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.register-page__checkbox {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-top: 4rpx;
|
||||
margin-right: 12rpx;
|
||||
border: 2rpx solid #cccccc;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
|
||||
&--checked {
|
||||
background: $color-primary;
|
||||
border-color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.register-page__checkmark {
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.register-page__agreement-text {
|
||||
flex: 1;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.register-page__agreement-normal {
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.register-page__agreement-link {
|
||||
color: $color-text-link;
|
||||
}
|
||||
|
||||
.register-page__submit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
margin-top: 48rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
background: $color-primary;
|
||||
border: none;
|
||||
border-radius: 48rpx;
|
||||
line-height: 96rpx;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.register-page__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: auto;
|
||||
padding: 60rpx 0 calc(60rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.register-page__footer-normal {
|
||||
font-size: 28rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.register-page__footer-link {
|
||||
margin-left: 4rpx;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-link;
|
||||
}
|
||||
</style>
|
||||
123
src/pages/Login/user-sms-login/components/CodeInput.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
options: {
|
||||
virtualHost: true,
|
||||
},
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const countdown = ref(0)
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
function onInput(e: { detail: { value: string } }) {
|
||||
emit('update:modelValue', e.detail.value)
|
||||
}
|
||||
|
||||
function handleGetCode() {
|
||||
if (countdown.value > 0)
|
||||
return
|
||||
|
||||
uni.showToast({ title: '验证码已发送', icon: 'none' })
|
||||
countdown.value = 60
|
||||
timer = setInterval(() => {
|
||||
countdown.value -= 1
|
||||
if (countdown.value <= 0 && timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="code-input">
|
||||
<image
|
||||
class="code-input__icon"
|
||||
src="/static/icons/icon-shield-check.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<input
|
||||
class="code-input__field"
|
||||
type="number"
|
||||
placeholder="请输入验证码"
|
||||
placeholder-class="code-input__placeholder"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
>
|
||||
<view
|
||||
class="code-input__btn"
|
||||
:class="{ 'code-input__btn--disabled': countdown > 0 }"
|
||||
@tap="handleGetCode"
|
||||
>
|
||||
<text class="code-input__btn-text">
|
||||
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.code-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
padding: 0 16rpx 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
background: $color-bg-input;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.code-input__icon {
|
||||
flex-shrink: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.code-input__field {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
font-size: 30rpx;
|
||||
color: $color-text-primary;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.code-input__placeholder {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
|
||||
.code-input__btn {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 64rpx;
|
||||
padding: 0 24rpx;
|
||||
background: $color-primary;
|
||||
border-radius: 32rpx;
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.code-input__btn-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
77
src/pages/Login/user-sms-login/components/LoginInput.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
options: {
|
||||
virtualHost: true,
|
||||
},
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
icon: string
|
||||
placeholder: string
|
||||
inputType?: 'text' | 'number'
|
||||
password?: boolean
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
function onInput(e: { detail: { value: string } }) {
|
||||
emit('update:modelValue', e.detail.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="login-input">
|
||||
<image
|
||||
class="login-input__icon"
|
||||
:src="icon"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<input
|
||||
class="login-input__field"
|
||||
:type="inputType || 'text'"
|
||||
:password="password"
|
||||
:placeholder="placeholder"
|
||||
placeholder-class="login-input__placeholder"
|
||||
:value="modelValue"
|
||||
@input="onInput"
|
||||
>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.login-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
padding: 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
background: $color-bg-input;
|
||||
border-radius: 48rpx;
|
||||
}
|
||||
|
||||
.login-input__icon {
|
||||
flex-shrink: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.login-input__field {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
font-size: 30rpx;
|
||||
color: $color-text-primary;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.login-input__placeholder {
|
||||
font-size: 30rpx;
|
||||
color: $color-text-placeholder;
|
||||
}
|
||||
</style>
|
||||
335
src/pages/Login/user-sms-login/index.vue
Normal file
@@ -0,0 +1,335 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import CodeInput from './components/CodeInput.vue'
|
||||
import LoginInput from './components/LoginInput.vue'
|
||||
|
||||
const phone = ref('')
|
||||
const code = ref('')
|
||||
const agreed = ref(false)
|
||||
|
||||
function toggleAgreed() {
|
||||
agreed.value = !agreed.value
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
if (!agreed.value) {
|
||||
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!phone.value || !code.value) {
|
||||
uni.showToast({ title: '请输入手机号和验证码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
function handlePasswordLogin() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function handleRegister() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/Login/user-register/index',
|
||||
})
|
||||
}
|
||||
|
||||
function handleUserAgreement() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/Login/user-agreement/index',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="sms-login-page">
|
||||
<view class="sms-login-page__header">
|
||||
<text class="sms-login-page__title">
|
||||
欢迎回来
|
||||
</text>
|
||||
<text class="sms-login-page__subtitle">
|
||||
使用手机号登录您的账号
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="sms-login-page__form">
|
||||
<LoginInput
|
||||
v-model="phone"
|
||||
icon="/static/icons/icon-phone.png"
|
||||
placeholder="请输入手机号"
|
||||
input-type="number"
|
||||
/>
|
||||
|
||||
<view class="sms-login-page__code-group">
|
||||
<CodeInput v-model="code" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="sms-login-page__agreement"
|
||||
@tap="toggleAgreed"
|
||||
>
|
||||
<view
|
||||
class="sms-login-page__checkbox"
|
||||
:class="{ 'sms-login-page__checkbox--checked': agreed }"
|
||||
>
|
||||
<text
|
||||
v-if="agreed"
|
||||
class="sms-login-page__checkmark"
|
||||
>
|
||||
✓
|
||||
</text>
|
||||
</view>
|
||||
<view class="sms-login-page__agreement-text">
|
||||
<text class="sms-login-page__agreement-normal">
|
||||
我已阅读并同意
|
||||
</text>
|
||||
<text
|
||||
class="sms-login-page__agreement-link"
|
||||
@tap.stop="handleUserAgreement"
|
||||
>
|
||||
用户协议
|
||||
</text>
|
||||
<text class="sms-login-page__agreement-normal">
|
||||
和
|
||||
</text>
|
||||
<text class="sms-login-page__agreement-link">
|
||||
隐私政策
|
||||
</text>
|
||||
<text class="sms-login-page__agreement-normal">
|
||||
以及
|
||||
</text>
|
||||
<text class="sms-login-page__agreement-link">
|
||||
定制条款
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button
|
||||
class="sms-login-page__submit"
|
||||
@tap="handleLogin"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="sms-login-page__other">
|
||||
<view class="sms-login-page__divider">
|
||||
<view class="sms-login-page__divider-line" />
|
||||
<text class="sms-login-page__divider-text">
|
||||
其他登录方式
|
||||
</text>
|
||||
<view class="sms-login-page__divider-line" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="sms-login-page__password"
|
||||
@tap="handlePasswordLogin"
|
||||
>
|
||||
<view class="sms-login-page__password-icon-wrap">
|
||||
<image
|
||||
class="sms-login-page__password-icon"
|
||||
src="/static/icons/icon-password-login.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<text class="sms-login-page__password-text">
|
||||
密码登录
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="sms-login-page__footer">
|
||||
<text class="sms-login-page__footer-normal">
|
||||
没有账号?
|
||||
</text>
|
||||
<text
|
||||
class="sms-login-page__footer-link"
|
||||
@tap="handleRegister"
|
||||
>
|
||||
立即注册
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.sms-login-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
padding: 0 64rpx;
|
||||
background: $color-bg-page;
|
||||
}
|
||||
|
||||
.sms-login-page__header {
|
||||
padding-top: 120rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sms-login-page__title {
|
||||
display: block;
|
||||
font-size: 56rpx;
|
||||
font-weight: 700;
|
||||
color: $color-text-primary;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.sms-login-page__subtitle {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 400;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.sms-login-page__form {
|
||||
width: 100%;
|
||||
margin-top: 80rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sms-login-page__code-group {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.sms-login-page__agreement {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.sms-login-page__checkbox {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-top: 4rpx;
|
||||
margin-right: 12rpx;
|
||||
border: 2rpx solid #cccccc;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s;
|
||||
|
||||
&--checked {
|
||||
background: $color-primary;
|
||||
border-color: $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.sms-login-page__checkmark {
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sms-login-page__agreement-text {
|
||||
flex: 1;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.sms-login-page__agreement-normal {
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.sms-login-page__agreement-link {
|
||||
color: $color-text-link;
|
||||
}
|
||||
|
||||
.sms-login-page__submit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
margin-top: 48rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
background: $color-primary;
|
||||
border: none;
|
||||
border-radius: 48rpx;
|
||||
line-height: 96rpx;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sms-login-page__other {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.sms-login-page__divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sms-login-page__divider-line {
|
||||
flex: 1;
|
||||
height: 1rpx;
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.sms-login-page__divider-text {
|
||||
padding: 0 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: $color-text-secondary;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sms-login-page__password {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.sms-login-page__password-icon-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
background: #B6CDFF;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.sms-login-page__password-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
.sms-login-page__password-text {
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.sms-login-page__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: calc(60rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.sms-login-page__footer-normal {
|
||||
font-size: 28rpx;
|
||||
color: $color-text-secondary;
|
||||
}
|
||||
|
||||
.sms-login-page__footer-link {
|
||||
margin-left: 4rpx;
|
||||
font-size: 28rpx;
|
||||
color: $color-text-link;
|
||||
}
|
||||
</style>
|
||||
BIN
src/static/fonts/AlimamaFangYuanTiVF-Thin.woff2
Normal file
3
src/static/icons/icon-back.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 6L9 12L15 18" stroke="#1A1A1A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 215 B |
6
src/static/icons/icon-eye-close.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 3L21 21" stroke="#999999" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M10.58 10.58C10.21 10.95 10 11.45 10 12C10 13.1 10.9 14 12 14C12.55 14 13.05 13.79 13.42 13.42" stroke="#999999" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M9.88 5.09C10.56 5.03 11.27 5 12 5C17 5 21.27 8.11 23 12.5C22.39 14.32 21.22 15.92 19.77 17.18" stroke="#999999" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.61 6.61C4.09 8.17 2.36 10.27 1 12.5C2.73 16.89 7 20 12 20C13.55 20 15.03 19.69 16.39 19.14" stroke="#999999" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 738 B |
4
src/static/icons/icon-eye-open.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 5C7 5 2.73 8.11 1 12.5C2.73 16.89 7 20 12 20C17 20 21.27 16.89 23 12.5C21.27 8.11 17 5 12 5Z" stroke="#999999" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="12.5" r="3" stroke="#999999" stroke-width="1.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 346 B |
BIN
src/static/icons/icon-lock.png
Normal file
|
After Width: | Height: | Size: 659 B |
5
src/static/icons/icon-login-failed.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="60" cy="60" r="60" fill="#FF4D4F"/>
|
||||
<path d="M60 34V68" stroke="#FFFFFF" stroke-width="8" stroke-linecap="round"/>
|
||||
<circle cx="60" cy="84" r="4" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 287 B |
BIN
src/static/icons/icon-mark.png
Normal file
|
After Width: | Height: | Size: 548 B |
BIN
src/static/icons/icon-password-login.png
Normal file
|
After Width: | Height: | Size: 554 B |
BIN
src/static/icons/icon-phone.png
Normal file
|
After Width: | Height: | Size: 609 B |
BIN
src/static/icons/icon-register-success.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
src/static/icons/icon-shield-check.png
Normal file
|
After Width: | Height: | Size: 836 B |
BIN
src/static/icons/icon-sms-login.png
Normal file
|
After Width: | Height: | Size: 697 B |
BIN
src/static/icons/logo.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
12
src/styles/fonts.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
@font-face {
|
||||
font-family: 'Alimama FangYuanTi VF';
|
||||
src: url('/static/fonts/AlimamaFangYuanTiVF-Thin.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Alimama FangYuanTi VF-Bold';
|
||||
src: url('/static/fonts/AlimamaFangYuanTiVF-Thin.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
font-weight: 700;
|
||||
}
|
||||
15
src/styles/variables.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
// 设计规范色值
|
||||
$color-primary: #2563EB;
|
||||
$color-primary-light: #e8f0ff;
|
||||
|
||||
$color-text-primary: #1a1a1a;
|
||||
$color-text-secondary: #999999;
|
||||
$color-text-placeholder: #b0b0b0;
|
||||
$color-text-link: #2563EB;
|
||||
|
||||
$color-bg-page: #ffffff;
|
||||
$color-bg-input: #f2f3f5;
|
||||
|
||||
$color-splash-gradient-start: #f0f7ff;
|
||||
$color-splash-gradient-end: #deedff;
|
||||
$color-splash-circle: rgba(180, 210, 255, 0.35);
|
||||
40
src/types/result-page.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export type ResultPageType = 'register' | 'resetPassword' | 'loginFailed'
|
||||
|
||||
export interface ResultPageConfig {
|
||||
title: string
|
||||
subtitleTarget: string
|
||||
icon: string
|
||||
redirectUrl: string
|
||||
}
|
||||
|
||||
export const RESULT_PAGE_CONFIG: Record<ResultPageType, ResultPageConfig> = {
|
||||
register: {
|
||||
title: '注册成功,请登录',
|
||||
subtitleTarget: '登录页面',
|
||||
icon: '/static/icons/icon-register-success.png',
|
||||
redirectUrl: '/pages/Login/user-login/index',
|
||||
},
|
||||
resetPassword: {
|
||||
title: '修改成功,请登录',
|
||||
subtitleTarget: '登录页面',
|
||||
icon: '/static/icons/icon-register-success.png',
|
||||
redirectUrl: '/pages/Login/user-login/index',
|
||||
},
|
||||
loginFailed: {
|
||||
title: '登录失败,请先注册账号',
|
||||
subtitleTarget: '注册页面',
|
||||
icon: '/static/icons/icon-login-failed.svg',
|
||||
redirectUrl: '/pages/Login/user-register/index',
|
||||
},
|
||||
}
|
||||
|
||||
export const RESULT_PAGE_ROUTE = '/pages/Login/user-register-success/index'
|
||||
|
||||
export const RESULT_REDIRECT_DELAY = 3000
|
||||
|
||||
// 兼容旧命名
|
||||
export type SuccessPageType = ResultPageType
|
||||
export const SUCCESS_PAGE_CONFIG = RESULT_PAGE_CONFIG
|
||||
export const SUCCESS_PAGE_ROUTE = RESULT_PAGE_ROUTE
|
||||
export const SUCCESS_REDIRECT_URL = RESULT_PAGE_CONFIG.register.redirectUrl
|
||||
export const SUCCESS_REDIRECT_DELAY = RESULT_REDIRECT_DELAY
|
||||
18
src/utils/navigate-result.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
RESULT_PAGE_ROUTE,
|
||||
type ResultPageType,
|
||||
} from '@/types/result-page'
|
||||
|
||||
export function navigateToResultPage(type: ResultPageType) {
|
||||
const url = `${RESULT_PAGE_ROUTE}?type=${type}`
|
||||
|
||||
uni.redirectTo({
|
||||
url,
|
||||
fail: () => {
|
||||
uni.navigateTo({ url })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 兼容旧命名
|
||||
export { navigateToResultPage as navigateToSuccessPage }
|
||||
42
src/utils/password-strength.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export type PasswordStrengthLevel = 0 | 1 | 2 | 3 | 4
|
||||
|
||||
export interface PasswordStrengthResult {
|
||||
level: PasswordStrengthLevel
|
||||
label: string
|
||||
color: string
|
||||
isTooWeak: boolean
|
||||
}
|
||||
|
||||
export function getPasswordStrength(password: string): PasswordStrengthResult {
|
||||
if (!password) {
|
||||
return { level: 0, label: '', color: '', isTooWeak: false }
|
||||
}
|
||||
|
||||
const hasLower = /[a-z]/.test(password)
|
||||
const hasUpper = /[A-Z]/.test(password)
|
||||
const hasDigit = /\d/.test(password)
|
||||
const hasSpecial = /[^a-zA-Z0-9]/.test(password)
|
||||
const typeCount = [hasLower, hasUpper, hasDigit, hasSpecial].filter(Boolean).length
|
||||
|
||||
let level: PasswordStrengthLevel = 1
|
||||
|
||||
if (password.length >= 8 && typeCount >= 3 && hasDigit && (hasLower || hasUpper)) {
|
||||
level = password.length >= 12 ? 4 : 3
|
||||
}
|
||||
else if (password.length >= 8 && typeCount >= 2) {
|
||||
level = 2
|
||||
}
|
||||
|
||||
if (password.length < 8 || /^\d+$/.test(password) || /^[a-zA-Z]+$/.test(password)) {
|
||||
level = 1
|
||||
}
|
||||
|
||||
const config: Record<Exclude<PasswordStrengthLevel, 0>, Omit<PasswordStrengthResult, 'level'>> = {
|
||||
1: { label: '弱', color: '#FF4D4F', isTooWeak: true },
|
||||
2: { label: '中', color: '#FAAD14', isTooWeak: false },
|
||||
3: { label: '强', color: '#52C41A', isTooWeak: false },
|
||||
4: { label: '很强', color: '#52C41A', isTooWeak: false },
|
||||
}
|
||||
|
||||
return { level, ...config[level] }
|
||||
}
|
||||