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-weixin": "3.0.0-5000720260410001",
|
||||||
"@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
|
"@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
|
||||||
"@dcloudio/uni-quickapp-webview": "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": "3.4.21",
|
||||||
"vue-i18n": "9.6.2",
|
"vue-i18n": "9.6.2",
|
||||||
"vue-router": "4.5.1"
|
"vue-router": "4.5.1"
|
||||||
@@ -3807,6 +3808,12 @@
|
|||||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
"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": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.2",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
"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-weixin": "3.0.0-5000720260410001",
|
||||||
"@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
|
"@dcloudio/uni-mp-xhs": "3.0.0-5000720260410001",
|
||||||
"@dcloudio/uni-quickapp-webview": "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": "3.4.21",
|
||||||
"vue-i18n": "9.6.2",
|
"vue-i18n": "9.6.2",
|
||||||
"vue-router": "4.5.1"
|
"vue-router": "4.5.1"
|
||||||
@@ -53,4 +54,4 @@
|
|||||||
"vite": "5.2.8",
|
"vite": "5.2.8",
|
||||||
"vue-tsc": "^3.2.7"
|
"vue-tsc": "^3.2.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,11 @@ import { onLaunch } from '@dcloudio/uni-app'
|
|||||||
onLaunch(() => {})
|
onLaunch(() => {})
|
||||||
</script>
|
</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": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"path": "pages/index",
|
"path": "pages/Login/splash/index",
|
||||||
"type": "home"
|
"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": {
|
"globalStyle": {
|
||||||
"backgroundColor": "@bgColor",
|
"backgroundColor": "#ffffff",
|
||||||
"backgroundColorBottom": "@bgColorBottom",
|
"backgroundColorBottom": "#ffffff",
|
||||||
"backgroundColorTop": "@bgColorTop",
|
"backgroundColorTop": "#ffffff",
|
||||||
"backgroundTextStyle": "@bgTxtStyle",
|
"backgroundTextStyle": "dark",
|
||||||
"navigationBarBackgroundColor": "#000000",
|
"navigationBarBackgroundColor": "#ffffff",
|
||||||
"navigationBarTextStyle": "@navTxtStyle",
|
"navigationBarTextStyle": "black",
|
||||||
"navigationBarTitleText": "Uni Creator",
|
"navigationBarTitleText": "",
|
||||||
"navigationStyle": "custom"
|
"navigationStyle": "custom"
|
||||||
},
|
},
|
||||||
"subPackages": []
|
"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] }
|
||||||
|
}
|
||||||