更新:登录功能
24
App.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
onLaunch: function() {
|
||||||
|
console.log('App Launch')
|
||||||
|
},
|
||||||
|
onShow: function() {
|
||||||
|
console.log('App Show')
|
||||||
|
},
|
||||||
|
onHide: function() {
|
||||||
|
console.log('App Hide')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import "@/static/tailwind.css";
|
||||||
|
.container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 DCloud
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
15
README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# hello-i18n
|
||||||
|
|
||||||
|
# en
|
||||||
|
|
||||||
|
A demo project for uni-app globalization
|
||||||
|
|
||||||
|
This template include uni-framework, manifest.json, pages.json, tabbar, Page, Component, API
|
||||||
|
|
||||||
|
|
||||||
|
# zh-hans
|
||||||
|
|
||||||
|
uni-app 国际化演示
|
||||||
|
|
||||||
|
包含 uni-framework、manifest.json、pages.json、tabbar、页面、组件、API
|
||||||
|
|
||||||
7
api/clients/main.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// api/clients/main.ts
|
||||||
|
import { createRequestClient } from '../request';
|
||||||
|
import { SERVICE_MAP } from '../config';
|
||||||
|
|
||||||
|
export const mainClient = createRequestClient({
|
||||||
|
baseURL: SERVICE_MAP.MAIN,
|
||||||
|
});
|
||||||
11
api/clients/payment.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// api/clients/payment.ts
|
||||||
|
/**
|
||||||
|
* 支付专用接口实例
|
||||||
|
*/
|
||||||
|
import { createRequestClient } from '../request';
|
||||||
|
import { SERVICE_MAP } from '../config';
|
||||||
|
|
||||||
|
export const paymentClient = createRequestClient({
|
||||||
|
baseURL: SERVICE_MAP.PAYMENT,
|
||||||
|
});
|
||||||
|
|
||||||
28
api/config.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// api/config.ts
|
||||||
|
export const ENV = process.env.NODE_ENV || 'development';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发/生产的 base url 组织成 SERVICE_MAP。
|
||||||
|
* 根据实际域名替换下面的地址。
|
||||||
|
*/
|
||||||
|
const BASE_URL_MAP = {
|
||||||
|
development: {
|
||||||
|
MAIN: 'http://192.168.110.100:9300/pb/',
|
||||||
|
// PAYMENT: 'https://dev-pay.example.com', // 暂时用不到
|
||||||
|
// CDN: 'https://cdn-dev.example.com', // 暂时用不到
|
||||||
|
},
|
||||||
|
production: {
|
||||||
|
MAIN: 'https://global.nuttyreading.com/',
|
||||||
|
// PAYMENT: 'https://pay.example.com', // 暂时用不到
|
||||||
|
// CDN: 'https://cdn.example.com', // 暂时用不到
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const APP_INFO = {
|
||||||
|
TYPE: 'abroad', // APP 名称
|
||||||
|
VERSION_CODE: '1.0.0', // APP 版本号,可能升级的时候会用,这里需要再确定?
|
||||||
|
}
|
||||||
|
|
||||||
|
export const REQUEST_TIMEOUT = 15000;
|
||||||
|
|
||||||
|
export const SERVICE_MAP = (BASE_URL_MAP as any)[ENV] ?? BASE_URL_MAP.development;
|
||||||
32
api/interceptors/request.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// 片段示例 - requestInterceptor 更稳健的写法
|
||||||
|
import type { IRequestOptions } from '../types'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { APP_INFO } from '@/api/config'
|
||||||
|
|
||||||
|
export function requestInterceptor(options: IRequestOptions): IRequestOptions {
|
||||||
|
const headers = { ...(options.headers || {}) }
|
||||||
|
|
||||||
|
// 更明确地调用 useUserStore
|
||||||
|
let token = ''
|
||||||
|
try {
|
||||||
|
const userStore = typeof useUserStore === 'function' ? useUserStore() : null
|
||||||
|
token = userStore?.token || uni.getStorageSync('token') || ''
|
||||||
|
} catch (e) {
|
||||||
|
token = uni.getStorageSync('token') || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) headers.token = token
|
||||||
|
|
||||||
|
// Content-Type:只有在没有 files 时才默认为 application/json
|
||||||
|
if (!options.files && !headers['Content-Type']) {
|
||||||
|
headers['Content-Type'] = 'application/json;charset=UTF-8'
|
||||||
|
}
|
||||||
|
|
||||||
|
headers['appType'] = APP_INFO.TYPE
|
||||||
|
headers['version_code'] = APP_INFO.VERSION_CODE || '1.0.0'
|
||||||
|
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
header: headers,
|
||||||
|
}
|
||||||
|
}
|
||||||
78
api/interceptors/response.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// api/interceptors/response.ts
|
||||||
|
import type { IApiResponse } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应拦截器:严格兼容原项目返回约定
|
||||||
|
*
|
||||||
|
* 原项目要点回顾:
|
||||||
|
* - 当 response.statusCode == 200 且 (body.success === true || body.code == 0) 时判定为成功;
|
||||||
|
* - 部分错误码("401", 1000,1001,1100,402 等)表示需要重新登录/清除状态并跳转;
|
||||||
|
* - 错误时会返回 Promise.reject({ statusCode: 0, errMsg: ... , data: body })
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
function handleAuthExpired() {
|
||||||
|
// 清空本地登录信息(保持与原项目一致)
|
||||||
|
try {
|
||||||
|
uni.removeStorageSync('userInfo');
|
||||||
|
} catch (e) {}
|
||||||
|
// 跳转 login,与原项目保持一致的路径
|
||||||
|
// 在小程序/APP/H5 情况下原项目分别做了适配,简单通用处理如下:
|
||||||
|
uni.showToast({ title: '登录失效,请重新登录', icon: 'none' });
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateTo({ url: '/pages/login/login' });
|
||||||
|
}, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function responseInterceptor(res: UniApp.RequestSuccessCallbackResult) {
|
||||||
|
// 先处理非 200 的 http 状态
|
||||||
|
if (res.statusCode && res.statusCode !== 200) {
|
||||||
|
const msg = `网络错误(${res.statusCode})`;
|
||||||
|
uni.showToast({ title: msg, icon: 'none' });
|
||||||
|
return Promise.reject({ statusCode: res.statusCode, errMsg: msg, response: res });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可能为字符串,尝试解析(原项目也做了类似处理)
|
||||||
|
let httpData: IApiResponse | string = res.data as any;
|
||||||
|
if (typeof httpData === 'string') {
|
||||||
|
try {
|
||||||
|
httpData = JSON.parse(httpData);
|
||||||
|
} catch (e) {
|
||||||
|
// 无法解析仍然返回原始 body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 规范化 message 字段
|
||||||
|
const message = (httpData as any).msg || (httpData as any).message || (httpData as any).errMsg || '';
|
||||||
|
|
||||||
|
// 成功判断:与原项目一致的条件
|
||||||
|
const successFlag = (httpData as any).success === true || (httpData as any).code === 0;
|
||||||
|
|
||||||
|
if (successFlag) {
|
||||||
|
// 返回原始 httpData(与原项目 dataFactory 返回 Promise.resolve(httpData) 保持一致)
|
||||||
|
// 但大多数调用者更关心 data 字段,这里返回整个 httpData,调用者可取 .data
|
||||||
|
return Promise.resolve(httpData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录失效或需要强制登录的一些 code(与原项目一致)
|
||||||
|
const code = (httpData as any).code;
|
||||||
|
if (code === '401' || code === 401) {
|
||||||
|
// 触发登出流程
|
||||||
|
handleAuthExpired();
|
||||||
|
return Promise.reject({ statusCode: 0, errMsg: '登录失效', data: httpData });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原项目还将 1000,1001,1100,402 等视作需要强制登录
|
||||||
|
if (code === '1000' || code === '1001' || code === 1000 || code === 1001 || code === 1100 || code === '402' || code === 402) {
|
||||||
|
handleAuthExpired();
|
||||||
|
return Promise.reject({ statusCode: 0, errMsg: message || '请登录', data: httpData });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他后端业务错误:toast 并 reject
|
||||||
|
const errMsg = message || '请求异常';
|
||||||
|
if (errMsg) {
|
||||||
|
uni.showToast({ title: errMsg, icon: 'none' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject({ statusCode: 0, errMsg, data: httpData });
|
||||||
|
}
|
||||||
49
api/modules/auth.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// api/modules/auth.ts
|
||||||
|
import { mainClient } from '@/api/clients/main'
|
||||||
|
import type { IApiResponse } from '@/api/types'
|
||||||
|
import type { IUserInfo, ILoginResponse } from '@/types/user'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码登录/注册
|
||||||
|
* @param tel 邮箱地址
|
||||||
|
* @param code 验证码
|
||||||
|
*/
|
||||||
|
export async function loginWithCode(tel: string, code: string) {
|
||||||
|
const res = await mainClient.request<IApiResponse<ILoginResponse>>({
|
||||||
|
url: 'book/user/registerOrLogin',
|
||||||
|
method: 'GET',
|
||||||
|
data: { tel, code }
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码登录
|
||||||
|
* @param phone 邮箱地址
|
||||||
|
* @param password 密码
|
||||||
|
*/
|
||||||
|
export async function loginWithPassword(phone: string, password: string) {
|
||||||
|
const res = await mainClient.request<IApiResponse<ILoginResponse>>({
|
||||||
|
url: 'book/user/login',
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
data: { phone, password }
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置密码
|
||||||
|
* @param phone 邮箱地址
|
||||||
|
* @param code 验证码
|
||||||
|
* @param password 新密码
|
||||||
|
*/
|
||||||
|
export async function resetPassword(phone: string, code: string, password: string) {
|
||||||
|
const res = await mainClient.request<IApiResponse>({
|
||||||
|
url: 'book/user/setPassword',
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
data: { phone, code, password }
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
31
api/modules/common.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// api/modules/common.ts
|
||||||
|
import { mainClient } from '@/api/clients/main'
|
||||||
|
import type { IApiResponse } from '@/api/types'
|
||||||
|
import type { IAgreement } from '@/types/user'
|
||||||
|
|
||||||
|
export const commonApi = {
|
||||||
|
/**
|
||||||
|
* 发送邮箱验证码
|
||||||
|
* @param email 邮箱地址
|
||||||
|
*/
|
||||||
|
sendMailCaptcha: async (email: string) => {
|
||||||
|
const res = await mainClient.request<IApiResponse>({
|
||||||
|
url: 'common/user/getMailCaptcha',
|
||||||
|
method: 'GET',
|
||||||
|
data: { email }
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取协议内容
|
||||||
|
* @param id 协议 ID (111: 用户协议, 112: 隐私政策)
|
||||||
|
*/
|
||||||
|
getAgreement: async (id: number) => {
|
||||||
|
const res = await mainClient.request<IApiResponse<IAgreement>>({
|
||||||
|
url: 'sys/agreement/getAgreement',
|
||||||
|
method: 'POST',
|
||||||
|
data: { id }
|
||||||
|
})
|
||||||
|
return res.agreement
|
||||||
|
}
|
||||||
|
}
|
||||||
45
api/request.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// api/request.ts
|
||||||
|
import { requestInterceptor } from './interceptors/request';
|
||||||
|
import { responseInterceptor } from './interceptors/response';
|
||||||
|
import type { IRequestOptions, ICreateClientConfig } from './types';
|
||||||
|
import { REQUEST_TIMEOUT } from './config';
|
||||||
|
|
||||||
|
export function createRequestClient(cfg: ICreateClientConfig) {
|
||||||
|
const baseURL = cfg.baseURL;
|
||||||
|
const timeout = cfg.timeout ?? REQUEST_TIMEOUT;
|
||||||
|
|
||||||
|
async function request<T = any>(options: IRequestOptions): Promise<T> {
|
||||||
|
// 组装 final options
|
||||||
|
const final: UniApp.RequestOptions = {
|
||||||
|
...options,
|
||||||
|
url: (options.url || '').startsWith('http') ? options.url : `${baseURL}${options.url}`,
|
||||||
|
timeout,
|
||||||
|
header: options.header || {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// run request interceptor to mutate headers, etc.
|
||||||
|
const intercepted = requestInterceptor(final as IRequestOptions);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.request({
|
||||||
|
...intercepted,
|
||||||
|
success(res: any) {
|
||||||
|
// delegate to response interceptor
|
||||||
|
responseInterceptor(res)
|
||||||
|
.then((r) => {
|
||||||
|
resolve(r as any);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fail(err: any) {
|
||||||
|
uni.showToast({ title: '网络连接失败', icon: 'none' });
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { request };
|
||||||
|
}
|
||||||
25
api/types.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// api/types.d.ts
|
||||||
|
export interface IApiResponse<T = any> {
|
||||||
|
/** 有些后端使用 code 字段,有些使用 success 字段 */
|
||||||
|
code?: number | string;
|
||||||
|
success?: boolean;
|
||||||
|
data?: T;
|
||||||
|
msg?: string;
|
||||||
|
message?: string;
|
||||||
|
errMsg?: string;
|
||||||
|
[k: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* createRequestClient 返回的 request 函数签名
|
||||||
|
*/
|
||||||
|
export interface IRequestOptions extends UniApp.RequestOptions {
|
||||||
|
// 允许扩展额外字段(例如 FILE 上传专用等)
|
||||||
|
maxSize?: number;
|
||||||
|
files?: Array<{ name: string; uri: string; fileType?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICreateClientConfig {
|
||||||
|
baseURL: string;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
72
auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
|
const computed: typeof import('vue')['computed']
|
||||||
|
const createApp: typeof import('vue')['createApp']
|
||||||
|
const customRef: typeof import('vue')['customRef']
|
||||||
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
|
const h: typeof import('vue')['h']
|
||||||
|
const inject: typeof import('vue')['inject']
|
||||||
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
|
const isRef: typeof import('vue')['isRef']
|
||||||
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
|
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||||
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
|
const onMounted: typeof import('vue')['onMounted']
|
||||||
|
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||||
|
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||||
|
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||||
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||||
|
const provide: typeof import('vue')['provide']
|
||||||
|
const reactive: typeof import('vue')['reactive']
|
||||||
|
const readonly: typeof import('vue')['readonly']
|
||||||
|
const ref: typeof import('vue')['ref']
|
||||||
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
|
const toRef: typeof import('vue')['toRef']
|
||||||
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
const toValue: typeof import('vue')['toValue']
|
||||||
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
|
const unref: typeof import('vue')['unref']
|
||||||
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
|
const useI18n: typeof import('vue-i18n')['useI18n']
|
||||||
|
const useId: typeof import('vue')['useId']
|
||||||
|
const useModel: typeof import('vue')['useModel']
|
||||||
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||||
|
const watch: typeof import('vue')['watch']
|
||||||
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
|
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
|
import('vue')
|
||||||
|
}
|
||||||
10
changelog.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
## 1.0.3(2025-06-26)
|
||||||
|
- index.html 模板中的 lang="en" 改成 lang="zh-CN"
|
||||||
|
## 1.0.2(2022-06-30)
|
||||||
|
- 新增 支持 ios 安全区
|
||||||
|
## 1.0.1(2021-11-26)
|
||||||
|
- 新增 schema 国际化 (HBuilderX 3.3+)
|
||||||
|
- 修复 非 Android 平台切换语音无法实时变化的问题
|
||||||
|
- 修复 设置某些语言下无法生效的问题 (HBuilderX 3.3+)
|
||||||
|
## 1.0.0(2021-10-20)
|
||||||
|
- 初始化
|
||||||
16
hooks/usePageAuth.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// hooks/usePageAuth.ts
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
export function usePageAuth(redirect = '/pages/login/login') {
|
||||||
|
const store = useUserStore()
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (!store.token) {
|
||||||
|
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({ url: redirect })
|
||||||
|
}, 800)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
35
hooks/useRequest.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// hooks/useRequest.ts
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
interface IUseRequestOptions<T> {
|
||||||
|
immediate?: boolean
|
||||||
|
initialData?: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRequest<T>(
|
||||||
|
func: () => Promise<T>,
|
||||||
|
options: IUseRequestOptions<T> = { immediate: false },
|
||||||
|
) {
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<any>(null)
|
||||||
|
const data = ref<T | undefined>(options.initialData)
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
const res = await func()
|
||||||
|
data.value = res
|
||||||
|
return res
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err
|
||||||
|
throw err
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options.immediate && run()
|
||||||
|
|
||||||
|
return { loading, error, data, run }
|
||||||
|
}
|
||||||
41
hooks/useUpload.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// hooks/useUpload.ts
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { getAuthToken } from '@/utils/auth'
|
||||||
|
|
||||||
|
export function useUpload(uploadUrl: string) {
|
||||||
|
const progress = ref(0)
|
||||||
|
const uploading = ref(false)
|
||||||
|
|
||||||
|
const upload = (filePath: string) => {
|
||||||
|
uploading.value = true
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const uploadTask = uni.uploadFile({
|
||||||
|
url: uploadUrl,
|
||||||
|
filePath,
|
||||||
|
name: 'file',
|
||||||
|
header: {
|
||||||
|
Authorization: `Bearer ${getAuthToken()}`,
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(res.data)
|
||||||
|
if (data.code === 200) resolve(data.data)
|
||||||
|
else reject(data)
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: reject,
|
||||||
|
complete: () => {
|
||||||
|
uploading.value = false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
uploadTask.onProgressUpdate((res) => {
|
||||||
|
progress.value = res.progress
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { progress, uploading, upload }
|
||||||
|
}
|
||||||
20
index.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script>
|
||||||
|
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||||
|
CSS.supports('top: constant(a)'))
|
||||||
|
document.write(
|
||||||
|
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||||
|
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||||
|
</script>
|
||||||
|
<title></title>
|
||||||
|
<!--preload-links-->
|
||||||
|
<!--app-context-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><!--app-html--></div>
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
77
locale/en.json
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"locale": {
|
||||||
|
"auto": "System",
|
||||||
|
"en": "English",
|
||||||
|
"zh-hans": "简体中文"
|
||||||
|
},
|
||||||
|
"global": {
|
||||||
|
"ok": "OK",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"close": "Close",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"loading": "Loading",
|
||||||
|
"tips": "Tips"
|
||||||
|
},
|
||||||
|
"tabar.course": "COURSE",
|
||||||
|
"tabar.book": "EBOOK",
|
||||||
|
"tabar.user": "My",
|
||||||
|
"index": {
|
||||||
|
"title": "Taimed International",
|
||||||
|
"schema": "Schema",
|
||||||
|
"demo": "uni-app globalization",
|
||||||
|
"demo-description": "Include uni-framework, manifest.json, pages.json, tabbar, Page, Component, API, Schema",
|
||||||
|
"detail": "Detail",
|
||||||
|
"language": "Language",
|
||||||
|
"language-info": "Settings",
|
||||||
|
"system-language": "System language",
|
||||||
|
"application-language": "Application language",
|
||||||
|
"language-change-confirm": "Applying this setting will restart the app"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"message": "Message"
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"name": "Name",
|
||||||
|
"add": "Add",
|
||||||
|
"add-success": "Add success"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "Hello! Welcome to Taimed International",
|
||||||
|
"codeLogin": "Verification Code Login/Registration",
|
||||||
|
"passwordLogin": "Password Login",
|
||||||
|
"email": "email",
|
||||||
|
"emailPlaceholder": "please enter your email address",
|
||||||
|
"emailError": "please enter a valid email address",
|
||||||
|
"code": "verification code",
|
||||||
|
"codePlaceholder": "please enter the verification code",
|
||||||
|
"sendCodeSuccess": "Verification code sent successfully",
|
||||||
|
"password": "password",
|
||||||
|
"passwordPlaceholder": "please input a password",
|
||||||
|
"getCode": "Get Code",
|
||||||
|
"agree": "I have agreed",
|
||||||
|
"userAgreement": "User Agreement",
|
||||||
|
"privacyPolicy": "Privacy Policy",
|
||||||
|
"goLogin": "Go Login",
|
||||||
|
"switchToPassword": "Password Login",
|
||||||
|
"switchToCode": "Verification Code Login",
|
||||||
|
"forgotPassword": "forgot password?",
|
||||||
|
"noLogin": "No login experience",
|
||||||
|
"agreeFirst": "Please agree to the User Agreement and Privacy Policy first",
|
||||||
|
"loginSuccess": "success",
|
||||||
|
"loginFailed": "failed"
|
||||||
|
},
|
||||||
|
"forget": {
|
||||||
|
"title": "Forgot Password",
|
||||||
|
"password": "password",
|
||||||
|
"passwordPlaceholder": "enter your password",
|
||||||
|
"passwordAgain": "password again",
|
||||||
|
"passwordAgainPlaceholder": "enter your password again",
|
||||||
|
"passwordNotMatch": "Passwords do not match",
|
||||||
|
"submit": "Submit",
|
||||||
|
"getCode": "Get Code",
|
||||||
|
"passwordStrengthStrong": "Strong password strength.",
|
||||||
|
"passwordStrengthMedium": "Medium password strength.",
|
||||||
|
"passwordStrengthWeak": "please use a password consisting of at least two types: uppercase and lowercase letters, numbers, and symbols, with a length of 8 characters.",
|
||||||
|
"passwordChanged": "Password changed successfully"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
locale/index.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import en from './en.json'
|
||||||
|
import zhHans from './zh-Hans.json'
|
||||||
|
// import zhHant from './zh-Hant.json'
|
||||||
|
// import ja from './ja.json'
|
||||||
|
export default {
|
||||||
|
en,
|
||||||
|
'zh-Hans': zhHans
|
||||||
|
}
|
||||||
78
locale/zh-Hans.json
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"locale": {
|
||||||
|
"auto": "系统",
|
||||||
|
"en": "English",
|
||||||
|
"zh-hans": "简体中文"
|
||||||
|
},
|
||||||
|
"global": {
|
||||||
|
"ok": "确定",
|
||||||
|
"cancel": "取消",
|
||||||
|
"close": "关闭",
|
||||||
|
"confirm": "确认",
|
||||||
|
"loading": "加载中",
|
||||||
|
"tips": "提示"
|
||||||
|
},
|
||||||
|
"tabar.course": "课程",
|
||||||
|
"tabar.book": "图书",
|
||||||
|
"tabar.user": "我的",
|
||||||
|
"index": {
|
||||||
|
"title": "太湖国际",
|
||||||
|
"schema": "Schema",
|
||||||
|
"demo": "uni-app 国际化演示",
|
||||||
|
"demo-description": "包含 uni-framework、manifest.json、pages.json、tabbar、页面、组件、API、Schema",
|
||||||
|
"detail": "详情",
|
||||||
|
"language": "语言",
|
||||||
|
"language-info": "语言信息",
|
||||||
|
"system-language": "系统语言",
|
||||||
|
"application-language": "应用语言",
|
||||||
|
"language-change-confirm": "应用此设置将重启App"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"message": "提示"
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"name": "姓名",
|
||||||
|
"add": "新增",
|
||||||
|
"add-success": "新增成功"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"title": "你好!欢迎来到 太湖国际",
|
||||||
|
"codeLogin": "验证码登录/注册",
|
||||||
|
"passwordLogin": "密码登录",
|
||||||
|
"email": "邮箱",
|
||||||
|
"emailPlaceholder": "请输入您的邮箱地址",
|
||||||
|
"emailError": "请输入正确的邮箱地址",
|
||||||
|
"code": "验证码",
|
||||||
|
"codePlaceholder": "请输入验证码",
|
||||||
|
"sendCodeSuccess": "验证码发送成功",
|
||||||
|
"password": "密码",
|
||||||
|
"passwordPlaceholder": "请输入密码",
|
||||||
|
"getCode": "获取验证码",
|
||||||
|
"agree": "我已同意",
|
||||||
|
"userAgreement": "用户协议",
|
||||||
|
"privacyPolicy": "隐私政策",
|
||||||
|
"goLogin": "登录",
|
||||||
|
"switchToPassword": "密码登录",
|
||||||
|
"switchToCode": "验证码登录",
|
||||||
|
"forgotPassword": "忘记密码?",
|
||||||
|
"noLogin": "游客体验",
|
||||||
|
"agreeFirst": "请先同意用户协议和隐私政策",
|
||||||
|
"loginSuccess": "登录成功",
|
||||||
|
"loginFailed": "登录失败"
|
||||||
|
},
|
||||||
|
"forget": {
|
||||||
|
"title": "忘记密码",
|
||||||
|
"email": "邮箱",
|
||||||
|
"password": "密码",
|
||||||
|
"passwordPlaceholder": "请输入密码",
|
||||||
|
"passwordAgain": "再次输入密码",
|
||||||
|
"passwordAgainPlaceholder": "请再次输入密码",
|
||||||
|
"passwordNotMatch": "两次密码不一致",
|
||||||
|
"submit": "提交",
|
||||||
|
"getCode": "获取验证码",
|
||||||
|
"passwordStrengthStrong": "密码强度强",
|
||||||
|
"passwordStrengthMedium": "密码强度中等",
|
||||||
|
"passwordStrengthWeak": "请使用至少包含大小写字母、数字、符号中的两种类型,长度为8个字符的密码",
|
||||||
|
"passwordChanged": "密码修改成功"
|
||||||
|
}
|
||||||
|
}
|
||||||
38
main.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import App from './App'
|
||||||
|
import messages from './locale/index'
|
||||||
|
import '@/uni.scss';
|
||||||
|
|
||||||
|
let i18nConfig = {
|
||||||
|
locale: uni.getLocale(),
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
|
||||||
|
// #ifndef VUE3
|
||||||
|
import Vue from 'vue'
|
||||||
|
import VueI18n from 'vue-i18n''
|
||||||
|
Vue.use(VueI18n)
|
||||||
|
export i18n = new VueI18n(i18nConfig)
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
App.mpType = 'app'
|
||||||
|
const app = new Vue({
|
||||||
|
i18n,
|
||||||
|
...App
|
||||||
|
})
|
||||||
|
app.$mount()
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef VUE3
|
||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
export const i18n = createI18n(i18nConfig)
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
const pinia = createPinia()
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(i18n)
|
||||||
|
return {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
72
manifest.json
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"name" : "EducationApp2",
|
||||||
|
"appid" : "__UNI__1250B39",
|
||||||
|
"description" : "",
|
||||||
|
"versionName" : "1.0.1",
|
||||||
|
"versionCode" : "100",
|
||||||
|
"transformPx" : false,
|
||||||
|
/* 5+App特有相关 */
|
||||||
|
"app-plus" : {
|
||||||
|
"usingComponents" : true,
|
||||||
|
"nvueStyleCompiler" : "uni-app",
|
||||||
|
"compilerVersion" : 3,
|
||||||
|
"splashscreen" : {
|
||||||
|
"alwaysShowBeforeRender" : true,
|
||||||
|
"waiting" : true,
|
||||||
|
"autoclose" : true,
|
||||||
|
"delay" : 0
|
||||||
|
},
|
||||||
|
/* 模块配置 */
|
||||||
|
"modules" : {},
|
||||||
|
/* 应用发布信息 */
|
||||||
|
"distribute" : {
|
||||||
|
/* android打包配置 */
|
||||||
|
"android" : {
|
||||||
|
"permissions" : [
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
/* ios打包配置 */
|
||||||
|
"ios" : {},
|
||||||
|
/* SDK配置 */
|
||||||
|
"sdkConfigs" : {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* 快应用特有相关 */
|
||||||
|
"quickapp" : {},
|
||||||
|
/* 小程序特有相关 */
|
||||||
|
"mp-weixin" : {
|
||||||
|
"appid" : "",
|
||||||
|
"setting" : {
|
||||||
|
"urlCheck" : false
|
||||||
|
},
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-alipay" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-baidu" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-toutiao" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"uniStatistics" : {
|
||||||
|
"enable" : false
|
||||||
|
},
|
||||||
|
"vueVersion" : "3"
|
||||||
|
}
|
||||||
107
package.json
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"id": "uni-taimed-international-app",
|
||||||
|
"name": "TaimedInternationalApp",
|
||||||
|
"displayName": "太湖国际",
|
||||||
|
"version": "1.0.3",
|
||||||
|
"description": "太湖国际",
|
||||||
|
"keywords": [
|
||||||
|
"太湖国际",
|
||||||
|
"中医学 国学 心理学"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"tailwind-dev": "npx @tailwindcss/cli -i ./tailwind-input.css -o ./static/tailwind.css --watch",
|
||||||
|
"tailwind-build": "npx @tailwindcss/cli -i ./tailwind-input.css -o ./static/tailwind.css"
|
||||||
|
},
|
||||||
|
"dcloudext": {
|
||||||
|
"sale": {
|
||||||
|
"regular": {
|
||||||
|
"price": "0.00"
|
||||||
|
},
|
||||||
|
"sourcecode": {
|
||||||
|
"price": "0.00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"qq": ""
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"ads": "无",
|
||||||
|
"data": "无",
|
||||||
|
"permissions": "无"
|
||||||
|
},
|
||||||
|
"npmurl": "",
|
||||||
|
"type": "uniapp-template-project",
|
||||||
|
"darkmode": "x",
|
||||||
|
"i18n": "√",
|
||||||
|
"widescreen": "x"
|
||||||
|
},
|
||||||
|
"repository": "",
|
||||||
|
"uni_modules": {
|
||||||
|
"platforms": {
|
||||||
|
"cloud": {
|
||||||
|
"tcb": "√",
|
||||||
|
"aliyun": "√",
|
||||||
|
"alipay": "x"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"uni-app": {
|
||||||
|
"vue": {
|
||||||
|
"vue2": "√",
|
||||||
|
"vue3": "√"
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"safari": "√",
|
||||||
|
"chrome": "√"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"vue": "√",
|
||||||
|
"nvue": "-",
|
||||||
|
"android": "√",
|
||||||
|
"ios": "√",
|
||||||
|
"harmony": "-"
|
||||||
|
},
|
||||||
|
"mp": {
|
||||||
|
"weixin": "√",
|
||||||
|
"alipay": "-",
|
||||||
|
"toutiao": "-",
|
||||||
|
"baidu": "-",
|
||||||
|
"kuaishou": "-",
|
||||||
|
"jd": "-",
|
||||||
|
"harmony": "-",
|
||||||
|
"qq": "-",
|
||||||
|
"lark": "-"
|
||||||
|
},
|
||||||
|
"quickapp": {
|
||||||
|
"huawei": "-",
|
||||||
|
"union": "-"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uni-app-x": {
|
||||||
|
"web": {
|
||||||
|
"safari": "-",
|
||||||
|
"chrome": "-"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"android": "-",
|
||||||
|
"ios": "-",
|
||||||
|
"harmony": "-"
|
||||||
|
},
|
||||||
|
"mp": {
|
||||||
|
"weixin": "-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"HBuilderX": "^3.1.0",
|
||||||
|
"uni-app": "^4.03",
|
||||||
|
"uni-app-x": ""
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tailwindcss": "^4.1.16"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/cli": "^4.1.16"
|
||||||
|
}
|
||||||
|
}
|
||||||
78
pages.json
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
|
||||||
|
{
|
||||||
|
"path": "pages/index/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "%index.title%"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"path": "pages/component/component",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "%index.component%",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"path": "pages/login/login",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "Login",
|
||||||
|
"navigationBarBackgroundColor": "#FFFFFF",
|
||||||
|
"navigationBarTextStyle": "black"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"path": "pages/login/forget",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "Forgot Password",
|
||||||
|
"navigationBarBackgroundColor": "#FFFFFF",
|
||||||
|
"navigationBarTextStyle": "black"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// "tabBar": {
|
||||||
|
// "color": "#7A7E83",
|
||||||
|
// "selectedColor": "#007AFF",
|
||||||
|
// "borderStyle": "black",
|
||||||
|
// "backgroundColor": "#F8F8F8",
|
||||||
|
// "list": [{
|
||||||
|
// "pagePath": "pages/index/index",
|
||||||
|
// "text": "%index.home%"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "pagePath": "pages/component/component",
|
||||||
|
// "text": "%index.component%"
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
"tabBar": {
|
||||||
|
"color": "#444444",
|
||||||
|
"selectedColor": "#079307",
|
||||||
|
"borderStyle": "black",
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"pagePath": "pages/index/index",
|
||||||
|
"iconPath": "static/tab/icon1_n.png",
|
||||||
|
"selectedIconPath": "static/tab/icon1_y.png",
|
||||||
|
"text": "%tabar.course%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/book/index",
|
||||||
|
"iconPath": "static/tab/icon3_n.png",
|
||||||
|
"selectedIconPath": "static/tab/icon3_y.png",
|
||||||
|
"text": "%tabar.book%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/my/my",
|
||||||
|
"iconPath": "static/tab/icon4_n.png",
|
||||||
|
"selectedIconPath": "static/tab/icon4_y.png",
|
||||||
|
"text": "%tabar.user%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarTitleText": "疯子读书",
|
||||||
|
"navigationBarBackgroundColor": "#FFFFFF",
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
pages/component/component.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<no-data></no-data>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: this.$t('')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
24
pages/index/index.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="title bg-[red] text-left text-[#fff]">这是一个等待开发的首页</view>
|
||||||
|
<view class="description bg-[red]">首页的内容是在线课程</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
163
pages/login/README.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# 登录功能说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
本模块实现了从 nuttyreading-hw2 项目迁移的完整登录功能,包括:
|
||||||
|
|
||||||
|
- ✅ 验证码登录/注册
|
||||||
|
- ✅ 密码登录
|
||||||
|
- ✅ 忘记密码
|
||||||
|
- ✅ 用户协议和隐私政策
|
||||||
|
- ✅ 游客体验入口
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- Vue3 Composition API
|
||||||
|
- TypeScript
|
||||||
|
- Pinia (状态管理)
|
||||||
|
- WotUI (UI 组件库)
|
||||||
|
- Tailwind CSS + SCSS
|
||||||
|
- UniApp
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
pages/user/
|
||||||
|
├── login.vue # 登录页面
|
||||||
|
└── forget.vue # 忘记密码页面
|
||||||
|
|
||||||
|
api/modules/
|
||||||
|
├── auth.ts # 认证相关 API
|
||||||
|
└── common.ts # 通用 API
|
||||||
|
|
||||||
|
stores/
|
||||||
|
└── user.ts # 用户状态管理
|
||||||
|
|
||||||
|
types/
|
||||||
|
└── user.ts # 用户相关类型定义
|
||||||
|
|
||||||
|
utils/
|
||||||
|
└── validator.ts # 表单验证工具
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
### 登录相关
|
||||||
|
|
||||||
|
1. **验证码登录/注册**
|
||||||
|
- 接口:`GET book/user/registerOrLogin`
|
||||||
|
- 参数:`{ tel: string, code: string }`
|
||||||
|
|
||||||
|
2. **密码登录**
|
||||||
|
- 接口:`POST book/user/login`
|
||||||
|
- 参数:`{ phone: string, password: string }`
|
||||||
|
|
||||||
|
3. **重置密码**
|
||||||
|
- 接口:`POST book/user/setPassword`
|
||||||
|
- 参数:`{ phone: string, code: string, password: string }`
|
||||||
|
|
||||||
|
### 通用接口
|
||||||
|
|
||||||
|
1. **发送邮箱验证码**
|
||||||
|
- 接口:`GET common/user/getMailCaptcha`
|
||||||
|
- 参数:`{ email: string }`
|
||||||
|
|
||||||
|
2. **获取协议内容**
|
||||||
|
- 接口:`GET common/agreement/detail`
|
||||||
|
- 参数:`{ id: number }` (111: 用户协议, 112: 隐私政策)
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
### 1. 登录页面
|
||||||
|
|
||||||
|
访问路径:`/pages/user/login`
|
||||||
|
|
||||||
|
**验证码登录**:
|
||||||
|
1. 输入邮箱地址
|
||||||
|
2. 点击"Get Code"获取验证码
|
||||||
|
3. 输入收到的验证码
|
||||||
|
4. 勾选用户协议
|
||||||
|
5. 点击"Go Login"登录
|
||||||
|
|
||||||
|
**密码登录**:
|
||||||
|
1. 点击"Password Login"切换到密码登录
|
||||||
|
2. 输入邮箱地址和密码
|
||||||
|
3. 勾选用户协议
|
||||||
|
4. 点击"Go Login"登录
|
||||||
|
|
||||||
|
### 2. 忘记密码
|
||||||
|
|
||||||
|
访问路径:`/pages/user/forget`
|
||||||
|
|
||||||
|
1. 输入邮箱地址
|
||||||
|
2. 点击"Get Code"获取验证码
|
||||||
|
3. 输入验证码
|
||||||
|
4. 输入新密码(需满足强度要求)
|
||||||
|
5. 再次输入新密码确认
|
||||||
|
6. 点击"Submit"提交
|
||||||
|
|
||||||
|
### 3. 密码强度要求
|
||||||
|
|
||||||
|
- **强密码**:8位以上,包含大小写字母、数字和特殊字符
|
||||||
|
- **中等密码**:8位以上,包含大小写字母、数字、特殊字符中的两项
|
||||||
|
- **弱密码**:8位以上
|
||||||
|
- **最低要求**:6-20位,必须包含字母和数字
|
||||||
|
|
||||||
|
## 状态管理
|
||||||
|
|
||||||
|
使用 Pinia 管理用户状态:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 登录成功后设置用户信息
|
||||||
|
userStore.setUserInfo(userInfo)
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
if (userStore.isLoggedIn) {
|
||||||
|
// 已登录
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
userStore.logout()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 国际化
|
||||||
|
|
||||||
|
支持中英文切换,翻译文件位于:
|
||||||
|
- `locale/en.json` - 英文
|
||||||
|
- `locale/zh-Hans.json` - 简体中文
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **API 地址**:已配置为 `https://global.nuttyreading.com/`
|
||||||
|
2. **请求头**:自动添加 `token`、`appType: 'abroad'`、`version_code`
|
||||||
|
3. **Token 失效**:自动处理 401 错误,清除用户信息并跳转登录页
|
||||||
|
4. **验证码倒计时**:60秒,防止重复发送
|
||||||
|
5. **协议同意**:登录和获取验证码前必须同意用户协议和隐私政策
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
1. 测试验证码登录流程
|
||||||
|
2. 测试密码登录流程
|
||||||
|
3. 测试忘记密码流程
|
||||||
|
4. 测试登录方式切换
|
||||||
|
5. 测试表单验证
|
||||||
|
6. 测试协议弹窗
|
||||||
|
7. 测试多平台兼容性(H5、小程序、APP)
|
||||||
|
|
||||||
|
## 已知问题
|
||||||
|
|
||||||
|
- 游客页面 `/pages/visitor/visitor` 需要单独实现
|
||||||
|
- 部分图标可能需要根据实际设计调整
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.0.0 (2025-11-02)
|
||||||
|
- ✅ 完成登录功能迁移
|
||||||
|
- ✅ 实现验证码登录和密码登录
|
||||||
|
- ✅ 实现忘记密码功能
|
||||||
|
- ✅ 添加用户协议和隐私政策
|
||||||
|
- ✅ 支持中英文国际化
|
||||||
364
pages/login/forget.vue
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<view class="title">{{ $t('forget.title') }}</view>
|
||||||
|
|
||||||
|
<!-- 邮箱输入 -->
|
||||||
|
<view class="input-box">
|
||||||
|
<text class="input-tit">{{ $t('login.email') }}</text>
|
||||||
|
<input
|
||||||
|
class="input-text"
|
||||||
|
type="text"
|
||||||
|
v-model="email"
|
||||||
|
:placeholder="$t('login.emailPlaceholder')"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 验证码输入 -->
|
||||||
|
<view class="input-box">
|
||||||
|
<text class="input-tit">{{ $t('login.code') }}</text>
|
||||||
|
<input
|
||||||
|
class="input-text"
|
||||||
|
type="number"
|
||||||
|
v-model="code"
|
||||||
|
:placeholder="$t('login.codePlaceholder')"
|
||||||
|
/>
|
||||||
|
<wd-button type="info" :class="['code-btn', { active: !readonly }]" @click="getCode">
|
||||||
|
{{ t('login.getCode') }}
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 密码输入 -->
|
||||||
|
<view class="input-box">
|
||||||
|
<text class="input-tit">{{ $t('forget.password') }}</text>
|
||||||
|
<input
|
||||||
|
class="input-text"
|
||||||
|
type="password"
|
||||||
|
maxlength="20"
|
||||||
|
v-model="password"
|
||||||
|
:placeholder="$t('forget.passwordPlaceholder')"
|
||||||
|
@input="inputMethod(password)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 密码强度提示 -->
|
||||||
|
<view v-if="note !== ''" class="password-hint" style="font-size: 28rpx; color: #999;">
|
||||||
|
<text style="line-height: 34rpx; padding-top: 15rpx; display: block;">
|
||||||
|
{{ note }}
|
||||||
|
</text>
|
||||||
|
<text v-html="str2" style="margin-top: 10rpx; display: block;"></text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 确认密码输入 -->
|
||||||
|
<view class="input-box">
|
||||||
|
<text class="input-tit">{{ $t('forget.passwordAgain') }}</text>
|
||||||
|
<input
|
||||||
|
class="input-text"
|
||||||
|
type="password"
|
||||||
|
maxlength="20"
|
||||||
|
v-model="confirmPassword"
|
||||||
|
:placeholder="$t('forget.passwordAgainPlaceholder')"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<view class="btn-box">
|
||||||
|
<button @click="onSubmit" class="submit-btn">{{ $t('forget.submit') }}</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { commonApi } from '@/api/modules/common'
|
||||||
|
import { resetPassword } from '@/api/modules/auth'
|
||||||
|
import { validateEmail, checkPasswordStrength } from '@/utils/validator'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const email = ref('')
|
||||||
|
const code = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const confirmPassword = ref('')
|
||||||
|
|
||||||
|
// 验证码相关
|
||||||
|
const codeText = ref('Get Code')
|
||||||
|
const readonly = ref(false)
|
||||||
|
|
||||||
|
// 密码强度相关
|
||||||
|
const passwordOk = ref(false)
|
||||||
|
const note = ref('')
|
||||||
|
const str2 = ref('')
|
||||||
|
|
||||||
|
let codeTimer: any = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单验证函数
|
||||||
|
*/
|
||||||
|
// 邮箱是否为空
|
||||||
|
const isEmailEmpty = () => {
|
||||||
|
if (!email.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('login.emailPlaceholder'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 邮箱格式验证
|
||||||
|
const isEmailVerified = (emailVal: string) => {
|
||||||
|
if (!validateEmail(emailVal)) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('login.emailError'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证码是否为空
|
||||||
|
const isCodeEmpty = () => {
|
||||||
|
if (!code.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('login.codePlaceholder'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码是否为空
|
||||||
|
const isPasswordEmpty = () => {
|
||||||
|
if (!password.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('forget.passwordPlaceholder'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认密码是否为空
|
||||||
|
const isConfirmPasswordEmpty = () => {
|
||||||
|
if (!confirmPassword.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('forget.passwordAgainPlaceholder'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码是否匹配
|
||||||
|
const isPasswordMatch = () => {
|
||||||
|
if (confirmPassword.value !== password.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('forget.passwordNotMatch'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码强度验证
|
||||||
|
const isPasswordStrongEnough = () => {
|
||||||
|
if (!passwordOk.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: note.value || t('forget.passwordStrengthWeak'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送验证码
|
||||||
|
*/
|
||||||
|
const getCode = async () => {
|
||||||
|
if (readonly.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEmailEmpty()) return
|
||||||
|
if (!isEmailVerified(email.value)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
uni.showLoading()
|
||||||
|
await commonApi.sendMailCaptcha(email.value)
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({
|
||||||
|
title: t('login.sendCodeSuccess'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
getCodeState()
|
||||||
|
} catch (error) {
|
||||||
|
uni.hideLoading()
|
||||||
|
console.error('Send code error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码倒计时
|
||||||
|
*/
|
||||||
|
const getCodeState = () => {
|
||||||
|
if (codeTimer) {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly.value = true
|
||||||
|
let countdown = 60
|
||||||
|
codeText.value = `60S`
|
||||||
|
|
||||||
|
codeTimer = setInterval(() => {
|
||||||
|
countdown--
|
||||||
|
codeText.value = `${countdown}S`
|
||||||
|
if (countdown <= 0) {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeText.value = t('login.getCode')
|
||||||
|
readonly.value = false
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码强度验证
|
||||||
|
*/
|
||||||
|
const inputMethod = (value: string) => {
|
||||||
|
passwordOk.value = false
|
||||||
|
const strength = checkPasswordStrength(value)
|
||||||
|
|
||||||
|
if (strength === 'strong') {
|
||||||
|
str2.value = `<span style='color:#18bc37'>${t('forget.passwordStrengthStrong')}</span>`
|
||||||
|
note.value = ''
|
||||||
|
passwordOk.value = true
|
||||||
|
} else if (strength === 'medium') {
|
||||||
|
note.value = t('forget.passwordStrengthWeak')
|
||||||
|
str2.value = `<span style='color:#2979ff'>${t('forget.passwordStrengthMedium')}</span>`
|
||||||
|
passwordOk.value = true
|
||||||
|
} else if (strength === 'weak') {
|
||||||
|
note.value = t('forget.passwordStrengthWeak')
|
||||||
|
str2.value = ''
|
||||||
|
} else {
|
||||||
|
passwordOk.value = false
|
||||||
|
note.value = t('forget.passwordStrengthWeak')
|
||||||
|
str2.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交重置密码
|
||||||
|
*/
|
||||||
|
const onSubmit = async () => {
|
||||||
|
// 表单验证
|
||||||
|
if (!isEmailEmpty()) return
|
||||||
|
if (!isEmailVerified(email.value)) return
|
||||||
|
if (!isCodeEmpty()) return
|
||||||
|
if (!isPasswordEmpty()) return
|
||||||
|
if (!isPasswordStrongEnough()) return
|
||||||
|
if (!isConfirmPasswordEmpty()) return
|
||||||
|
if (!isPasswordMatch()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
uni.showLoading()
|
||||||
|
await resetPassword(email.value, code.value, password.value)
|
||||||
|
uni.hideLoading()
|
||||||
|
|
||||||
|
uni.showModal({
|
||||||
|
title: t('global.tips'),
|
||||||
|
content: t('forget.passwordChanged'),
|
||||||
|
showCancel: false,
|
||||||
|
success: () => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
uni.hideLoading()
|
||||||
|
console.error('Reset password error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 0 30rpx;
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding: 50rpx 0 50rpx 30rpx;
|
||||||
|
font-size: 60rpx;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 100rpx;
|
||||||
|
padding-top: 30rpx;
|
||||||
|
border-bottom: 1rpx solid #eeeeee;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.input-tit {
|
||||||
|
font-size: 30rpx;
|
||||||
|
line-height: 34rpx;
|
||||||
|
width: 230rpx;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 25rpx;
|
||||||
|
padding-bottom: 10rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-text {
|
||||||
|
flex: 1;
|
||||||
|
height: 70rpx;
|
||||||
|
line-height: 70rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn {
|
||||||
|
height: 60rpx;
|
||||||
|
background-color: #f8f9fb;
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 0 14rpx;
|
||||||
|
min-width: 0;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 60rpx;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
border: none;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $app-theme-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-hint {
|
||||||
|
padding: 10rpx 0 10rpx 205rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-box {
|
||||||
|
margin-top: 70rpx;
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
font-size: 32rpx;
|
||||||
|
background: linear-gradient(90deg, #54a966 0%, #54a966 100%);
|
||||||
|
color: #fff;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
border-radius: 50rpx;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
667
pages/login/login.vue
Normal file
@@ -0,0 +1,667 @@
|
|||||||
|
<template>
|
||||||
|
<view class="login-page">
|
||||||
|
<!-- Logo 背景区域 -->
|
||||||
|
<view class="logo-bg">
|
||||||
|
<text class="welcome-text">Hello! Welcome to<br>Taimed International</text>
|
||||||
|
<image src="@/static/icon/login_icon.png" mode="aspectFit" class="icon-hua-1"></image>
|
||||||
|
<image src="@/static/icon/login_icon.png" mode="aspectFit" class="icon-hua-2"></image>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 登录表单区域 -->
|
||||||
|
<view class="form-box">
|
||||||
|
<!-- 登录方式标题 -->
|
||||||
|
<view class="login-method">
|
||||||
|
<view class="title active">
|
||||||
|
<template v-if="loginType === 2000">{{ $t('login.codeLogin') }}</template>
|
||||||
|
<template v-if="loginType === 1000">{{ $t('login.passwordLogin') }}</template>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 验证码登录 -->
|
||||||
|
<view v-if="loginType === 2000">
|
||||||
|
<view class="input-tit">{{ $t('login.email') }}</view>
|
||||||
|
<view class="input-box">
|
||||||
|
<input
|
||||||
|
v-model="email"
|
||||||
|
:placeholder="$t('login.emailPlaceholder')"
|
||||||
|
placeholder-class="grey"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="input-tit">{{ $t('login.code') }}</view>
|
||||||
|
<view class="input-box">
|
||||||
|
<input
|
||||||
|
v-model="code"
|
||||||
|
:placeholder="$t('login.codePlaceholder')"
|
||||||
|
placeholder-class="grey"
|
||||||
|
maxlength="6"
|
||||||
|
@confirm="onSubmit"
|
||||||
|
/>
|
||||||
|
<wd-button type="info" :class="['code-btn', { active: !readonly }]" @click="onSetCode">
|
||||||
|
{{ t('login.getCode') }}
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 密码登录 -->
|
||||||
|
<view v-if="loginType === 1000">
|
||||||
|
<view class="input-tit">{{ $t('login.email') }}</view>
|
||||||
|
<view class="input-box">
|
||||||
|
<input
|
||||||
|
v-model="phoneEmail"
|
||||||
|
:placeholder="$t('login.emailPlaceholder')"
|
||||||
|
placeholder-class="grey"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="input-tit">{{ $t('login.password') }}</view>
|
||||||
|
<view class="input-box">
|
||||||
|
<input
|
||||||
|
class="input-item"
|
||||||
|
v-model="password"
|
||||||
|
:password="!isSee"
|
||||||
|
:placeholder="$t('login.passwordPlaceholder')"
|
||||||
|
placeholder-class="grey"
|
||||||
|
@confirm="onSubmit"
|
||||||
|
/>
|
||||||
|
<image
|
||||||
|
v-if="isSee"
|
||||||
|
src="@/static/icon/ic_logon_display.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="eye-icon"
|
||||||
|
@click="isSee = false"
|
||||||
|
></image>
|
||||||
|
<image
|
||||||
|
v-else
|
||||||
|
src="@/static/icon/ic_logon_hide.png"
|
||||||
|
mode="aspectFit"
|
||||||
|
class="eye-icon"
|
||||||
|
@click="isSee = true"
|
||||||
|
></image>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 协议同意 -->
|
||||||
|
<view class="protocol-box">
|
||||||
|
<view class="select" :class="{ active: agree }" @click="agreeAgreements"></view>
|
||||||
|
<view class="protocol-text">
|
||||||
|
{{ $t('login.agree') }}
|
||||||
|
<text class="highlight" @click="yhxy">《{{ $t('login.userAgreement') }}》</text>
|
||||||
|
and
|
||||||
|
<text class="highlight" @click="yszc">《{{ $t('login.privacyPolicy') }}》</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 登录按钮 -->
|
||||||
|
<view class="btn-box">
|
||||||
|
<button @click="onSubmit" class="login-btn" :class="{ active: btnShow }">
|
||||||
|
{{ $t('login.goLogin') }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 切换登录方式 -->
|
||||||
|
<view class="qie-huan">
|
||||||
|
<view v-if="loginType === 2000" @click="changeLoginType(1000)">
|
||||||
|
{{ $t('login.switchToPassword') }}
|
||||||
|
</view>
|
||||||
|
<view v-if="loginType === 1000" class="switch-links">
|
||||||
|
<text @click="changeLoginType(2000)">{{ $t('login.switchToCode') }}</text>
|
||||||
|
<text @click="onPageJump('/pages/login/forget')">{{ $t('login.forgotPassword') }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 游客体验 -->
|
||||||
|
<view class="youke-l">
|
||||||
|
<view @click="onPageJump('/pages/visitor/visitor')">
|
||||||
|
{{ $t('login.noLogin') }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 用户协议弹窗 -->
|
||||||
|
<wd-popup v-model="yhxyShow" position="bottom">
|
||||||
|
<view class="tanchu">
|
||||||
|
<view class="dp-title" v-html="yhxyText.title"></view>
|
||||||
|
<view class="dp-content" v-html="yhxyText.content"></view>
|
||||||
|
</view>
|
||||||
|
</wd-popup>
|
||||||
|
|
||||||
|
<!-- 隐私政策弹窗 -->
|
||||||
|
<wd-popup v-model="yszcShow" position="bottom">
|
||||||
|
<view class="tanchu">
|
||||||
|
<view class="dp-title" v-html="yszcText.title"></view>
|
||||||
|
<view class="dp-content" v-html="yszcText.content"></view>
|
||||||
|
</view>
|
||||||
|
</wd-popup>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { loginWithCode, loginWithPassword } from '@/api/modules/auth'
|
||||||
|
import { commonApi } from '@/api/modules/common'
|
||||||
|
import { validateEmail } from '@/utils/validator'
|
||||||
|
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 登录类型:2000-验证码登录,1000-密码登录
|
||||||
|
const loginType = ref<1000 | 2000>(2000)
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const email = ref('')
|
||||||
|
const code = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const phoneEmail = ref('')
|
||||||
|
const agree = ref(false)
|
||||||
|
const isSee = ref(false)
|
||||||
|
|
||||||
|
// 验证码相关
|
||||||
|
const codeText = ref('Get Code')
|
||||||
|
const readonly = ref(false)
|
||||||
|
const btnShow = ref(true)
|
||||||
|
|
||||||
|
// 用户协议和隐私政策
|
||||||
|
const yhxyShow = ref(false)
|
||||||
|
const yszcShow = ref(false)
|
||||||
|
const yhxyText = ref<any>({
|
||||||
|
title: '',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
const yszcText = ref<any>({
|
||||||
|
title: '',
|
||||||
|
content: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
let codeTimer: any = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换登录方式
|
||||||
|
* @param type 1000-密码登录,2000-验证码登录
|
||||||
|
*/
|
||||||
|
const changeLoginType = (type: 1000 | 2000) => {
|
||||||
|
loginType.value = type
|
||||||
|
code.value = ''
|
||||||
|
password.value = ''
|
||||||
|
const temporaryEmail = email.value || phoneEmail.value
|
||||||
|
email.value = temporaryEmail
|
||||||
|
phoneEmail.value = temporaryEmail
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表单验证
|
||||||
|
*/
|
||||||
|
// 是否同意协议
|
||||||
|
const isAgree = () => {
|
||||||
|
if (!agree.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('login.agreeFirst'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 是否填写邮箱
|
||||||
|
const isEmailEmpty = () => {
|
||||||
|
if (!email.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('login.emailPlaceholder'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 是否填写验证码
|
||||||
|
const isCodeEmpty = () => {
|
||||||
|
if (!code.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('login.codePlaceholder'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 邮箱格式验证
|
||||||
|
const isEmailVerified = (email: string) => {
|
||||||
|
console.log(email, validateEmail(email))
|
||||||
|
if (!validateEmail(email)) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('login.emailError'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 是否填写手机号或邮箱
|
||||||
|
const isPhoneEmailEmpty = () => {
|
||||||
|
if (!phoneEmail.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('login.emailPlaceholder'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 是否填写密码
|
||||||
|
const isPasswordEmpty = () => {
|
||||||
|
if (!password.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: t('login.passwordPlaceholder'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 提交登录
|
||||||
|
*/
|
||||||
|
// 验证码登录
|
||||||
|
const verifyCodeLogin = async () => {
|
||||||
|
if (!isEmailEmpty()) return false
|
||||||
|
|
||||||
|
if (!isEmailVerified(email.value)) return false
|
||||||
|
|
||||||
|
if (!isCodeEmpty()) return false
|
||||||
|
|
||||||
|
const res = await loginWithCode(email.value, code.value)
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
// 密码登录
|
||||||
|
const passwordLogin = async () => {
|
||||||
|
if (!isPhoneEmailEmpty()) return false
|
||||||
|
|
||||||
|
if (!isEmailVerified(phoneEmail.value)) return false
|
||||||
|
|
||||||
|
if (!isPasswordEmpty) return false
|
||||||
|
|
||||||
|
const res = await loginWithPassword(phoneEmail.value, password.value)
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
// 提交登录
|
||||||
|
const onSubmit = async () => {
|
||||||
|
if(!isAgree()) return false
|
||||||
|
|
||||||
|
let res = null
|
||||||
|
|
||||||
|
switch (loginType.value) {
|
||||||
|
case 2000:
|
||||||
|
res = await verifyCodeLogin()
|
||||||
|
break
|
||||||
|
case 1000:
|
||||||
|
res = await passwordLogin()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('res', res)
|
||||||
|
|
||||||
|
if (res && res.userInfo && res.token) {
|
||||||
|
res.userInfo.token = res.token.token
|
||||||
|
console.log('设置用户信息login', res.userInfo)
|
||||||
|
userStore.setUserInfo(res.userInfo)
|
||||||
|
uni.showToast({
|
||||||
|
title: t('login.loginSuccess'),
|
||||||
|
duration: 600,
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
})
|
||||||
|
}, 600)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送验证码
|
||||||
|
*/
|
||||||
|
const onSetCode = async () => {
|
||||||
|
if (readonly.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isAgree()) return false
|
||||||
|
|
||||||
|
if (!isEmailEmpty()) return false
|
||||||
|
|
||||||
|
if (!isEmailVerified(email.value)) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
uni.showLoading()
|
||||||
|
await commonApi.sendMailCaptcha(email.value)
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({
|
||||||
|
title: t('login.sendCodeSuccess'),
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
getCodeState()
|
||||||
|
} catch (error) {
|
||||||
|
uni.hideLoading()
|
||||||
|
console.error('Send code error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码倒计时
|
||||||
|
*/
|
||||||
|
const getCodeState = () => {
|
||||||
|
if (codeTimer) {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly.value = true
|
||||||
|
let countdown = 60
|
||||||
|
codeText.value = '60S'
|
||||||
|
|
||||||
|
codeTimer = setInterval(() => {
|
||||||
|
countdown--
|
||||||
|
codeText.value = `${countdown}S`
|
||||||
|
if (countdown <= 0) {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeText.value = t('login.getCode')
|
||||||
|
readonly.value = false
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示用户协议
|
||||||
|
*/
|
||||||
|
const yhxy = () => {
|
||||||
|
yhxyShow.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示隐私政策
|
||||||
|
*/
|
||||||
|
const yszc = () => {
|
||||||
|
yszcShow.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面跳转
|
||||||
|
*/
|
||||||
|
const onPageJump = (url: string) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取协议内容
|
||||||
|
*/
|
||||||
|
const getAgreements = async (id: number) => {
|
||||||
|
const res = await commonApi.getAgreement(id)
|
||||||
|
if (!res.content) return false
|
||||||
|
let content = res.content || ''
|
||||||
|
content = content.replace(
|
||||||
|
/<h5>/g,
|
||||||
|
'<view style="font-weight: bold;font-size: 32rpx;margin-top: 20rpx;margin-bottom: 20rpx;">'
|
||||||
|
)
|
||||||
|
content = content.replace(/<\/h5>/g, '</view>')
|
||||||
|
return {
|
||||||
|
title: res.title,
|
||||||
|
content: content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const loadAgreements = async () => {
|
||||||
|
// 获取用户协议
|
||||||
|
const yhxyRes = await getAgreements(111)
|
||||||
|
yhxyText.value = yhxyRes
|
||||||
|
|
||||||
|
// 获取隐私政策
|
||||||
|
const yszcRes = await getAgreements(112)
|
||||||
|
yszcText.value = yszcRes
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同意协议
|
||||||
|
const agreeAgreements = () => {
|
||||||
|
agree.value = !agree.value;
|
||||||
|
uni.setStorageSync('Agreements_agreed', agree.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadAgreements()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.login-page {
|
||||||
|
background-color: #fff;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-bg {
|
||||||
|
background-image: url('@/static/icon/login_bg.png');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
height: 25vh;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
font-size: 45rpx;
|
||||||
|
line-height: 65rpx;
|
||||||
|
position: absolute;
|
||||||
|
top: 120rpx;
|
||||||
|
left: 60rpx;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-hua-1 {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 60rpx;
|
||||||
|
left: 245rpx;
|
||||||
|
width: 150rpx;
|
||||||
|
height: 150rpx;
|
||||||
|
opacity: 0.08;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-hua-2 {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10rpx;
|
||||||
|
right: 30rpx;
|
||||||
|
width: 250rpx;
|
||||||
|
height: 250rpx;
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-box {
|
||||||
|
padding: calc(var(--status-bar-height) + 40rpx) 60rpx 50rpx 60rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
min-height: 75vh;
|
||||||
|
|
||||||
|
.login-method {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 96rpx;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 40rpx;
|
||||||
|
letter-spacing: 3rpx;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
position: relative;
|
||||||
|
color: $app-theme-color;
|
||||||
|
padding-bottom: 35rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-tit {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: $app-theme-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
border-bottom: solid 2rpx #efeef4;
|
||||||
|
margin: 30rpx 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
height: 70rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-item {
|
||||||
|
font-size: 28rpx;
|
||||||
|
flex: 1;
|
||||||
|
height: 70rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn {
|
||||||
|
height: 60rpx;
|
||||||
|
background-color: #f8f9fb;
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 0 14rpx;
|
||||||
|
min-width: 0;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 60rpx;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $app-theme-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.eye-icon {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grey {
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-box {
|
||||||
|
line-height: 38rpx;
|
||||||
|
margin-top: 40rpx;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
|
||||||
|
.select {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
background-image: url('@/static/icon/ic_gender_unselected.png');
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% auto;
|
||||||
|
margin-right: 15rpx;
|
||||||
|
margin-top: 2rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-image: url('@/static/icon/ic_agreed.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: $app-theme-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-box {
|
||||||
|
margin-top: 40rpx;
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
font-size: 32rpx;
|
||||||
|
background-color: #e5e5e5;
|
||||||
|
color: #fff;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
border-radius: 50rpx;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: linear-gradient(90deg, #54a966 0%, #54a966 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qie-huan {
|
||||||
|
font-size: 26rpx;
|
||||||
|
margin: 20rpx 0 0 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
.switch-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.youke-l {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 60rpx 0 0 0;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $app-theme-color;
|
||||||
|
|
||||||
|
view {
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px solid $app-theme-color;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
padding: 5rpx 15rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tanchu {
|
||||||
|
padding: 40rpx 10rpx;
|
||||||
|
position: relative;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.dp-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
margin-bottom: 50rpx;
|
||||||
|
color: #555;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp-content {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #555;
|
||||||
|
line-height: 45rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
BIN
static/biaoqing.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
static/emojis/qq/0@2x.gif
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
static/emojis/qq/0@2x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
static/emojis/qq/100@2x.gif
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
static/emojis/qq/100@2x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
static/emojis/qq/101@2x.gif
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
static/emojis/qq/101@2x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
static/emojis/qq/102@2x.gif
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
static/emojis/qq/102@2x.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
static/emojis/qq/103@2x.gif
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
static/emojis/qq/103@2x.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
static/emojis/qq/104@2x.gif
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
static/emojis/qq/104@2x.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
static/emojis/qq/105@2x.gif
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
static/emojis/qq/105@2x.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
static/emojis/qq/106@2x.gif
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/emojis/qq/106@2x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
static/emojis/qq/107@2x.gif
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
static/emojis/qq/107@2x.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
static/emojis/qq/108@2x.gif
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
static/emojis/qq/108@2x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
static/emojis/qq/109@2x.gif
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
static/emojis/qq/109@2x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
static/emojis/qq/10@2x.gif
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
static/emojis/qq/10@2x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
static/emojis/qq/110@2x.gif
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
static/emojis/qq/110@2x.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
static/emojis/qq/111@2x.gif
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
static/emojis/qq/111@2x.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
static/emojis/qq/112@2x.gif
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
static/emojis/qq/112@2x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
static/emojis/qq/113@2x.gif
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
static/emojis/qq/113@2x.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
static/emojis/qq/114@2x.gif
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
static/emojis/qq/114@2x.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
static/emojis/qq/115@2x.gif
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
static/emojis/qq/115@2x.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
static/emojis/qq/116@2x.gif
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
static/emojis/qq/116@2x.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
static/emojis/qq/117@2x.gif
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
static/emojis/qq/117@2x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
static/emojis/qq/118@2x.gif
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
static/emojis/qq/118@2x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
static/emojis/qq/119@2x.gif
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
static/emojis/qq/119@2x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
static/emojis/qq/11@2x.gif
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
static/emojis/qq/11@2x.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
static/emojis/qq/120@2x.gif
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
static/emojis/qq/120@2x.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
static/emojis/qq/121@2x.gif
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
static/emojis/qq/121@2x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
static/emojis/qq/122@2x.gif
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
static/emojis/qq/122@2x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
static/emojis/qq/123@2x.gif
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
static/emojis/qq/123@2x.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
static/emojis/qq/124@2x.gif
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
static/emojis/qq/124@2x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
static/emojis/qq/125@2x.gif
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/emojis/qq/125@2x.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
static/emojis/qq/126@2x.gif
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
static/emojis/qq/126@2x.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
static/emojis/qq/127@2x.gif
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
static/emojis/qq/127@2x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
static/emojis/qq/128@2x.gif
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
static/emojis/qq/128@2x.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
static/emojis/qq/129@2x.gif
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
static/emojis/qq/129@2x.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
static/emojis/qq/12@2x.gif
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
static/emojis/qq/12@2x.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
static/emojis/qq/130@2x.gif
Normal file
|
After Width: | Height: | Size: 7.9 KiB |