Merge pull request 'feature/0905_登录注册流程重构' (#44) from feature/0905_登录注册流程重构 into main

Reviewed-on: ai-team/lingji-work-fe#44
This commit is contained in:
2025-09-15 07:31:11 +00:00
65 changed files with 3029 additions and 1115 deletions

3
.gitignore vendored
View File

@ -34,3 +34,6 @@ config/unplugin/*.d.ts
config/unplugin/.eslintrc*
analyzer.html
.cursor/*
.lingma/*

View File

@ -11,7 +11,7 @@
- dayjs
- lodash
- ant-design-vue
- less
- scss
- eslint + prettier
### 项目启动
@ -182,8 +182,3 @@ export { default as Comp } from './comp/index.vue';
<i-icon-a />
</template>
```
### `ui` 框架 [`Ant Design Vue`](https://antdv.com/)
- 自动导入组件,无需再次导入
- 图标库已配置,同组件使用

View File

@ -1,6 +1,6 @@
<template>
<ConfigProvider :locale="zhCN" :theme="redTheme">
<router-view v-if="$route.path === '/login' || ['ExploreList', 'ExploreDetail'].includes($route.name)" />
<router-view v-if="$route.meta.withoutLayout" />
<LayoutBasic v-else />
</ConfigProvider>
</template>
@ -31,10 +31,9 @@ const redTheme = {
const init = async () => {
const { isLogin } = userStore;
// 已开通
if (isLogin) {
await initApp();
sidebarStore.startUnreadInfoPolling();
} else {
sidebarStore.stopUnreadInfoPolling();
}
@ -42,6 +41,7 @@ const init = async () => {
onMounted(() => {
init();
// 监听全局未处理错误
window.addEventListener('unhandledrejection', (event) => {
event.preventDefault();

View File

@ -46,9 +46,7 @@ export const getHeaders = () => {
};
};
/**
* 获取智能体信息
*/
// 获取智能体信息
export const getAgentData = async () => {
const { data } = await axios.get(`${BASE_PYTHON_URL}/api/agent/info`, {
headers: getHeaders(),
@ -56,12 +54,19 @@ export const getAgentData = async () => {
return data;
};
/**
* 生成会话id
*/
// 生成会话id
export const createSession = async () => {
const { data } = await axios.get(`${BASE_PYTHON_URL}/api/agent/create_session`, {
headers: getHeaders(),
});
return data;
};
// 中断智能体执行
export const stopAgentTask = async (params = {}) => {
const { data } = await axios.get(`${BASE_PYTHON_URL}/api/agent/stop-task`, {
headers: getHeaders(),
params,
});
return data;
};

View File

@ -72,3 +72,8 @@ export const getImagePreSignedUrl = (params = {}) => {
export const getVideoPreSignedUrl = (params = {}) => {
return Http.get('/v1/oss/video-pre-signed-url', params);
};
// 清除限流
export const postClearRateLimiter = (params = {}) => {
return Http.post(`/v1/rate-limiter/clear`, params);
};

View File

@ -122,7 +122,7 @@ export const sendUpdateMobileCaptcha = (data: any) => {
// 修改绑定的手机号
export const updateMobile = (data: any) => {
return Http.post(`/v1/me/mobile`, data);
return Http.patch(`/v1/me/mobile`, data);
};
// 修改我的信息

View File

@ -44,6 +44,67 @@ export const fetchBindPhone = (params = {}) => {
};
// 根据id获取企业信息
export const fetchEnterpriseInfo = (id: number) => {
export const fetchEnterpriseInfo = (id: number | string) => {
return Http.get(`/v1/enterprises/${id}`);
};
// 手机密码登录
export const postLoginPassword = (params = {}) => {
return Http.post('/v1/authorizations/password', params);
};
// 用户注册
export const postRegister = (params = {}) => {
return Http.post('/v1/users/register', params);
};
// 发送注册验证码
export const postRegisterCaptcha = (params = {}) => {
return Http.post('/v1/sms/register-captcha', params);
};
// 发送忘记密码验证码
export const postForgetPasswordCaptcha = (params = {}) => {
return Http.post('/v1/sms/forget-password-captcha', params);
};
// 忘记密码
export const postForgetPassword = (params = {}) => {
return Http.post('/v1/users/forget-password', params);
};
// 创建企业
export const postCreateEnterprises = (params = {}) => {
return Http.post('/v1/enterprises', params);
};
// 获取我的企业列表
export const getMyEnterprises = () => {
return Http.get('/v1/me/enterprises');
};
// 获取我的主企业信息
export const getMyPrimaryEnterprise = () => {
return Http.get('/v1/me/primary-enterprise');
};
// 发送修改手机号验证码
export const postUpdateMobileCaptcha = (params = {}) => {
return Http.post('/v1/sms/update-mobile-captcha', params);
};
// 发送修改密码验证码
export const postUpdatePasswordCaptcha = (params = {}) => {
return Http.post('/v1/sms/update-password-captcha', params);
};
// 验证修改密码验证码
export const postCheckUpdatePasswordCaptcha = (params = {}) => {
return Http.post('/v1/sms/check-update-password-captcha', params);
};
// 修改密码
export const postUpdatePassword = (params = {}) => {
return Http.patch('/v1/me/password', params);
};

View File

@ -61,9 +61,12 @@ export class Request {
this.instance.interceptors.response.use(
(res: AxiosResponse) => {
const { data } = res;
switch (data.code) {
const { data, status } = res;
switch (status) {
case HttpStatusCode.Success:
if (data.code !== HttpStatusCode.Success) {
message.error(data.message);
}
return data;
default:
return Promise.reject(data);

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M14.25 9.75C15.4926 9.75 16.5 10.7574 16.5 12V14.25C16.5 15.4926 15.4926 16.5 14.25 16.5H3.75C2.50736 16.5 1.5 15.4926 1.5 14.25V12C1.5 10.7574 2.50736 9.75 3.75 9.75H14.25Z" fill="#6D4CFE"/>
<path d="M13.875 8.625H3.75C2.50736 8.625 1.5 7.61764 1.5 6.375V4.125C1.5 2.88236 2.50736 1.875 3.75 1.875H13.875V8.625Z" fill="#6D4CFE"/>
<path d="M12.75 1.5C14.8211 1.5 16.5 3.17893 16.5 5.25C16.5 7.32107 14.8211 9 12.75 9C10.6789 9 9 7.32107 9 5.25C9 3.17893 10.6789 1.5 12.75 1.5Z" fill="#39C6E9"/>
<path d="M12.75 6.375C13.3713 6.375 13.875 5.87132 13.875 5.25C13.875 4.62868 13.3713 4.125 12.75 4.125C12.1287 4.125 11.625 4.62868 11.625 5.25C11.625 5.87132 12.1287 6.375 12.75 6.375Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 818 B

View File

@ -1,6 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.6666 8.66671C13.7712 8.66671 14.6666 9.56214 14.6666 10.6667V12.6667C14.6666 13.7713 13.7712 14.6667 12.6666 14.6667H3.33325C2.22868 14.6667 1.33325 13.7713 1.33325 12.6667V10.6667C1.33325 9.56214 2.22868 8.66671 3.33325 8.66671H12.6666ZM3.33325 10C2.96506 10 2.66659 10.2985 2.66659 10.6667V12.6667C2.66659 13.0349 2.96506 13.3334 3.33325 13.3334H12.6666C13.0348 13.3334 13.3333 13.0349 13.3333 12.6667V10.6667C13.3333 10.2985 13.0348 10 12.6666 10H3.33325Z" fill="currentColor"/>
<path d="M11.3333 3.66671C11.8855 3.66671 12.3333 4.11442 12.3333 4.66671C12.3333 5.21899 11.8855 5.66671 11.3333 5.66671C10.781 5.66671 10.3333 5.21899 10.3333 4.66671C10.3333 4.11442 10.781 3.66671 11.3333 3.66671Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3333 1.33337C13.1742 1.33337 14.6666 2.82576 14.6666 4.66671C14.6666 6.50766 13.1742 8.00004 11.3333 8.00004C9.4923 8.00004 7.99992 6.50766 7.99992 4.66671C7.99992 2.82576 9.4923 1.33337 11.3333 1.33337ZM11.3333 2.66671C10.2287 2.66671 9.33325 3.56214 9.33325 4.66671C9.33325 5.77128 10.2287 6.66671 11.3333 6.66671C12.4378 6.66671 13.3333 5.77128 13.3333 4.66671C13.3333 3.56214 12.4378 2.66671 11.3333 2.66671Z" fill="currentColor"/>
<path d="M6.66659 1.66671C7.03478 1.66671 7.33325 1.96518 7.33325 2.33337C7.33325 2.70156 7.03478 3.00004 6.66659 3.00004H3.33325C2.96506 3.00004 2.66659 3.29852 2.66659 3.66671V5.66671C2.66659 6.03489 2.96507 6.33337 3.33325 6.33337H6.66659C7.03478 6.33337 7.33325 6.63185 7.33325 7.00004C7.33325 7.36823 7.03478 7.66671 6.66659 7.66671H3.33325C2.22868 7.66671 1.33325 6.77126 1.33325 5.66671V3.66671C1.33325 2.56214 2.22868 1.66671 3.33325 1.66671H6.66659Z" fill="currentColor"/>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.25 9.75C15.4926 9.75 16.5 10.7574 16.5 12V14.25C16.5 15.4926 15.4926 16.5 14.25 16.5H3.75C2.50736 16.5 1.5 15.4926 1.5 14.25V12C1.5 10.7574 2.50736 9.75 3.75 9.75H14.25ZM3.75 11.25C3.33579 11.25 3 11.5858 3 12V14.25C3 14.6642 3.33579 15 3.75 15H14.25C14.6642 15 15 14.6642 15 14.25V12C15 11.5858 14.6642 11.25 14.25 11.25H3.75Z" fill="currentColor"/>
<path d="M12.75 4.125C13.3713 4.125 13.875 4.62868 13.875 5.25C13.875 5.87132 13.3713 6.375 12.75 6.375C12.1287 6.375 11.625 5.87132 11.625 5.25C11.625 4.62868 12.1287 4.125 12.75 4.125Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.75 1.5C14.8211 1.5 16.5 3.17893 16.5 5.25C16.5 7.32107 14.8211 9 12.75 9C10.6789 9 9 7.32107 9 5.25C9 3.17893 10.6789 1.5 12.75 1.5ZM12.75 3C11.5074 3 10.5 4.00736 10.5 5.25C10.5 6.49264 11.5074 7.5 12.75 7.5C13.9926 7.5 15 6.49264 15 5.25C15 4.00736 13.9926 3 12.75 3Z" fill="currentColor"/>
<path d="M7.5 1.875C7.91421 1.875 8.25 2.21079 8.25 2.625C8.25 3.03921 7.91421 3.375 7.5 3.375H3.75C3.33579 3.375 3 3.71079 3 4.125V6.375C3 6.7892 3.33579 7.125 3.75 7.125H7.5C7.91421 7.125 8.25 7.46079 8.25 7.875C8.25 8.28921 7.91421 8.625 7.5 8.625H3.75C2.50735 8.625 1.5 7.61762 1.5 6.375V4.125C1.5 2.88236 2.50736 1.875 3.75 1.875H7.5Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M8.625 16.5C8.625 16.9142 8.28921 17.25 7.875 17.25H3.375C2.96079 17.25 2.625 16.9142 2.625 16.5V8.25C2.625 8.01393 2.73644 7.79179 2.92529 7.65015L8.625 3.375V16.5Z" fill="#39C6E9"/>
<path d="M7.52856 0.834838C7.77656 0.705717 8.07585 0.725097 8.30493 0.885375L12.0549 3.51038C12.2554 3.65073 12.375 3.88017 12.375 4.12488V8.55823L15.364 10.2194C15.6021 10.3516 15.75 10.6025 15.75 10.8749V16.4999C15.7499 16.914 15.4142 17.2499 15 17.2499H7.125V1.49988L7.13232 1.39661C7.16541 1.15837 7.31155 0.947913 7.52856 0.834838Z" fill="#6D4CFE"/>
</svg>

After

Width:  |  Height:  |  Size: 656 B

View File

@ -1,10 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<g clip-path="url(#clip0_1822_2135)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.69214 0.742281C6.91258 0.627508 7.17861 0.644734 7.38224 0.787203L10.7156 3.12054C10.8938 3.24529 11.0001 3.44924 11.0001 3.66676V7.657L13.0541 9.12444C13.2293 9.24959 13.3334 9.45149 13.3334 9.66676V14.0001H14.6667C15.0349 14.0001 15.3334 14.2986 15.3334 14.6668C15.3334 15.0349 15.0349 15.3334 14.6667 15.3334H1.33341C0.965261 15.3334 0.666807 15.0349 0.666748 14.6668C0.666748 14.2986 0.965225 14.0001 1.33341 14.0001H3.00008V6.66676C3.00008 6.44924 3.1064 6.24529 3.28459 6.12054L6.33341 3.98577V1.33343L6.33992 1.24163C6.36934 1.02986 6.49924 0.842793 6.69214 0.742281ZM4.33341 7.01377V14.0001H6.33341V5.61338L4.33341 7.01377ZM7.66675 14.0001H12.0001V10.0092L9.94604 8.54241C9.77088 8.4173 9.66679 8.21534 9.66675 8.00009V4.01377L7.66675 2.61338V14.0001Z" fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_1822_2135">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.52856 0.834838C7.77656 0.705717 8.07585 0.725097 8.30493 0.885375L12.0549 3.51038C12.2554 3.65073 12.375 3.88017 12.375 4.12488V8.55823L15.364 10.2194C15.6021 10.3516 15.75 10.6025 15.75 10.8749V16.4999C15.7499 16.914 15.4142 17.2499 15 17.2499H3.375C2.96083 17.2499 2.62507 16.914 2.625 16.4999V8.24988C2.625 8.01383 2.73647 7.79167 2.92529 7.65002L7.125 4.49988V1.49988L7.13232 1.39661C7.16541 1.15837 7.31155 0.947913 7.52856 0.834838ZM4.125 8.62415V15.7499H7.125V6.37415L4.125 8.62415ZM8.625 15.7499H14.25V11.3158L11.261 9.6554C11.0229 9.52314 10.8751 9.27219 10.875 8.99988V4.51526L8.625 2.93982V15.7499Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 790 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M9.19082 9.80683H6.14149C3.30767 9.80683 1.00293 12.1138 1.00293 14.9502V15.2554C1.00293 16.7382 3.27081 16.7382 6.14149 16.7382H9.19084C11.9486 16.7382 14.3299 16.7382 14.3299 15.2554V14.9502C14.3299 12.1143 12.0246 9.80683 9.19082 9.80683ZM7.51381 9.41426C9.7578 9.41426 11.5836 7.58587 11.5836 5.338C11.5836 3.09023 9.7578 1.26172 7.51381 1.26172C5.2699 1.26172 3.44395 3.09062 3.44395 5.33809C3.44395 7.58538 5.2699 9.41426 7.51381 9.41426Z" fill="#6D4CFE"/>
<rect x="12.75" y="6" width="4.5" height="1.5" rx="0.75" fill="#39C6E9"/>
<rect x="14.25" y="9" width="3" height="1.5" rx="0.75" fill="#39C6E9"/>
</svg>

After

Width:  |  Height:  |  Size: 727 B

View File

@ -1,6 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6.66675 2.66663C7.03494 2.66663 7.33341 2.9651 7.33341 3.33329C7.33341 3.70148 7.03494 3.99996 6.66675 3.99996H2.00008V12.6666H14.0001V11.8333C14.0001 11.4651 14.2986 11.1666 14.6667 11.1666C15.0349 11.1666 15.3334 11.4651 15.3334 11.8333V12.6666C15.3334 13.403 14.7365 14 14.0001 14H2.00008C1.26371 14 0.666748 13.403 0.666748 12.6666V3.99996C0.666748 3.26356 1.26371 2.66663 2.00008 2.66663H6.66675Z" fill="currentColor"/>
<path d="M11.3334 9.66663C11.7016 9.66663 12.0001 9.9651 12.0001 10.3333C12.0001 10.7015 11.7016 11 11.3334 11H3.33341C2.96522 11 2.66675 10.7015 2.66675 10.3333C2.66675 9.9651 2.96522 9.66663 3.33341 9.66663H11.3334Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3334 2.66663C12.8062 2.66663 14.0001 3.86053 14.0001 5.33329C14.0001 6.04266 13.722 6.68629 13.2703 7.16402C14.107 7.60518 14.8215 8.31147 15.2605 9.16923C15.428 9.49687 15.2983 9.89858 14.9708 10.0664C14.6431 10.2341 14.2414 10.1043 14.0736 9.77665C13.526 8.70664 12.3939 8.0001 11.3334 7.99996C10.7314 7.99999 10.3219 8.08478 10.0027 8.2161C9.68314 8.34763 9.41097 8.54307 9.09839 8.82678C8.82582 9.07418 8.40446 9.05368 8.15698 8.78121C7.90953 8.50858 7.92994 8.08726 8.20255 7.8398C8.52921 7.54331 8.87871 7.27263 9.30802 7.06702C8.90854 6.60077 8.66675 5.99542 8.66675 5.33329C8.66675 3.86053 9.86065 2.66663 11.3334 2.66663ZM11.3334 3.99996C10.597 3.99996 10.0001 4.59691 10.0001 5.33329C10.0001 6.06967 10.597 6.66663 11.3334 6.66663C12.0698 6.66663 12.6667 6.06967 12.6667 5.33329C12.6667 4.59691 12.0698 3.99996 11.3334 3.99996Z" fill="currentColor"/>
<path d="M6.00008 6.99996C6.36827 6.99996 6.66675 7.29844 6.66675 7.66663C6.66675 8.03482 6.36827 8.33329 6.00008 8.33329H3.33341C2.96522 8.33329 2.66675 8.03482 2.66675 7.66663C2.66675 7.29844 2.96522 6.99996 3.33341 6.99996H6.00008Z" fill="currentColor"/>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M6.1416 10.5566H9.19043C11.6093 10.5566 13.5801 12.5279 13.5801 14.9502V15.2559C13.58 15.387 13.5437 15.4508 13.4521 15.5225C13.3221 15.6241 13.0738 15.7337 12.6494 15.8174C11.7979 15.9853 10.5984 15.9883 9.19043 15.9883H6.1416C4.67522 15.9883 3.47825 15.9847 2.64258 15.8174C2.22729 15.7342 1.9932 15.6267 1.87402 15.5303C1.79278 15.4645 1.75301 15.4004 1.75293 15.2559V14.9502C1.75295 12.5273 3.72267 10.5566 6.1416 10.5566ZM7.51367 2.01172C9.3423 2.01172 10.8339 3.50328 10.834 5.33789C10.834 7.17264 9.34235 8.66406 7.51367 8.66406C5.68514 8.66399 4.19434 7.17203 4.19434 5.33789C4.19444 3.50364 5.68523 2.0118 7.51367 2.01172Z" stroke="currentColor" stroke-width="1.5"/>
<rect x="12.75" y="6" width="4.5" height="1.5" rx="0.75" fill="currentColor"/>
<rect x="14.25" y="9" width="3" height="1.5" rx="0.75" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 950 B

View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<g clip-path="url(#clip0_214_27738)">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M7.52856 0.834838C7.77656 0.705717 8.07585 0.725097 8.30493 0.885375L12.0549 3.51037C12.2554 3.65073 12.375 3.88017 12.375 4.12488V8.61389L14.6858 10.2648C14.8828 10.4056 15 10.6327 15 10.8749V15.7499H16.5C16.9142 15.7499 17.25 16.0857 17.25 16.4999C17.2499 16.914 16.9142 17.2499 16.5 17.2499H1.5C1.08583 17.2499 0.750066 16.914 0.75 16.4999C0.75 16.0857 1.08579 15.7499 1.5 15.7499H3.375V7.49988C3.375 7.25517 3.49461 7.02572 3.69507 6.88537L7.125 4.48376V1.49988L7.13232 1.39661C7.16541 1.15837 7.31155 0.947913 7.52856 0.834838ZM4.875 7.89026V15.7499H7.125V6.31482L4.875 7.89026ZM8.625 15.7499H13.5V11.2601L11.1892 9.60998C10.9922 9.46923 10.875 9.24203 10.875 8.99988V4.51526L8.625 2.93982V15.7499Z"
fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_214_27738">
<rect width="18" height="18" rx="3.6" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -5,7 +5,7 @@ import SenderInput from './components/sender-input/index.vue';
import RightView from './components/right-view/index.vue';
import { useChatStore } from '@/stores/modules/chat';
import { getConversationList } from '@/api/all/chat';
import { getConversationList, stopAgentTask } from '@/api/all/chat';
import querySSE from '@/utils/querySSE';
import useChatHandler from './useChatHandler';
import { QUESTION_ROLE, LOADING_ROLE } from './constants';
@ -58,6 +58,7 @@ export default {
if (generateLoading.value) {
bubbleListRef.value?.abortTypingByKey(generateTeamRunTaskId.value);
sseController.value?.abort?.();
stopAgentTask({ run_id: generateTeamRunTaskId.value });
}
if (showRightView.value) {
rightViewRef.value?.abortTyping?.();
@ -165,7 +166,7 @@ export default {
<SenderInput
v-model={searchValue.value}
ref={senderRef}
style={{ 'width': 'var(--max-content-width)' }}
style={{ width: 'var(--max-content-width)' }}
placeholder="继续追问..."
loading={generateLoading.value}
onSubmit={handleSubmit}

View File

@ -2,17 +2,27 @@
import { Input } from 'ant-design-vue';
// import { handleUserHome } from '@/utils/user.ts';
import { useChatStore } from '@/stores/modules/chat';
import { useUserStore } from '@/stores';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
export default {
setup(props, { emit, expose }) {
const chatStore = useChatStore();
const userStore = useUserStore();
const enterpriseStore = useEnterpriseStore();
const keyWord = ref('');
const hasOpenEnterprise = computed(() => enterpriseStore.isOpenEnterprise);
const handleSearch = () => {
chatStore.setSearchValue(keyWord.value);
chatStore.onCreateSession();
keyWord.value = '';
};
if (!hasOpenEnterprise.value) return null;
return () => (
<div class="middle-wrap h-100% flex items-center justify-center">
<Input
@ -35,6 +45,6 @@ export default {
};
</script>
<style scoped lang="scss">
<style lang="scss" scoped>
@import './style.scss';
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

View File

@ -1,17 +1,31 @@
<template>
<div class="right-wrap">
<!-- 任务中心 -->
<div class="relative p-6px rounded-30px flex items-center justify-center task-icon" @click="setUnread">
<div
class="relative p-6px rounded-30px flex items-center justify-center task-icon"
@click="setUnread"
v-if="hasOpenEnterprise"
>
<SvgIcon name="svg-taskCenter" size="20" class="color-#737478" @click="openDownloadCenter" />
<div class="w-6px h-6px rounded-50% bg-#F64B31 absolute top-6px right-6px" v-if="hasUnreadInfo"></div>
</div>
<!-- 灵机空间入口 -->
<div class="agent-entry mx-16px" :class="isAgentRoute ? 'agent' : ''" @click="handleAgentClick"></div>
<div
class="agent-entry mx-16px"
:class="isAgentRoute ? 'agent' : ''"
@click="handleAgentClick"
v-if="hasOpenEnterprise"
></div>
<!-- 头像设置 -->
<Dropdown trigger="click" overlayClassName="layout-avatar-dropdown">
<img alt="avatar" src="@/assets/avatar.svg" class="cursor-pointer w-32px h-32px rounded-50%" />
<div class="cursor-pointer">
<Avatar :src="userInfo.head_image" :size="32" v-if="userInfo.head_image" />
<div v-else class="w-32px h-32px rounded-50% bg-#6D4CFE flex items-center justify-center">
<span class="color-#FFF text-14px font-400 lh-22px">{{ userInfo.mobile?.slice(-3) }}</span>
</div>
</div>
<template #overlay>
<Menu>
<MenuItem>
@ -23,7 +37,7 @@
<icon-right size="12" />
</div>
</MenuItem>
<MenuItem>
<MenuItem v-if="enterprises.length > 0">
<SubMenu value="option-1" position="lt" trigger="hover" popupClassName="enterprises-dsubmenu">
<template #title>
<div class="flex justify-between w-100% h-full items-center">
@ -34,11 +48,11 @@
<icon-right size="12" />
</div>
</template>
<div v-for="(item, index) in enterprises" :key="index">
<MenuItem
v-for="(item, index) in enterprises"
:key="index"
class="rounded-8px hover:bg-#F2F3F5"
@click="onEnterpriseItemClick(item)"
v-if="!primary_enterprise || item.id !== primary_enterprise.id"
>
<div
class="flex items-center w-100% justify-between"
@ -48,6 +62,54 @@
<icon-check v-if="enterpriseInfo?.id === item.id" size="16" />
</div>
</MenuItem>
<template v-else>
<template v-if="item.audit_status === 1">
<div class="w-full h-1px bg-#E6E6E8 mb-8px" v-if="enterprises.length > 0"></div>
<MenuItem class="rounded-8px hover:bg-#F2F3F5 h-36px !mb-0" @click="onCreate(item)">
<div class="flex items-center">
<SvgIcon size="16.5" name="svg-organization" class="color-#737478 mr-8px" />
<span class="color-#211F24 mr-4px">创建企业账号</span>
<div class="px-8px h-20px rounded-2px bg-#FFF7E5 flex items-center">
<span class="color-#FFAE00 !text-12px !lh-20px font-400">申请中</span>
</div>
</div>
</MenuItem>
</template>
<div v-else>
<MenuItem class="rounded-8px hover:bg-#F2F3F5" @click="handleSubAccountClick(item)">
<div class="flex justify-between items-center overflow-hidden">
<div
class="flex items-center w-100% flex-1 overflow-hidden"
:class="enterpriseInfo?.id === item.id ? '!color-#6D4CFE' : ''"
>
<TextoverTips :context="item.name" />
<img :src="icon4" width="12" height="12" class="ml-4px" />
</div>
<div
class="px-8px h-20px rounded-2px flex items-center flex-shrink-0 ml-4px"
:class="_map.get(item.subscribe_status)?.bg"
v-if="[2, 3].includes(item.subscribe_status)"
>
<span class="!text-12px !lh-20px font-400" :class="_map.get(item.subscribe_status)?.color">{{
_map.get(item.subscribe_status)?.label
}}</span>
</div>
</div>
</MenuItem>
</div>
</template>
</div>
<template v-if="!primary_enterprise">
<div class="w-full h-1px bg-#E6E6E8 mb-8px" v-if="enterprises?.length > 0"></div>
<MenuItem class="rounded-8px hover:bg-#F2F3F5 h-36px !mb-0" @click="onCreate(null)">
<div class="flex items-center">
<SvgIcon size="16.5" name="svg-organization" class="color-#737478 mr-8px" />
<span class="color-#211F24 mr-4px">创建企业账号</span>
</div>
</MenuItem>
</template>
</SubMenu>
</MenuItem>
<MenuItem>
@ -69,8 +131,8 @@
</template>
<script setup>
import { Dropdown, Menu, MenuItem, SubMenu } from 'ant-design-vue';
import router from '@/router';
import { Dropdown, Menu, MenuItem, SubMenu, Avatar } from 'ant-design-vue';
import { useRouter } from 'vue-router'; // import router from '@/router';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import { useSidebarStore } from '@/stores/modules/side-bar';
import { useUserStore } from '@/stores';
@ -82,6 +144,8 @@ import DownloadCenterModal from '../task-center-modal';
import icon1 from '@/assets/option.svg';
import icon2 from '@/assets/exit.svg';
import icon3 from '@/assets/change.svg';
import icon4 from './img/admin.png';
import TextoverTips from '@/components/text-over-tips/index.vue';
const props = defineProps({
isAgentRoute: {
@ -90,19 +154,27 @@ const props = defineProps({
},
});
const _map = new Map([
[2, { label: '试用中', bg: 'bg-#F0EDFF', color: 'color-#6D4CFE' }],
[3, { label: '已到期', bg: 'bg-#F2F3F5', color: 'color-#55585F' }],
]);
const enterpriseStore = useEnterpriseStore();
const userStore = useUserStore();
const sideBarStore = useSidebarStore();
const route = useRoute();
const hasUnreadInfo = computed(() => sideBarStore.unreadInfo.length);
const router = useRouter();
const exitAccountModalRef = ref(null);
const downloadCenterModalRef = ref(null);
const hasUnreadInfo = computed(() => sideBarStore.unreadInfo.length);
const hasOpenEnterprise = computed(() => enterpriseStore.isOpenEnterprise);
const primary_enterprise = computed(() => userStore.userInfo?.primary_enterprise);
const enterprises = computed(() => {
return userStore.userInfo?.enterprises ?? [];
});
const userInfo = computed(() => userStore.userInfo);
const enterpriseInfo = computed(() => {
return enterpriseStore?.enterpriseInfo ?? {};
});
@ -114,6 +186,7 @@ const onEnterpriseItemClick = async (item) => {
enterpriseStore.setEnterpriseInfo(item);
window.location.reload();
};
const clickExit = async () => {
exitAccountModalRef.value?.open();
};
@ -128,6 +201,20 @@ const setUnread = () => {
const handleAgentClick = () => {
props.isAgentRoute ? handleUserHome() : router.push({ name: 'AgentIndex' });
};
const onCreate = (item) => {
if (item && item.id === primary_enterprise.value.id) {
enterpriseStore.setEnterpriseInfo(item);
}
router.push({ name: 'Trial' });
};
const handleSubAccountClick = (item) => {
enterpriseStore.setEnterpriseInfo(item);
if (item.subscribe_status === 3) {
router.push({ name: 'Trial' });
return;
}
window.location.reload();
};
</script>
<style scoped lang="scss">
@ -141,32 +228,45 @@ const handleAgentClick = () => {
border: 1px solid var(--BG-300, #e6e6e8);
background: var(--BG-white, #fff);
padding: 12px 0px;
.ant-dropdown-menu-item {
padding: 0 12px;
margin-bottom: 4px;
margin-bottom: 8px;
font-family: $font-family-regular;
color: var(--Text-1, #211f24);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
.ant-dropdown-menu-title-content {
display: flex;
height: 40px;
height: 32px;
width: 100%;
padding: 10px 24px;
align-items: center;
.ant-dropdown-menu-submenu {
width: 100%;
.ant-dropdown-menu-submenu-title {
padding: 0;
&:hover {
background: none;
}
.ant-dropdown-menu-title-content {
padding: 0 !important;
}
}
.ant-dropdown-menu-submenu-arrow {
display: none;
}
}
}
.menu-item-text {
color: var(--Text-2, #3c4043);
font-family: $font-family-regular;
@ -175,11 +275,14 @@ const handleAgentClick = () => {
font-weight: 400;
line-height: 22px;
}
.ant-dropdown-menu-title-content {
border-radius: 8px !important;
}
&:not(.ant-dropdown-menu-item):hover {
background-color: transparent;
.ant-dropdown-menu-title-content {
background: var(--BG-200, #f2f3f5);
}
@ -187,21 +290,28 @@ const handleAgentClick = () => {
}
}
}
.layout-avatar-dropdown,
.enterprises-dsubmenu {
width: 200px;
.ant-dropdown-menu {
padding: 12px 4px;
padding: 8px 4px;
margin: 0;
.ant-dropdown-menu-item {
padding: 0 !important;
.ant-dropdown-menu-title-content {
padding: 0 12px !important;
}
}
}
.ant-dropdown-option-suffix {
display: none;
}
// .enterprises-doption {
// .ant-dropdown-menu-title-content {
// padding: 0 !important;

View File

@ -1,8 +1,7 @@
<template>
<div class="navbar-wrap px-24px">
<div class="w-full h-full relative flex justify-between">
<div class="left-wrap flex items-center cursor-pointer" @click="handleUserHome">
<div class="left-wrap flex items-center cursor-pointer" @click="onLogoClick">
<img src="@/assets/img/icon-logo.png" alt="" width="96" height="24" />
</div>
<!-- <div class="flex-1"> -->
@ -10,7 +9,6 @@
<!-- </div> -->
<RightSide :isAgentRoute="isAgentRoute" v-if="userStore.isLogin" />
</div>
</div>
</template>
@ -19,12 +17,15 @@ import MiddleSide from './components/middle-side';
import RightSide from './components/right-side';
import { useUserStore } from '@/stores';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import { handleUserHome } from '@/utils/user.ts';
import router from '@/router';
const route = useRoute();
const userStore = useUserStore();
const enterpriseStore = useEnterpriseStore();
const hasOpenEnterprise = computed(() => enterpriseStore.isOpenEnterprise);
const isAgentRoute = computed(() => {
return route.meta?.isAgentRoute;
});
@ -32,6 +33,16 @@ const isAgentRoute = computed(() => {
const isHomeRoute = computed(() => {
return route.name === 'Home';
});
const onLogoClick = () => {
if (hasOpenEnterprise.value) {
handleUserHome();
} else {
router.push({
name: 'Trial',
});
}
};
</script>
<style scoped lang="scss">
.navbar-wrap {
@ -58,16 +69,20 @@ const isHomeRoute = computed(() => {
display: flex;
align-items: center;
}
.arco-dropdown-option-suffix {
display: none;
}
.enterprises-doption {
.arco-dropdown-option-content {
padding: 0 !important;
border-radius: 8px;
}
&:not(.arco-dropdown-option-disabled):hover {
background-color: transparent;
.arco-dropdown-option-content {
background: var(--BG-200, #f2f3f5);
}

View File

@ -1,14 +1,14 @@
<script lang="tsx">
import { Dropdown, Menu, Layout } from 'ant-design-vue';
import type { RouteMeta, RouteRecordRaw } from 'vue-router';
import { Dropdown, Layout, Menu } from 'ant-design-vue';
import { useRoute } from 'vue-router';
import SvgIcon from '@/components/svg-icon/index.vue';
import { useAppStore } from '@/stores';
import { useAppStore, useUserStore } from '@/stores';
import { useSidebarStore } from '@/stores/modules/side-bar';
import { MENU_LIST } from './menu-list';
import type { typeMenuItem } from './menu-list';
import { MENU_LIST } from './menu-list';
import { handleUserHome } from '@/utils/user';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import icon1 from '@/assets/img/agent/icon1.png';
@ -21,14 +21,18 @@ export default defineComponent({
const sidebarStore = useSidebarStore();
const appStore = useAppStore();
const userStore = useUserStore();
const enterpriseStore = useEnterpriseStore();
const currentMenuList = ref<typeMenuItem[]>([]);
// const currentMenuList = ref<typeMenuItem[]>([]);
const currentMenuModInfo = ref<typeMenuItem>({});
const currentRouteName = computed(() => route.name as string);
const currentRouteGroup = computed(() => route.meta?.group ?? 'GroupMain');
const isHomeRoute = computed(() => currentRouteName.value === 'Home');
const showAiSearch = computed(() => !route.meta?.hideAiSearch);
const currentMenuList = computed(() => sidebarStore.currentMenuList);
const hasOpenEnterprise = computed(() => enterpriseStore.isOpenEnterprise);
const collapsed = computed(() => {
return sidebarStore.menuCollapse;
@ -70,10 +74,18 @@ export default defineComponent({
}
router.push({ name: targetRoute });
};
const renderMenuItem = (item: typeMenuItem, hideLabel = false) => {
const renderMenuItem = ({
item,
hideLabel = false,
menuItemClass = '',
}: {
item: typeMenuItem;
hideLabel?: boolean;
menuItemClass?: string;
}) => {
const getMenuItemClass = () => {
const hasChildren = item.children?.length;
let target = !hasChildren ? 'sub-menu-item ' : '';
let target = '';
if (hasChildren) {
target += getCollapseMenuKey(currentRouteName.value) === item.key ? 'active' : '';
} else {
@ -83,9 +95,9 @@ export default defineComponent({
};
return (
<Menu.Item class={`menu-item ${getMenuItemClass()}`} onClick={() => onClickItem(item)}>
<Menu.Item class={`menu-item ${getMenuItemClass()} ${menuItemClass}`} onClick={() => onClickItem(item)}>
{(() => {
const isActive = getMenuItemClass() === 'active';
const isActive = getMenuItemClass().includes('active');
const iconName = Array.isArray(item.icon)
? isActive
? item.icon[1] ?? item.icon[0]
@ -100,7 +112,7 @@ export default defineComponent({
const renderMenuList = () => {
return currentMenuList.value.map((item) => {
if (!item.children) {
return renderMenuItem(item, collapsed.value);
return renderMenuItem({ item, hideLabel: collapsed.value });
}
return (
<Dropdown
@ -112,22 +124,41 @@ export default defineComponent({
return (
<div class="p-8px bg-#fff container w-139px">
{item.children.map((child) => {
return renderMenuItem(child);
return renderMenuItem({ item: child, menuItemClass: 'sub-menu-item' });
})}
</div>
);
},
}}
>
{renderMenuItem(item, collapsed.value)}
{renderMenuItem({ item, hideLabel: collapsed.value })}
</Dropdown>
);
});
};
const initMenuList = () => {
const groupMenuList = MENU_LIST?.[currentRouteGroup.value as string] ?? [];
currentMenuList.value = cloneDeep(groupMenuList);
let groupMenuList = MENU_LIST?.[currentRouteGroup.value as string] ?? [];
// 如果企业未开通,过滤掉 requireAuth 为 true 的菜单项
if (!hasOpenEnterprise.value) {
groupMenuList = groupMenuList.filter((item) => {
if (item.requireAuth === true) {
return false;
}
if (item.children && item.children.length > 0) {
const filteredChildren = item.children.filter((child) => !child.requireAuth);
if (filteredChildren.length === 0 && !item.routeName) {
return false;
}
item.children = filteredChildren;
}
return true;
});
}
sidebarStore.setCurrentMenuList(groupMenuList);
};
const initCollapse = () => {
@ -209,29 +240,25 @@ export default defineComponent({
</style>
<style lang="scss">
@import './style.scss';
.layout-sider-dropdown-xt {
.container {
border-radius: 8px;
background: var(--BG-White, #fff);
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
.menu-item {
@include menu-item;
padding: 8px;
&:hover {
background-color: rgba(109, 76, 254, 0.08) !important;
color: #6d4cfe !important;
.svg-icon {
color: #6d4cfe !important;
}
}
// &.active {
// background-color: rgba(109, 76, 254, 0.08);
// color: #6d4cfe;
// .label,
// .svg-icon {
// color: #6d4cfe;
// }
// }
}
}
}

View File

@ -26,6 +26,7 @@ export const MENU_LIST = <Record<string, typeMenuItem[]>>{
label: '账号管理',
routeName: 'MediaAccountAccountManagement',
requireLogin: true,
requireAuth: true,
activeMatch: ['MediaAccountAccountManagement'],
},
{
@ -34,6 +35,7 @@ export const MENU_LIST = <Record<string, typeMenuItem[]>>{
label: '账号数据',
routeName: 'MediaAccountAccountDashboard',
requireLogin: true,
requireAuth: true,
activeMatch: ['MediaAccountAccountDashboard', 'MediaAccountAccountDetails'],
},
// {
@ -42,6 +44,7 @@ export const MENU_LIST = <Record<string, typeMenuItem[]>>{
// label: '账户管理',
// routeName: 'PutAccountAccountManagement',
// requireLogin: true,
// requireAuth: true,
// activeMatch: ['PutAccountAccountManagement'],
// },
// {
@ -50,6 +53,7 @@ export const MENU_LIST = <Record<string, typeMenuItem[]>>{
// label: '账户数据',
// routeName: 'PutAccountAccountData',
// requireLogin: true,
// requireAuth: true,
// activeMatch: ['PutAccountAccountData'],
// },
// {
@ -58,6 +62,7 @@ export const MENU_LIST = <Record<string, typeMenuItem[]>>{
// label: '投放表现分析',
// routeName: 'PutAccountAccountDashboard',
// requireLogin: true,
// requireAuth: true,
// activeMatch: ['PutAccountAccountDashboard'],
// },
// {
@ -66,6 +71,7 @@ export const MENU_LIST = <Record<string, typeMenuItem[]>>{
// label: '投放指南',
// routeName: 'PutAccountInvestmentGuidelines',
// requireLogin: true,
// requireAuth: true,
// activeMatch: ['PutAccountInvestmentGuidelines', 'PutAccountInvestmentGuidelinesDetail'],
// },
],
@ -81,6 +87,7 @@ export const MENU_LIST = <Record<string, typeMenuItem[]>>{
label: '成品库',
routeName: 'MaterialCenterFinishedProducts',
requireLogin: true,
requireAuth: true,
activeMatch: [
'MaterialCenterFinishedProducts',
'ManuscriptUpload',
@ -96,6 +103,7 @@ export const MENU_LIST = <Record<string, typeMenuItem[]>>{
label: '原料库',
routeName: 'MaterialCenterRawMaterial',
requireLogin: true,
requireAuth: true,
activeMatch: ['MaterialCenterRawMaterial'],
},
],
@ -119,6 +127,7 @@ export const MENU_LIST = <Record<string, typeMenuItem[]>>{
label: '成品库',
routeName: 'WriterMaterialCenterFinishedProducts',
requireLogin: true,
requireAuth: true,
activeMatch: [
'WriterMaterialCenterFinishedProducts',
'WriterManuscriptUpload',
@ -134,20 +143,16 @@ export const MENU_LIST = <Record<string, typeMenuItem[]>>{
// label: '原料库',
// routeName: 'WriterMaterialCenterRawMaterial',
// requireLogin: true,
// requireAuth: true,
// activeMatch: ['WriterMaterialCenterRawMaterial'],
// },
],
},
],
[GROUP_MANAGEMENT_NAME]:[
{
key: 'ModManagement',
label: '管理中心',
icon: ['svg-management', 'svg-management-active'],
children: [
{
key: 'ModManagementPerson',
icon: 'svg-managementPerson',
icon: ['svg-managementPerson', 'svg-managementPerson-active'],
label: '个人信息',
routeName: 'ManagementPerson',
requireLogin: true,
@ -157,21 +162,23 @@ export const MENU_LIST = <Record<string, typeMenuItem[]>>{
},
{
key: 'ModManagementEnterprise',
icon: 'svg-managementEnterprise',
icon: ['svg-managementEnterprise', 'svg-managementEnterprise-active'],
label: '企业信息',
routeName: 'ManagementEnterprise',
requireLogin: true,
activeMatch: ['ManagementEnterprise'],
requireAuth: true,
activeMatch: [
'ManagementEnterprise',
],
},
{
key: 'ModManagementAccount',
icon: 'svg-managementAccount',
icon: ['svg-managementAccount', 'svg-managementAccount-active'],
label: '账号管理',
routeName: 'ManagementAccount',
requireLogin: true,
requireAuth: true,
activeMatch: ['ManagementAccount'],
},
],
}
]
};

View File

@ -1,62 +0,0 @@
// /*
// * @Author: RenXiaoDong
// * @Date: 2025-06-19 01:45:53
// */
// import type { RouteRecordRaw, RouteRecordNormalized } from 'vue-router';
// import { useRouter } from 'vue-router';
// import { useSidebarStore } from '@/stores/modules/side-bar';
// export default function useMenuTree() {
// const router = useRouter();
// const appRoutes = router.options?.routes ?? [];
// const sidebarStore = useSidebarStore();
// const appRoute = computed(() => {
// const _filterRoutes = appRoutes.filter((v) => v.meta?.id === sidebarStore.activeMenuKey);
// return _filterRoutes;
// });
// const menuTree = computed(() => {
// const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
// copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
// return (a.meta.order || 0) - (b.meta.order || 0);
// });
// function travel(_routes: RouteRecordRaw[], layer: number) {
// if (!_routes) return null;
// const collector: any = _routes.map((element) => {
// // leaf node
// if (element.meta?.hideChildrenInMenu || !element.children) {
// element.children = [];
// return element;
// }
// // route filter hideInMenu true
// element.children = element.children.filter((x) => x.meta?.hideInMenu !== true);
// // Associated child node
// const subItem = travel(element.children, layer + 1);
// if (subItem.length) {
// element.children = subItem;
// return element;
// }
// // the else logic
// if (layer > 1) {
// element.children = subItem;
// return element;
// }
// if (element.meta?.hideInMenu === false) {
// return element;
// }
// return null;
// });
// return collector.filter(Boolean);
// }
// return travel(copyRouter, 0);
// });
// return {
// menuTree,
// };
// }

View File

@ -1,9 +1,14 @@
// import { useUserStore } from '@/stores/modules/user';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
// export function checkRoutePermission(routeName: string) {
// // const userStore = useUserStore();
// // const allowAccessRoutes = userStore.allowAccessRoutes;
export function checkRoutePermission(routeName: string) {
const enterpriseStore = useEnterpriseStore();
// // if (!routeName) return false;
// // return allowAccessRoutes.includes(routeName);
// }
if (!routeName) return false;
if (!enterpriseStore.isOpenEnterprise) {
return false;
}
return true;
}

View File

@ -7,7 +7,7 @@ import { message } from 'ant-design-vue';
import NProgress from 'nprogress';
import { goUserLogin } from '@/utils/user';
// import router from '@/router';
// import { checkRoutePermission } from '@/permission/permission';
import { checkRoutePermission } from '@/permission/permission';
import { useUserStore } from '@/stores/modules/user';
@ -27,16 +27,17 @@ export default function setupUserLoginInfoGuard(router: Router) {
return;
}
// if (requiresAuth) {
// const hasPermission = checkRoutePermission(routeName);
// if (!hasPermission) {
// message.error('您没有权限访问该页面');
// next('/');
// return;
// }
// next();
// return;
// }
if (requiresAuth) {
const hasPermission = checkRoutePermission(routeName);
if (!hasPermission) {
message.error('您没有权限访问该页面');
next('/trial');
return;
}
next();
return;
}
next();
});

View File

@ -18,10 +18,11 @@ export const router = createRouter({
{
path: '/login',
name: 'UserLogin',
component: () => import('@/views/components/login/index.vue'),
component: () => import('@/views/login/index.vue'),
meta: {
requiresAuth: false,
requireLogin: false,
withoutLayout: true,
},
},
{
@ -32,15 +33,25 @@ export const router = createRouter({
requiresAuth: false,
requireLogin: true,
group: GROUP_MAIN_NAME,
}
},
},
{
path: '/chat/:conversationId?',
name: 'Home',
component: () => import('@/views/home/index.vue'),
meta: {
requiresAuth: true,
requireLogin: true,
},
},
{
path: '/trial',
name: 'Trial',
component: () => import('@/views/trial/index.vue'),
meta: {
requiresAuth: false,
requireLogin: true,
withoutLayout: true,
},
},

View File

@ -120,6 +120,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
requireLogin: false,
hideFooter: true,
hideSidebar: true,
withoutLayout: true,
roles: ['*'],
},
component: () => import('@/views/material-center/components/finished-products/explore/list/index.vue'),
@ -133,6 +134,7 @@ const COMPONENTS: AppRouteRecordRaw[] = [
requireLogin: false,
hideFooter: true,
hideSidebar: true,
withoutLayout: true,
roles: ['*'],
},
component: () => import('@/views/material-center/components/finished-products/explore/detail/index.vue'),

View File

@ -19,5 +19,6 @@ declare module 'vue-router' {
requireLogin?: boolean; // 是否需要登陆才能访问
independent?: boolean; // 独立于layout的路由
group?: string; // 路由分组
withoutLayout?: boolean; // 不使用layout默认false
}
}

View File

@ -1,7 +1,5 @@
import { fetchEnterpriseInfo } from '@/api/all/login';
import { useSidebarStore } from '@/stores/modules/side-bar';
import { useUserStore } from '@/stores/modules/user';
import { glsWithCatch, slsWithCatch, rlsWithCatch } from '@/utils/stroage';
import { glsWithCatch, slsWithCatch } from '@/utils/stroage';
interface EnterpriseInfo {
id: number | string;
@ -11,6 +9,8 @@ interface EnterpriseInfo {
sub_account_quota: number;
used_sub_account_count: number;
permissions: string[];
[key: string]: any;
}
interface EnterpriseState {
@ -21,6 +21,12 @@ export const useEnterpriseStore = defineStore('enterprise', {
state: (): EnterpriseState => ({
enterpriseInfo: (glsWithCatch('enterpriseInfo') && JSON.parse(glsWithCatch('enterpriseInfo') as string)) || null,
}),
getters: {
// 企业已开通/试用中
isOpenEnterprise(): boolean {
return [1, 2].includes(this.enterpriseInfo?.subscribe_status);
},
},
actions: {
setEnterpriseInfo(enterpriseInfo: EnterpriseInfo) {
this.enterpriseInfo = enterpriseInfo;

View File

@ -1,8 +1,8 @@
import router from '@/router';
import { defineStore } from 'pinia';
import { fetchProfileInfo } from '@/api/all/login';
import { useSidebarStore } from '@/stores/modules/side-bar';
import { glsWithCatch, slsWithCatch, rlsWithCatch } from '@/utils/stroage';
// import { useSidebarStore } from '@/stores/modules/side-bar';
import { glsWithCatch, rlsWithCatch, slsWithCatch } from '@/utils/stroage';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
interface UserInfo {
id: number;
@ -10,6 +10,9 @@ interface UserInfo {
head_image: String;
current_enterprise_id: number;
mobile: string;
primary_enterprise: any; // 主企业信息
[key: string]: any;
// 添加其他用户属性...
}
@ -67,9 +70,17 @@ export const useUserStore = defineStore('user', {
// 获取用户信息
async getUserInfo() {
const enterpriseStore = useEnterpriseStore();
const { code, data } = await fetchProfileInfo();
if (code === 200) {
this.setUserInfo(data);
const { primary_enterprise } = data;
if (!enterpriseStore.enterpriseInfo && !isEmpty(primary_enterprise)) {
enterpriseStore.setEnterpriseInfo(primary_enterprise);
}
}
},
// clearUserAllowAccessRoutes() {

View File

@ -1,5 +1,5 @@
.ant-btn {
border-radius: 2px !important;
border-radius: 4px !important;
border: 1px solid #d7d7d9 !important;
color: #3c4043 !important;
font-family: $font-family-regular;

View File

@ -6,12 +6,7 @@
background-color: #fff !important;
padding: 0 12px;
}
.ant-input-affix-wrapper {
@include box;
&.ant-input-affix-wrapper-disabled {
background-color: var(--BG-200, #f2f3f5) !important;
}
}
.ant-input,
.ant-input-password {
@include box;
@ -32,7 +27,7 @@
&.ant-input-focus,
&.ant-textarea-focus {
background-color: var(--color-bg-2) !important;
border-color: rgb(var(--primary-6)) !important;
border-color: $color-primary !important;
box-shadow: 0 0 0 0 var(--color-primary-light-2) !important;
}
&.ant-input-disabled {
@ -44,8 +39,10 @@
}
&.ant-input-status-error,
&.ant-input-affix-wrapper-status-error {
&:not(.ant-input-disabled) {
border-color: $color-error !important;
}
}
}
input.ant-input {
height: 32px;
@ -62,9 +59,20 @@ textarea.ant-input {
.ant-input-affix-wrapper {
padding-top: 0;
padding-bottom: 0;
@include box;
&.ant-input-affix-wrapper-disabled {
background-color: var(--BG-200, #f2f3f5) !important;
}
&.ant-input-affix-wrapper-status-error {
&:not(.ant-input-affix-wrapper-disabled) {
border-color: $color-error !important;
}
}
&:focus,
&-focused {
box-shadow: none !important;
border-color: $color-primary !important;
}
.ant-input {
height: 30px;

View File

@ -17,7 +17,7 @@
color: #211f24;
font-size: 16px;
font-style: normal;
font-weight: 400;
font-weight: 500;
justify-content: flex-start;
}
}

View File

@ -35,6 +35,74 @@
&.ant-select-status-error {
border-color: $color-error !important;
}
}
.ant-select {
.ant-select-selector {
height: 32px !important;
}
&.ant-select-lg {
.ant-select-selector {
height: 36px !important;
}
}
&.ant-select-sm {
.ant-select-selector {
height: 28px !important;
}
}
&.ant-select-multiple {
.ant-select-selector {
padding: 0 12px 0 4px !important;
.ant-select-selection-overflow-item {
.ant-select-selection-item {
border-radius: 4px;
background: #E6E6E8;
border: none;
padding: 1px 8px;
.ant-select-selection-item-remove {
color: #737478;
}
}
}
}
&.ant-select-lg {
.ant-select-selector {
.ant-select-selection-overflow-item {
.ant-select-selection-item {
height: 24px;
line-height: 24px;
}
}
}
}
&.ant-select-sm {
.ant-select-selector {
.ant-select-selection-overflow-item {
.ant-select-selection-item {
height: 22px;
line-height: 22px;
.ant-select-selection-item-content {
font-size: 12px;
}
}
}
}
}
}
}
.ant-select.ant-select-single {
@ -52,18 +120,3 @@
}
}
}
.ant-select.ant-select-multiple {
.ant-select-selection-search-input {
min-height: 32px !important;
}
&.ant-select-lg {
.ant-select-selection-search-input {
min-height: 36px !important;
}
}
&.ant-select-sm {
.ant-select-selection-search-input {
min-height: 28px !important;
}
}
}

View File

@ -53,4 +53,20 @@
}
}
}
&.ant-steps-dot {
.ant-steps-item-process {
.ant-steps-item-tail {
&::after {
background-color: #e6e6e8 !important;
}
}
}
.ant-steps-item-finish {
.ant-steps-item-tail {
&::after {
background-color: #6d4cfe !important;
}
}
}
}
}

View File

@ -18,27 +18,27 @@
}
.font-family-regular {
font-family: $font-family-regular;
font-family: $font-family-regular !important;
}
.font-family-medium {
font-family: $font-family-medium;
font-family: $font-family-medium !important;
}
.font-family-light {
font-family: $font-family-light;
font-family: $font-family-light !important;
}
.font-family-bold {
font-family: $font-family-bold;
font-family: $font-family-bold !important;
}
.font-family-manrope-regular {
font-family: $font-family-manrope-regular;
font-family: $font-family-manrope-regular !important;
}
.font-family-manrope-medium {
font-family: $font-family-manrope-medium;
font-family: $font-family-manrope-medium !important;
}
.font-family-manrope-bold {
font-family: $font-family-manrope-bold;
font-family: $font-family-manrope-bold !important;
}
.font-family-manrope-semiBold {
font-family: $font-family-manrope-semiBold;
font-family: $font-family-manrope-semiBold !important;
}

View File

@ -1,7 +1,7 @@
declare global {
namespace MESSAGE {
type RUN_TASK_STATUS = 'RunStarted' | 'RunResponseContent' | 'RunCompleted';
type TEAM_RUN_TASK_STATUS = 'TeamRunStarted' | 'TeamRunResponseContent' | 'TeamRunCompleted';
type RUN_TASK_STATUS = 'L2Started' | 'L2RunContent' | 'L2Completed';
type TEAM_RUN_TASK_STATUS = 'TaskRunStarted' | 'TeamRunResponseContent' | 'TaskRunCompleted';
interface Answer {
message?: string;

View File

@ -11,14 +11,25 @@ import { useChatStore } from '@/stores/modules/chat';
// 登录
export function goUserLogin(query?: any) {
// console.log('goUserLogin', router)
router.push({ name: 'UserLogin', query });
}
// 首页
export function handleUserHome(params?: any) {
const enterpriseStore = useEnterpriseStore();
// 已开通
if (enterpriseStore.isOpenEnterprise) {
router.push({ name: 'Home', params });
} else {
router.push({ name: 'Trial' });
}
}
export const getUserEnterpriseInfo = async () => {
const enterpriseStore = useEnterpriseStore();
// const sidebarStore = useSidebarStore();
// const userStore = useUserStore();
await enterpriseStore.getEnterpriseInfo(); // 初始化企业信息
// sidebarStore.getUserNavbarMenuList(); // 初始化navbar菜单
// userStore.getUserAllowAccessRoutes(); // 初始化允许访问的路由
@ -27,30 +38,30 @@ export const getUserEnterpriseInfo = async () => {
export async function initApp() {
const userStore = useUserStore();
const chatStore = useChatStore();
const sidebarStore = useSidebarStore();
const enterpriseStore = useEnterpriseStore();
chatStore.getAgentInfo(); // 初始化智能体信息
await userStore.getUserInfo(); // 初始化用户信息
await getUserEnterpriseInfo(); // 初始化企业信息、navbar菜单、允许访问的路由
if (enterpriseStore.isOpenEnterprise) {
await getUserEnterpriseInfo(); // 初始化企业信息
chatStore.getAgentInfo(); // 初始化智能体信息
sidebarStore.startUnreadInfoPolling(); // 初始化未读信息
}
}
// 登录处理
export async function handleUserLogin() {
const sidebarStore = useSidebarStore();
await initApp();
sidebarStore.startUnreadInfoPolling(); // 初始化未读信息
handleUserHome();
}
// 首页
export function handleUserHome(params?: any) {
router.push({ name: 'Home', params });
}
// 登出处理
export function handleUserLogout() {
goUserLogin();
const userStore = useUserStore();
const enterpriseStore = useEnterpriseStore();
const sidebarStore = useSidebarStore();
@ -64,6 +75,4 @@ export function handleUserLogout() {
sidebarStore.stopUnreadInfoPolling(); // 清除未读消息
sidebarStore.clearActiveMenuKey(); // 清除active菜单id
sidebarStore.clearMenuCollapse(); // 清除active菜单id
goUserLogin();
}

View File

@ -1,346 +0,0 @@
<!-- eslint-disable vue/no-duplicate-attributes -->
<template>
<div
class="relative w-100vw h-100vh min-h-175 flex justify-center items-center bg-cover bg-center bg-#f0edff login-wrap"
>
<section class="login-bg"></section>
<section class="relative flex justify-between w-1200 h-100% my-0 mx-auto">
<div class="flex flex-col justify-center flex-column h-100% mt--12.5">
<img src="@/assets/img/Frame.svg" class="w-480 h-480 mr-40" alt="" />
</div>
<div class="flex items-center w-400 h-100%">
<Space
direction="vertical"
size="large"
align="center"
class="w-400 bg-#fff rounded-8px shadow-[0_4px_10px_0_#6D4CFE33] px-40px py-48px"
>
<img src="@/assets/img/icon-logo.png" alt="" width="96" height="24" class="mb-8px" />
<span class="text-4 color-#737478">AI营销工具</span>
<Form ref="formRef" :model="loginForm" :rules="formRules" auto-label-width class="w-320 mt-48px form-wrap">
<FormItem name="mobile">
<Input
v-model:value="loginForm.mobile"
placeholder="输入手机号"
class="form-input border border-solid !border-#d7d7d9 w-100% h-48px !text-14px rounded-4px color-#333 bg-#fff"
clearable
allowClear
/>
</FormItem>
<FormItem name="captcha">
<div
class="form-input border border-solid !border-#d7d7d9 w-100% h-48px !text-14px rounded-4px color-#333 bg-#fff flex justify-between items-center"
>
<Input
v-model:value="loginForm.captcha"
placeholder="验证码"
style="background-color: #fff; border: none !important;"
allowClear
class="form-input"
:maxlength="6"
/>
<span
class="w-120 font-400 text-right mr-4 text-16px"
:style="{
color: countdown > 0 || hasGetCode ? '#6D4CFE' : '#211F24',
cursor: countdown > 0 ? 'not-allowed' : 'pointer',
}"
@click="getCode"
>{{ countdown > 0 ? `${countdown}s` : hasGetCode ? '重新发送' : '发送验证码' }}</span
>
</div>
</FormItem>
<FormItem class="mt-68px mb-16px">
<Button
type="primary"
class="w-full h-48 !text-16px !rounded-8px"
:class="disabledSubmitBtn ? 'cursor-no-drop' : 'cursor-pointer'"
:disabled="disabledSubmitBtn"
@click="handleSubmit"
>
{{ isLogin ? '登录' : '注册并开通企业账号' }}
</Button>
</FormItem>
</Form>
<Space class="text-12px color-#737478 justify-start items-center">
<Checkbox v-model:checked="hasCheck" class="!text-12px mr-8px"></Checkbox>
<span class="text-12px color-#737478">{{ isLogin ? '登录' : '注册' }}即代表同意</span>
<Link href="link" class="form-link color-#211F24" target="_blank">用户协议</Link>
<span class="text-12px color-#737478"></span>
<Link href="link" class="form-link color-#211f24" target="_blank">隐私政策</Link>
</Space>
</Space>
</div>
</section>
<section class="login-footer">
<p class="text">闽公网安备 352018502850842 闽ICP备20250520582号 © 2025小题科技</p>
</section>
</div>
<PuzzleVerification
:show="isVerificationVisible"
@submit="handleVerificationSubmit"
@cancel="isVerificationVisible = false"
/>
<Modal v-model:open="visible" centered unmountOnClose @cancel="handleCancel">
<template #title>
<span style="text-align: left; width: 100%">选择账号</span>
</template>
<div class="account-bind-container">
<Card :bordered="false" class="bind-card">
<div class="bind-header">
<Typography.Text class="mobile-number">{{ mobileNumber }} 已在以下企业绑定了账号</Typography.Text>
</div>
<List :bordered="false" :split="false" class="account-list">
<List.Item
v-for="(account, index) in accounts"
:key="index"
class="account-item"
:class="{
selected: selectedAccountIndex === index,
'cursor-no-drop': account.status === 0,
'cursor-pointer': account.status !== 0,
}"
@click="selectAccount(account, index)"
>
<List.Item.Meta>
<template #title>
<div style="display: flex; align-items: center; gap: 12px">
<Checkbox :checked="selectedAccountIndex === index" />
<Typography.Text>{{ account.name || '-' }}</Typography.Text>
</div>
</template>
</List.Item.Meta>
</List.Item>
</List>
</Card>
</div>
<template #footer>
<div class="flex">
<Button type="primary" @click="handleOk">确定</Button>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import { Checkbox, Modal, Button, Form, FormItem, Input, Space, message, Typography, Card, List } from 'ant-design-vue';
const { Link } = Typography;
import PuzzleVerification from './components/PuzzleVerification.vue';
import { fetchLoginCaptCha, fetchAuthorizationsCaptcha, fetchProfileInfo } from '@/api/all/login';
import { joinEnterpriseByInviteCode } from '@/api/all';
import { ref, reactive, onUnmounted, computed } from 'vue';
import { useUserStore } from '@/stores';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import { handleUserLogin } from '@/utils/user';
import router from '@/router';
import { useRoute } from 'vue-router';
const formRef = ref();
const route = useRoute();
const userStore = useUserStore();
const enterpriseStore = useEnterpriseStore();
const countdown = ref(0);
let timer = ref();
const isLogin = ref(true);
const isVerificationVisible = ref(false);
const visible = ref(false);
const hasGetCode = ref(false);
const submitting = ref(false);
const hasCheck = ref(false);
const mobileNumber = ref('');
const selectedAccountIndex = ref(0);
const accounts = ref([]);
const loginForm = reactive({
mobile: '',
captcha: '',
});
// 表单校验规则
const formRules = {
mobile: [
{
required: true,
validator: (_rule: any, value: string) => {
if (!value) {
return Promise.reject('请填写手机号');
}
if (!/^1[3-9]\d{9}$/.test(value)) {
return Promise.reject('手机号格式不正确');
} else {
return Promise.resolve();
}
},
trigger: ['blur', 'change'],
},
],
captcha: [
{
required: true,
validator: (_rule: any, value: string) => {
if (!value) {
return Promise.reject('请填写验证码');
}
if (!/^\d{6}$/.test(value)) {
return Promise.reject('验证码必须是6位数字');
} else {
return Promise.resolve();
}
},
trigger: ['blur', 'change'],
},
],
};
// 表单是否有效
const isFormValid = computed(() => {
return (
loginForm.mobile.trim() !== '' &&
/^1[3-9]\d{9}$/.test(loginForm.mobile) &&
loginForm.captcha.trim() !== '' &&
/^\d{6}$/.test(loginForm.captcha)
);
});
const disabledSubmitBtn = computed(() => {
return !isFormValid.value;
});
const selectAccount = (account: any, index: any) => {
if (account.status === 0) {
return;
}
enterpriseStore.setEnterpriseInfo(account);
selectedAccountIndex.value = index;
};
const validateField = (field: string) => {
formRef.value.validateFields(field);
};
const clearError = (field: string) => {
formRef.value.clearValidate(field);
};
const handleOk = async () => {
visible.value = false;
handleUserLogin();
};
const handleCancel = () => {
visible.value = false;
};
const getCode = async () => {
if (countdown.value > 0) return;
// 先重置验证状态
formRef.value.clearValidate('mobile');
formRef.value.validateFields('mobile').then(() => {
isVerificationVisible.value = true;
});
};
// 验证码验证通过后
const handleVerificationSubmit = async () => {
isVerificationVisible.value = false;
startCountdown();
try {
const { code, message: msg } = await fetchLoginCaptCha({ mobile: loginForm.mobile });
if (code === 200) {
message.success(msg);
}
} catch (error) {
// 重置倒计时
countdown.value = 0;
clearInterval(timer.value);
}
};
// 获取用户信息
const getProfileInfo = async () => {
const { code, data } = await fetchProfileInfo();
if (code === 200) {
let enterprises = data['enterprises'];
mobileNumber.value = data['mobile'];
accounts.value = enterprises;
if (enterprises.length > 0) {
enterpriseStore.setEnterpriseInfo(data.enterprises[0]);
if (enterprises.length === 1) {
handleUserLogin();
} else {
// 多个企业时候需要弹窗让用户选择企业
visible.value = true;
}
}
}
};
// 提交表单
const handleSubmit = async () => {
if (disabledSubmitBtn.value) return;
try {
// 校验所有字段
await formRef.value.validate();
if (!hasCheck.value) {
message.error('请先勾选同意用户协议');
return;
}
submitting.value = true;
const { code, data } = await fetchAuthorizationsCaptcha(loginForm);
if (code === 200) {
// 处理登录成功逻辑
message.success(isLogin.value ? '登录成功' : '注册成功');
userStore.setToken(data.access_token);
const { invite_code } = route.query;
if (invite_code) {
const { code } = await joinEnterpriseByInviteCode(invite_code as string);
if (code === 200) {
message.success('加入企业成功');
}
}
getProfileInfo();
}
} catch (error) {
// 错误信息会显示在输入框下方
} finally {
submitting.value = false;
}
};
// 开始倒计时
const startCountdown = () => {
countdown.value = 60;
hasGetCode.value = true;
timer.value = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer.value as number);
timer.value = null;
}
}, 1000);
};
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value);
}
});
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,215 @@
<template>
<Modal
class="change-mobile-modal"
centered
v-model:open="visible"
title="修改手机号"
@cancel="onClose"
:footer="null"
>
<Form
:model="form"
ref="formRef"
:rules="formRules"
labelAlign="right"
:labelCol="{ span: 5 }"
:wrapperCol="{ span: 19 }"
>
<FormItem label="手机号" name="mobile">
<Input v-model:value="form.mobile" placeholder="请输入新的手机号" size="large" ref="inputRef" />
</FormItem>
<FormItem label="获取验证码" name="captcha">
<Input v-model:value="form.captcha" placeholder="请输入验证码" size="large" :maxlength="6">
<template #suffix>
<div class="w-79px flex justify-center whitespace-nowrap">
<span
class="color-#939499 font-family-regular text-16px font-400 lh-24px cursor-not-allowed"
:class="{
'!color-#6D4CFE': isLegalMobile || countdown > 0,
'!cursor-pointer': canGetCaptcha,
}"
@click="getCode"
>{{ countdown > 0 ? `${countdown}s` : hasGetCode ? '重新发送' : '发送验证码' }}</span
>
</div>
</template>
</Input>
</FormItem>
</Form>
<div class="flex justify-end mt-20px">
<Button class="mr-16px" size="large" @click="onClose">取消</Button>
<Button type="primary" size="large" :loading="submitLoading" @click="handleConfirm">确定</Button>
</div>
</Modal>
</template>
<script setup>
import { computed, ref } from 'vue';
import { Button, Form, FormItem, Input, message, Modal } from 'ant-design-vue';
import { postUpdateMobileCaptcha } from '@/api/all/login';
import { updateMobile } from '@/api/all';
import { useUserStore } from '@/stores';
const store = useUserStore();
const formRef = ref();
const inputRef = ref();
const visible = ref(false);
const timer = ref();
const isLegalMobile = ref(false);
const hasGetCode = ref(false);
const submitLoading = ref(false);
const countdown = ref(0);
const formRules = {
mobile: [
{
required: true,
validator: (_rule, value) => {
if (!value) {
isLegalMobile.value = false;
return Promise.reject('手机号不能为空');
}
if (value && !/^1[3-9]\d{9}$/.test(value)) {
isLegalMobile.value = false;
return Promise.reject('手机号格式不正确');
} else {
isLegalMobile.value = true;
return Promise.resolve();
}
},
trigger: ['blur'],
},
],
captcha: [
{
required: true,
validator: (_rule, value) => {
if (!value) {
return Promise.reject('请输入验证码');
}
if (!/^\d{6}$/.test(value)) {
return Promise.reject('验证码必须是6位数字');
} else {
return Promise.resolve();
}
},
trigger: ['blur'],
},
],
};
const form = ref({
mobile: '',
captcha: '',
});
const canGetCaptcha = computed(() => {
return isLegalMobile.value && countdown.value === 0;
});
// 打开弹窗
const open = () => {
visible.value = true;
nextTick(() => {
inputRef.value?.focus();
});
};
// 发送验证码
const getCode = async () => {
if (!canGetCaptcha.value) return;
formRef.value.validateFields('mobile').then(() => {
getCaptcha();
});
};
const getCaptcha = async () => {
try {
const { code, message: msg } = await postUpdateMobileCaptcha({ mobile: form.value.mobile });
if (code === 200) {
startCountdown();
message.success(msg);
}
} catch (error) {
// 重置倒计时
countdown.value = 0;
clearInterval(timer.value);
}
};
const startCountdown = () => {
countdown.value = 60;
hasGetCode.value = true;
timer.value = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearTimer();
}
}, 1000);
};
// 确定按钮点击
const handleConfirm = () => {
formRef.value.validate().then(async () => {
try {
submitLoading.value = true;
const { code } = await updateMobile(form.value);
if (code === 200) {
await store.getUserInfo();
message.success('修改成功!');
onClose();
}
} finally {
submitLoading.value = false;
}
});
};
const clearTimer = () => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
};
// 取消按钮点击
const onClose = () => {
form.value = {
mobile: '',
captcha: '',
};
countdown.value = 0;
visible.value = false;
hasGetCode.value = false;
isLegalMobile.value = false;
submitLoading.value = false;
formRef.value.resetFields();
formRef.value.clearValidate();
clearTimer();
};
// 暴露方法供外部调用
defineExpose({
open,
});
</script>
<style lang="scss">
.change-mobile-modal {
.ant-modal-header {
border-bottom: none !important;
}
.ant-modal-body {
padding: 20px 24px !important;
}
.ant-modal-footer {
height: 56px !important;
border-top: none !important;
}
}
</style>

View File

@ -0,0 +1,90 @@
<template>
<Modal
class="change-name-modal"
v-model:open="visible"
centered
:width="480"
title="修改昵称"
:footer="null"
@cancel="onClose"
>
<Form :model="form" ref="formRef">
<FormItem label="昵称" name="name">
<Input v-model:value="form.name" placeholder="请输入新昵称" size="large" ref="inputRef" />
</FormItem>
</Form>
<div class="flex justify-end mt-20px">
<Button class="mr-16px" size="large" @click="onClose">取消</Button>
<Button type="primary" size="large" :loading="submitLoading" @click="handleNicknameConfirm">确定</Button>
</div>
</Modal>
</template>
<script setup>
import { ref } from 'vue';
import { Button, Form, FormItem, Input, message, Modal } from 'ant-design-vue';
import { updateMyInfo } from '@/api/all';
import { useUserStore } from '@/stores';
const store = useUserStore();
const formRef = ref();
const inputRef = ref();
const visible = ref(false);
const submitLoading = ref(false);
const form = ref({
name: '',
});
// 打开弹窗
const open = (userName) => {
form.value.name = userName;
visible.value = true;
nextTick(() => {
inputRef.value?.focus();
});
};
// 确定按钮点击
const handleNicknameConfirm = async () => {
try {
submitLoading.value = true;
const { code } = await updateMyInfo(form.value);
if (code === 200) {
await store.getUserInfo();
message.success('修改成功!');
onClose();
}
} finally {
submitLoading.value = false;
}
};
// 取消按钮点击
const onClose = () => {
form.value.name = '';
visible.value = false;
submitLoading.valeu = false;
};
// 暴露方法供外部调用
defineExpose({
open,
});
</script>
<style lang="scss">
.change-name-modal {
.ant-modal-header {
border-bottom: none !important;
}
.ant-modal-body {
padding: 20px 24px !important;
}
.ant-modal-footer {
height: 56px !important;
border-top: none !important;
}
}
</style>

View File

@ -0,0 +1,231 @@
<template>
<Modal
v-model:open="visible"
:title="title"
:width="412"
centered
:footer="null"
class="change-password-modal"
@cancel="onClose"
>
<div class="modal-content">
<p class="title-text">安全验证</p>
<template v-if="isCheckPass">
<p class="cts mt-36px !color-#211F24 mb-24px">验证成功请更改密码</p>
<Form ref="formRef" :model="formData" :rules="formRules" auto-label-width class="w-348 form-wrap">
<FormItem name="password" class="password-form-item">
<Input.Password v-model:value="formData.password" placeholder="新密码" size="large" class="!h-48px">
<template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template>
</Input.Password>
</FormItem>
<FormItem name="confirm_password" class="password-form-item">
<Input.Password
v-model:value="formData.confirm_password"
placeholder="密码确认"
size="large"
class="!h-48px"
>
<template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template>
</Input.Password>
</FormItem>
</Form>
<Button
type="primary"
:disabled="disabledSubmitBtn"
:loading="submitLoading"
class="w-full !h-48px mt-36px !rounded-8px"
@click="handleSubmit"
>完成更改
</Button>
</template>
<template v-else>
<p class="cts text-center mt-16px">为进一步保证您的账号安全确保为您本人操作</p>
<p class="cts mb-36px text-center">请先完成安全验证</p>
<p class="cts !color-#211F24 mb-24px">请输入发送至{{ showMobile }} 的短信验证码</p>
<div class="verify-code-wrap mb-24px">
<VerificationCode ref="verificationCodeRef" @success="onCheckPass" />
</div>
<p class="cts !text-12px !lh-20px !color-#939499" v-if="countdown > 0">
{{ countdown }} 秒后您可以再次发送验证码
</p>
<Button type="text" class="pl-0" v-if="hasGetCode && countdown === 0" @click="getCaptcha">重新发送</Button>
</template>
</div>
<PuzzleVerification
:show="isVerificationVisible"
@submit="handleVerificationSubmit"
@cancel="isVerificationVisible = false"
/>
</Modal>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Button, Form, FormItem, Input, message, Modal } from 'ant-design-vue';
import VerificationCode from '../verification-code/index.vue';
import PuzzleVerification from '@/views/login/components/PuzzleVerification.vue';
import { postUpdatePasswordCaptcha, postUpdatePassword } from '@/api/all/login';
import { postClearRateLimiter } from '@/api/all/common';
import icon1 from '@/assets/img/login/icon-close.png';
import icon2 from '@/assets/img/login/icon-open.png';
const emit = defineEmits(['success', 'cancel']);
const visible = ref(false);
const phone = ref('');
const isCheckPass = ref(false);
const formRef = ref(null);
const isVerificationVisible = ref(false);
// 验证码输入相关
const countdown = ref(0);
const timer = ref<number | null>(null);
const hasGetCode = ref(false);
const submitLoading = ref(false);
const verificationCodeRef = ref(null);
const formData = ref({
captcha: '',
password: '',
confirm_password: '',
});
const formRules = {
password: [
{
required: true,
validator: (_rule, value) => {
if (formData.value.confirm_password) {
formRef.value.validateFields('confirm_password');
}
return Promise.resolve();
},
trigger: ['blur'],
},
],
confirm_password: [
{
required: true,
validator: (_rule, value) => {
if (value !== formData.value.password) {
return Promise.reject('确认密码与设置的密码不同');
}
return Promise.resolve();
},
trigger: ['blur'],
},
],
};
const title = computed(() => (isCheckPass.value ? '更改密码' : '安全验证'));
const showMobile = computed(() => {
return phone.value?.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
});
const isPassPassword = computed(() => {
return (
formData.value.confirm_password &&
formData.value.password &&
formData.value.confirm_password === formData.value.password
);
});
const disabledSubmitBtn = computed(() => {
return !isPassPassword.value || !formData.value.password.trim();
});
const onCheckPass = (captcha) => {
isCheckPass.value = true;
formData.value.captcha = captcha;
};
const getCaptcha = async () => {
try {
const { code, message: msg } = await postUpdatePasswordCaptcha();
if (code === 200) {
startCountdown();
message.success(msg);
}
} catch (error) {
// 重置倒计时
countdown.value = 0;
clearInterval(timer.value);
if (error.status === 429) {
isVerificationVisible.value = true;
}
}
};
const handleVerificationSubmit = async () => {
isVerificationVisible.value = false;
verificationCodeRef.value?.init();
await postClearRateLimiter();
getCaptcha();
};
// 开始倒计时
const startCountdown = () => {
countdown.value = 60;
hasGetCode.value = true;
timer.value = window.setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearTimer();
}
}, 1000);
};
const clearTimer = () => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
};
// 提交验证
const handleSubmit = async () => {
try {
submitLoading.value = true;
const { code } = await postUpdatePassword(formData.value);
if (code === 200) {
message.success('更改成功');
onClose();
}
} finally {
submitLoading.value = false;
}
};
const open = (mobile) => {
getCaptcha();
phone.value = mobile;
visible.value = true;
nextTick(() => {
verificationCodeRef.value?.init();
});
};
const onClose = () => {
visible.value = false;
phone.value = '';
countdown.value = 0;
hasGetCode.value = false;
submitLoading.value = false;
formRef.value?.resetFields();
formRef.value?.clearValidate();
clearTimer();
};
defineExpose({
open,
});
</script>
<style lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,62 @@
.change-password-modal {
.ant-modal-content {
border-radius: 16px;
.ant-modal-header {
border-radius: 16px 16px 0 0;
border-bottom: none !important;
.ant-modal-title {
display: none;
}
}
.ant-modal-body {
padding: 0 32px 8px !important;
position: relative;
top: -24px;
display: flex;
flex-direction: column;
justify-content: center;
.cts {
color: var(--Text-3, #737478);
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
}
.title-text {
color: var(--Text-1, #211F24);
text-align: center;
font-family: $font-family-medium;
font-size: 24px;
font-style: normal;
font-weight: 500;
line-height: 32px;
}
.ant-form {
.ant-form-item {
&:not(:last-child) {
margin-bottom: 24px !important;
}
.ant-input-affix-wrapper {
border-radius: 8px !important;
}
}
}
}
.ant-modal-footer {
height: 56px !important;
border-top: none !important;
}
}
}

View File

@ -0,0 +1,156 @@
<template>
<div class="verify-code-container">
<!-- 验证码输入区域 -->
<div class="code-inputs">
<Input
v-for="(item, index) in codeArray"
ref="inputRef"
:key="index"
:value="item.value"
:class="{ error: item.error, fill: item.value }"
@input="handleInput(index, $event.target.value)"
@focus="handleFocus(index)"
@blur="handleBlur(index)"
:maxlength="1"
placeholder=""
/>
</div>
<!-- 错误提示 -->
<p v-if="errorMessage" class="error-message">
{{ errorMessage }}
</p>
</div>
</template>
<script setup>
import { Input } from 'ant-design-vue';
import { ref, watch } from 'vue';
import { postCheckUpdatePasswordCaptcha } from '@/api/all/login';
const emits = defineEmits(['success']);
const codeArray = ref([]);
const activeIndex = ref(0); // 当前激活的输入框索引
const errorMessage = ref('');
const inputRef = ref(null);
// 处理输入事件
const handleInput = (index, value) => {
if (!value || !/^\d$/.test(value)) return;
codeArray.value[index].value = value;
if (index < 5) {
activeIndex.value = index + 1;
inputRef.value?.[activeIndex.value]?.focus?.();
}
};
const handleFocus = (index) => {
activeIndex.value = index;
};
const handleBlur = (index) => {
const _value = codeArray.value[index].value;
if (!_value || !/^\d$/.test(_value)) {
codeArray.value[index].error = true;
} else {
if (errorMessage.value) {
codeArray.value.forEach((item) => {
item.error = false;
});
}
errorMessage.value = '';
codeArray.value[index].error = false;
}
};
// 验证验证码逻辑
const validateCode = async () => {
const captcha = codeArray.value.map((item) => item.value).join('');
const { code } = await postCheckUpdatePasswordCaptcha({ captcha });
if (code === 200) {
emits('success', captcha);
} else {
activeIndex.value = 0;
inputRef.value?.[activeIndex.value]?.focus?.();
errorMessage.value = '验证码错误,请检查后重试';
codeArray.value.forEach((item) => {
item.error = true;
item.value = '';
});
}
};
const init = () => {
activeIndex.value = 0;
errorMessage.value = '';
inputRef.value = null;
codeArray.value = new Array(6).fill().map(() => ({ value: '', error: false }));
nextTick(() => {
inputRef.value?.[activeIndex.value]?.focus?.();
});
};
// 监听输入变化
watch(
() => codeArray.value,
(newVal) => {
if (newVal.every((item) => item.value)) {
validateCode();
}
},
{ deep: true },
);
defineExpose({ init });
</script>
<style scoped lang="scss">
.verify-code-container {
.code-inputs {
display: flex;
gap: 12px;
.ant-input {
width: 48px;
height: 48px;
border-radius: 8px !important;
text-align: center;
font-family: $font-family-medium;
font-size: 24px !important;
font-style: normal;
font-weight: 500;
line-height: 32px;
border: 1px solid #d7d7d9;
outline: none;
caret-color: #6d4cfe;
&:hover {
border-color: #6d4cfe !important;
}
&.fill {
color: #6d4cfe;
}
&.error {
border-color: #f64b31 !important;
}
}
}
}
.error-message {
color: var(--Functional-Red-6, #f64b31);
font-family: $font-family-regular;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
margin-top: 4px;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,333 +1,126 @@
<template>
<div class="bg-#fff rounded-8px w-100% py-0 px-20px pb-24px">
<div class="title-row">
<span class="title">个人信息</span>
</div>
<Table :dataSource="dataSource" :pagination="false" :showSorterTooltip="false" class="mt-8px">
<Table.Column title="用户信息" dataIndex="info">
<template #customRender="{ record }">
<div class="pt-3px pb-3px">
<Avatar :src="record.head_image" :size="32" />
{{ record.name || '-' }}
<icon-edit size="13" class="ml-8px" @click="openEditInfoModal" />
</div>
</template>
</Table.Column>
<Table.Column title="手机号" dataIndex="mobile">
<template #customRender="{ record }">
{{ record.mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') }}
<icon-edit size="13" class="ml-8px" @click="openEditMobileModal" />
</template>
</Table.Column>
<template #emptyText>
<NoData />
</template>
</Table>
<Modal v-model:open="infoVisible" centered title="修改用户信息" @ok="handleSubmitUserInfo">
<Form
class="form"
:rules="rules"
:model="userInfoForm"
:label-col-props="{ span: 3, offset: 0 }"
:wrapper-col-props="{ span: 21, offset: 0 }"
>
<FormItem name="head_image" label="头像">
<div class="bg-#fff rounded-16px w-100% p-36px person-wrap">
<p class="title mb-32px">个人信息</p>
<div class="flex items-center">
<Avatar :src="userInfoForm.file_url" :size="48" />
<span class="upload-button" @click="triggerFileInput">
<input
ref="uploadInputRef"
<Upload
action="/"
:showUploadList="false"
accept="image/*"
type="file"
style="display: none"
@change="handleFileChange"
/>
<Button><icon-upload />上传新头像</Button>
</span>
</div>
</FormItem>
<FormItem name="name" label="昵称">
<Input v-model:value="userInfoForm.name" placeholder="请输入昵称" />
</FormItem>
</Form>
</Modal>
<Modal v-model:open="imageVisible" centered title="头像裁剪">
<VueCropper></VueCropper>
</Modal>
<Modal v-model:open="mobileVisible" centered title="修改手机号" @ok="handleUpdateMobile">
<Form
ref="formRef"
:model="form"
class="form"
:rules="formRules"
labelAlign="right"
:labelCol="{ span: 4 }"
:wrapperCol="{ span: 20 }"
v-if="dataSource"
:customRequest="handleUpload"
class="mr-60px relative cursor-pointer"
>
<FormItem required name="mobile" label="新手机号">
<Input v-model:value="form.mobile" size="small" placeholder="请输入新的手机号" />
</FormItem>
<FormItem required name="captcha" label="获取验证码">
<Input v-model:value="form.captcha" size="small" placeholder="请输入验证码">
<template #suffix>
<span v-if="countdown <= 0" @click="sendCaptcha">发送验证码</span>
<span v-else>{{ countdown }}s</span>
</template>
</Input>
</FormItem>
</Form>
<PuzzleVerification
:show="verificationVisible"
@submit="handleVerificationSubmit"
@cancel="verificationVisible = false"
/>
</Modal>
<Avatar :src="dataSource.head_image" :size="100" v-if="dataSource.head_image" />
<div v-else class="w-100 h-100 rounded-50% bg-#6D4CFE flex items-center justify-center">
<span class="color-#FFF text-46px font-400">{{ dataSource.mobile?.slice(-3) }}</span>
</div>
<img :src="icon1" width="20" height="20" class="absolute right-5px bottom-4px" />
</Upload>
<div class="flex-1 flex h-68px">
<div class="flex flex-1">
<span class="cts mr-4p">昵称</span>
<span class="cts !color-#211F24 bold !font-500 mr-12px">{{ dataSource.name || '-' }}</span>
<Button type="text" size="small" class="!p-0 !h-22px m-0" @click="openChangeNameModal">编辑</Button>
</div>
<div class="flex-1">
<div class="flex items-center mb-24px">
<span class="cts mr-4px w-56px">手机号</span>
<span class="cts !color-#211F24 bold !font-500 mr-12px">{{ mobileLabel }}</span>
<Button type="text" size="small" class="!p-0 !h-22px m-0" @click="openChangeMobileModal">更改</Button>
</div>
<div class="flex items-center">
<span class="cts mr-4px w-56px">密码</span>
<span
class="!bg-#211F24 bold !font-500 w-6px h-6px rounded-50% mr-2px"
v-for="(item, index) in new Array(10).fill(0)"
:key="index"
></span>
<Button type="text" size="small" class="!p-0 !h-22px ml-10px" @click="openChangePasswordModal">更改</Button>
</div>
</div>
</div>
</div>
<ChangePasswordModal ref="changePasswordModalRef" />
<ChangeNameModal ref="changeNameModalRef" />
<ChangeMobileModal ref="changeMobileModalRef" />
</div>
</template>
<script setup lang="ts">
import { Button, Form, FormItem, Input, Table, message, Avatar } from 'ant-design-vue';
import Container from '@/components/container.vue';
import Modal from '@/components/modal.vue';
import PuzzleVerification from '@/views/components/login/components/PuzzleVerification.vue';
import { ref, reactive } from 'vue';
import 'vue-cropper/dist/index.css';
import { VueCropper } from 'vue-cropper';
import { sendUpdateMobileCaptcha, updateMobile, fetchImageUploadFile, updateMyInfo } from '@/api/all';
import { Avatar, Button, Upload, message } from 'ant-design-vue';
import ChangePasswordModal from './components/change-password-modal/index.vue';
import ChangeNameModal from './components/change-name-modal/index.vue';
import ChangeMobileModal from './components/change-mobile-modal/index.vue';
import { reactive, ref } from 'vue';
import axios from 'axios';
import { fetchImageUploadFile, updateMyInfo } from '@/api/all';
import { useUserStore } from '@/stores';
import icon1 from './img/icon1.png';
const store = useUserStore();
const userInfo = computed(() => {
return store.userInfo ?? {};
});
const columns = [
{
title: '用户信息',
slotName: 'info',
},
{
title: '手机号',
slotName: 'mobile',
},
];
const infoVisible = ref(false);
const imageVisible = ref(false);
const mobileVisible = ref(false);
const verificationVisible = ref(false);
const timer = ref();
const countdown = ref(0);
const changePasswordModalRef = ref(null);
const changeNameModalRef = ref(null);
const changeMobileModalRef = ref(null);
const formRef = ref();
const isSendCaptcha = ref(false);
const uploadInputRef = ref();
const dataSource = computed(() => {
return userInfo.value ? [userInfo.value] : [];
return store.userInfo;
});
const mobileLabel = computed(() => {
const _mobile = dataSource.value.mobile;
if (!_mobile) return '-';
return _mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
});
// 表单校验规则
const formRules = {
mobile: [
{
validator: (_rule: any, value: string) => {
if (!value) {
return Promise.reject('请填写手机号');
}
if (!/^1[3-9]\d{9}$/.test(value)) {
return Promise.reject('手机号格式不正确');
}
return Promise.resolve();
},
required: true,
trigger: ['blur', 'change'],
},
],
captcha: [
{
required: true,
validator: (rule, value) => {
if (!value) {
return Promise.reject('请填写验证码');
}
if (!/^\d{6}$/.test(value)) {
return Promise.reject('验证码必须是6位数字');
}
return Promise.resolve();
},
// const form = reactive({
// mobile: '',
// captcha: '',
// });
trigger: ['blur', 'change'],
},
],
const openChangeNameModal = () => {
changeNameModalRef.value.open(dataSource.name);
};
const openChangeMobileModal = () => {
changeMobileModalRef.value.open();
};
const openChangePasswordModal = () => {
changePasswordModalRef.value.open(dataSource.value.mobile);
};
const form = reactive({
mobile: '',
captcha: '',
});
const getFileExtension: string = (filename: string) => {
const match = filename.match(/\.([^.]+)$/);
return match ? match[1].toLowerCase() : '';
};
const userInfoForm = reactive({
name: '',
head_image: '',
file_url: '',
});
function triggerFileInput() {
uploadInputRef.value.click();
}
function openEditInfoModal() {
infoVisible.value = true;
}
async function handleFileChange(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
const handleUpload = async (option) => {
const { file } = option;
try {
if (file) {
const fileExtension = getFileExtension(file.name);
const { data } = await fetchImageUploadFile({
suffix: fileExtension,
});
const { upload_url, file_name, file_url } = data;
const { upload_url, file_url } = data;
const blob = new Blob([file], { type: file.type });
await axios.put(upload_url, blob, {
headers: { 'Content-Type': file.type },
});
userInfoForm.head_image = file_name;
userInfoForm.file_url = file_url;
const { code } = await updateMyInfo({ head_image: file_url });
if (code === 200) {
message.success('修改成功');
store.userInfo.head_image = file_url;
}
}
function openEditImageModal() {
imageVisible.value = true;
}
function openEditMobileModal() {
mobileVisible.value = true;
}
async function handleSubmitUserInfo() {
await updateMyInfo(userInfoForm);
message.success('修改成功!');
}
async function sendCaptcha() {
try {
const result = await formRef.value.validateField('mobile');
if (result === true || result === undefined) {
verificationVisible.value = true;
isSendCaptcha.value = true;
}
message.error('请填写正确的手机号!');
} catch (error) {
console.log('手机号验证失败:', error);
message.error('上传失败');
}
}
function beginCountdown() {
timer.value = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer.value);
timer.value = null;
}
}, 1000);
}
async function handleVerificationSubmit() {
await sendUpdateMobileCaptcha({ mobile: form.mobile });
message.success('发送成功');
verificationVisible.value = false;
countdown.value = 60;
beginCountdown();
}
async function handleUpdateMobile() {
if (!isSendCaptcha.value) {
message.error('请先获取验证码!');
return false;
}
const res = await formRef.value.validate();
if (res === true || res === undefined) {
await updateMobile(form);
message.success('修改成功!');
}
}
function getFileExtension(filename: string): string {
const match = filename.match(/\.([^.]+)$/);
return match ? match[1].toLowerCase() : '';
}
};
</script>
<style scoped lang="scss">
.form {
margin-top: 13px;
:deep(.arco-row) {
align-items: center;
}
:deep(.arco-form-item-label) {
font-family: $font-family-medium;
font-weight: 400;
font-size: 14px;
margin: 0;
}
:deep(.arco-input-wrapper) {
background: white;
border: 1px solid var(--BG-400, rgba(215, 215, 217, 1));
font-family: $font-family-medium;
font-weight: 400;
font-size: 14px;
padding: 4px 12px;
input::placeholder {
font-family: $font-family-medium;
font-weight: 400;
font-size: 14px;
color: var(--Text-4, rgba(147, 148, 153, 1));
}
}
:deep(.arco-input-disabled) {
background: var(--BG-200, rgba(242, 243, 245, 1));
border: 1px solid var(--BG-400, rgba(215, 215, 217, 1));
.arco-input:disabled {
font-family: $font-family-medium;
font-weight: 400;
font-size: 14px;
}
}
:deep(.arco-input-focus) {
border: 1px solid var(--Brand-Brand-6, rgba(109, 76, 254, 1));
box-shadow: 0 2px 4px 0 rgba(109, 76, 254, 0.2);
}
}
.upload-button {
width: 104px;
height: 24px;
margin-left: 12px;
:deep(.arco-btn) {
width: 104px;
height: 24px;
border-radius: 4px;
padding: 2px 12px;
border: 1px solid var(--BG-500, rgba(177, 178, 181, 1));
font-family: $font-family-medium;
font-weight: 400;
font-size: 12px;
vertical-align: middle;
color: var(--Text-2, rgba(60, 64, 67, 1));
}
}
.title-row {
display: flex;
height: 64px;
padding: 10px 0 2px 0;
align-items: center;
.title {
color: var(--Text-1, #211f24);
font-family: $font-family-medium;
font-size: 18px;
font-style: normal;
font-weight: 400;
line-height: 24px; /* 150% */
}
}
@import './style.scss';
</style>

View File

@ -0,0 +1,22 @@
.person-wrap {
.title {
color: var(--Text-1, #211f24);
font-family: $font-family-medium;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 26px; /* 150% */
}
.cts {
color: var(--Text-4, #939499);
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
&.bold {
font-family: $font-family-medium;
}
}
}

View File

@ -0,0 +1,360 @@
<!-- eslint-disable vue/no-duplicate-attributes -->
<template>
<div class="flex items-center w-400 h-100%">
<div class="w-full bg-#fff rounded-16px px-40px py-32px flex flex-col items-center">
<img src="@/assets/img/icon-logo.png" alt="" width="144" height="36" class="mb-24px" />
<Tabs v-model:activeKey="activeKey" class="mb-24px" @change="onTabChange">
<TabPane tab="密码登录" key="1" />
<TabPane tab="短信登录" key="2" />
</Tabs>
<Form ref="formRef" :model="loginForm" :rules="formRules" class="w-320 form-wrap">
<FormItem name="mobile">
<Input
v-model:value="loginForm.mobile"
placeholder="请输入手机号"
allowClear
:maxlength="11"
@change="clearErrorMsg"
/>
</FormItem>
<FormItem v-if="isCaptchaLogin" name="captcha" class="captcha-form-item">
<Input v-model:value="loginForm.captcha" placeholder="请输入验证码" :maxlength="6" @change="clearErrorMsg">
<template #suffix>
<div class="w-79px flex justify-center whitespace-nowrap">
<span
class="color-#939499 font-family-regular text-16px font-400 lh-24px cursor-not-allowed"
:class="{
'!color-#6D4CFE': isLegalMobile || countdown > 0,
'!cursor-pointer': canGetCaptcha,
}"
@click="getCode"
>{{ countdown > 0 ? `${countdown}s` : hasGetCode ? '重新发送' : '发送验证码' }}</span
>
</div>
</template>
</Input>
<p class="color-#F64B31 text-12px font-400 lh-20px font-family-regular" v-show="errMsg">
{{ errMsg }}
</p>
</FormItem>
<FormItem v-else name="password" class="password-form-item">
<Input.Password v-model:value="loginForm.password" placeholder="请输入密码" @change="clearErrorMsg">
<template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template>
</Input.Password>
<p class="color-#F64B31 text-12px font-400 lh-20px font-family-regular" v-show="errMsg">
{{ errMsg }}
</p>
</FormItem>
<FormItem class="mt-52px">
<div class="text-12px flex justify-center items-center mb-16px">
<Checkbox v-model:checked="hasCheck" class="mr-8px"></Checkbox>
<span class="text-12px color-#737478 font-400 lh-20px font-family-regular"
>登录即代表同意<span class="color-#6D4CFE"> 用户协议 </span><span class="color-#6D4CFE">
隐私政策</span
></span
>
</div>
<Button
type="primary"
class="w-full h-48 mb-8px !text-16px !font-500 !rounded-8px btn-login"
:class="disabledSubmitBtn ? 'cursor-no-drop' : 'cursor-pointer'"
:disabled="disabledSubmitBtn"
@click="handleSubmit"
>
登录
</Button>
<div class="flex justify-between btn-row">
<Button
type="text"
class="!color-#939499 !p-0 !h-22px hover:color-#6D4CFE"
size="small"
@click="onForgetPassword"
>
忘记密码
</Button>
<Button
type="text"
class="!color-#939499 !p-0 !h-22px hover:color-#6D4CFE"
size="small"
@click="onRegister"
>
注册
</Button>
</div>
</FormItem>
</Form>
</div>
</div>
<PuzzleVerification
:show="isVerificationVisible"
@submit="handleVerificationSubmit"
@cancel="isVerificationVisible = false"
/>
<SelectAccountModal ref="selectAccountModalRef" :mobileNumber="mobileNumber" :accounts="accounts" />
</template>
<script setup lang="ts">
import { Button, Checkbox, Form, FormItem, Input, message, Tabs, Typography } from 'ant-design-vue';
import PuzzleVerification from '../PuzzleVerification.vue';
import SelectAccountModal from '../select-account-modal/index.vue';
import { fetchAuthorizationsCaptcha, fetchLoginCaptCha, fetchProfileInfo, postLoginPassword } from '@/api/all/login';
import { postClearRateLimiter } from '@/api/all/common';
import { joinEnterpriseByInviteCode } from '@/api/all';
import { computed, onUnmounted, reactive, ref } from 'vue';
import { useUserStore } from '@/stores';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import { handleUserLogin } from '@/utils/user';
import { useRoute } from 'vue-router';
import icon1 from '@/assets/img/login/icon-close.png';
import icon2 from '@/assets/img/login/icon-open.png';
const { Link } = Typography;
const { TabPane } = Tabs;
const setPageType = inject('setPageType');
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const enterpriseStore = useEnterpriseStore();
const formRef = ref();
const countdown = ref(0);
let timer = ref();
const isLogin = ref(true);
const isVerificationVisible = ref(false);
const hasGetCode = ref(false);
const submitting = ref(false);
const hasCheck = ref(false);
const mobileNumber = ref('');
const selectAccountModalRef = ref(null);
const accounts = ref([]);
const activeKey = ref('1');
const isLegalMobile = ref(false);
const errMsg = ref('');
const loginForm = reactive({
mobile: '',
captcha: '',
password: '',
});
// 表单校验规则
const formRules = {
mobile: [
{
required: true,
validator: (_rule: any, value: string) => {
if (!value) {
isLegalMobile.value = false;
// return Promise.reject('手机号不能为空');
}
if (value && !/^1[3-9]\d{9}$/.test(value)) {
isLegalMobile.value = false;
return Promise.reject('手机号格式不正确');
} else {
isLegalMobile.value = true;
return Promise.resolve();
}
},
trigger: ['blur'],
},
],
password: [
{
// required: true,
// validator: (_rule: any, value: string) => {
// if (!value) {
// return Promise.reject('请输入密码');
// }
// if (value.length < 6) {
// return Promise.reject('密码长度不能小于6位');
// }
// return Promise.resolve();
// },
// trigger: ['blur'],
},
],
captcha: [
{
// required: true,
// validator: (_rule: any, value: string) => {
// if (!value) {
// return Promise.reject('请输入验证码');
// }
// if (!/^\d{6}$/.test(value)) {
// return Promise.reject('验证码必须是6位数字');
// } else {
// return Promise.resolve();
// }
// },
// trigger: ['blur'],
},
],
};
const isCaptchaLogin = computed(() => {
return activeKey.value === '2';
});
const canGetCaptcha = computed(() => {
return isLegalMobile.value && countdown.value === 0;
});
const clearErrorMsg = () => {
errMsg.value = '';
};
const disabledSubmitBtn = computed(() => {
if (isCaptchaLogin.value) {
return !hasCheck.value || !isLegalMobile.value || !loginForm.captcha.trim() || !/^\d{6}$/.test(loginForm.captcha);
}
// 密码登录时的验证逻辑
return !hasCheck.value || !isLegalMobile.value || !loginForm.password.trim();
});
const validateField = (field: string) => {
formRef.value.validateFields(field);
};
const clearError = (field: string) => {
formRef.value.clearValidate(field);
};
const getCode = async () => {
if (!canGetCaptcha.value) return;
formRef.value.validateFields('mobile').then(() => {
getCaptcha();
});
};
const getCaptcha = async () => {
try {
const { code, message: msg } = await fetchLoginCaptCha({ mobile: loginForm.mobile });
if (code === 200) {
startCountdown();
message.success(msg);
}
} catch (error) {
// 重置倒计时
countdown.value = 0;
clearInterval(timer.value);
if (error.status === 429) {
isVerificationVisible.value = true;
}
}
};
// 验证码验证通过后
const handleVerificationSubmit = async () => {
isVerificationVisible.value = false;
await postClearRateLimiter();
getCaptcha();
};
const onTabChange = () => {
errMsg.value = '';
};
// 获取用户信息
const getProfileInfo = async () => {
const { code, data } = await fetchProfileInfo();
if (code === 200) {
const enterprises = data['enterprises'];
mobileNumber.value = data['mobile'];
accounts.value = enterprises;
if (enterprises.length > 0) {
enterpriseStore.setEnterpriseInfo(data.enterprises[0]);
if (enterprises.length === 1) {
handleUserLogin();
} else {
// 多个企业时候需要弹窗让用户选择企业
selectAccountModalRef.value.open();
}
} else {
router.push({ name: 'Trial' });
}
}
};
// 提交表单
const handleSubmit = async () => {
if (disabledSubmitBtn.value) return;
try {
// 校验所有字段
await formRef.value.validate();
if (!hasCheck.value) {
message.error('请先勾选同意用户协议');
return;
}
submitting.value = true;
const _fn = isCaptchaLogin.value ? fetchAuthorizationsCaptcha : postLoginPassword;
const { code, data, message: errorInfo } = await _fn(loginForm);
if (code === 10001) {
errMsg.value = errorInfo;
return;
}
if (code === 200) {
// 处理登录成功逻辑
message.success('登录成功');
userStore.setToken(data.access_token);
const { invite_code } = route.query;
if (invite_code) {
const { code } = await joinEnterpriseByInviteCode(invite_code as string);
if (code === 200) {
message.success('加入企业成功');
}
}
getProfileInfo();
}
} finally {
submitting.value = false;
}
};
// 开始倒计时
const startCountdown = () => {
countdown.value = 60;
hasGetCode.value = true;
timer.value = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer.value as number);
timer.value = null;
}
}, 1000);
};
const onForgetPassword = () => {
setPageType('resetPasswordForm');
};
const onRegister = () => {
console.log('onRegister');
setPageType('registerForm');
};
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value);
}
});
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,34 @@
:deep(.ant-tabs) {
.ant-tabs-nav {
padding: 0;
&::before {
display: none;
}
.ant-tabs-nav-list {
.ant-tabs-tab {
padding: 0 0 4px;
.ant-tabs-tab-btn {
color: #939499;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 26px;
font-family: $font-family-medium;
}
&.ant-tabs-tab-active {
.ant-tabs-tab-btn {
color: #211f24;
}
}
}
.ant-tabs-ink-bar {
border-radius: 4px;
background: var(--Brand-6, #6d4cfe);
width: 24px !important;
height: 4px;
margin-left: 24px;
}
}
}
}

View File

@ -0,0 +1,387 @@
<!-- eslint-disable vue/no-duplicate-attributes -->
<template>
<div class="flex items-center w-400 h-100%">
<div class="w-full bg-#fff rounded-16px px-40px py-32px flex flex-col items-center">
<div class="flex items-center mb-24px w-full cursor-pointer" @click="onBack">
<icon-left size="24" class="mr-4px color-#000" />
<span class="color-#000 text-20px font-500 lh-28px font-family-medium">{{
isResetPassword ? '重置密码' : '手机注册'
}}</span>
</div>
<Form ref="formRef" :model="formData" :rules="formRules" auto-label-width class="w-320 form-wrap">
<FormItem name="mobile">
<Input
v-model:value="formData.mobile"
placeholder="请输入手机号"
allowClear
:maxlength="11"
@change="clearErrorMsg"
/>
</FormItem>
<FormItem name="password" class="password-form-item">
<Input.Password v-model:value="formData.password" placeholder="新密码" @change="clearErrorMsg">
<template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template>
</Input.Password>
</FormItem>
<FormItem name="confirm_password" class="password-form-item">
<Input.Password v-model:value="formData.confirm_password" placeholder="密码确认" @change="clearErrorMsg">
<template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template>
</Input.Password>
</FormItem>
<FormItem name="captcha" class="captcha-form-item">
<Input v-model:value="formData.captcha" placeholder="请输入验证码" :maxlength="6" @change="clearErrorMsg">
<template #suffix>
<div class="w-79px flex justify-center whitespace-nowrap">
<span
class="color-#939499 font-family-regular text-16px font-400 lh-24px cursor-not-allowed"
:class="{
'!color-#6D4CFE': (isPassPassword && isLegalMobile) || countdown > 0,
'!cursor-pointer': canGetCaptcha,
}"
@click="getCode"
>{{ countdown > 0 ? `${countdown}s` : hasGetCode ? '重新发送' : '发送验证码' }}</span
>
</div>
</template>
</Input>
<p class="color-#F64B31 text-12px font-400 lh-20px font-family-regular" v-show="errMsg">
{{ errMsg }}
</p>
</FormItem>
<FormItem class="mt-52px">
<div class="text-12px flex justify-center items-center mb-16px">
<Checkbox v-model:checked="hasCheck" class="mr-8px"></Checkbox>
<span class="text-12px color-#737478 font-400 lh-20px font-family-regular"
>登录即代表同意<span class="color-#6D4CFE"> 用户协议 </span><span class="color-#6D4CFE">
隐私政策</span
></span
>
</div>
<Button
type="primary"
class="w-full h-48 mb-8px !text-16px !font-500 !rounded-8px btn-login"
:class="disabledSubmitBtn ? 'cursor-no-drop' : 'cursor-pointer'"
:disabled="disabledSubmitBtn"
@click="handleSubmit"
>
{{ isResetPassword ? '重置' : '注册' }}
</Button>
</FormItem>
</Form>
</div>
</div>
<PuzzleVerification
:show="isVerificationVisible"
@submit="handleVerificationSubmit"
@cancel="isVerificationVisible = false"
/>
<SelectAccountModal ref="selectAccountModalRef" :mobileNumber="mobileNumber" :accounts="accounts" />
</template>
<script setup lang="js">
import { Button, Checkbox, Form, FormItem, Input, message, Tabs, Typography } from 'ant-design-vue';
import PuzzleVerification from '../PuzzleVerification.vue';
import SelectAccountModal from '../select-account-modal/index.vue';
import { postClearRateLimiter } from '@/api/all/common';
import {
fetchProfileInfo,
postForgetPassword,
postForgetPasswordCaptcha,
postRegister,
postRegisterCaptcha,
} from '@/api/all/login';
import { joinEnterpriseByInviteCode } from '@/api/all';
import { computed, onUnmounted, ref } from 'vue';
import { useUserStore } from '@/stores';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import { handleUserLogin } from '@/utils/user';
import { useRoute } from 'vue-router';
import icon1 from '@/assets/img/login/icon-close.png';
import icon2 from '@/assets/img/login/icon-open.png';
const { Link } = Typography;
const { TabPane } = Tabs;
const setPageType = inject('setPageType');
const pageType = inject('pageType');
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const enterpriseStore = useEnterpriseStore();
const formRef = ref();
const countdown = ref(0);
let timer = ref();
const isLogin = ref(true);
const isVerificationVisible = ref(false);
const hasGetCode = ref(false);
const submitting = ref(false);
const hasCheck = ref(false);
const mobileNumber = ref('');
const selectAccountModalRef = ref(null);
const accounts = ref([]);
const isLegalMobile = ref(false);
const errMsg = ref('');
const formData = ref({
mobile: '',
captcha: '',
password: '',
confirm_password: '',
});
// 表单校验规则
const formRules = {
mobile: [
{
required: true,
validator: (_rule, value) => {
if (!value) {
isLegalMobile.value = false;
}
if (value && !/^1[3-9]\d{9}$/.test(value)) {
isLegalMobile.value = false;
return Promise.reject('手机号格式不正确');
} else {
isLegalMobile.value = true;
return Promise.resolve();
}
},
trigger: ['blur'],
},
],
password: [
{
required: true,
validator: (_rule, value) => {
// if (!value) {
// return Promise.reject('请输入新密码');
// }
// if (value.length < 6) {
// return Promise.reject('密码长度不能小于6位');
// }
if (formData.value.confirm_password) {
formRef.value.validateFields('confirm_password');
}
return Promise.resolve();
},
trigger: ['blur'],
},
],
confirm_password: [
{
required: true,
validator: (_rule, value) => {
// if (!value) {
// return Promise.reject('请输入密码确认');
// }
// if (value.length < 6) {
// return Promise.reject('密码长度不能小于6位');
// }
if (value !== formData.value.password) {
return Promise.reject('确认密码与设置的密码不同');
}
return Promise.resolve();
},
trigger: ['blur'],
},
],
captcha: [
// {
// required: true,
// validator: (_rule, value) => {
// // if (!value) {
// // return Promise.reject('请输入验证码');
// // }
// if (value && !/^\d{6}$/.test(value)) {
// return Promise.reject('验证码必须是6位数字');
// } else {
// return Promise.resolve();
// }
// },
// trigger: ['blur'],
// },
],
};
// 重置密码
const isResetPassword = computed(() => {
return pageType.value === 'resetPasswordForm';
});
const isPassPassword = computed(() => {
return (
formData.value.confirm_password &&
formData.value.password &&
formData.value.confirm_password === formData.value.password
);
});
const canGetCaptcha = computed(() => {
return isLegalMobile.value && countdown.value === 0 && isPassPassword.value;
});
const disabledSubmitBtn = computed(() => {
return !isPassPassword.value || !hasCheck.value || !isLegalMobile.value || !formData.value.password.trim();
});
const clearErrorMsg = () => {
errMsg.value = '';
};
const validateField = (field) => {
formRef.value.validateFields(field);
};
const clearError = (field) => {
formRef.value.clearValidate(field);
};
const getCode = async () => {
if (!canGetCaptcha.value) {
if (!formData.value.mobile.trim()) {
formRef.value.validateFields('mobile');
return;
}
return;
}
formRef.value.validateFields('mobile').then(() => {
getCaptcha();
});
};
const getCaptcha = async () => {
try {
const fn = isResetPassword.value ? postForgetPasswordCaptcha : postRegisterCaptcha;
const { code, message: msg } = await fn({ mobile: formData.value.mobile });
if (code === 200) {
startCountdown();
message.success(msg);
}
} catch (error) {
// 重置倒计时
countdown.value = 0;
clearInterval(timer.value);
if (error.status === 429) {
isVerificationVisible.value = true;
}
}
};
// 验证码验证通过后
const handleVerificationSubmit = async () => {
isVerificationVisible.value = false;
await postClearRateLimiter();
getCaptcha();
};
// 获取用户信息
const getProfileInfo = async () => {
const { code, data } = await fetchProfileInfo();
if (code === 200) {
const enterprises = data['enterprises'];
mobileNumber.value = data['mobile'];
accounts.value = enterprises;
if (enterprises.length > 0) {
enterpriseStore.setEnterpriseInfo(data.enterprises[0]);
if (enterprises.length === 1) {
setTimeout(() => {
handleUserLogin();
}, 1500);
} else {
selectAccountModalRef.value.open();
}
} else {
router.push({ name: 'Trial' });
}
}
};
// 提交表单
const handleSubmit = async () => {
if (disabledSubmitBtn.value) return;
try {
// 校验所有字段
await formRef.value.validate();
if (!hasCheck.value) {
message.error('请先勾选同意用户协议');
return;
}
submitting.value = true;
const _fn = isResetPassword.value ? postForgetPassword : postRegister;
const { code, data, message: errorInfo } = await _fn(formData.value);
if (code === 10001) {
errMsg.value = errorInfo;
return;
}
if (code === 200) {
message.success(isResetPassword.value ? '重置成功' : '注册成功');
// 注册成功后跳转登录页
if (!isResetPassword.value) {
setTimeout(() => {
setPageType('loginForm');
}, 1500);
return;
}
;
userStore.setToken(data.access_token);
const { invite_code } = route.query;
if (invite_code) {
const { code } = await joinEnterpriseByInviteCode(invite_code);
if (code === 200) {
message.success('加入企业成功');
}
}
getProfileInfo();
}
} finally {
submitting.value = false;
}
};
const onBack = () => {
setPageType('loginForm');
};
// 开始倒计时
const startCountdown = () => {
countdown.value = 60;
hasGetCode.value = true;
timer.value = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer.value);
timer.value = null;
}
}, 1000);
};
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value);
}
});
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,171 @@
<template>
<Modal v-model:open="visible" centered unmountOnClose @cancel="handleCancel">
<template #title>
<span style="text-align: left; width: 100%">选择账号</span>
</template>
<div class="account-bind-container">
<Card :bordered="false" class="bind-card">
<div class="bind-header">
<Typography.Text class="mobile-number">{{ mobileNumber }} 已在以下企业绑定了账号</Typography.Text>
</div>
<List :bordered="false" :split="false" class="account-list">
<List.Item
v-for="(account, index) in accounts"
:key="index"
class="account-item"
:class="{
selected: selectedAccountIndex === index,
'cursor-no-drop': account.status === 0,
'cursor-pointer': account.status !== 0,
}"
@click="selectAccount(account, index)"
>
<List.Item.Meta>
<template #title>
<div style="display: flex; align-items: center; gap: 12px">
<Checkbox :checked="selectedAccountIndex === index" />
<Typography.Text>{{ account.name || '-' }}</Typography.Text>
</div>
</template>
</List.Item.Meta>
</List.Item>
</List>
</Card>
</div>
<template #footer>
<div class="flex">
<Button type="primary" @click="handleOk">确定</Button>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import { Checkbox, Modal, Button, message, Card, List, Typography } from 'ant-design-vue';
import { handleUserLogin } from '@/utils/user';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
const props = defineProps({
mobileNumber: {
type: String,
default: '',
},
accounts: {
type: Array,
default: () => [],
},
});
const enterpriseStore = useEnterpriseStore();
const visible = ref(false);
const selectedAccountIndex = ref(0);
const open = () => {
visible.value = true;
};
const handleCancel = () => {
visible.value = false;
};
const handleOk = async () => {
visible.value = false;
handleUserLogin();
};
const selectAccount = (account, index) => {
if (account.status === 0) {
return;
}
enterpriseStore.setEnterpriseInfo(account);
selectedAccountIndex.value = index;
};
defineExpose({
open,
});
</script>
<style scoped lang="scss">
.account-bind-container {
width: 100%;
max-width: 400px;
margin: 0 auto;
display: flex;
.bind-card {
background-color: var(--color-bg-2);
width: 100%;
flex-direction: column;
align-items: start;
box-shadow: none;
}
.bind-header {
margin-bottom: 8px;
text-align: left;
}
.phone-number {
font-size: 14px;
color: var(--color-text-4);
text-align: left;
}
.account-list {
margin-top: 16px;
.account-item {
padding: 12px 16px;
margin-bottom: 8px;
background-color: var(--color-bg-2);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
}
.account-item {
padding: 12px 16px;
margin-bottom: 8px;
background-color: var(--color-bg-2);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid var(--color-border-2);
box-shadow: 0 2px 4px 0 #b1b2b5;
&.selected {
border-color: #6d4cfe;
background-color: rgba(109, 76, 254, 0.1);
box-shadow: 0 2px 4px 0 rgba(109, 76, 254, 0.5);
}
:deep(.ant-list-item-meta-title) {
margin: 0;
}
}
.account-item:deep(.arco-list-item-main) {
padding: 0;
}
:deep(.arco-list-item-actions) {
margin-left: 12px;
}
:deep(.arco-checkbox) {
margin-right: 0;
}
:deep(.arco-checkbox-checked .arco-checkbox-mask) {
background-color: #6d4cfe;
border-color: #6d4cfe;
}
}
}
</style>

40
src/views/login/index.vue Normal file
View File

@ -0,0 +1,40 @@
<!-- eslint-disable vue/no-duplicate-attributes -->
<template>
<div
class="relative w-100vw h-100vh min-h-175 flex justify-center items-center bg-cover bg-center bg-#f0edff login-wrap"
>
<section class="login-bg"></section>
<section class="relative flex justify-between w-1200 h-100% my-0 mx-auto">
<div class="flex flex-col justify-center flex-column h-100% mt--12.5">
<img src="@/assets/img/Frame.svg" class="w-480 h-480 mr-40" alt="" />
</div>
<component :is="pageType === 'loginForm' ? LoginForm : RegisterForm" ref="formRef" :pageType="pageType" />
<section class="login-footer">
<p class="text">闽公网安备 352018502850842 闽ICP备20250520582号 © 2025小题科技</p>
</section>
</section>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import LoginForm from './components/login-form/index.vue';
import RegisterForm from './components/register-form/index.vue';
type ShowPageType = 'loginForm' | 'registerForm' | 'resetPasswordForm';
const pageType = ref<ShowPageType>('loginForm');
const formRef = ref(null);
const setPageType = (val: ShowPageType) => {
pageType.value = val;
};
provide('setPageType', setPageType);
provide('pageType', pageType);
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -1,22 +1,57 @@
.login-wrap {
// :deep(.ant-input),
// .arco-select-view-single,
// .arco-textarea-wrapper,
// .arco-picker,
// .arco-select-view-multiple {
// border-color: #d7d7d9 !important;
// background-color: #fff !important;
// &:focus-within,
// &.arco-input-focus {
// background-color: var(--color-bg-2);
// // border-color: rgb(var(--primary-6));
// box-shadow: 0 0 0 0 var(--color-primary-light-2);
// }
// }
:deep(.ant-form) {
color: red !important;
.ant-form-item {
&:not(:last-child) {
margin-bottom: 24px !important;
}
.ant-input-affix-wrapper {
height: 48px;
padding: 0 10px;
border-radius: 8px !important;
.ant-input {
height: 100%;
border-radius: 8px !important;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
height: 24px;
}
}
}
.captcha-form-item {
.ant-input-suffix {
position: relative;
&::before {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
width: 1px;
height: 16px;
background-color: #d7d7d9;
}
margin-inline-start: 0;
padding-left: 16px;
// border-left: 1px solid #d7d7d9;
}
}
.password-form-item {
.ant-input-suffix {
cursor: pointer;
padding: 0;
}
}
}
:deep(.btn-login) {
&:disabled {
background-color: #c5b7ff !important;
}
&:not(:disabled) {
&:hover {
background-color: $color-primary-3 !important;
}
}
}
.login-bg {
@ -31,11 +66,6 @@
background-position: center;
background-size: cover;
}
.form-link {
color: #211f24;
font-size: 12px;
margin: 0px;
}
.login-footer {
position: absolute;

View File

@ -1,12 +1,12 @@
<template>
<div class="highlight-textarea-container">
<a-textarea
<TextArea
ref="textareaWrapRef"
v-model="inputValue"
v-model:value="inputValue"
placeholder="请输入作品描述"
:disabled="disabled"
show-word-limit
:max-length="1000"
showCount
:maxlength="1000"
size="large"
class="textarea-input h-full w-full"
@input="handleInput"
@ -25,7 +25,8 @@
<script setup lang="ts">
import { Input } from 'ant-design-vue';
const {TextArea} = Input
const { TextArea } = Input;
import { ref, computed, watch, defineProps, defineEmits, onMounted, onUnmounted } from 'vue';
import { escapeRegExp } from './constants';
@ -69,8 +70,8 @@ watch(
);
// 处理输入事件
const handleInput = (value: string) => {
emit('update:modelValue', value);
const handleInput = (e) => {
emit('update:modelValue', e.currentTarget.value);
};
const escapeHtml = (str: string): string => {
@ -109,14 +110,12 @@ const generateHighlightedHtml = (): string => {
const levelStyle = props.levelMap?.get(wordInfo.risk_level);
const color = levelStyle?.color || '#F64B31';
return `<span class="text-14px font-400 lh-22px" style="color: ${color};">${escapeHtml(match)}</span>`;
return `<span class="text-14px font-400 lh-22px font-family-regular" style="color: ${color};">${escapeHtml(match)}</span>`;
});
};
onMounted(() => {
nativeTextarea =
(textareaWrapRef.value?.$el || textareaWrapRef.value)?.querySelector?.('textarea.arco-textarea') ||
document.querySelector('.textarea-input .arco-textarea');
nativeTextarea = textareaWrapRef.value?.$el?.querySelector?.('textarea.ant-input');
if (nativeTextarea) {
nativeTextarea.addEventListener('scroll', handleTextareaScroll);
@ -173,8 +172,9 @@ const handleCompositionUpdate = () => {
height: 100%;
font-size: 14px;
line-height: 22px;
font-weight: 400px;
border: 1px solid #d7d7d9;
font-weight: 400;
font-family: $font-family-regular;
//border: 1px solid #d7d7d9;
border-radius: 4px;
resize: none;
white-space: pre-wrap;
@ -192,43 +192,43 @@ const handleCompositionUpdate = () => {
background: #fff;
overflow: hidden;
@include textarea-padding;
&.focus {
border-color: rgb(var(--primary-6)) !important;
box-shadow: 0 0 0 0 var(--color-primary-light-2) !important;
}
}
:deep(.arco-textarea-wrapper) {
:deep(.ant-input-textarea) {
@include textarea-style;
.arco-textarea {
textarea {
padding: 0;
overflow: hidden;
position: absolute;
z-index: 1;
width: 100%;
background: transparent;
background-color: transparent !important;
color: transparent;
caret-color: #211f24 !important;
resize: none;
@include textarea-padding;
overflow-y: auto;
&.ant-input-disabled {
background: #f2f3f5 !important;
-webkit-text-fill-color: rgba(0, 0, 0, 0.25) !important;
}
.arco-textarea-word-limit {
}
&.ant-input-textarea-show-count {
&::after {
position: absolute;
z-index: 2;
}
&.arco-textarea-disabled {
.arco-textarea {
background: #f2f3f5;
font-size: 12px;
right: 10px;
bottom: 6px;
user-select: none;
}
}
}
// 处于中文输入法合成态时,显示真实文本并隐藏高亮层
:deep(.textarea-input.composing .arco-textarea) {
:deep(.ant-input-textarea.composing textarea) {
color: #211f24 !important;
-webkit-text-fill-color: #211f24 !important;
}

View File

@ -12,7 +12,7 @@
}
.card-item {
border-radius: 8px;
// border: 1px solid var(--BG-300, #e6e6e8);
border: 1px solid transparent;
background: var(--BG-white, #fff);
padding: 12px 16px 16px;
position: relative;
@ -103,7 +103,7 @@
padding-top: 8px;
}
&.checked {
border: 1px solid var(--Brand-6, #6d4cfe);
border-color: #6d4cfe;
}
}
}

View File

@ -422,7 +422,7 @@ export default {
label: () => renderLabel('Cookie值', 'Cookie无需扫码授权'),
}}
>
<Switch v-model={isCustomCookie.value} size="large" />
<Switch v-model:checked={isCustomCookie.value} size="large" />
</FormItem>
{isCustomCookie.value && (
<FormItem label=" " name="cookie">

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

252
src/views/trial/index.vue Normal file
View File

@ -0,0 +1,252 @@
<template>
<Layout class="flex justify-center items-center trial-wrap">
<Layout.Header class="header-wrap cursor-pointer" @click="onLogoClick">
<div class="h-full px-24px">
<div class="w-full h-full relative flex justify-between">
<div class="flex items-center">
<img src="@/assets/img/icon-logo.png" alt="" width="96" height="24" />
</div>
<RightSide />
</div>
</div>
</Layout.Header>
<Layout class="flex trial-content items-center">
<div class="w-800px">
<!-- 未建联 -->
<section class="w-full" v-if="!primary_enterprise">
<div class="rounded-16px mb-16px bg-#fff px-24px py-16px flex justify-between">
<div class="flex items-center">
<span class="cts !text-18px !lh-26px !color-#000 mr-8px">申请试用</span>
<div class="rounded-2px bg-#F0EDFF px-8px flex items-center">
<span class="cts !color-#6D4CFE">0-3个工作日</span>
</div>
</div>
</div>
<div class="rounded-16px mb-16px bg-#fff p-24px">
<p class="cts !text-16px !lh-24px !color-#000 mb-32px">基本信息</p>
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical" class="w-full form-wrap">
<FormItem name="contact" label="联系人">
<Input
v-model:value="formData.contact"
placeholder="请输入您的姓名"
size="large"
allowClear
class="w-500px"
/>
</FormItem>
<FormItem name="name" label="公司名称">
<Input
v-model:value="formData.name"
placeholder="请输入您的公司名称,个人用户写无"
size="large"
allowClear
class="w-500px"
/>
</FormItem>
<FormItem name="mobile" label="联系电话">
<Input
v-model:value="formData.mobile"
placeholder="请输入您的联系电话"
size="large"
allowClear
class="w-500px"
/>
</FormItem>
</Form>
</div>
<div class="rounded-16px bg-#fff px-24px py-16px">
<Button type="primary" class="mb-10px" @click="handleSubmit">免费申请</Button>
<span class="cts !font-400 !color-#737478"
>申请成功后您可开启
<span class="color-#6D4CFE !font-500">7</span>
天免费试用试用结束后若满意可直接升级正式版数据将自动保留</span
>
</div>
</section>
<!-- 建立商务联系中 -->
<section class="w-full" v-if="enterpriseStore.enterpriseInfo?.audit_status === 1">
<div class="rounded-16px bg-#fff px-36px pt-80px pb-60px flex flex-col items-center">
<img :src="icon2" width="96" height="96" class="mb-8px" />
<p class="cts !text-18px !lh-26px mb-8px">您的试用申请已提交</p>
<p class="cts !font-400 !color-#737478 font-family-regular">
我们将在 1-3 个工作日内完成审核审核结果会通过电话/短信告知您
</p>
<p class="cts !font-400 !color-#737478 font-family-regular mb-40px">
若超过 3 个工作日未收到通知可拨打客服电话 <span class="!color-#6D4CFE">153 5932 0192</span> 咨询
</p>
<div class="px-24px py-16px rounded-12px bg-#F2F3F5 w-full">
<p class="mb-16px cts !color-#000 !text-18px !lh-26px">进度</p>
<Steps :current="1" :items="trialingStepsItems">
<template #progressDot="{ prefixCls }">
<span :class="`${prefixCls}-icon-dot`" />
</template>
</Steps>
</div>
</div>
</section>
<!-- 试用到期 -->
<section class="w-full" v-if="enterpriseStore.enterpriseInfo?.subscribe_status === 4">
<div class="rounded-16px bg-#fff px-36px pt-80px pb-60px flex flex-col items-center">
<img :src="icon1" width="96" height="96" class="mb-8px" />
<p class="cts !text-18px !lh-26px mb-8px">试用已到期</p>
<p class="cts !font-400 !color-#737478 font-family-regular">
您的 7 天产品试用已到期若想继续使用可拨打客服电话
</p>
<p class="cts !font-400 !color-#737478 font-family-regular mb-40px">
<span class="!color-#6D4CFE">153 5932 0192</span> 咨询续用事宜
</p>
<div class="px-24px py-16px rounded-12px bg-#F2F3F5 w-full">
<p class="mb-16px cts !color-#000 !text-18px !lh-26px">进度</p>
<Steps :current="2" :items="trialEndStepsItems">
<template #progressDot="{ prefixCls }">
<span :class="`${prefixCls}-icon-dot`" />
</template>
</Steps>
</div>
</div>
</section>
</div>
</Layout>
</Layout>
</template>
<script setup lang="ts">
import { Button, Form, FormItem, Input, Layout, Steps } from 'ant-design-vue';
import RightSide from '@/layouts/components/navbar/components/right-side/index.vue';
import { useRouter } from 'vue-router';
import { postCreateEnterprises } from '@/api/all/login';
import { exactFormatTime } from '@/utils/tools';
import { handleUserHome } from '@/utils/user';
import { useUserStore } from '@/stores';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import icon1 from './img/icon-info.png';
import icon2 from './img/icon-check.png';
// 0-未开通1-已开通2-试用中3-已到期4-试用结束
type Status = 0 | 1 | 2 | 3 | 4;
const router = useRouter();
const userStore = useUserStore();
const enterpriseStore = useEnterpriseStore();
const formRef = ref();
const submitting = ref(false);
// const status = ref<Status>(1);
const formData = ref({
mobile: '',
contact: '',
name: '',
});
const formRules = {
contact: {
required: true,
trigger: ['blur'],
validator: (_rule, value) => {
if (!value) {
return Promise.reject('请填写姓名');
} else {
return Promise.resolve();
}
},
},
name: {
required: true,
trigger: ['blur'],
validator: (_rule, value) => {
if (!value) {
return Promise.reject('请填写公司名称');
} else {
return Promise.resolve();
}
},
},
mobile: {
required: true,
trigger: ['blur'],
validator: (_rule, value) => {
if (!value) {
return Promise.reject('请填写联系电话');
} else {
return Promise.resolve();
}
},
},
};
const primary_enterprise = computed(() => userStore.userInfo?.primary_enterprise);
const hasOpenEnterprise = computed(() => enterpriseStore.isOpenEnterprise);
const trialingStepsItems = computed(() => {
return [
{
title: '提交申请',
description: exactFormatTime(enterpriseStore.enterpriseInfo?.created_at),
},
{
title: '人工审核',
description: '处理中',
},
{
title: '开始试用',
description: '等待',
},
];
});
const trialEndStepsItems = computed(() => {
return [
{
title: '提交申请',
description: exactFormatTime(enterpriseStore.enterpriseInfo?.created_at),
},
{
title: '人工审核',
description: '已完成',
},
{
title: '试用到期',
description: '已完成',
},
];
});
const onLogoClick = () => {
if (hasOpenEnterprise.value) {
handleUserHome();
} else {
router.push({
name: 'Trial',
});
}
};
const handleSubmit = async () => {
submitting.value = true;
formRef.value
.validate()
.then(async () => {
const { code } = await postCreateEnterprises(formData.value);
if (code === 200) {
await userStore.getUserInfo();
const { primary_enterprise } = userStore.userInfo;
if (primary_enterprise) {
enterpriseStore.setEnterpriseInfo(primary_enterprise);
}
}
})
.finally(() => {
submitting.value = false;
});
};
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,95 @@
.trial-wrap {
background: transparent;
min-width: 1200px;
.cts {
color: #211f24;
font-family: $font-family-medium;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 22px;
}
:deep(.ant-steps) {
.ant-steps-item {
margin-left: 0;
.ant-steps-item-icon {
width: 10px;
height: 10px;
}
.ant-steps-item-content {
margin-top: 8px;
.ant-steps-item-title {
color: var(--Text-1, #211f24);
font-family: $font-family-regular;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
}
.ant-steps-item-description {
color: var(--Text-3, #737478);
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
}
&.ant-steps-item-wait {
.ant-steps-icon {
.ant-steps-icon-dot {
background: #d7d7d9;
}
}
}
&.ant-steps-item-active,
&.ant-steps-item-finish {
.ant-steps-icon {
.ant-steps-icon-dot {
background: #6d4cfe;
}
}
.ant-steps-item-content {
.ant-steps-item-title {
padding: 0;
font-family: $font-family-medium;
}
}
}
}
}
.header-wrap {
background: transparent;
height: $navbar-height;
line-height: $navbar-height;
padding-inline: inherit;
color: inherit;
width: 100%;
min-width: $layout-min-width;
:deep(.right-wrap) {
.task-icon,
.agent-entry {
display: none;
}
}
}
.trial-content {
width: 100%;
height: 100%;
background: transparent;
min-height: calc(100vh - $navbar-height);
:deep(.ant-form) {
.ant-form-item {
&:not(:last-child) {
margin-bottom: 24px !important;
}
.ant-form-item-label {
> label {
font-family: $font-family-medium;
font-weight: 500;
}
}
}
}
}
}

View File

@ -1,12 +1,12 @@
<template>
<div class="highlight-textarea-container">
<a-textarea
<TextArea
ref="textareaWrapRef"
v-model="inputValue"
v-model:value="inputValue"
placeholder="请输入作品描述"
:disabled="disabled"
show-word-limit
:max-length="1000"
showCount
:maxlength="1000"
size="large"
class="textarea-input h-full w-full"
@input="handleInput"
@ -25,7 +25,8 @@
<script setup lang="ts">
import { Input } from 'ant-design-vue';
const {TextArea} = Input
const { TextArea } = Input;
import { ref, computed, watch, defineProps, defineEmits, onMounted, onUnmounted } from 'vue';
import { escapeRegExp } from './constants';
@ -69,8 +70,8 @@ watch(
);
// 处理输入事件
const handleInput = (value: string) => {
emit('update:modelValue', value);
const handleInput = (e) => {
emit('update:modelValue', e.currentTarget.value);
};
const escapeHtml = (str: string): string => {
@ -109,14 +110,12 @@ const generateHighlightedHtml = (): string => {
const levelStyle = props.levelMap?.get(wordInfo.risk_level);
const color = levelStyle?.color || '#F64B31';
return `<span class="text-14px font-400 lh-22px" style="color: ${color};">${escapeHtml(match)}</span>`;
return `<span class="text-14px font-400 lh-22px font-family-regular" style="color: ${color};">${escapeHtml(match)}</span>`;
});
};
onMounted(() => {
nativeTextarea =
(textareaWrapRef.value?.$el || textareaWrapRef.value)?.querySelector?.('textarea.arco-textarea') ||
document.querySelector('.textarea-input .arco-textarea');
nativeTextarea = textareaWrapRef.value?.$el?.querySelector?.('textarea.ant-input');
if (nativeTextarea) {
nativeTextarea.addEventListener('scroll', handleTextareaScroll);
@ -173,8 +172,9 @@ const handleCompositionUpdate = () => {
height: 100%;
font-size: 14px;
line-height: 22px;
font-weight: 400px;
border: 1px solid #d7d7d9;
font-weight: 400;
font-family: $font-family-regular;
//border: 1px solid #d7d7d9;
border-radius: 4px;
resize: none;
white-space: pre-wrap;
@ -192,43 +192,43 @@ const handleCompositionUpdate = () => {
background: #fff;
overflow: hidden;
@include textarea-padding;
&.focus {
border-color: rgb(var(--primary-6)) !important;
box-shadow: 0 0 0 0 var(--color-primary-light-2) !important;
}
}
:deep(.arco-textarea-wrapper) {
:deep(.ant-input-textarea) {
@include textarea-style;
.arco-textarea {
textarea {
padding: 0;
overflow: hidden;
position: absolute;
z-index: 1;
width: 100%;
background: transparent;
background-color: transparent !important;
color: transparent;
caret-color: #211f24 !important;
resize: none;
@include textarea-padding;
overflow-y: auto;
&.ant-input-disabled {
background: #f2f3f5 !important;
-webkit-text-fill-color: rgba(0, 0, 0, 0.25) !important;
}
.arco-textarea-word-limit {
}
&.ant-input-textarea-show-count {
&::after {
position: absolute;
z-index: 2;
}
&.arco-textarea-disabled {
.arco-textarea {
background: #f2f3f5;
font-size: 12px;
right: 10px;
bottom: 6px;
user-select: none;
}
}
}
// 处于中文输入法合成态时,显示真实文本并隐藏高亮层
:deep(.textarea-input.composing .arco-textarea) {
:deep(.ant-input-textarea.composing textarea) {
color: #211f24 !important;
-webkit-text-fill-color: #211f24 !important;
}