add UserAgreement.vue
This commit is contained in:
@@ -32,7 +32,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div v-else class="empty-block" v-text="emptyText" />
|
<div v-else class="center-tips" v-text="emptyText" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -115,22 +115,6 @@ function handleRemove(customer: Customer): void {
|
|||||||
width: 3.5rem;
|
width: 3.5rem;
|
||||||
height: 3.5rem;
|
height: 3.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-block {
|
|
||||||
color: #52607a;
|
|
||||||
margin: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
background: #f8fbff;
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.avatar .t-image__img) {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: inherit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(html.theme-dark) {
|
:global(html.theme-dark) {
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ async function bootstrap(): Promise<void> {
|
|||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
await router.isReady();
|
await router.isReady();
|
||||||
|
await import("tdesign-mobile-vue");
|
||||||
|
await import("../../timi-web/src/index");
|
||||||
await import("timi-tdesign-mobile");
|
await import("timi-tdesign-mobile");
|
||||||
app.mount("#root");
|
app.mount("#root");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,8 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</t-form>
|
</t-form>
|
||||||
<t-button class="submit" theme="primary" size="medium" :loading="isSubmitting" :disabled="isExpired || isSubmitting" @click="register">注册并登录</t-button>
|
<user-agreement v-model="hasAgreed" />
|
||||||
|
<t-button class="submit" theme="primary" size="medium" :loading="isSubmitting" :disabled="isExpired || isSubmitting || !hasAgreed" @click="register">注册并登录</t-button>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -61,6 +62,7 @@ import { CommonAPI, NotifyAPI, useUserStore, type LoginResponse } from "timi-web
|
|||||||
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
import ManagedUserAPI from "@/api/ManagedUserAPI";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import Logo from "@/pages/user/component/Logo.vue";
|
import Logo from "@/pages/user/component/Logo.vue";
|
||||||
|
import UserAgreement from "@/pages/user/component/UserAgreement.vue";
|
||||||
|
|
||||||
type CaptchaExpose = { captchaId: string; update: () => Promise<void> };
|
type CaptchaExpose = { captchaId: string; update: () => Promise<void> };
|
||||||
|
|
||||||
@@ -75,6 +77,7 @@ const isValid = ref(false);
|
|||||||
const expireAt = ref(0);
|
const expireAt = ref(0);
|
||||||
const invalidMessage = ref("邀请码无效或已过期,请联系邀请人重新生成。");
|
const invalidMessage = ref("邀请码无效或已过期,请联系邀请人重新生成。");
|
||||||
const isSubmitting = ref(false);
|
const isSubmitting = ref(false);
|
||||||
|
const hasAgreed = ref(false);
|
||||||
const captchaRef = ref<CaptchaExpose | null>(null);
|
const captchaRef = ref<CaptchaExpose | null>(null);
|
||||||
const formModel = reactive({
|
const formModel = reactive({
|
||||||
phoneNo: "",
|
phoneNo: "",
|
||||||
@@ -193,7 +196,11 @@ async function sendSms(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function register(): Promise<void> {
|
async function register(): Promise<void> {
|
||||||
if (isSubmitting.value || isExpired.value || !validateForm()) {
|
if (isSubmitting.value || !hasAgreed.value || isExpired.value || !validateForm()) {
|
||||||
|
if (!hasAgreed.value) {
|
||||||
|
Message.warning("请先阅读并同意用户协议");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isExpired.value) {
|
if (isExpired.value) {
|
||||||
Message.warning("邀请码已失效,请联系邀请人重新生成");
|
Message.warning("邀请码已失效,请联系邀请人重新生成");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,9 @@
|
|||||||
:options="loginTypeOptions"
|
:options="loginTypeOptions"
|
||||||
v-entrance-transition="140"
|
v-entrance-transition="140"
|
||||||
/>
|
/>
|
||||||
<password-login v-if="loginType === 'password'" class="form" />
|
<password-login v-if="loginType === 'password'" class="form" :agreement-accepted="hasAgreed" />
|
||||||
<sms-login v-else class="form" />
|
<sms-login v-else class="form" :agreement-accepted="hasAgreed" />
|
||||||
|
<user-agreement v-model="hasAgreed" />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
import PasswordLogin from "@/pages/user/component/PasswordLogin.vue";
|
import PasswordLogin from "@/pages/user/component/PasswordLogin.vue";
|
||||||
import SmsLogin from "@/pages/user/component/SmsLogin.vue";
|
import SmsLogin from "@/pages/user/component/SmsLogin.vue";
|
||||||
import Logo from "@/pages/user/component/Logo.vue";
|
import Logo from "@/pages/user/component/Logo.vue";
|
||||||
|
import UserAgreement from "@/pages/user/component/UserAgreement.vue";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "LoginIndex"
|
name: "LoginIndex"
|
||||||
@@ -29,6 +31,7 @@ defineOptions({
|
|||||||
type LoginType = "password" | "sms";
|
type LoginType = "password" | "sms";
|
||||||
|
|
||||||
const loginType = ref<LoginType>("password");
|
const loginType = ref<LoginType>("password");
|
||||||
|
const hasAgreed = ref(false);
|
||||||
const loginTypeOptions = [
|
const loginTypeOptions = [
|
||||||
{ value: "password" as LoginType, label: "密码登录" },
|
{ value: "password" as LoginType, label: "密码登录" },
|
||||||
{ value: "sms" as LoginType, label: "短信登录" }
|
{ value: "sms" as LoginType, label: "短信登录" }
|
||||||
|
|||||||
@@ -39,7 +39,6 @@
|
|||||||
<t-button
|
<t-button
|
||||||
class="red bg-white cell-button"
|
class="red bg-white cell-button"
|
||||||
variant="text"
|
variant="text"
|
||||||
block
|
|
||||||
v-entrance-transition="140"
|
v-entrance-transition="140"
|
||||||
@click="confirmLogout"
|
@click="confirmLogout"
|
||||||
>退出登录</t-button>
|
>退出登录</t-button>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<captcha class="captcha" ref="captchaRef" :width="120" :height="40" :request="CommonAPI.captcha" />
|
<captcha class="captcha" ref="captchaRef" :width="120" :height="40" :request="CommonAPI.captcha" />
|
||||||
</div>
|
</div>
|
||||||
<div class="exec">
|
<div class="exec">
|
||||||
<t-button theme="primary" class="button" type="submit" :loading="isSubmitting" :disabled="isSubmitting">登录</t-button>
|
<t-button theme="primary" class="button" type="submit" :loading="isSubmitting" :disabled="isSubmitting || !props.agreementAccepted">登录</t-button>
|
||||||
</div>
|
</div>
|
||||||
</t-form>
|
</t-form>
|
||||||
</template>
|
</template>
|
||||||
@@ -39,6 +39,12 @@ defineOptions({
|
|||||||
name: "PasswordLogin"
|
name: "PasswordLogin"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
agreementAccepted?: boolean;
|
||||||
|
}>(), {
|
||||||
|
agreementAccepted: false
|
||||||
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -56,6 +62,10 @@ function getRedirectPath(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateForm(): boolean {
|
function validateForm(): boolean {
|
||||||
|
if (!props.agreementAccepted) {
|
||||||
|
Message.warning("请先阅读并同意用户协议");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!formModel.user || !formModel.password || !formModel.captcha || !captchaRef.value?.captchaId) {
|
if (!formModel.user || !formModel.password || !formModel.captcha || !captchaRef.value?.captchaId) {
|
||||||
Message.warning("请输入账号、密码和验证码");
|
Message.warning("请输入账号、密码和验证码");
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="exec">
|
<div class="exec">
|
||||||
<t-button theme="primary" class="button" type="submit" :loading="isSubmitting" :disabled="isSubmitting">登录</t-button>
|
<t-button theme="primary" class="button" type="submit" :loading="isSubmitting" :disabled="isSubmitting || !props.agreementAccepted">登录</t-button>
|
||||||
</div>
|
</div>
|
||||||
</t-form>
|
</t-form>
|
||||||
</template>
|
</template>
|
||||||
@@ -51,6 +51,12 @@ defineOptions({
|
|||||||
name: "SmsLogin"
|
name: "SmsLogin"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
agreementAccepted?: boolean;
|
||||||
|
}>(), {
|
||||||
|
agreementAccepted: false
|
||||||
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -136,6 +142,10 @@ async function sendSms(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateForm(): boolean {
|
function validateForm(): boolean {
|
||||||
|
if (!props.agreementAccepted) {
|
||||||
|
Message.warning("请先阅读并同意用户协议");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!/^1\d{10}$/.test(formModel.mobilePhone)) {
|
if (!/^1\d{10}$/.test(formModel.mobilePhone)) {
|
||||||
Message.warning("请输入正确的 11 位手机号");
|
Message.warning("请输入正确的 11 位手机号");
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-agreement">
|
||||||
|
<t-checkbox
|
||||||
|
:model-value="props.modelValue"
|
||||||
|
class="checkbox"
|
||||||
|
borderless
|
||||||
|
@change="handleCheckboxChange"
|
||||||
|
>
|
||||||
|
<span>我已阅读并同意用户协议</span>
|
||||||
|
</t-checkbox>
|
||||||
|
</div>
|
||||||
|
<t-dialog
|
||||||
|
class="user-agreement-dialog"
|
||||||
|
v-model:visible="dialogVisible"
|
||||||
|
title="用户协议"
|
||||||
|
cancel-btn="拒绝"
|
||||||
|
confirm-btn="同意"
|
||||||
|
:close-btn="false"
|
||||||
|
:close-on-overlay-click="false"
|
||||||
|
destroy-on-close
|
||||||
|
@cancel="rejectAgreement"
|
||||||
|
@confirm="agreeAgreement"
|
||||||
|
>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<t-tabs
|
||||||
|
v-model="activeAgreementCode"
|
||||||
|
class="tabs"
|
||||||
|
theme="line"
|
||||||
|
bottom-line-mode="full"
|
||||||
|
:split="false"
|
||||||
|
:space-evenly="true"
|
||||||
|
:swipeable="true"
|
||||||
|
>
|
||||||
|
<t-tab-panel
|
||||||
|
v-for="item in agreementList"
|
||||||
|
:key="item.code"
|
||||||
|
:value="item.code"
|
||||||
|
:label="item.title"
|
||||||
|
lazy
|
||||||
|
>
|
||||||
|
<div class="agreement-item">
|
||||||
|
<t-loading v-if="item.loading" class="loading" text="正在加载协议" />
|
||||||
|
<article-view
|
||||||
|
v-else-if="item.article"
|
||||||
|
:article="item.article"
|
||||||
|
empty-text="暂无协议正文"
|
||||||
|
:resolve-attachment-url="resolveAttachmentUrl"
|
||||||
|
/>
|
||||||
|
<p v-else class="error" v-text="item.errorMessage || '协议加载失败,请拒绝后重试。'" />
|
||||||
|
</div>
|
||||||
|
</t-tab-panel>
|
||||||
|
</t-tabs>
|
||||||
|
<div class="tips">
|
||||||
|
<span>请阅读以上全部协议</span>
|
||||||
|
<br />
|
||||||
|
<span>阅读完成后点击“同意”继续</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ArticleAPI, AttachmentAPI, SettingAPI, type Article } from "timi-web-api";
|
||||||
|
import { ArticleView } from "timi-tdesign-mobile";
|
||||||
|
import { getErrorMessage } from "@/utils/error";
|
||||||
|
|
||||||
|
type AgreementCode = "SERVICE_LICENSE" | "PRIVACY_LICENSE" | "OPEN_SOURCE_LICENSE";
|
||||||
|
type AgreementItem = {
|
||||||
|
code: AgreementCode;
|
||||||
|
title: string;
|
||||||
|
article?: Article;
|
||||||
|
errorMessage?: string;
|
||||||
|
loading: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseAgreementList: Array<Pick<AgreementItem, "code" | "title">> = [
|
||||||
|
{
|
||||||
|
code: "SERVICE_LICENSE",
|
||||||
|
title: "服务协议"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "PRIVACY_LICENSE",
|
||||||
|
title: "隐私协议"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: "OPEN_SOURCE_LICENSE",
|
||||||
|
title: "开源软件协议"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "UserAgreement"
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
modelValue?: boolean;
|
||||||
|
}>(), {
|
||||||
|
modelValue: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": [value: boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const agreementList = ref<AgreementItem[]>(createAgreementList());
|
||||||
|
const activeAgreementCode = ref<AgreementCode>("SERVICE_LICENSE");
|
||||||
|
|
||||||
|
function createAgreementList(): AgreementItem[] {
|
||||||
|
return baseAgreementList.map(item => ({
|
||||||
|
...item,
|
||||||
|
loading: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCheckboxChange(checked: boolean): void {
|
||||||
|
if (!checked) {
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
dialogVisible.value = true;
|
||||||
|
activeAgreementCode.value = "SERVICE_LICENSE";
|
||||||
|
void loadAgreements();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAgreements(): Promise<void> {
|
||||||
|
agreementList.value = createAgreementList().map(item => ({
|
||||||
|
...item,
|
||||||
|
loading: true
|
||||||
|
}));
|
||||||
|
try {
|
||||||
|
const settingMap = await SettingAPI.map("GAO_SYSTEM");
|
||||||
|
agreementList.value = await Promise.all(agreementList.value.map(async item => {
|
||||||
|
const articleId = settingMap[item.code]?.value?.trim();
|
||||||
|
if (!articleId) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
loading: false,
|
||||||
|
errorMessage: `${item.title}暂未配置`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
article: await ArticleAPI.detail(articleId),
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
loading: false,
|
||||||
|
errorMessage: getErrorMessage(error, `${item.title}加载失败`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = getErrorMessage(error, "协议配置加载失败");
|
||||||
|
agreementList.value = createAgreementList().map(item => ({
|
||||||
|
...item,
|
||||||
|
loading: false,
|
||||||
|
errorMessage
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAttachmentUrl(attachmentId: string): string {
|
||||||
|
return AttachmentAPI.getReadURL(attachmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function rejectAgreement(): void {
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function agreeAgreement(): void {
|
||||||
|
emit("update:modelValue", true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.user-agreement {
|
||||||
|
display: flex;
|
||||||
|
margin-top: .75rem;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
color: #52607a;
|
||||||
|
font-size: .8125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-agreement-dialog {
|
||||||
|
|
||||||
|
:global(.user-agreement-dialog.t-dialog .t-dialog__content) {
|
||||||
|
height: min(62vh, 36rem);
|
||||||
|
max-height: min(62vh, 36rem);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.user-agreement-dialog.t-dialog .t-dialog__body) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.user-agreement-dialog.t-dialog .t-dialog__body-text) {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.user-agreement-dialog.t-dialog .t-dialog__footer) {
|
||||||
|
--td-spacer-1: 0;
|
||||||
|
--td-button-border-radius: 1rem;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
--td-spacer-3: 10px;
|
||||||
|
|
||||||
|
gap: .5rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
--td-tab-item-height: 2rem;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
:deep(.t-tabs__content) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-tabs__scroll) {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-tabs__item) {
|
||||||
|
padding: 0;
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-item {
|
||||||
|
padding-top: 1rem;
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
margin: 0;
|
||||||
|
color: #b42318;
|
||||||
|
font-size: .8125rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.content) {
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 0;
|
||||||
|
color: #52607a;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.theme-dark) {
|
||||||
|
.user-agreement {
|
||||||
|
.checkbox {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-agreement-dialog {
|
||||||
|
.dialog-content {
|
||||||
|
.hint {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agreement-item {
|
||||||
|
.error {
|
||||||
|
color: #fda29b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user