feat:增加主包(tabbar)、system(系统管理)、infra(基础设施)、bpm(工作流程)的页面
This commit is contained in:
135
src/pages/auth/code-login.vue
Normal file
135
src/pages/auth/code-login.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<view class="auth-container">
|
||||
<!-- 顶部 -->
|
||||
<Header />
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-container">
|
||||
<TenantPicker ref="tenantPickerRef" />
|
||||
<view class="input-item">
|
||||
<wd-icon name="phone" size="20px" color="#1890ff" />
|
||||
<wd-input
|
||||
v-model="formData.mobile"
|
||||
placeholder="请输入手机号"
|
||||
clearable
|
||||
clear-trigger="focus"
|
||||
no-border
|
||||
type="number"
|
||||
:maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
<CodeInput
|
||||
v-model="formData.code"
|
||||
:mobile="formData.mobile"
|
||||
:scene="21"
|
||||
:before-send="validateBeforeSend"
|
||||
/>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<view class="mb-2 mt-2 flex justify-between">
|
||||
<text class="text-28rpx text-[#1890ff]" @click="goToLogin">
|
||||
账号登录
|
||||
</text>
|
||||
<text class="text-28rpx text-[#1890ff]" @click="goToForgetPassword">
|
||||
忘记密码?
|
||||
</text>
|
||||
</view>
|
||||
<wd-button block :loading="loading" type="primary" @click="handleLogin">
|
||||
登录
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { FORGET_PASSWORD_PAGE, LOGIN_PAGE } from "@/router/config";
|
||||
import { useTokenStore } from "@/store/token";
|
||||
import { ensureDecodeURIComponent, redirectAfterLogin } from "@/utils";
|
||||
import { isMobile } from "@/utils/validator";
|
||||
import CodeInput from "./components/code-input.vue";
|
||||
import Header from "./components/header.vue";
|
||||
import TenantPicker from "./components/tenant-picker.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "SmsLoginPage",
|
||||
});
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationStyle: "custom",
|
||||
},
|
||||
excludeLoginPath: true,
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const loading = ref(false); // 加载状态
|
||||
const redirectUrl = ref<string>(); // 重定向地址
|
||||
const tenantPickerRef = ref<InstanceType<typeof TenantPicker>>(); // 租户选择器引用
|
||||
|
||||
const formData = reactive({
|
||||
mobile: "",
|
||||
code: "",
|
||||
}); // 表单数据
|
||||
|
||||
/** 页面加载时处理重定向 */
|
||||
onLoad((options) => {
|
||||
if (options?.redirect) {
|
||||
redirectUrl.value = ensureDecodeURIComponent(options.redirect);
|
||||
}
|
||||
});
|
||||
|
||||
/** 发送验证码前的校验 */
|
||||
function validateBeforeSend(): boolean {
|
||||
return tenantPickerRef.value?.validate() ?? false;
|
||||
}
|
||||
|
||||
/** 登录处理 */
|
||||
async function handleLogin() {
|
||||
// 校验租户
|
||||
if (!tenantPickerRef.value?.validate()) {
|
||||
return;
|
||||
}
|
||||
if (!formData.mobile) {
|
||||
toast.warning("请输入手机号");
|
||||
return;
|
||||
}
|
||||
if (!isMobile(formData.mobile)) {
|
||||
toast.warning("请输入正确的手机号");
|
||||
return;
|
||||
}
|
||||
if (!formData.code) {
|
||||
toast.warning("请输入验证码");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
// 调用短信登录接口
|
||||
const tokenStore = useTokenStore();
|
||||
await tokenStore.login({
|
||||
type: "sms",
|
||||
...formData,
|
||||
});
|
||||
// 处理跳转
|
||||
redirectAfterLogin(redirectUrl.value);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳转到账号密码登录 */
|
||||
function goToLogin() {
|
||||
uni.navigateTo({ url: LOGIN_PAGE });
|
||||
}
|
||||
|
||||
/** 跳转到忘记密码 */
|
||||
function goToForgetPassword() {
|
||||
uni.navigateTo({ url: FORGET_PASSWORD_PAGE });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./styles/auth.scss";
|
||||
</style>
|
||||
90
src/pages/auth/components/code-input.vue
Normal file
90
src/pages/auth/components/code-input.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<view class="input-item">
|
||||
<wd-icon name="lock-on" size="20px" color="#1890ff" />
|
||||
<wd-input
|
||||
:model-value="modelValue"
|
||||
placeholder="请输入验证码"
|
||||
clearable
|
||||
clear-trigger="focus"
|
||||
no-border
|
||||
type="number"
|
||||
:maxlength="6"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
/>
|
||||
<view
|
||||
class="whitespace-nowrap border-l-1rpx border-l-[#e5e5e5] border-l-solid px-20rpx text-28rpx text-[#1890ff]"
|
||||
@click="handleSendCode"
|
||||
>
|
||||
<text :class="{ 'text-gray-400': countdown > 0 }">
|
||||
{{ countdown > 0 ? `${countdown} 秒后重发` : "获取验证码" }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted, ref } from "vue";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { sendSmsCode } from "@/api/login";
|
||||
import { isMobile } from "@/utils/validator";
|
||||
|
||||
defineOptions({
|
||||
name: "CodeInput",
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string; // 验证码值 (v-model)
|
||||
mobile: string; // 手机号
|
||||
scene: number; // 短信场景:21-登录 23-重置密码
|
||||
beforeSend?: () => boolean; // 发送前的校验函数,返回 false 则不发送
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
"update:modelValue": [value: string];
|
||||
}>();
|
||||
|
||||
const toast = useToast();
|
||||
const countdown = ref(0); // 验证码倒计时,单位秒
|
||||
let countdownTimer: ReturnType<typeof setInterval> | null = null; // 倒计时定时器
|
||||
|
||||
/** 页面卸载时清除倒计时定时器 */
|
||||
onUnmounted(() => {
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
/** 发送验证码 */
|
||||
async function handleSendCode() {
|
||||
// 执行前置校验
|
||||
if (props.beforeSend && !props.beforeSend()) {
|
||||
return;
|
||||
}
|
||||
if (countdown.value > 0) {
|
||||
return;
|
||||
}
|
||||
if (!props.mobile) {
|
||||
toast.warning("请输入手机号");
|
||||
return;
|
||||
}
|
||||
if (!isMobile(props.mobile)) {
|
||||
toast.warning("请输入正确的手机号");
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
await sendSmsCode({ mobile: props.mobile, scene: props.scene });
|
||||
toast.success("验证码已发送");
|
||||
|
||||
// 开始倒计时
|
||||
countdown.value = 60;
|
||||
countdownTimer = setInterval(() => {
|
||||
countdown.value--;
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(countdownTimer!);
|
||||
countdownTimer = null;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
12
src/pages/auth/components/header.vue
Normal file
12
src/pages/auth/components/header.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<view class="header flex flex-col items-center pb-60rpx pt-120rpx">
|
||||
<image class="mb-24rpx h-160rpx w-160rpx" src="/static/logo.svg" mode="aspectFit" />
|
||||
<view class="text-44rpx text-[#1890ff] font-bold">
|
||||
{{ title }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const title = import.meta.env.VITE_APP_TITLE // 应用标题
|
||||
</script>
|
||||
134
src/pages/auth/components/tenant-picker.vue
Normal file
134
src/pages/auth/components/tenant-picker.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<view v-if="tenantEnabled" class="input-item">
|
||||
<wd-icon name="home" size="20px" color="#1890ff" />
|
||||
<wd-picker
|
||||
:model-value="tenantId"
|
||||
:columns="tenantList"
|
||||
label-key="name"
|
||||
value-key="id"
|
||||
label=""
|
||||
placeholder="请选择租户"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import {
|
||||
getTenantByWebsite,
|
||||
getTenantSimpleList,
|
||||
type TenantVO,
|
||||
} from "@/api/login";
|
||||
import { useUserStore } from "@/store/user";
|
||||
|
||||
const toast = useToast();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const tenantEnabled = computed(
|
||||
() => import.meta.env.VITE_APP_TENANT_ENABLE === "true",
|
||||
); // 租户开关:通过环境变量控制
|
||||
const tenantList = ref<TenantVO[]>([]); // 租户列表数据
|
||||
|
||||
const tenantId = computed(
|
||||
() =>
|
||||
userStore.tenantId ||
|
||||
Number(import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT_ID) ||
|
||||
undefined,
|
||||
); // 当前选中的租户
|
||||
|
||||
/** 获取租户列表,并根据域名/appId 自动选中租户 */
|
||||
async function fetchTenantList() {
|
||||
if (!tenantEnabled.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 1. 并行获取租户列表和域名对应的租户
|
||||
const websiteTenantPromise = fetchTenantByWebsite();
|
||||
const list = await getTenantSimpleList();
|
||||
tenantList.value = list || [];
|
||||
|
||||
// 2. 确定选中的租户:域名/appId > store 中的租户 > 列表第一个
|
||||
let selectedTenantId: number | null = null;
|
||||
// 2.1 优先使用域名/appId 对应的租户
|
||||
const websiteTenant = await websiteTenantPromise;
|
||||
if (websiteTenant?.id) {
|
||||
selectedTenantId = websiteTenant.id;
|
||||
}
|
||||
// 2.2 如果没有从域名获取到,使用 store 中的租户
|
||||
if (!selectedTenantId && userStore.tenantId) {
|
||||
selectedTenantId = userStore.tenantId;
|
||||
}
|
||||
// 2.3 如果还是没有,使用列表第一个
|
||||
if (!selectedTenantId && tenantList.value.length > 0) {
|
||||
selectedTenantId = tenantList.value[0].id;
|
||||
}
|
||||
|
||||
// 3. 设置选中的租户
|
||||
if (selectedTenantId && selectedTenantId !== userStore.tenantId) {
|
||||
userStore.setTenantId(selectedTenantId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取租户列表失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 根据域名或 appId 获取租户 */
|
||||
async function fetchTenantByWebsite(): Promise<TenantVO | null> {
|
||||
try {
|
||||
let website: string | null = null;
|
||||
|
||||
// #ifdef H5
|
||||
// H5 环境:使用域名
|
||||
if (window?.location?.hostname) {
|
||||
website = window.location.hostname;
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP
|
||||
// 小程序环境:使用 appId
|
||||
const appId = uni.getAccountInfoSync?.()?.miniProgram?.appId;
|
||||
if (appId) {
|
||||
website = appId;
|
||||
}
|
||||
// #endif
|
||||
|
||||
if (website) {
|
||||
return await getTenantByWebsite(website);
|
||||
}
|
||||
} catch (error) {
|
||||
// 域名未配置租户时会报错,忽略即可
|
||||
console.debug("根据域名获取租户失败:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 租户选择确认 */
|
||||
function handleConfirm({ value }: { value: number }) {
|
||||
userStore.setTenantId(value);
|
||||
}
|
||||
|
||||
/** 校验租户是否已选择 */
|
||||
function validate(): boolean {
|
||||
if (!tenantEnabled.value) {
|
||||
return true;
|
||||
}
|
||||
if (!tenantId.value) {
|
||||
toast.warning("请选择租户");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 页面加载时获取租户列表 */
|
||||
onMounted(() => {
|
||||
fetchTenantList();
|
||||
});
|
||||
|
||||
defineExpose({ validate });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../styles/auth.scss";
|
||||
</style>
|
||||
160
src/pages/auth/forget-password.vue
Normal file
160
src/pages/auth/forget-password.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<view class="auth-container">
|
||||
<!-- 顶部 -->
|
||||
<Header />
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-container">
|
||||
<TenantPicker ref="tenantPickerRef" />
|
||||
<view class="input-item">
|
||||
<wd-icon name="phone" size="20px" color="#1890ff" />
|
||||
<wd-input
|
||||
v-model="formData.mobile"
|
||||
placeholder="请输入手机号"
|
||||
clearable
|
||||
clear-trigger="focus"
|
||||
no-border
|
||||
type="number"
|
||||
:maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
<CodeInput
|
||||
v-model="formData.code"
|
||||
:mobile="formData.mobile"
|
||||
:scene="23"
|
||||
:before-send="validateBeforeSend"
|
||||
/>
|
||||
<view class="input-item">
|
||||
<wd-icon name="lock-on" size="20px" color="#1890ff" />
|
||||
<wd-input
|
||||
v-model="formData.password"
|
||||
placeholder="请输入新密码"
|
||||
clearable
|
||||
clear-trigger="focus"
|
||||
show-password
|
||||
no-border
|
||||
/>
|
||||
</view>
|
||||
<view class="input-item">
|
||||
<wd-icon name="lock-on" size="20px" color="#1890ff" />
|
||||
<wd-input
|
||||
v-model="formData.confirmPassword"
|
||||
placeholder="请确认新密码"
|
||||
clearable
|
||||
clear-trigger="focus"
|
||||
show-password
|
||||
no-border
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 重置密码按钮 -->
|
||||
<wd-button
|
||||
block
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
@click="handleResetPassword"
|
||||
>
|
||||
重置密码
|
||||
</wd-button>
|
||||
<wd-button class="mt-2" block type="info" @click="goToLogin">
|
||||
返回登录
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { smsResetPassword } from "@/api/login";
|
||||
import { LOGIN_PAGE } from "@/router/config";
|
||||
import { isMobile } from "@/utils/validator";
|
||||
import CodeInput from "./components/code-input.vue";
|
||||
import Header from "./components/header.vue";
|
||||
import TenantPicker from "./components/tenant-picker.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "ForgetPasswordPage",
|
||||
});
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationStyle: "custom",
|
||||
},
|
||||
excludeLoginPath: true,
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const loading = ref(false); // 加载状态
|
||||
const tenantPickerRef = ref<InstanceType<typeof TenantPicker>>(); // 租户选择器引用
|
||||
|
||||
const formData = reactive({
|
||||
mobile: "",
|
||||
code: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
}); // 表单数据
|
||||
|
||||
/** 发送验证码前的校验 */
|
||||
function validateBeforeSend(): boolean {
|
||||
return tenantPickerRef.value?.validate() ?? false;
|
||||
}
|
||||
|
||||
/** 重置密码处理 */
|
||||
async function handleResetPassword() {
|
||||
// 校验租户
|
||||
if (!tenantPickerRef.value?.validate()) {
|
||||
return;
|
||||
}
|
||||
if (!formData.mobile) {
|
||||
toast.warning("请输入手机号");
|
||||
return;
|
||||
}
|
||||
if (!isMobile(formData.mobile)) {
|
||||
toast.warning("请输入正确的手机号");
|
||||
return;
|
||||
}
|
||||
if (!formData.code) {
|
||||
toast.warning("请输入验证码");
|
||||
return;
|
||||
}
|
||||
if (!formData.password) {
|
||||
toast.warning("请输入新密码");
|
||||
return;
|
||||
}
|
||||
if (!formData.confirmPassword) {
|
||||
toast.warning("请确认新密码");
|
||||
return;
|
||||
}
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
toast.warning("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
// 调用重置密码接口
|
||||
await smsResetPassword({
|
||||
mobile: formData.mobile,
|
||||
code: formData.code,
|
||||
password: formData.password,
|
||||
});
|
||||
toast.success("密码重置成功");
|
||||
// 跳转到登录页
|
||||
setTimeout(() => {
|
||||
goToLogin();
|
||||
}, 500);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳转到登录页面 */
|
||||
function goToLogin() {
|
||||
uni.navigateTo({ url: LOGIN_PAGE });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./styles/auth.scss";
|
||||
</style>
|
||||
185
src/pages/auth/login.vue
Normal file
185
src/pages/auth/login.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<view class="auth-container">
|
||||
<!-- 顶部 -->
|
||||
<Header />
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-container">
|
||||
<TenantPicker ref="tenantPickerRef" />
|
||||
<view class="input-item">
|
||||
<wd-icon name="user" size="20px" color="#1890ff" />
|
||||
<wd-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
clear-trigger="focus"
|
||||
no-border
|
||||
/>
|
||||
</view>
|
||||
<view class="input-item">
|
||||
<wd-icon name="lock-on" size="20px" color="#1890ff" />
|
||||
<wd-input
|
||||
v-model="formData.password"
|
||||
placeholder="请输入密码"
|
||||
clearable
|
||||
clear-trigger="focus"
|
||||
show-password
|
||||
no-border
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<view class="mb-2 mt-2 flex justify-between">
|
||||
<text class="text-28rpx text-[#1890ff]" @click="goToSmsLogin">
|
||||
验证码登录
|
||||
</text>
|
||||
<text class="text-28rpx text-[#1890ff]" @click="goToForgetPassword">
|
||||
忘记密码?
|
||||
</text>
|
||||
</view>
|
||||
<wd-button block :loading="loading" type="primary" @click="handleLogin">
|
||||
登录
|
||||
</wd-button>
|
||||
|
||||
<!-- 第三方登录 -->
|
||||
<view class="mt-100rpx">
|
||||
<view class="divider mb-40rpx flex items-center justify-center">
|
||||
<view class="h-1rpx flex-1 bg-[#e5e5e5]" />
|
||||
<text class="px-24rpx text-26rpx text-[#999]">其他登录方式</text>
|
||||
<view class="h-1rpx flex-1 bg-[#e5e5e5]" />
|
||||
</view>
|
||||
<!-- TODO @芋艿:图标换下! -->
|
||||
<view class="icons flex justify-center gap-60rpx">
|
||||
<view class="icon-item" @click="handleWechatLogin">
|
||||
<wd-icon name="chat" size="24px" color="#07c160" />
|
||||
</view>
|
||||
<view class="icon-item" @click="handleDingTalkLogin">
|
||||
<wd-icon name="computer" size="24px" color="#3370ff" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 创建账号 -->
|
||||
<view class="mt-40rpx flex items-center justify-center">
|
||||
<text class="text-28rpx text-[#666]">还没有账号?</text>
|
||||
<text class="text-28rpx text-[#1890ff]" @click="goToRegister">
|
||||
创建账号
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import {
|
||||
CODE_LOGIN_PAGE,
|
||||
FORGET_PASSWORD_PAGE,
|
||||
REGISTER_PAGE,
|
||||
} from "@/router/config";
|
||||
import { useTokenStore } from "@/store/token";
|
||||
import { ensureDecodeURIComponent, redirectAfterLogin } from "@/utils";
|
||||
import Header from "./components/header.vue";
|
||||
import TenantPicker from "./components/tenant-picker.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "LoginPage",
|
||||
style: {
|
||||
navigationStyle: "custom",
|
||||
},
|
||||
});
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationStyle: "custom",
|
||||
},
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const loading = ref(false); // 加载状态
|
||||
const redirectUrl = ref<string>(); // 重定向地址
|
||||
const tenantPickerRef = ref<InstanceType<typeof TenantPicker>>(); // 租户选择器引用
|
||||
|
||||
const formData = reactive({
|
||||
username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || "",
|
||||
password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || "",
|
||||
}); // 表单数据
|
||||
|
||||
/** 页面加载时处理重定向 */
|
||||
onLoad((options) => {
|
||||
if (options?.redirect) {
|
||||
redirectUrl.value = ensureDecodeURIComponent(options.redirect);
|
||||
}
|
||||
});
|
||||
|
||||
/** 登录处理 */
|
||||
async function handleLogin() {
|
||||
if (!tenantPickerRef.value?.validate()) {
|
||||
return;
|
||||
}
|
||||
if (!formData.username) {
|
||||
toast.warning("请输入用户名");
|
||||
return;
|
||||
}
|
||||
if (!formData.password) {
|
||||
toast.warning("请输入密码");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
// 调用登录接口
|
||||
const tokenStore = useTokenStore();
|
||||
await tokenStore.login({
|
||||
type: "username",
|
||||
...formData,
|
||||
});
|
||||
// 处理跳转
|
||||
redirectAfterLogin(redirectUrl.value);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳转到注册页面 */
|
||||
function goToRegister() {
|
||||
uni.navigateTo({ url: REGISTER_PAGE });
|
||||
}
|
||||
|
||||
/** 跳转到验证码登录 */
|
||||
function goToSmsLogin() {
|
||||
uni.navigateTo({ url: CODE_LOGIN_PAGE });
|
||||
}
|
||||
|
||||
/** 跳转到忘记密码 */
|
||||
function goToForgetPassword() {
|
||||
uni.navigateTo({ url: FORGET_PASSWORD_PAGE });
|
||||
}
|
||||
|
||||
/** 微信登录 */
|
||||
// TODO @芋艿:后续开发
|
||||
function handleWechatLogin() {
|
||||
toast.info("微信登录功能开发中");
|
||||
}
|
||||
|
||||
/** 钉钉登录 */
|
||||
// TODO @芋艿:后续开发
|
||||
function handleDingTalkLogin() {
|
||||
toast.info("钉钉登录功能开发中");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./styles/auth.scss";
|
||||
|
||||
// 第三方登录图标
|
||||
.icon-item {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
179
src/pages/auth/register.vue
Normal file
179
src/pages/auth/register.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<view class="auth-container">
|
||||
<!-- 顶部 -->
|
||||
<Header />
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-container">
|
||||
<TenantPicker ref="tenantPickerRef" />
|
||||
<view class="input-item">
|
||||
<wd-icon name="user" size="20px" color="#1890ff" />
|
||||
<wd-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
clear-trigger="focus"
|
||||
no-border
|
||||
/>
|
||||
</view>
|
||||
<view class="input-item">
|
||||
<wd-icon name="person" size="20px" color="#1890ff" />
|
||||
<wd-input
|
||||
v-model="formData.nickname"
|
||||
placeholder="请输入昵称"
|
||||
clearable
|
||||
clear-trigger="focus"
|
||||
no-border
|
||||
/>
|
||||
</view>
|
||||
<view class="input-item">
|
||||
<wd-icon name="lock-on" size="20px" color="#1890ff" />
|
||||
<wd-input
|
||||
v-model="formData.password"
|
||||
placeholder="请输入密码"
|
||||
clearable
|
||||
clear-trigger="focus"
|
||||
show-password
|
||||
no-border
|
||||
/>
|
||||
</view>
|
||||
<view class="input-item">
|
||||
<wd-icon name="lock-on" size="20px" color="#1890ff" />
|
||||
<wd-input
|
||||
v-model="formData.confirmPassword"
|
||||
placeholder="请确认密码"
|
||||
clearable
|
||||
clear-trigger="focus"
|
||||
show-password
|
||||
no-border
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 用户协议 -->
|
||||
<view class="mb-24rpx flex items-center">
|
||||
<wd-checkbox v-model="agreePolicy" shape="square" />
|
||||
<text class="text-24rpx text-[#666]">我已阅读并同意</text>
|
||||
<text class="text-24rpx text-[#1890ff]" @click="goToUserAgreement">
|
||||
《用户协议》
|
||||
</text>
|
||||
<text class="text-24rpx text-[#666]">与</text>
|
||||
<text class="text-24rpx text-[#1890ff]" @click="goToPrivacyPolicy">
|
||||
《隐私政策》
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<wd-button
|
||||
block
|
||||
:loading="loading"
|
||||
type="primary"
|
||||
@click="handleRegister"
|
||||
>
|
||||
注册
|
||||
</wd-button>
|
||||
|
||||
<!-- 已有账号 -->
|
||||
<view class="mt-40rpx flex items-center justify-center">
|
||||
<text class="text-28rpx text-[#666]">已有账号?</text>
|
||||
<text class="text-28rpx text-[#1890ff]" @click="goToLogin">去登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import { useToast } from "wot-design-uni";
|
||||
import { LOGIN_PAGE } from "@/router/config";
|
||||
import { useTokenStore } from "@/store/token";
|
||||
import { redirectAfterLogin } from "@/utils";
|
||||
import Header from "./components/header.vue";
|
||||
import TenantPicker from "./components/tenant-picker.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "RegisterPage",
|
||||
});
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationStyle: "custom",
|
||||
},
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const loading = ref(false); // 加载状态
|
||||
const agreePolicy = ref(false); // 用户协议勾选
|
||||
const tenantPickerRef = ref<InstanceType<typeof TenantPicker>>(); // 租户选择器引用
|
||||
|
||||
const formData = reactive({
|
||||
username: "",
|
||||
nickname: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
}); // 表单数据
|
||||
|
||||
/** 注册处理 */
|
||||
async function handleRegister() {
|
||||
if (!tenantPickerRef.value?.validate()) {
|
||||
return;
|
||||
}
|
||||
if (!agreePolicy.value) {
|
||||
toast.warning("请阅读并同意《用户协议》与《隐私政策》");
|
||||
return;
|
||||
}
|
||||
if (!formData.username) {
|
||||
toast.warning("请输入用户名");
|
||||
return;
|
||||
}
|
||||
if (!formData.nickname) {
|
||||
toast.warning("请输入昵称");
|
||||
return;
|
||||
}
|
||||
if (!formData.password) {
|
||||
toast.warning("请输入密码");
|
||||
return;
|
||||
}
|
||||
if (!formData.confirmPassword) {
|
||||
toast.warning("请确认密码");
|
||||
return;
|
||||
}
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
toast.warning("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
// 调用注册接口
|
||||
const tokenStore = useTokenStore();
|
||||
await tokenStore.login({
|
||||
type: "register",
|
||||
...formData,
|
||||
});
|
||||
toast.success("注册成功");
|
||||
// 处理跳转
|
||||
redirectAfterLogin();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳转到登录页面 */
|
||||
function goToLogin() {
|
||||
uni.navigateTo({ url: LOGIN_PAGE });
|
||||
}
|
||||
|
||||
/** 跳转到用户协议 */
|
||||
function goToUserAgreement() {
|
||||
uni.navigateTo({ url: "/pages/user/settings/agreement/index" });
|
||||
}
|
||||
|
||||
/** 跳转到隐私政策 */
|
||||
function goToPrivacyPolicy() {
|
||||
uni.navigateTo({ url: "/pages/user/settings/privacy/index" });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./styles/auth.scss";
|
||||
</style>
|
||||
63
src/pages/auth/styles/auth.scss
Normal file
63
src/pages/auth/styles/auth.scss
Normal file
@@ -0,0 +1,63 @@
|
||||
/** 认证页面公共样式 */
|
||||
|
||||
// 页面容器
|
||||
.auth-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #e8f4ff 0%, #fff 50%);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// 表单容器
|
||||
.form-container {
|
||||
flex: 1;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
background: #fff;
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
// 输入项
|
||||
.input-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
background: #f5f7fa;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
:deep(.wd-input) {
|
||||
flex: 1;
|
||||
margin-left: 16rpx;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.wd-picker) {
|
||||
flex: 1;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
:deep(.wd-picker__field) {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// 移除 picker 的边框,保持与 input 一致
|
||||
:deep(.wd-picker__cell) {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.wd-cell) {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
|
||||
&::after {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.wd-cell__wrapper) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user