Commit ed9626ca authored by youjie's avatar youjie

邮箱注册

parent 70966e97
# 开发环境配置 - 使用Mock数据
VITE_OTA_API_BASE_URL=http://localhost:19002/api/app/
VITE_OTA_API_BASE_URL=http://192.168.5.39:8081/api/app/
VITE_ERP_API_BASE_URL=http://192.168.5.214:8700/
VITE_FILEUPLOAD_API_BASE_URL=http://192.168.5.214:8120/
VITE_FILEPREVIEW_API_BASE_URL=http://192.168.5.214:8130/
......
......@@ -37,8 +37,6 @@ service.interceptors.request.use(
config.headers['__tenant'] = systemConfigStore.tenantId;
}
console.log(systemConfigStore.erpAdminUserId);
// 优先检查临时令牌(OTA 授权场景)
const tempToken = sessionStorage.getItem('tempToken');
if (tempToken) {
......
......@@ -2,6 +2,8 @@ import { enCountries } from './countries'
import customFieldsEn from './fields/custom-fields-en.json'
export default {
// 国家名称(基于ISO2代码)
countries: enCountries,
login: {
// Page titles
title: 'Tten Joy',
......
......@@ -2,6 +2,8 @@ import { viCountries } from './countries'
import customFieldsVi from './fields/custom-fields-vi.json'
export default {
// 国家名称(基于ISO2代码)
countries: viCountries,
login: {
// Tiêu đề trang
title: 'Tten Joy',
......
......@@ -2,6 +2,8 @@ import { zhCNCountries } from './countries'
import customFieldsZhCN from './fields/custom-fields-zh-CN.json'
export default {
// 国家名称(基于ISO2代码)
countries: zhCNCountries,
login: {
// 页面标题
title: 'Tten Joy',
......@@ -65,6 +67,29 @@ export default {
loginError: '登录失败',
emailLogin: '邮箱',
emailPasswordPlaceholder: '请输入邮箱密码',
emailFormat: '请输入正确的邮箱地址',
repeatedpassword: '重复密码',
passwordFormat: '密码必须包含字母和数字,且长度为8位',
passwordMismatch: '两次输入的密码不一致',
emailRequired: '请输入邮箱地址',
emailInvalid: '请输入有效的邮箱地址',
resendCode: '{seconds}秒后重新发送',
resend: '重新发送',
pleaseComplete: '请完成必填项',
pleaseVerifyEmail: '请先验证邮箱',
pleaseVerifyCode: '请先完成邮箱验证',
pleaseAgreement: '请勾选隐私政策',
codeVerified: '验证成功',
verifyCodeFailed: '验证失败',
noCodeReceived: '没有收到验证码?',
codeInvalid: '验证码错误或已过期',
codeVerifyFailed: '验证码验证失败',
registerFailed: '注册失败',
lineId: '请输入LINE ID',
lineIdOrWechat: 'LINE ID和微信账号需二填一',
phoneCode: '区号',
googleLoginFailed: '谷歌登录失败',
isReceivePush: '「我同意接收优惠与电子报」',
},
common: {
language: '语言',
......
......@@ -2,6 +2,8 @@ import { zhTWCountries } from './countries'
import customFieldsZhTW from './fields/custom-fields-zh-TW.json'
export default {
// 国家名称(基于ISO2代码)
countries: zhTWCountries,
login: {
// 頁面標題
title: 'Tten Joy',
......
......@@ -15,13 +15,13 @@ const router = createRouter({
path: "/register",
name: "register",
component: () => import("../views/auth/register.vue"),
meta: { title: "register.registerButton" },
meta: { title: "login.register" },
},
{
path: "/forgePassword",
name: "forgePassword",
component: () => import("../views/auth/forgePassword.vue"),
meta: { title: "forgePassword.forgePasswordButton" },
meta: { title: "login.forgetPassword" },
},
{
path: "/products/:type/:city",
......
/**
* 国家相关 API 服务
*/
import OtaRequest from '@/api/OtaRequest'
import type { Country, CountrySimple, CountryPagedParams, PagedResult } from '@/types/country'
/**
* 国家服务类
*/
class CountryService {
/**
* 获取国家分页列表
* @param params 分页参数
* @returns 分页结果
*/
static async getPagedList(params: CountryPagedParams = {}): Promise<PagedResult<Country>> {
const { data } = await OtaRequest.get('/sys-management/country/paged-list', params)
return data
}
/**
* 获取所有国家列表(不分页)
* @param onlyEnabled 仅获取启用的国家
* @returns 国家列表
*/
static async getAll(onlyEnabled: boolean = true): Promise<Country[]> {
const { data } = await OtaRequest.get('/sys-management/country', { onlyEnabled })
return data
}
/**
* 获取简单国家列表(用于下拉框,使用缓存优化)
* @param onlyEnabled 仅获取启用的国家
* @returns 国家简单列表
*/
static async getSimpleList(onlyEnabled: boolean = true): Promise<CountrySimple[]> {
const data = await OtaRequest.get('/sys-management/country/simple-list', { onlyEnabled })
return data as CountrySimple
}
/**
* 搜索国家(支持按名称、编码搜索)
* @param keyword 搜索关键词
* @param onlyEnabled 仅搜索启用的国家
* @returns 国家列表
*/
static async search(keyword: string, onlyEnabled: boolean = true): Promise<Country[]> {
const { data } = await OtaRequest.post('/sys-management/country/search', null, {
params: { keyword, onlyEnabled }
})
return data
}
/**
* 获取指定数量的国家列表(用于下拉框)
* @param limit 最大数量,默认 500
* @param onlyEnabled 仅获取启用的国家
* @returns 国家列表
*/
static async getCountriesForSelect(limit: number = 500, onlyEnabled: boolean = true): Promise<Country[]> {
const {items} = await OtaRequest.get('/sys-management/country/paged-list', {
skipCount: 0,
maxResultCount: limit,
sorting: 'orderNum asc'
})
return items || []
}
}
export default CountryService
import OtaRequest from '@/api/OtaRequest'
/**
* OTA 授权服务 - 处理 OTA 会员转代理商的授权流程
*/
// ==================== 接口定义 ====================
/**
* OTA 用户信息
*/
export interface OtaUserInfo {
/** 用户ID */
userId: string
/** 邮箱 */
email: string
/** 昵称 */
nick: string
/** 手机号 */
phone: string
}
/**
* 代理商状态信息
*/
export interface DistributorStatusInfo {
/** 是否已经是代理商 */
isDistributor: boolean
/** 建议跳转的路由 */
suggestedRoute: string
/** 申请状态(如果已申请) */
status?: 'PendingReview' | 'Approved' | 'Rejected'
/** 驳回原因(如果被驳回) */
rejectionReason?: string
/** 代理商ID(如果已是代理商) */
distributorId?: number
}
/**
* 验证 OTA 授权码的响应
*/
export interface ValidateOtaTokenResponse {
/** 是否成功 */
success: boolean
/** 用户信息 */
userInfo: OtaUserInfo
/** 代理商状态 */
distributorStatus: DistributorStatusInfo
/** 临时访问令牌(15分钟有效) */
temporaryAccessToken: string
}
/**
* 从 OTA 提交代理商申请的请求参数
*/
export interface RegisterFromOtaDto {
/** 分销商类型 */
type: 'Enterprise' | 'Individual'
/** 公司/品牌名称 */
companyOrBrandName: string
/** 统一编号(企业营业执照号/个人身份证号) */
unifiedNumber?: string
/** 联络人姓名 */
contactPerson: string
/** 联络电话/手机 */
contactPhone: string
/** 联络地址 */
contactAddress?: string
/** LINE ID */
lineId?: string
/** 微信ID */
wechatId?: string
/** 公司登记证明/身份影本/名片 */
documentFilePath: string
/** 开户支行名称 */
openBankName: string
/** 银行名称(所属银行全称) */
bankName: string
/** 账户持有人姓名 */
accountHolder: string
/** 银行卡号 */
cardNum: string
/** 税务登记号 */
dutyNo?: string
}
/**
* 提交申请的响应
*/
export interface RegisterFromOtaResponse {
/** 是否成功 */
success: boolean
/** 消息 */
message?: string
/** 代理商ID */
distributorId?: number
/** 申请状态 */
status: 'PendingReview' | 'Approved' | 'Rejected'
}
/**
* 发送验证码请求参数
*/
export interface SendVerificationCodeDto {
/** 邮箱地址 */
email: string
/** 租户ID */
tenantId?: string
}
/**
* 发送验证码响应
*/
export interface SendVerificationCodeResponseDto {
/** 是否成功 */
success: boolean
/** 消息 */
message?: string
/** 验证码过期时间(秒) */
expiresIn?: number
}
/**
* 验证邮箱验证码请求参数
*/
export interface VerifyEmailCodeDto {
/** 邮箱地址 */
email: string
/** 验证码 */
code: string
/** 租户ID */
tenantId?: string
}
/**
* 验证邮箱验证码响应
*/
export interface VerifyEmailCodeResponseDto {
/** 是否验证成功 */
success: boolean
/** 消息 */
message?: string
/** 验证令牌(验证成功后返回,用于后续注册) */
verificationToken?: string
}
// ==================== 服务类 ====================
/**
* OTA 授权服务
*/
class OtaAuthService {
/**
* 验证 OTA 授权码
* @param token OTA 授权码
* @returns 验证结果,包含用户信息、代理商状态和临时访问令牌
*/
static async validateOtaToken(token: string): Promise<ValidateOtaTokenResponse> {
const response = await OtaRequest.post('/distributor-auth/validate-ota-token', { token })
return response as unknown as ValidateOtaTokenResponse
}
/**
* 从 OTA 提交代理商申请(使用临时令牌)
* @param data 申请信息
* @returns 申请响应
*/
static async registerFromOta(data: RegisterFromOtaDto): Promise<RegisterFromOtaResponse> {
// 临时令牌会在 OtaRequest 拦截器中自动从 sessionStorage 获取并添加到请求头
const response = await OtaRequest.post('/distributor-auth/register-from-ota-member', data)
return response as unknown as RegisterFromOtaResponse
}
/**
* 重新提交申请(使用临时令牌)
* @param data 申请信息
* @returns 申请响应
*/
static async resubmit(data: RegisterFromOtaDto): Promise<any> {
const response = await OtaRequest.post('/distributor-auth/resubmit-application', data)
return response
}
/**
* 获取当前申请状态(使用临时令牌)
* @returns 代理商状态信息
*/
static async getCurrentStatus(): Promise<DistributorStatusInfo> {
const response = await OtaRequest.get('/distributor-auth/current-status')
return response as unknown as DistributorStatusInfo
}
/**
* 发送邮箱验证码
* @param email 邮箱地址
* @param tenantId 租户ID(可选)
* @returns 发送结果
*/
static async sendVerificationCodeAsync(
email: string,
tenantId?: string
): Promise<SendVerificationCodeResponseDto> {
const data: SendVerificationCodeDto = {
email,
tenantId
}
const response = await OtaRequest.post(
'/distributor-auth/send-verification-code',
data,
{
headers: tenantId ? {
'__tenant': tenantId
} : {}
}
)
return response as unknown as SendVerificationCodeResponseDto
}
/**
* 验证邮箱验证码
* @param email 邮箱地址
* @param code 验证码
* @param tenantId 租户ID(可选)
* @returns 验证结果
*/
static async verifyEmailCodeAsync(
email: string,
code: string,
tenantId?: string
): Promise<VerifyEmailCodeResponseDto> {
const data: VerifyEmailCodeDto = {
email,
code,
tenantId
}
const response = await OtaRequest.post(
'/distributor-auth/verify-email-code',
data,
{
headers: tenantId ? {
'__tenant': tenantId
} : {}
}
)
return response as unknown as VerifyEmailCodeResponseDto
}
}
export default OtaAuthService
......@@ -61,45 +61,43 @@ export interface DistributorLoginDto {
}
/**
* 代理商自助注册请求参数
* 注册请求参数
*/
export interface DistributorSelfRegisterDto {
/** 租户ID(手动传入,用于多租户隔离) */
tenantId: string
/** 分销商类型 */
type: DistributorType
/** 公司/品牌名称 */
companyOrBrandName: string
/** 统一编号(企业营业执照号/个人身份证号) */
unifiedNumber?: string
/** 联络人姓名 */
contactPerson: string
/** 联络电话/手机 */
contactPhone: string
/** 联络地址 */
contactAddress?: string
/** LINE ID */
lineId?: string
/** 微信ID */
wechatId?: string
/** 电子邮箱(需要判断唯一,作为登录账号) */
email: string
/** 密码 */
password: string
/** 确认密码 */
confirmPassword: string
/** 公司登记证明/身份影本/名片 */
documentFilePath: string
/** 开户支行名称 */
openBankName: string
/** 银行名称(所属银行全称) */
bankName: string
/** 账户持有人姓名 */
accountHolder: string
/** 银行卡号 */
cardNum: string
/** 税务登记号 */
dutyNo?: string
/*** 出生日期*/
birthday?: Date | null | string;
/*** 确认密码*/
confirmPassword: string;
/*** 分销商推广码*/
distributorCode?: null | string;
/*** 分销商ID*/
distributorId?: number;
/*** 电子邮箱(需要判断唯一,作为登录账号)*/
email: string;
/*** 勾選「我願意接收優惠與電子報」 1是*/
isReceivePush?: number;
/*** LINE ID*/
lineId?: null | string;
/*** 会员昵称*/
name?: null | string;
/*** 父级会员ID*/
parentId?: null | string;
/*** 密码*/
password: string;
/*** 电话*/
phone?: null | string;
/*** 居住地區*/
residentialArea?: null | string;
/*** 注册类型 0邮箱验证码 1谷歌授权 3LINE授权 7FaceBook授权*/
reType?: number;
/*** 性别 1男 2女*/
sex?: number;
/*** 临时令牌token*/
temporaryToken?: null | string;
/*** 租户ID(手动传入,用于多租户隔离)*/
tenantId: string;
/*** 微信ID*/
wechatId?: null | string;
}
/**
......@@ -326,6 +324,8 @@ export interface SendVerificationCodeDto {
email: string
/** 租户ID */
tenantId?: string
/** 场景 */
scene?: string
}
/**
......@@ -348,6 +348,10 @@ export interface VerifyEmailCodeDto {
email: string
/** 验证码 */
code: string
/** 租户ID */
tenantId?: string
/** 场景 */
scene?: string
}
/**
......@@ -390,6 +394,25 @@ export interface ResetPasswordResponseDto {
* 用户服务
*/
class UserService {
/**
* 代理商自助注册
* @param data 注册信息
* @returns 注册响应
*/
static async distributorSelfRegisterAsync(
data: DistributorSelfRegisterDto
): Promise<DistributorSelfRegisterResultDto> {
const response = await OtaRequest.post(
'/member-auth/self-register',
data,
{
headers: {
'__tenant': data.tenantId
}
}
)
return response as unknown as DistributorSelfRegisterResultDto
}
/**
* 登录
* @param email 邮箱
......@@ -463,7 +486,7 @@ class UserService {
tenantId
}
const response = await OtaRequest.post(
'/distributor-auth/validate-email',
'/member-auth/validate-email',
data,
{
headers: tenantId ? {
......@@ -482,14 +505,16 @@ class UserService {
*/
static async sendVerificationCodeAsync(
email: string,
tenantId?: string
tenantId?: string,
scene?: string,
): Promise<SendVerificationCodeResponseDto> {
const data: SendVerificationCodeDto = {
email,
tenantId
tenantId,
scene,
}
const response = await OtaRequest.post(
'/distributor-auth/send-email-verification-code',
'/member-auth/send-email-verification-code',
data,
{
headers: tenantId ? {
......@@ -510,15 +535,17 @@ class UserService {
static async verifyEmailCodeAsync(
email: string,
code: string,
tenantId?: string
tenantId?: string,
scene?: string,
): Promise<VerifyEmailCodeResponseDto> {
const data: VerifyEmailCodeDto = {
email,
code,
tenantId
tenantId,
scene
}
const response = await OtaRequest.post(
'/distributor-auth/verify-email-code',
'/member-auth/verify-email-code',
data,
{
headers: tenantId ? {
......
import { defineStore } from 'pinia'
import { ApiResult } from '@/types/ApiResult'
import ErpUserService from '@/services/ErpUserService'
import { type StorageLike } from 'pinia-plugin-persistedstate'
import SecureLS from 'secure-ls'
import UserService, {
......@@ -31,6 +33,15 @@ export interface UserLoginResult {
applicationStatus?: string
}
export interface RegisterResult {
success: boolean
message: string
applicationId?: string
distributorId?: number
status?: ApplicationStatus
data?: any
}
export const useUserStore = defineStore('user', {
state: () => ({
token: '' as string,
......@@ -93,6 +104,41 @@ export const useUserStore = defineStore('user', {
}
}
},
/**
* 谷歌登录
* @param credential 谷歌凭证
*/
async setUserGoogleLoginAsync(credential: string): Promise<UserLoginResult> {
try {
const response = await ErpUserService.GoogleLoginAsync(credential)
if (response.data.resultCode === ApiResult.SUCCESS) {
this.token = response.data.data.token || ''
this.userInfo = response.data.data || {}
this.denied = false
return {
status: 'SUCCESS',
verify: false,
data: response.data.data
}
} else {
ResultMessage.Error(response.data.message || i18n.global.t('login.googleLoginFailed'))
return {
status: 'ERROR',
verify: false
}
}
} catch (error: any) {
console.error('Google login error:', error)
ResultMessage.Error(error.message || i18n.global.t('login.googleLoginFailed'))
return {
status: 'ERROR',
verify: false
}
}
},
setUserLoginOut() {
this.token = ''
this.userInfo = {}
......@@ -103,7 +149,42 @@ export const useUserStore = defineStore('user', {
*/
setUserDeniedStatus(denied: boolean) {
this.denied = denied
}
},
/**
* 代理商自助注册
* @param data 注册信息
*/
async registerDistributorAsync(data: DistributorSelfRegisterDto): Promise<RegisterResult> {
try {
const response = await UserService.distributorSelfRegisterAsync(data)
console.log('Register response:', response)
return {
success: true,
message: response.message || i18n.global.t('login.registerSuccess'),
distributorId: response.distributorId,
status: response.status,
data: response
}
} catch (error: any) {
console.error('Register error:', error)
// 处理 ABP 错误响应
if (error.error) {
const errorInfo = error.error
const errorMessage = errorInfo.message || i18n.global.t('login.registerFailed')
return {
success: false,
message: errorMessage
}
} else {
return {
success: false,
message: error.message || i18n.global.t('httpError.networkError')
}
}
}
},
},
persist: {
storage: st,
......
/**
* 国家数据类型定义
*/
/**
* 国家完整信息(从后端返回)
*/
export interface Country {
/** 国家ID */
id: string
/** 国家编码(ISO Alpha-2) */
code2: string
/** 国家编码(ISO Alpha-3) */
code3: string
/** 国家名称 */
name: string
/** 电话区号 */
phoneCode: string
/** 时区 */
timeZone: string
/** 首都 */
capital: string
/** 货币代码 */
currencyCode: string
/** 语言代码 */
languageCode: string
/** 行驶方向 */
drivingSide: string
/** 排序号 */
orderNum?: number
/** 状态 */
state?: boolean
/** 备注 */
remark?: string
}
/**
* 国家简单信息(用于下拉框)
*/
export interface CountrySimple {
/** 国家ID */
id: string
/** 国家编码(ISO Alpha-2) */
code2: string
/** 国家编码(ISO Alpha-3) */
code3: string
/** 国家名称 */
name: string
/** 电话区号 */
phoneCode: string
/** 时区 */
timeZone: string
/** 是否禁用 */
disabled?: boolean
}
/**
* 国家分页查询参数
*/
export interface CountryPagedParams {
/** 排序字段 */
sorting?: string
/** 跳过数量 */
skipCount?: number
/** 最大结果数 */
maxResultCount?: number
}
/**
* 分页结果
*/
export interface PagedResult<T> {
/** 数据列表 */
items: T[]
/** 总数量 */
totalCount: number
}
......@@ -55,4 +55,13 @@ export interface PartnerCenterConfigDto {
creationTime?: string
/** 最后修改时间 */
lastModificationTime?: string
}
/**
* 租户信息DTO(根据域名获取)
*/
export interface TenantInfoByDomainDto {
tenantId?: string
domainName?: string
platformConfig?: PlatformConfigDto
}
\ No newline at end of file
This diff is collapsed.
......@@ -5,14 +5,14 @@
:src="systemConfigStore.config.logo"
:alt="systemConfigStore.config.webSiteName"
class="h-[40px]" />
<svg v-else viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<svg v-else-if="systemConfigStore.config?.webSiteName" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="45" fill="#165dff" />
<text x="50" y="65" text-anchor="middle" fill="white" font-size="40" font-weight="bold">
{{ (systemConfigStore.config.webSiteName || 'T').charAt(0).toUpperCase() }}
</text>
</svg>
</div>
<div class="flex items-center">
<div class="flex items-center" v-if="!currentStep||currentStep<3">
<div class="flex items-center mr-[67px]">
<img
src="../../../assets/images/login-home.png"
......@@ -25,7 +25,7 @@
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import { ref,inject } from "vue";
import { useI18n } from "vue-i18n";
import { useSystemConfigStore } from '@/stores/index'
import LanguageSwitcher from "@/components/common/LanguageSwitcher.vue"
......@@ -33,8 +33,6 @@ import LanguageSwitcher from "@/components/common/LanguageSwitcher.vue"
const { t } = useI18n();
const systemConfigStore = useSystemConfigStore()
const currentStep = ref(inject('currentStep'))
onMounted(() => {
});
</script>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment