更新最新代码
This commit is contained in:
7
apps/web-naive/.env.analyze
Normal file
7
apps/web-naive/.env.analyze
Normal file
@@ -0,0 +1,7 @@
|
||||
# public path
|
||||
VITE_BASE=/
|
||||
|
||||
# Basic interface address SPA
|
||||
VITE_GLOB_API_URL=/api
|
||||
|
||||
VITE_VISUALIZER=true
|
||||
21
apps/web-naive/.env.development
Normal file
21
apps/web-naive/.env.development
Normal file
@@ -0,0 +1,21 @@
|
||||
# 端口号
|
||||
VITE_PORT=5888
|
||||
|
||||
VITE_BASE=/
|
||||
|
||||
# 请求路径
|
||||
VITE_BASE_URL=http://127.0.0.1:48080
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=/admin-api
|
||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||
VITE_UPLOAD_TYPE=server
|
||||
# 是否打开 devtools,true 为打开,false 为关闭
|
||||
VITE_DEVTOOLS=false
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
|
||||
# 默认登录用户名
|
||||
VITE_APP_DEFAULT_USERNAME=admin
|
||||
# 默认登录密码
|
||||
VITE_APP_DEFAULT_PASSWORD=admin123
|
||||
23
apps/web-naive/.env.production
Normal file
23
apps/web-naive/.env.production
Normal file
@@ -0,0 +1,23 @@
|
||||
VITE_BASE=/
|
||||
|
||||
# 请求路径
|
||||
VITE_BASE_URL=http://127.0.0.1:48080
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=http://127.0.0.1:48080/admin-api
|
||||
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||
VITE_UPLOAD_TYPE=server
|
||||
|
||||
# 是否开启压缩,可以设置为 none, brotli, gzip
|
||||
VITE_COMPRESS=none
|
||||
|
||||
# 是否开启 PWA
|
||||
VITE_PWA=false
|
||||
|
||||
# vue-router 的模式
|
||||
VITE_ROUTER_HISTORY=hash
|
||||
|
||||
# 是否注入全局loading
|
||||
VITE_INJECT_APP_LOADING=true
|
||||
|
||||
# 打包后是否生成dist.zip
|
||||
VITE_ARCHIVER=true
|
||||
35
apps/web-naive/index.html
Normal file
35
apps/web-naive/index.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!doctype html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="renderer" content="webkit" />
|
||||
<meta name="description" content="A Modern Back-end Management System" />
|
||||
<meta name="keywords" content="Vben Admin Vue3 Vite" />
|
||||
<meta name="author" content="Vben" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
|
||||
/>
|
||||
<!-- 由 vite 注入 VITE_APP_TITLE 变量,在 .env 文件内配置 -->
|
||||
<title><%= VITE_APP_TITLE %></title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<script>
|
||||
// 生产环境下注入百度统计
|
||||
if (window._VBEN_ADMIN_PRO_APP_CONF_) {
|
||||
var _hmt = _hmt || [];
|
||||
(function () {
|
||||
var hm = document.createElement('script');
|
||||
hm.src =
|
||||
'https://hm.baidu.com/hm.js?24bb3eb91dfe4ebfcbcee6952a107cb6';
|
||||
var s = document.getElementsByTagName('script')[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
1
apps/web-naive/postcss.config.mjs
Normal file
1
apps/web-naive/postcss.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config/postcss';
|
||||
BIN
apps/web-naive/public/favicon.ico
Normal file
BIN
apps/web-naive/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
25
apps/web-naive/src/adapter/naive.ts
Normal file
25
apps/web-naive/src/adapter/naive.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { preferences } from '@vben/preferences';
|
||||
import '@vben/styles';
|
||||
|
||||
import { createDiscreteApi, darkTheme, lightTheme } from 'naive-ui';
|
||||
|
||||
const themeOverridesProviderProps = computed(() => ({
|
||||
themeOverrides: preferences.theme.mode === 'light' ? lightTheme : darkTheme,
|
||||
}));
|
||||
|
||||
const themeProviderProps = computed(() => ({
|
||||
theme: preferences.theme.mode === 'light' ? lightTheme : darkTheme,
|
||||
}));
|
||||
|
||||
export const { dialog, loadingBar, message, modal, notification } =
|
||||
createDiscreteApi(
|
||||
['message', 'dialog', 'notification', 'loadingBar', 'modal'],
|
||||
{
|
||||
configProviderProps: themeProviderProps,
|
||||
loadingBarProviderProps: themeOverridesProviderProps,
|
||||
messageProviderProps: themeOverridesProviderProps,
|
||||
notificationProviderProps: themeOverridesProviderProps,
|
||||
},
|
||||
);
|
||||
1
apps/web-naive/src/api/core/index.ts
Normal file
1
apps/web-naive/src/api/core/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './auth';
|
||||
1
apps/web-naive/src/api/index.ts
Normal file
1
apps/web-naive/src/api/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './core';
|
||||
44
apps/web-naive/src/api/infra/api-access-log/index.ts
Normal file
44
apps/web-naive/src/api/infra/api-access-log/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace InfraApiAccessLogApi {
|
||||
/** API 访问日志信息 */
|
||||
export interface ApiAccessLog {
|
||||
id: number;
|
||||
traceId: string;
|
||||
userId: number;
|
||||
userType: number;
|
||||
applicationName: string;
|
||||
requestMethod: string;
|
||||
requestParams: string;
|
||||
responseBody: string;
|
||||
requestUrl: string;
|
||||
userIp: string;
|
||||
userAgent: string;
|
||||
operateModule: string;
|
||||
operateName: string;
|
||||
operateType: number;
|
||||
beginTime: string;
|
||||
endTime: string;
|
||||
duration: number;
|
||||
resultCode: number;
|
||||
resultMsg: string;
|
||||
createTime: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询 API 访问日志列表 */
|
||||
export function getApiAccessLogPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<InfraApiAccessLogApi.ApiAccessLog>>(
|
||||
'/infra/api-access-log/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出 API 访问日志 */
|
||||
export function exportApiAccessLog(params: any) {
|
||||
return requestClient.download('/infra/api-access-log/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
55
apps/web-naive/src/api/infra/api-error-log/index.ts
Normal file
55
apps/web-naive/src/api/infra/api-error-log/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace InfraApiErrorLogApi {
|
||||
/** API 错误日志信息 */
|
||||
export interface ApiErrorLog {
|
||||
id: number;
|
||||
traceId: string;
|
||||
userId: number;
|
||||
userType: number;
|
||||
applicationName: string;
|
||||
requestMethod: string;
|
||||
requestParams: string;
|
||||
requestUrl: string;
|
||||
userIp: string;
|
||||
userAgent: string;
|
||||
exceptionTime: string;
|
||||
exceptionName: string;
|
||||
exceptionMessage: string;
|
||||
exceptionRootCauseMessage: string;
|
||||
exceptionStackTrace: string;
|
||||
exceptionClassName: string;
|
||||
exceptionFileName: string;
|
||||
exceptionMethodName: string;
|
||||
exceptionLineNumber: number;
|
||||
processUserId: number;
|
||||
processStatus: number;
|
||||
processTime: string;
|
||||
resultCode: number;
|
||||
createTime: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询 API 错误日志列表 */
|
||||
export function getApiErrorLogPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<InfraApiErrorLogApi.ApiErrorLog>>(
|
||||
'/infra/api-error-log/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 更新 API 错误日志的处理状态 */
|
||||
export function updateApiErrorLogStatus(id: number, processStatus: number) {
|
||||
return requestClient.put(
|
||||
`/infra/api-error-log/update-status?id=${id}&processStatus=${processStatus}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出 API 错误日志 */
|
||||
export function exportApiErrorLog(params: any) {
|
||||
return requestClient.download('/infra/api-error-log/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
41
apps/web-naive/src/api/infra/job-log/index.ts
Normal file
41
apps/web-naive/src/api/infra/job-log/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace InfraJobLogApi {
|
||||
/** 任务日志信息 */
|
||||
export interface JobLog {
|
||||
id?: number;
|
||||
jobId: number;
|
||||
handlerName: string;
|
||||
handlerParam: string;
|
||||
cronExpression: string;
|
||||
executeIndex: string;
|
||||
beginTime: Date;
|
||||
endTime: Date;
|
||||
duration: string;
|
||||
status: number;
|
||||
createTime?: string;
|
||||
result: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询任务日志列表 */
|
||||
export function getJobLogPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<InfraJobLogApi.JobLog>>(
|
||||
'/infra/job-log/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询任务日志详情 */
|
||||
export function getJobLog(id: number) {
|
||||
return requestClient.get<InfraJobLogApi.JobLog>(
|
||||
`/infra/job-log/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出定时任务日志 */
|
||||
export function exportJobLog(params: any) {
|
||||
return requestClient.download('/infra/job-log/export-excel', { params });
|
||||
}
|
||||
190
apps/web-naive/src/api/infra/redis/index.ts
Normal file
190
apps/web-naive/src/api/infra/redis/index.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace InfraRedisApi {
|
||||
/** Redis 信息 */
|
||||
export interface RedisInfo {
|
||||
io_threaded_reads_processed: string;
|
||||
tracking_clients: string;
|
||||
uptime_in_seconds: string;
|
||||
cluster_connections: string;
|
||||
current_cow_size: string;
|
||||
maxmemory_human: string;
|
||||
aof_last_cow_size: string;
|
||||
master_replid2: string;
|
||||
mem_replication_backlog: string;
|
||||
aof_rewrite_scheduled: string;
|
||||
total_net_input_bytes: string;
|
||||
rss_overhead_ratio: string;
|
||||
hz: string;
|
||||
current_cow_size_age: string;
|
||||
redis_build_id: string;
|
||||
errorstat_BUSYGROUP: string;
|
||||
aof_last_bgrewrite_status: string;
|
||||
multiplexing_api: string;
|
||||
client_recent_max_output_buffer: string;
|
||||
allocator_resident: string;
|
||||
mem_fragmentation_bytes: string;
|
||||
aof_current_size: string;
|
||||
repl_backlog_first_byte_offset: string;
|
||||
tracking_total_prefixes: string;
|
||||
redis_mode: string;
|
||||
redis_git_dirty: string;
|
||||
aof_delayed_fsync: string;
|
||||
allocator_rss_bytes: string;
|
||||
repl_backlog_histlen: string;
|
||||
io_threads_active: string;
|
||||
rss_overhead_bytes: string;
|
||||
total_system_memory: string;
|
||||
loading: string;
|
||||
evicted_keys: string;
|
||||
maxclients: string;
|
||||
cluster_enabled: string;
|
||||
redis_version: string;
|
||||
repl_backlog_active: string;
|
||||
mem_aof_buffer: string;
|
||||
allocator_frag_bytes: string;
|
||||
io_threaded_writes_processed: string;
|
||||
instantaneous_ops_per_sec: string;
|
||||
used_memory_human: string;
|
||||
total_error_replies: string;
|
||||
role: string;
|
||||
maxmemory: string;
|
||||
used_memory_lua: string;
|
||||
rdb_current_bgsave_time_sec: string;
|
||||
used_memory_startup: string;
|
||||
used_cpu_sys_main_thread: string;
|
||||
lazyfree_pending_objects: string;
|
||||
aof_pending_bio_fsync: string;
|
||||
used_memory_dataset_perc: string;
|
||||
allocator_frag_ratio: string;
|
||||
arch_bits: string;
|
||||
used_cpu_user_main_thread: string;
|
||||
mem_clients_normal: string;
|
||||
expired_time_cap_reached_count: string;
|
||||
unexpected_error_replies: string;
|
||||
mem_fragmentation_ratio: string;
|
||||
aof_last_rewrite_time_sec: string;
|
||||
master_replid: string;
|
||||
aof_rewrite_in_progress: string;
|
||||
lru_clock: string;
|
||||
maxmemory_policy: string;
|
||||
run_id: string;
|
||||
latest_fork_usec: string;
|
||||
tracking_total_items: string;
|
||||
total_commands_processed: string;
|
||||
expired_keys: string;
|
||||
errorstat_ERR: string;
|
||||
used_memory: string;
|
||||
module_fork_in_progress: string;
|
||||
errorstat_WRONGPASS: string;
|
||||
aof_buffer_length: string;
|
||||
dump_payload_sanitizations: string;
|
||||
mem_clients_slaves: string;
|
||||
keyspace_misses: string;
|
||||
server_time_usec: string;
|
||||
executable: string;
|
||||
lazyfreed_objects: string;
|
||||
db0: string;
|
||||
used_memory_peak_human: string;
|
||||
keyspace_hits: string;
|
||||
rdb_last_cow_size: string;
|
||||
aof_pending_rewrite: string;
|
||||
used_memory_overhead: string;
|
||||
active_defrag_hits: string;
|
||||
tcp_port: string;
|
||||
uptime_in_days: string;
|
||||
used_memory_peak_perc: string;
|
||||
current_save_keys_processed: string;
|
||||
blocked_clients: string;
|
||||
total_reads_processed: string;
|
||||
expire_cycle_cpu_milliseconds: string;
|
||||
sync_partial_err: string;
|
||||
used_memory_scripts_human: string;
|
||||
aof_current_rewrite_time_sec: string;
|
||||
aof_enabled: string;
|
||||
process_supervised: string;
|
||||
master_repl_offset: string;
|
||||
used_memory_dataset: string;
|
||||
used_cpu_user: string;
|
||||
rdb_last_bgsave_status: string;
|
||||
tracking_total_keys: string;
|
||||
atomicvar_api: string;
|
||||
allocator_rss_ratio: string;
|
||||
client_recent_max_input_buffer: string;
|
||||
clients_in_timeout_table: string;
|
||||
aof_last_write_status: string;
|
||||
mem_allocator: string;
|
||||
used_memory_scripts: string;
|
||||
used_memory_peak: string;
|
||||
process_id: string;
|
||||
master_failover_state: string;
|
||||
errorstat_NOAUTH: string;
|
||||
used_cpu_sys: string;
|
||||
repl_backlog_size: string;
|
||||
connected_slaves: string;
|
||||
current_save_keys_total: string;
|
||||
gcc_version: string;
|
||||
total_system_memory_human: string;
|
||||
sync_full: string;
|
||||
connected_clients: string;
|
||||
module_fork_last_cow_size: string;
|
||||
total_writes_processed: string;
|
||||
allocator_active: string;
|
||||
total_net_output_bytes: string;
|
||||
pubsub_channels: string;
|
||||
current_fork_perc: string;
|
||||
active_defrag_key_hits: string;
|
||||
rdb_changes_since_last_save: string;
|
||||
instantaneous_input_kbps: string;
|
||||
used_memory_rss_human: string;
|
||||
configured_hz: string;
|
||||
expired_stale_perc: string;
|
||||
active_defrag_misses: string;
|
||||
used_cpu_sys_children: string;
|
||||
number_of_cached_scripts: string;
|
||||
sync_partial_ok: string;
|
||||
used_memory_lua_human: string;
|
||||
rdb_last_save_time: string;
|
||||
pubsub_patterns: string;
|
||||
slave_expires_tracked_keys: string;
|
||||
redis_git_sha1: string;
|
||||
used_memory_rss: string;
|
||||
rdb_last_bgsave_time_sec: string;
|
||||
os: string;
|
||||
mem_not_counted_for_evict: string;
|
||||
active_defrag_running: string;
|
||||
rejected_connections: string;
|
||||
aof_rewrite_buffer_length: string;
|
||||
total_forks: string;
|
||||
active_defrag_key_misses: string;
|
||||
allocator_allocated: string;
|
||||
aof_base_size: string;
|
||||
instantaneous_output_kbps: string;
|
||||
second_repl_offset: string;
|
||||
rdb_bgsave_in_progress: string;
|
||||
used_cpu_user_children: string;
|
||||
total_connections_received: string;
|
||||
migrate_cached_sockets: string;
|
||||
}
|
||||
|
||||
/** Redis 命令统计 */
|
||||
export interface RedisCommandStats {
|
||||
command: string;
|
||||
calls: number;
|
||||
usec: number;
|
||||
}
|
||||
|
||||
/** Redis 监控信息 */
|
||||
export interface RedisMonitorInfo {
|
||||
info: RedisInfo;
|
||||
dbSize: number;
|
||||
commandStats: RedisCommandStats[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取 Redis 监控信息 */
|
||||
export function getRedisMonitorInfo() {
|
||||
return requestClient.get<InfraRedisApi.RedisMonitorInfo>(
|
||||
'/infra/redis/get-monitor-info',
|
||||
);
|
||||
}
|
||||
24
apps/web-naive/src/api/system/area/index.ts
Normal file
24
apps/web-naive/src/api/system/area/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemAreaApi {
|
||||
/** 地区信息 */
|
||||
export interface Area {
|
||||
id?: number;
|
||||
name: string;
|
||||
code: string;
|
||||
parentId?: number;
|
||||
sort?: number;
|
||||
status?: number;
|
||||
createTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 获得地区树 */
|
||||
export function getAreaTree() {
|
||||
return requestClient.get<SystemAreaApi.Area[]>('/system/area/tree');
|
||||
}
|
||||
|
||||
/** 获得 IP 对应的地区名 */
|
||||
export function getAreaByIp(ip: string) {
|
||||
return requestClient.get<string>(`/system/area/get-by-ip?ip=${ip}`);
|
||||
}
|
||||
33
apps/web-naive/src/api/system/login-log/index.ts
Normal file
33
apps/web-naive/src/api/system/login-log/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemLoginLogApi {
|
||||
/** 登录日志信息 */
|
||||
export interface LoginLog {
|
||||
id: number;
|
||||
logType: number;
|
||||
traceId: number;
|
||||
userId: number;
|
||||
userType: number;
|
||||
username: string;
|
||||
result: number;
|
||||
status: number;
|
||||
userIp: string;
|
||||
userAgent: string;
|
||||
createTime: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询登录日志列表 */
|
||||
export function getLoginLogPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemLoginLogApi.LoginLog>>(
|
||||
'/system/login-log/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出登录日志 */
|
||||
export function exportLoginLog(params: any) {
|
||||
return requestClient.download('/system/login-log/export-excel', { params });
|
||||
}
|
||||
65
apps/web-naive/src/api/system/notify/message/index.ts
Normal file
65
apps/web-naive/src/api/system/notify/message/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemNotifyMessageApi {
|
||||
/** 站内信消息信息 */
|
||||
export interface NotifyMessage {
|
||||
id: number;
|
||||
userId: number;
|
||||
userType: number;
|
||||
templateId: number;
|
||||
templateCode: string;
|
||||
templateNickname: string;
|
||||
templateContent: string;
|
||||
templateType: number;
|
||||
templateParams: string;
|
||||
readStatus: boolean;
|
||||
readTime: Date;
|
||||
createTime: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询站内信消息列表 */
|
||||
export function getNotifyMessagePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemNotifyMessageApi.NotifyMessage>>(
|
||||
'/system/notify-message/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 获得我的站内信分页 */
|
||||
export function getMyNotifyMessagePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemNotifyMessageApi.NotifyMessage>>(
|
||||
'/system/notify-message/my-page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 批量标记已读 */
|
||||
export function updateNotifyMessageRead(ids: number[]) {
|
||||
return requestClient.put(
|
||||
'/system/notify-message/update-read',
|
||||
{},
|
||||
{
|
||||
params: { ids },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 标记所有站内信为已读 */
|
||||
export function updateAllNotifyMessageRead() {
|
||||
return requestClient.put('/system/notify-message/update-all-read');
|
||||
}
|
||||
|
||||
/** 获取当前用户的最新站内信列表 */
|
||||
export function getUnreadNotifyMessageList() {
|
||||
return requestClient.get<SystemNotifyMessageApi.NotifyMessage[]>(
|
||||
'/system/notify-message/get-unread-list',
|
||||
);
|
||||
}
|
||||
|
||||
/** 获得当前用户的未读站内信数量 */
|
||||
export function getUnreadNotifyMessageCount() {
|
||||
return requestClient.get<number>('/system/notify-message/get-unread-count');
|
||||
}
|
||||
58
apps/web-naive/src/api/system/oauth2/open/index.ts
Normal file
58
apps/web-naive/src/api/system/oauth2/open/index.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/** OAuth2.0 授权信息响应 */
|
||||
export namespace SystemOAuth2ClientApi {
|
||||
/** 授权信息 */
|
||||
export interface AuthorizeInfoRespVO {
|
||||
client: {
|
||||
logo: string;
|
||||
name: string;
|
||||
};
|
||||
scopes: {
|
||||
key: string;
|
||||
value: boolean;
|
||||
}[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 获得授权信息 */
|
||||
export function getAuthorize(clientId: string) {
|
||||
return requestClient.get<SystemOAuth2ClientApi.AuthorizeInfoRespVO>(
|
||||
`/system/oauth2/authorize?clientId=${clientId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 发起授权 */
|
||||
export function authorize(
|
||||
responseType: string,
|
||||
clientId: string,
|
||||
redirectUri: string,
|
||||
state: string,
|
||||
autoApprove: boolean,
|
||||
checkedScopes: string[],
|
||||
uncheckedScopes: string[],
|
||||
) {
|
||||
// 构建 scopes
|
||||
const scopes: Record<string, boolean> = {};
|
||||
for (const scope of checkedScopes) {
|
||||
scopes[scope] = true;
|
||||
}
|
||||
for (const scope of uncheckedScopes) {
|
||||
scopes[scope] = false;
|
||||
}
|
||||
|
||||
// 发起请求
|
||||
return requestClient.post<string>('/system/oauth2/authorize', null, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
params: {
|
||||
response_type: responseType,
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
state,
|
||||
auto_approve: autoApprove,
|
||||
scope: JSON.stringify(scopes),
|
||||
},
|
||||
});
|
||||
}
|
||||
34
apps/web-naive/src/api/system/oauth2/token/index.ts
Normal file
34
apps/web-naive/src/api/system/oauth2/token/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemOAuth2TokenApi {
|
||||
/** OAuth2.0 令牌信息 */
|
||||
export interface OAuth2Token {
|
||||
id?: number;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
userId: number;
|
||||
userType: number;
|
||||
clientId: string;
|
||||
createTime?: Date;
|
||||
expiresTime?: Date;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询 OAuth2.0 令牌列表 */
|
||||
export function getOAuth2TokenPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemOAuth2TokenApi.OAuth2Token>>(
|
||||
'/system/oauth2-token/page',
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 删除 OAuth2.0 令牌 */
|
||||
export function deleteOAuth2Token(accessToken: string) {
|
||||
return requestClient.delete(
|
||||
`/system/oauth2-token/delete?accessToken=${accessToken}`,
|
||||
);
|
||||
}
|
||||
39
apps/web-naive/src/api/system/operate-log/index.ts
Normal file
39
apps/web-naive/src/api/system/operate-log/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemOperateLogApi {
|
||||
/** 操作日志信息 */
|
||||
export interface OperateLog {
|
||||
id: number;
|
||||
traceId: string;
|
||||
userType: number;
|
||||
userId: number;
|
||||
userName: string;
|
||||
type: string;
|
||||
subType: string;
|
||||
bizId: number;
|
||||
action: string;
|
||||
extra: string;
|
||||
requestMethod: string;
|
||||
requestUrl: string;
|
||||
userIp: string;
|
||||
userAgent: string;
|
||||
creator: string;
|
||||
creatorName: string;
|
||||
createTime: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询操作日志列表 */
|
||||
export function getOperateLogPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemOperateLogApi.OperateLog>>(
|
||||
'/system/operate-log/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出操作日志 */
|
||||
export function exportOperateLog(params: any) {
|
||||
return requestClient.download('/system/operate-log/export-excel', { params });
|
||||
}
|
||||
57
apps/web-naive/src/api/system/permission/index.ts
Normal file
57
apps/web-naive/src/api/system/permission/index.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemPermissionApi {
|
||||
/** 分配用户角色请求 */
|
||||
export interface AssignUserRoleReqVO {
|
||||
userId: number;
|
||||
roleIds: number[];
|
||||
}
|
||||
|
||||
/** 分配角色菜单请求 */
|
||||
export interface AssignRoleMenuReqVO {
|
||||
roleId: number;
|
||||
menuIds: number[];
|
||||
}
|
||||
|
||||
/** 分配角色数据权限请求 */
|
||||
export interface AssignRoleDataScopeReqVO {
|
||||
roleId: number;
|
||||
dataScope: number;
|
||||
dataScopeDeptIds: number[];
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询角色拥有的菜单权限 */
|
||||
export async function getRoleMenuList(roleId: number) {
|
||||
return requestClient.get(
|
||||
`/system/permission/list-role-menus?roleId=${roleId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 赋予角色菜单权限 */
|
||||
export async function assignRoleMenu(
|
||||
data: SystemPermissionApi.AssignRoleMenuReqVO,
|
||||
) {
|
||||
return requestClient.post('/system/permission/assign-role-menu', data);
|
||||
}
|
||||
|
||||
/** 赋予角色数据权限 */
|
||||
export async function assignRoleDataScope(
|
||||
data: SystemPermissionApi.AssignRoleDataScopeReqVO,
|
||||
) {
|
||||
return requestClient.post('/system/permission/assign-role-data-scope', data);
|
||||
}
|
||||
|
||||
/** 查询用户拥有的角色数组 */
|
||||
export async function getUserRoleList(userId: number) {
|
||||
return requestClient.get(
|
||||
`/system/permission/list-user-roles?userId=${userId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 赋予用户角色 */
|
||||
export async function assignUserRole(
|
||||
data: SystemPermissionApi.AssignUserRoleReqVO,
|
||||
) {
|
||||
return requestClient.post('/system/permission/assign-user-role', data);
|
||||
}
|
||||
45
apps/web-naive/src/api/system/sms/log/index.ts
Normal file
45
apps/web-naive/src/api/system/sms/log/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemSmsLogApi {
|
||||
/** 短信日志信息 */
|
||||
export interface SmsLog {
|
||||
id?: number;
|
||||
channelId?: number;
|
||||
channelCode: string;
|
||||
templateId?: number;
|
||||
templateCode: string;
|
||||
templateType?: number;
|
||||
templateContent: string;
|
||||
templateParams?: Record<string, any>;
|
||||
apiTemplateId: string;
|
||||
mobile: string;
|
||||
userId?: number;
|
||||
userType?: number;
|
||||
sendStatus?: number;
|
||||
sendTime?: string;
|
||||
apiSendCode: string;
|
||||
apiSendMsg: string;
|
||||
apiRequestId: string;
|
||||
apiSerialNo: string;
|
||||
receiveStatus?: number;
|
||||
receiveTime?: string;
|
||||
apiReceiveCode: string;
|
||||
apiReceiveMsg: string;
|
||||
createTime: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询短信日志列表 */
|
||||
export function getSmsLogPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemSmsLogApi.SmsLog>>(
|
||||
'/system/sms-log/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出短信日志 */
|
||||
export function exportSmsLog(params: any) {
|
||||
return requestClient.download('/system/sms-log/export-excel', { params });
|
||||
}
|
||||
66
apps/web-naive/src/api/system/social/user/index.ts
Normal file
66
apps/web-naive/src/api/system/social/user/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemSocialUserApi {
|
||||
/** 社交用户信息 */
|
||||
export interface SocialUser {
|
||||
id?: number;
|
||||
type: number;
|
||||
openid: string;
|
||||
token: string;
|
||||
rawTokenInfo: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
rawUserInfo: string;
|
||||
code: string;
|
||||
state: string;
|
||||
createTime?: Date;
|
||||
updateTime?: Date;
|
||||
}
|
||||
|
||||
/** 社交绑定请求 */
|
||||
export interface SocialUserBindReqVO {
|
||||
type: number;
|
||||
code: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
/** 取消社交绑定请求 */
|
||||
export interface SocialUserUnbindReqVO {
|
||||
type: number;
|
||||
openid: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询社交用户列表 */
|
||||
export function getSocialUserPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<SystemSocialUserApi.SocialUser>>(
|
||||
'/system/social-user/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询社交用户详情 */
|
||||
export function getSocialUser(id: number) {
|
||||
return requestClient.get<SystemSocialUserApi.SocialUser>(
|
||||
`/system/social-user/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 社交绑定,使用 code 授权码 */
|
||||
export function socialBind(data: SystemSocialUserApi.SocialUserBindReqVO) {
|
||||
return requestClient.post<boolean>('/system/social-user/bind', data);
|
||||
}
|
||||
|
||||
/** 取消社交绑定 */
|
||||
export function socialUnbind(data: SystemSocialUserApi.SocialUserUnbindReqVO) {
|
||||
return requestClient.delete<boolean>('/system/social-user/unbind', { data });
|
||||
}
|
||||
|
||||
/** 获得绑定社交用户列表 */
|
||||
export function getBindSocialUserList() {
|
||||
return requestClient.get<SystemSocialUserApi.SocialUser[]>(
|
||||
'/system/social-user/get-bind-list',
|
||||
);
|
||||
}
|
||||
56
apps/web-naive/src/api/system/user/profile/index.ts
Normal file
56
apps/web-naive/src/api/system/user/profile/index.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemUserProfileApi {
|
||||
/** 用户个人中心信息 */
|
||||
export interface UserProfileRespVO {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email?: string;
|
||||
mobile?: string;
|
||||
sex?: number;
|
||||
avatar?: string;
|
||||
loginIp: string;
|
||||
loginDate: string;
|
||||
createTime: string;
|
||||
roles: any[];
|
||||
dept: any;
|
||||
posts: any[];
|
||||
}
|
||||
|
||||
/** 更新密码请求 */
|
||||
export interface UpdatePasswordReqVO {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
/** 更新个人信息请求 */
|
||||
export interface UpdateProfileReqVO {
|
||||
nickname?: string;
|
||||
email?: string;
|
||||
mobile?: string;
|
||||
sex?: number;
|
||||
avatar?: string;
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取登录用户信息 */
|
||||
export function getUserProfile() {
|
||||
return requestClient.get<SystemUserProfileApi.UserProfileRespVO>(
|
||||
'/system/user/profile/get',
|
||||
);
|
||||
}
|
||||
|
||||
/** 修改用户个人信息 */
|
||||
export function updateUserProfile(
|
||||
data: SystemUserProfileApi.UpdateProfileReqVO,
|
||||
) {
|
||||
return requestClient.put('/system/user/profile/update', data);
|
||||
}
|
||||
|
||||
/** 修改用户个人密码 */
|
||||
export function updateUserPassword(
|
||||
data: SystemUserProfileApi.UpdatePasswordReqVO,
|
||||
) {
|
||||
return requestClient.put('/system/user/profile/update-password', data);
|
||||
}
|
||||
56
apps/web-naive/src/app.vue
Normal file
56
apps/web-naive/src/app.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useNaiveDesignTokens } from '@vben/hooks';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import {
|
||||
darkTheme,
|
||||
dateEnUS,
|
||||
dateZhCN,
|
||||
enUS,
|
||||
lightTheme,
|
||||
NConfigProvider,
|
||||
NMessageProvider,
|
||||
NNotificationProvider,
|
||||
zhCN,
|
||||
} from 'naive-ui';
|
||||
|
||||
defineOptions({ name: 'App' });
|
||||
|
||||
const { commonTokens } = useNaiveDesignTokens();
|
||||
|
||||
const tokenLocale = computed(() =>
|
||||
preferences.app.locale === 'zh-CN' ? zhCN : enUS,
|
||||
);
|
||||
const tokenDateLocale = computed(() =>
|
||||
preferences.app.locale === 'zh-CN' ? dateZhCN : dateEnUS,
|
||||
);
|
||||
const tokenTheme = computed(() =>
|
||||
preferences.theme.mode === 'dark' ? darkTheme : lightTheme,
|
||||
);
|
||||
|
||||
const themeOverrides = computed((): GlobalThemeOverrides => {
|
||||
return {
|
||||
common: commonTokens,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NConfigProvider
|
||||
:date-locale="tokenDateLocale"
|
||||
:locale="tokenLocale"
|
||||
:theme="tokenTheme"
|
||||
:theme-overrides="themeOverrides"
|
||||
class="h-full"
|
||||
>
|
||||
<NNotificationProvider>
|
||||
<NMessageProvider>
|
||||
<RouterView />
|
||||
</NMessageProvider>
|
||||
</NNotificationProvider>
|
||||
</NConfigProvider>
|
||||
</template>
|
||||
76
apps/web-naive/src/bootstrap.ts
Normal file
76
apps/web-naive/src/bootstrap.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { createApp, watchEffect } from 'vue';
|
||||
|
||||
import { registerAccessDirective } from '@vben/access';
|
||||
import { registerLoadingDirective } from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { initStores } from '@vben/stores';
|
||||
import '@vben/styles';
|
||||
import '@vben/styles/naive';
|
||||
|
||||
import { useTitle } from '@vueuse/core';
|
||||
|
||||
import { $t, setupI18n } from '#/locales';
|
||||
|
||||
import { initComponentAdapter } from './adapter/component';
|
||||
import { initSetupVbenForm } from './adapter/form';
|
||||
import App from './app.vue';
|
||||
import { router } from './router';
|
||||
|
||||
async function bootstrap(namespace: string) {
|
||||
// 初始化组件适配器
|
||||
await initComponentAdapter();
|
||||
|
||||
// 初始化表单组件
|
||||
await initSetupVbenForm();
|
||||
|
||||
// // 设置弹窗的默认配置
|
||||
// setDefaultModalProps({
|
||||
// fullscreenButton: false,
|
||||
// });
|
||||
// // 设置抽屉的默认配置
|
||||
// setDefaultDrawerProps({
|
||||
// // zIndex: 2000,
|
||||
// });
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
// 注册v-loading指令
|
||||
registerLoadingDirective(app, {
|
||||
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
|
||||
spinning: 'spinning',
|
||||
});
|
||||
|
||||
// 国际化 i18n 配置
|
||||
await setupI18n(app);
|
||||
|
||||
// 配置 pinia-tore
|
||||
await initStores(app, { namespace });
|
||||
|
||||
// 安装权限指令
|
||||
registerAccessDirective(app);
|
||||
|
||||
// 初始化 tippy
|
||||
const { initTippy } = await import('@vben/common-ui/es/tippy');
|
||||
initTippy(app);
|
||||
|
||||
// 配置路由及路由守卫
|
||||
app.use(router);
|
||||
|
||||
// 配置Motion插件
|
||||
const { MotionPlugin } = await import('@vben/plugins/motion');
|
||||
app.use(MotionPlugin);
|
||||
|
||||
// 动态更新标题
|
||||
watchEffect(() => {
|
||||
if (preferences.app.dynamicTitle) {
|
||||
const routeTitle = router.currentRoute.value.meta?.title;
|
||||
const pageTitle =
|
||||
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
|
||||
useTitle(pageTitle);
|
||||
}
|
||||
});
|
||||
|
||||
app.mount('#app');
|
||||
}
|
||||
|
||||
export { bootstrap };
|
||||
1
apps/web-naive/src/components/dict-tag/index.ts
Normal file
1
apps/web-naive/src/components/dict-tag/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as DictTag } from './dict-tag.vue';
|
||||
6
apps/web-naive/src/layouts/index.ts
Normal file
6
apps/web-naive/src/layouts/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
const BasicLayout = () => import('./basic.vue');
|
||||
const AuthPageLayout = () => import('./auth.vue');
|
||||
|
||||
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
|
||||
|
||||
export { AuthPageLayout, BasicLayout, IFrameView };
|
||||
3
apps/web-naive/src/locales/README.md
Normal file
3
apps/web-naive/src/locales/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# locale
|
||||
|
||||
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
|
||||
14
apps/web-naive/src/locales/langs/en-US/utils.json
Normal file
14
apps/web-naive/src/locales/langs/en-US/utils.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"rangePicker": {
|
||||
"today": "Today",
|
||||
"last7Days": "Last 7 Days",
|
||||
"last30Days": "Last 30 Days",
|
||||
"yesterday": "Yesterday",
|
||||
"thisWeek": "This Week",
|
||||
"thisMonth": "This Month",
|
||||
"lastWeek": "Last Week",
|
||||
"lastMonth": "Last Month",
|
||||
"beginTime": "Begin Time",
|
||||
"endTime": "End Time"
|
||||
}
|
||||
}
|
||||
14
apps/web-naive/src/locales/langs/zh-CN/utils.json
Normal file
14
apps/web-naive/src/locales/langs/zh-CN/utils.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"rangePicker": {
|
||||
"today": "今天",
|
||||
"last7Days": "最近 7 天",
|
||||
"last30Days": "最近 30 天",
|
||||
"yesterday": "昨天",
|
||||
"thisWeek": "本周",
|
||||
"thisMonth": "本月",
|
||||
"lastWeek": "上周",
|
||||
"lastMonth": "上月",
|
||||
"beginTime": "开始时间",
|
||||
"endTime": "结束时间"
|
||||
}
|
||||
}
|
||||
31
apps/web-naive/src/main.ts
Normal file
31
apps/web-naive/src/main.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { initPreferences } from '@vben/preferences';
|
||||
import { unmountGlobalLoading } from '@vben/utils';
|
||||
|
||||
import { overridesPreferences } from './preferences';
|
||||
|
||||
/**
|
||||
* 应用初始化完成之后再进行页面加载渲染
|
||||
*/
|
||||
async function initApplication() {
|
||||
// name用于指定项目唯一标识
|
||||
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
|
||||
const env = import.meta.env.PROD ? 'prod' : 'dev';
|
||||
const appVersion = import.meta.env.VITE_APP_VERSION;
|
||||
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
|
||||
|
||||
// app偏好设置初始化
|
||||
await initPreferences({
|
||||
namespace,
|
||||
overrides: overridesPreferences,
|
||||
});
|
||||
|
||||
// 启动应用并挂载
|
||||
// vue应用主要逻辑及视图
|
||||
const { bootstrap } = await import('./bootstrap');
|
||||
await bootstrap(namespace);
|
||||
|
||||
// 移除并销毁loading
|
||||
unmountGlobalLoading();
|
||||
}
|
||||
|
||||
initApplication();
|
||||
25
apps/web-naive/src/preferences.ts
Normal file
25
apps/web-naive/src/preferences.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineOverridesPreferences } from '@vben/preferences';
|
||||
|
||||
/**
|
||||
* @description 项目配置文件
|
||||
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
|
||||
* !!! 更改配置后请清空缓存,否则可能不生效
|
||||
*/
|
||||
export const overridesPreferences = defineOverridesPreferences({
|
||||
// overrides
|
||||
app: {
|
||||
/** 后端路由模式 */
|
||||
accessMode: 'backend',
|
||||
name: import.meta.env.VITE_APP_TITLE,
|
||||
enableRefreshToken: true,
|
||||
},
|
||||
footer: {
|
||||
/** 默认关闭 footer 页脚,因为有一定遮挡 */
|
||||
enable: false,
|
||||
fixed: false,
|
||||
},
|
||||
copyright: {
|
||||
companyName: import.meta.env.VITE_APP_TITLE,
|
||||
companySiteLink: 'https://gitee.com/yudaocode/yudao-ui-admin-vben',
|
||||
},
|
||||
});
|
||||
41
apps/web-naive/src/router/access.ts
Normal file
41
apps/web-naive/src/router/access.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type {
|
||||
AppRouteRecordRaw,
|
||||
ComponentRecordType,
|
||||
GenerateMenuAndRoutesOptions,
|
||||
} from '@vben/types';
|
||||
|
||||
import { generateAccessible } from '@vben/access';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { convertServerMenuToRouteRecordStringComponent } from '@vben/utils';
|
||||
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
|
||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||
|
||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
const layoutMap: ComponentRecordType = {
|
||||
BasicLayout,
|
||||
IFrameView,
|
||||
};
|
||||
|
||||
return await generateAccessible(preferences.app.accessMode, {
|
||||
...options,
|
||||
fetchMenuListAsync: async () => {
|
||||
// 由于 yudao 通过 accessStore 读取,所以不在进行 message.loading 提示
|
||||
// 补充说明:accessStore.accessMenus 一开始是 AppRouteRecordRaw 类型(后端加载),后面被赋值成 MenuRecordRaw 类型(前端转换)
|
||||
const accessMenus = accessStore.accessMenus as AppRouteRecordRaw[];
|
||||
return convertServerMenuToRouteRecordStringComponent(accessMenus);
|
||||
},
|
||||
// 可以指定没有权限跳转403页面
|
||||
forbiddenComponent,
|
||||
// 如果 route.meta.menuVisibleWithForbidden = true
|
||||
layoutMap,
|
||||
pageMap,
|
||||
});
|
||||
}
|
||||
|
||||
export { generateAccess };
|
||||
40
apps/web-naive/src/router/index.ts
Normal file
40
apps/web-naive/src/router/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
createRouter,
|
||||
createWebHashHistory,
|
||||
createWebHistory,
|
||||
} from 'vue-router';
|
||||
|
||||
import { resetStaticRoutes } from '@vben/utils';
|
||||
|
||||
import { createRouterGuard } from './guard';
|
||||
import { routes } from './routes';
|
||||
import { setupBaiduTongJi } from './tongji';
|
||||
|
||||
/**
|
||||
* @zh_CN 创建vue-router实例
|
||||
*/
|
||||
const router = createRouter({
|
||||
history:
|
||||
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
|
||||
? createWebHashHistory(import.meta.env.VITE_BASE)
|
||||
: createWebHistory(import.meta.env.VITE_BASE),
|
||||
// 应该添加到路由的初始路由列表。
|
||||
routes,
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
if (savedPosition) {
|
||||
return savedPosition;
|
||||
}
|
||||
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
|
||||
},
|
||||
// 是否应该禁止尾部斜杠。
|
||||
// strict: true,
|
||||
});
|
||||
|
||||
const resetRoutes = () => resetStaticRoutes(router, routes);
|
||||
|
||||
// 创建路由守卫
|
||||
createRouterGuard(router);
|
||||
// 设置百度统计
|
||||
setupBaiduTongJi(router);
|
||||
|
||||
export { resetRoutes, router };
|
||||
114
apps/web-naive/src/router/routes/core.ts
Normal file
114
apps/web-naive/src/router/routes/core.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const BasicLayout = () => import('#/layouts/basic.vue');
|
||||
const AuthPageLayout = () => import('#/layouts/auth.vue');
|
||||
/** 全局404页面 */
|
||||
const fallbackNotFoundRoute: RouteRecordRaw = {
|
||||
component: () => import('#/views/_core/fallback/not-found.vue'),
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
hideInMenu: true,
|
||||
hideInTab: true,
|
||||
title: '404',
|
||||
},
|
||||
name: 'FallbackNotFound',
|
||||
path: '/:path(.*)*',
|
||||
};
|
||||
|
||||
/** 基本路由,这些路由是必须存在的 */
|
||||
const coreRoutes: RouteRecordRaw[] = [
|
||||
/**
|
||||
* 根路由
|
||||
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
|
||||
* 此路由必须存在,且不应修改
|
||||
*/
|
||||
{
|
||||
component: BasicLayout,
|
||||
meta: {
|
||||
hideInBreadcrumb: true,
|
||||
title: 'Root',
|
||||
},
|
||||
name: 'Root',
|
||||
path: '/',
|
||||
redirect: preferences.app.defaultHomePath,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
component: AuthPageLayout,
|
||||
meta: {
|
||||
hideInTab: true,
|
||||
title: 'Authentication',
|
||||
},
|
||||
name: 'Authentication',
|
||||
path: '/auth',
|
||||
redirect: LOGIN_PATH,
|
||||
children: [
|
||||
{
|
||||
name: 'Login',
|
||||
path: 'login',
|
||||
component: () => import('#/views/_core/authentication/login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.login'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'CodeLogin',
|
||||
path: 'code-login',
|
||||
component: () => import('#/views/_core/authentication/code-login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.codeLogin'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'QrCodeLogin',
|
||||
path: 'qrcode-login',
|
||||
component: () =>
|
||||
import('#/views/_core/authentication/qrcode-login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.qrcodeLogin'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ForgetPassword',
|
||||
path: 'forget-password',
|
||||
component: () =>
|
||||
import('#/views/_core/authentication/forget-password.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.forgetPassword'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Register',
|
||||
path: 'register',
|
||||
component: () => import('#/views/_core/authentication/register.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.register'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'SocialLogin',
|
||||
path: 'social-login',
|
||||
component: () =>
|
||||
import('#/views/_core/authentication/social-login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.login'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'SSOLogin',
|
||||
path: 'sso-login',
|
||||
component: () => import('#/views/_core/authentication/sso-login.vue'),
|
||||
meta: {
|
||||
title: $t('page.auth.login'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export { coreRoutes, fallbackNotFoundRoute };
|
||||
47
apps/web-naive/src/router/routes/index.ts
Normal file
47
apps/web-naive/src/router/routes/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
|
||||
|
||||
import { coreRoutes, fallbackNotFoundRoute } from './core';
|
||||
|
||||
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
|
||||
eager: true,
|
||||
});
|
||||
|
||||
// 有需要可以自行打开注释,并创建文件夹
|
||||
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
|
||||
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
|
||||
|
||||
/** 动态路由 */
|
||||
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
|
||||
|
||||
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
|
||||
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
|
||||
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
|
||||
const staticRoutes: RouteRecordRaw[] = [];
|
||||
const externalRoutes: RouteRecordRaw[] = [];
|
||||
|
||||
/** 路由列表,由基本路由、外部路由和404兜底路由组成
|
||||
* 无需走权限验证(会一直显示在菜单中) */
|
||||
const routes: RouteRecordRaw[] = [
|
||||
...coreRoutes,
|
||||
...externalRoutes,
|
||||
fallbackNotFoundRoute,
|
||||
];
|
||||
|
||||
/** 基本路由列表,这些路由不需要进入权限拦截 */
|
||||
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
|
||||
|
||||
/** 有权限校验的路由列表,包含动态路由和静态路由 */
|
||||
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
|
||||
|
||||
// add by 芋艿:from https://github.com/vbenjs/vue-vben-admin/blob/main/playground/src/router/routes/index.ts#L38-L45
|
||||
const componentKeys: string[] = Object.keys(
|
||||
import.meta.glob('../../views/**/*.vue'),
|
||||
)
|
||||
.filter((item) => !item.includes('/modules/'))
|
||||
.map((v) => {
|
||||
const path = v.replace('../../views/', '/');
|
||||
return path.endsWith('.vue') ? path.slice(0, -4) : path;
|
||||
});
|
||||
export { accessRoutes, componentKeys, coreRouteNames, routes };
|
||||
16
apps/web-naive/src/router/routes/modules/system.ts
Normal file
16
apps/web-naive/src/router/routes/modules/system.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/system/notify-message',
|
||||
component: () => import('#/views/system/notify/my/index.vue'),
|
||||
name: 'MyNotifyMessage',
|
||||
meta: {
|
||||
title: '我的站内信',
|
||||
icon: 'ant-design:message-filled',
|
||||
hideInMenu: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
30
apps/web-naive/src/router/tongji.ts
Normal file
30
apps/web-naive/src/router/tongji.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Router } from 'vue-router';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
_hmt: any[];
|
||||
}
|
||||
}
|
||||
|
||||
const HM_ID = import.meta.env.VITE_APP_BAIDU_CODE;
|
||||
|
||||
/**
|
||||
* 设置百度统计
|
||||
* @param router
|
||||
*/
|
||||
function setupBaiduTongJi(router: Router) {
|
||||
// 如果没有配置百度统计的 ID,则不进行设置
|
||||
if (!HM_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
// _hmt:用于 router push
|
||||
window._hmt = window._hmt || [];
|
||||
|
||||
router.afterEach((to) => {
|
||||
// 添加到 _hmt 中
|
||||
window._hmt.push(['_trackPageview', to.fullPath]);
|
||||
});
|
||||
}
|
||||
|
||||
export { setupBaiduTongJi };
|
||||
3
apps/web-naive/src/views/_core/README.md
Normal file
3
apps/web-naive/src/views/_core/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# \_core
|
||||
|
||||
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
|
||||
9
apps/web-naive/src/views/_core/about/index.vue
Normal file
9
apps/web-naive/src/views/_core/about/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { About } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'About' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<About />
|
||||
</template>
|
||||
172
apps/web-naive/src/views/_core/authentication/code-login.vue
Normal file
172
apps/web-naive/src/views/_core/authentication/code-login.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { AuthApi } from '#/api';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { AuthenticationCodeLogin, z } from '@vben/common-ui';
|
||||
import { isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { sendSmsCode } from '#/api';
|
||||
import { getTenantByWebsite, getTenantSimpleList } from '#/api/core/auth';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'CodeLogin' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const accessStore = useAccessStore();
|
||||
const tenantEnable = isTenantEnable();
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 4;
|
||||
|
||||
const loginRef = ref();
|
||||
|
||||
/** 获取租户列表,并默认选中 */
|
||||
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||
async function fetchTenantList() {
|
||||
if (!tenantEnable) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 获取租户列表、域名对应租户
|
||||
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||
tenantList.value = await getTenantSimpleList();
|
||||
|
||||
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||
let tenantId: null | number = null;
|
||||
const websiteTenant = await websiteTenantPromise;
|
||||
if (websiteTenant?.id) {
|
||||
tenantId = websiteTenant.id;
|
||||
}
|
||||
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||
if (!tenantId && accessStore.tenantId) {
|
||||
tenantId = accessStore.tenantId;
|
||||
}
|
||||
// 如果还是没有租户,使用列表中的第一个
|
||||
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||
tenantId = tenantList.value[0].id;
|
||||
}
|
||||
|
||||
// 设置选中的租户编号
|
||||
accessStore.setTenantId(tenantId);
|
||||
loginRef.value.getFormApi().setFieldValue('tenantId', tenantId?.toString());
|
||||
} catch (error) {
|
||||
console.error('获取租户列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(() => {
|
||||
fetchTenantList();
|
||||
});
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: tenantList.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id.toString(),
|
||||
})),
|
||||
placeholder: $t('authentication.tenantTip'),
|
||||
},
|
||||
fieldName: 'tenantId',
|
||||
label: $t('authentication.tenant'),
|
||||
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||
dependencies: {
|
||||
triggerFields: ['tenantId'],
|
||||
if: tenantEnable,
|
||||
trigger(values) {
|
||||
if (values.tenantId) {
|
||||
accessStore.setTenantId(Number(values.tenantId));
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.mobile'),
|
||||
},
|
||||
fieldName: 'mobile',
|
||||
label: $t('authentication.mobile'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.mobileTip') })
|
||||
.refine((v) => /^\d{11}$/.test(v), {
|
||||
message: $t('authentication.mobileErrortip'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'VbenPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
? $t('authentication.sendText', [countdown])
|
||||
: $t('authentication.sendCode');
|
||||
return text;
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
handleSendCode: async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const formApi = loginRef.value?.getFormApi();
|
||||
if (!formApi) {
|
||||
throw new Error('表单未准备好');
|
||||
}
|
||||
// 验证手机号
|
||||
await formApi.validateField('mobile');
|
||||
const isMobileValid = await formApi.isFieldValid('mobile');
|
||||
if (!isMobileValid) {
|
||||
throw new Error('请输入有效的手机号码');
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
const { mobile } = await formApi.getValues();
|
||||
const scene = 21; // 场景:短信验证码登录
|
||||
await sendSmsCode({ mobile, scene });
|
||||
message.success('验证码发送成功');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param values 登录表单数据
|
||||
*/
|
||||
async function handleLogin(values: Recordable<any>) {
|
||||
try {
|
||||
await authStore.authLogin('mobile', values);
|
||||
} catch (error) {
|
||||
console.error('Error in handleLogin:', error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationCodeLogin
|
||||
ref="loginRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleLogin"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,215 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { AuthApi } from '#/api';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { AuthenticationForgetPassword, z } from '@vben/common-ui';
|
||||
import { isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { message } from '#/adapter/naive';
|
||||
import { sendSmsCode, smsResetPassword } from '#/api';
|
||||
import { getTenantByWebsite, getTenantSimpleList } from '#/api/core/auth';
|
||||
|
||||
defineOptions({ name: 'ForgetPassword' });
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const router = useRouter();
|
||||
const tenantEnable = isTenantEnable();
|
||||
|
||||
const loading = ref(false);
|
||||
const CODE_LENGTH = 4;
|
||||
const forgetPasswordRef = ref();
|
||||
|
||||
/** 获取租户列表,并默认选中 */
|
||||
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||
async function fetchTenantList() {
|
||||
if (!tenantEnable) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 获取租户列表、域名对应租户
|
||||
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||
tenantList.value = await getTenantSimpleList();
|
||||
|
||||
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||
let tenantId: null | number = null;
|
||||
const websiteTenant = await websiteTenantPromise;
|
||||
if (websiteTenant?.id) {
|
||||
tenantId = websiteTenant.id;
|
||||
}
|
||||
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||
if (!tenantId && accessStore.tenantId) {
|
||||
tenantId = accessStore.tenantId;
|
||||
}
|
||||
// 如果还是没有租户,使用列表中的第一个
|
||||
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||
tenantId = tenantList.value[0].id;
|
||||
}
|
||||
|
||||
// 设置选中的租户编号
|
||||
accessStore.setTenantId(tenantId);
|
||||
forgetPasswordRef.value
|
||||
.getFormApi()
|
||||
.setFieldValue('tenantId', tenantId?.toString());
|
||||
} catch (error) {
|
||||
console.error('获取租户列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(() => {
|
||||
fetchTenantList();
|
||||
});
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: tenantList.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id.toString(),
|
||||
})),
|
||||
placeholder: $t('authentication.tenantTip'),
|
||||
},
|
||||
fieldName: 'tenantId',
|
||||
label: $t('authentication.tenant'),
|
||||
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||
dependencies: {
|
||||
triggerFields: ['tenantId'],
|
||||
if: tenantEnable,
|
||||
trigger(values) {
|
||||
if (values.tenantId) {
|
||||
accessStore.setTenantId(Number(values.tenantId));
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.mobile'),
|
||||
},
|
||||
fieldName: 'mobile',
|
||||
label: $t('authentication.mobile'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.mobileTip') })
|
||||
.refine((v) => /^\d{11}$/.test(v), {
|
||||
message: $t('authentication.mobileErrortip'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'VbenPinInput',
|
||||
componentProps: {
|
||||
codeLength: CODE_LENGTH,
|
||||
createText: (countdown: number) => {
|
||||
const text =
|
||||
countdown > 0
|
||||
? $t('authentication.sendText', [countdown])
|
||||
: $t('authentication.sendCode');
|
||||
return text;
|
||||
},
|
||||
placeholder: $t('authentication.code'),
|
||||
handleSendCode: async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const formApi = forgetPasswordRef.value?.getFormApi();
|
||||
if (!formApi) {
|
||||
throw new Error('表单未准备好');
|
||||
}
|
||||
// 验证手机号
|
||||
await formApi.validateField('mobile');
|
||||
const isMobileValid = await formApi.isFieldValid('mobile');
|
||||
if (!isMobileValid) {
|
||||
throw new Error('请输入有效的手机号码');
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
const { mobile } = await formApi.getValues();
|
||||
const scene = 23; // 场景:重置密码
|
||||
await sendSmsCode({ mobile, scene });
|
||||
message.success('验证码发送成功');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
fieldName: 'code',
|
||||
label: $t('authentication.code'),
|
||||
rules: z.string().length(CODE_LENGTH, {
|
||||
message: $t('authentication.codeTip', [CODE_LENGTH]),
|
||||
}),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
renderComponentContent() {
|
||||
return {
|
||||
strengthText: () => $t('authentication.passwordStrength'),
|
||||
};
|
||||
},
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.confirmPassword'),
|
||||
},
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
const { password } = values;
|
||||
return z
|
||||
.string({ required_error: $t('authentication.passwordTip') })
|
||||
.min(1, { message: $t('authentication.passwordTip') })
|
||||
.refine((value) => value === password, {
|
||||
message: $t('authentication.confirmPasswordTip'),
|
||||
});
|
||||
},
|
||||
triggerFields: ['password'],
|
||||
},
|
||||
fieldName: 'confirmPassword',
|
||||
label: $t('authentication.confirmPassword'),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
/**
|
||||
* 处理重置密码操作
|
||||
* @param values 表单数据
|
||||
*/
|
||||
async function handleSubmit(values: Recordable<any>) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { mobile, code, password } = values;
|
||||
await smsResetPassword({ mobile, code, password });
|
||||
message.success($t('authentication.resetPasswordSuccess'));
|
||||
// 重置成功后跳转到首页
|
||||
router.push('/');
|
||||
} catch (error) {
|
||||
console.error('重置密码失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationForgetPassword
|
||||
ref="forgetPasswordRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
192
apps/web-naive/src/views/_core/authentication/login.vue
Normal file
192
apps/web-naive/src/views/_core/authentication/login.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import type { AuthApi } from '#/api/core/auth';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { AuthenticationLogin, Verification, z } from '@vben/common-ui';
|
||||
import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import {
|
||||
checkCaptcha,
|
||||
getCaptcha,
|
||||
getTenantByWebsite,
|
||||
getTenantSimpleList,
|
||||
socialAuthRedirect,
|
||||
} from '#/api/core/auth';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Login' });
|
||||
|
||||
const { query } = useRoute();
|
||||
const authStore = useAuthStore();
|
||||
const accessStore = useAccessStore();
|
||||
const tenantEnable = isTenantEnable();
|
||||
const captchaEnable = isCaptchaEnable();
|
||||
|
||||
const loginRef = ref();
|
||||
const verifyRef = ref();
|
||||
|
||||
const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWord'
|
||||
|
||||
/** 获取租户列表,并默认选中 */
|
||||
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||
async function fetchTenantList() {
|
||||
if (!tenantEnable) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 获取租户列表、域名对应租户
|
||||
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||
tenantList.value = await getTenantSimpleList();
|
||||
|
||||
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||
let tenantId: null | number = null;
|
||||
const websiteTenant = await websiteTenantPromise;
|
||||
if (websiteTenant?.id) {
|
||||
tenantId = websiteTenant.id;
|
||||
}
|
||||
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||
if (!tenantId && accessStore.tenantId) {
|
||||
tenantId = accessStore.tenantId;
|
||||
}
|
||||
// 如果还是没有租户,使用列表中的第一个
|
||||
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||
tenantId = tenantList.value[0].id;
|
||||
}
|
||||
|
||||
// 设置选中的租户编号
|
||||
accessStore.setTenantId(tenantId);
|
||||
loginRef.value.getFormApi().setFieldValue('tenantId', tenantId?.toString());
|
||||
} catch (error) {
|
||||
console.error('获取租户列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理登录 */
|
||||
async function handleLogin(values: any) {
|
||||
// 如果开启验证码,则先验证验证码
|
||||
if (captchaEnable) {
|
||||
verifyRef.value.show();
|
||||
return;
|
||||
}
|
||||
// 无验证码,直接登录
|
||||
await authStore.authLogin('username', values);
|
||||
}
|
||||
|
||||
/** 验证码通过,执行登录 */
|
||||
async function handleVerifySuccess({ captchaVerification }: any) {
|
||||
try {
|
||||
await authStore.authLogin('username', {
|
||||
...(await loginRef.value.getFormApi().getValues()),
|
||||
captchaVerification,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleLogin:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理第三方登录 */
|
||||
const redirect = query?.redirect;
|
||||
async function handleThirdLogin(type: number) {
|
||||
if (type <= 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 计算 redirectUri
|
||||
// tricky: type、redirect 需要先 encode 一次,否则钉钉回调会丢失。配合 social-login.vue#getUrlValue() 使用
|
||||
const redirectUri = `${
|
||||
location.origin
|
||||
}/auth/social-login?${encodeURIComponent(
|
||||
`type=${type}&redirect=${redirect || '/'}`,
|
||||
)}`;
|
||||
|
||||
// 进行跳转
|
||||
window.location.href = await socialAuthRedirect(type, redirectUri);
|
||||
} catch (error) {
|
||||
console.error('第三方登录处理失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(() => {
|
||||
fetchTenantList();
|
||||
});
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: tenantList.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id.toString(),
|
||||
})),
|
||||
placeholder: $t('authentication.tenantTip'),
|
||||
},
|
||||
fieldName: 'tenantId',
|
||||
label: $t('authentication.tenant'),
|
||||
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||
dependencies: {
|
||||
triggerFields: ['tenantId'],
|
||||
if: tenantEnable,
|
||||
trigger(values) {
|
||||
if (values.tenantId) {
|
||||
accessStore.setTenantId(Number(values.tenantId));
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.usernameTip') })
|
||||
.default(import.meta.env.VITE_APP_DEFAULT_USERNAME),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.passwordTip'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.passwordTip') })
|
||||
.default(import.meta.env.VITE_APP_DEFAULT_PASSWORD),
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AuthenticationLogin
|
||||
ref="loginRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
@submit="handleLogin"
|
||||
@third-login="handleThirdLogin"
|
||||
/>
|
||||
<Verification
|
||||
ref="verifyRef"
|
||||
v-if="captchaEnable"
|
||||
:captcha-type="captchaType"
|
||||
:check-captcha-api="checkCaptcha"
|
||||
:get-captcha-api="getCaptcha"
|
||||
:img-size="{ width: '400px', height: '200px' }"
|
||||
mode="pop"
|
||||
@on-success="handleVerifySuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { AuthenticationQrCodeLogin } from '@vben/common-ui';
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
|
||||
defineOptions({ name: 'QrCodeLogin' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AuthenticationQrCodeLogin :login-path="LOGIN_PATH" />
|
||||
</template>
|
||||
221
apps/web-naive/src/views/_core/authentication/register.vue
Normal file
221
apps/web-naive/src/views/_core/authentication/register.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import type { AuthApi } from '#/api/core/auth';
|
||||
|
||||
import { computed, h, onMounted, ref } from 'vue';
|
||||
|
||||
import { AuthenticationRegister, Verification, z } from '@vben/common-ui';
|
||||
import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import {
|
||||
checkCaptcha,
|
||||
getCaptcha,
|
||||
getTenantByWebsite,
|
||||
getTenantSimpleList,
|
||||
} from '#/api/core/auth';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'Register' });
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const authStore = useAuthStore();
|
||||
const tenantEnable = isTenantEnable();
|
||||
const captchaEnable = isCaptchaEnable();
|
||||
|
||||
const registerRef = ref();
|
||||
const verifyRef = ref();
|
||||
|
||||
const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWord'
|
||||
|
||||
/** 获取租户列表,并默认选中 */
|
||||
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||
async function fetchTenantList() {
|
||||
if (!tenantEnable) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 获取租户列表、域名对应租户
|
||||
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||
tenantList.value = await getTenantSimpleList();
|
||||
|
||||
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||
let tenantId: null | number = null;
|
||||
const websiteTenant = await websiteTenantPromise;
|
||||
if (websiteTenant?.id) {
|
||||
tenantId = websiteTenant.id;
|
||||
}
|
||||
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||
if (!tenantId && accessStore.tenantId) {
|
||||
tenantId = accessStore.tenantId;
|
||||
}
|
||||
// 如果还是没有租户,使用列表中的第一个
|
||||
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||
tenantId = tenantList.value[0].id;
|
||||
}
|
||||
|
||||
// 设置选中的租户编号
|
||||
accessStore.setTenantId(tenantId);
|
||||
registerRef.value
|
||||
.getFormApi()
|
||||
.setFieldValue('tenantId', tenantId?.toString());
|
||||
} catch (error) {
|
||||
console.error('获取租户列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 执行注册 */
|
||||
async function handleRegister(values: any) {
|
||||
// 如果开启验证码,则先验证验证码
|
||||
if (captchaEnable) {
|
||||
verifyRef.value.show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 无验证码,直接登录
|
||||
await authStore.authLogin('register', values);
|
||||
}
|
||||
|
||||
/** 验证码通过,执行注册 */
|
||||
const handleVerifySuccess = async ({ captchaVerification }: any) => {
|
||||
try {
|
||||
await authStore.authLogin('register', {
|
||||
...(await registerRef.value.getFormApi().getValues()),
|
||||
captchaVerification,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleRegister:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(() => {
|
||||
fetchTenantList();
|
||||
});
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: tenantList.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id.toString(),
|
||||
})),
|
||||
placeholder: $t('authentication.tenantTip'),
|
||||
},
|
||||
fieldName: 'tenantId',
|
||||
label: $t('authentication.tenant'),
|
||||
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||
dependencies: {
|
||||
triggerFields: ['tenantId'],
|
||||
if: tenantEnable,
|
||||
trigger(values) {
|
||||
if (values.tenantId) {
|
||||
accessStore.setTenantId(Number(values.tenantId));
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z.string().min(1, { message: $t('authentication.usernameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.nicknameTip'),
|
||||
},
|
||||
fieldName: 'nickname',
|
||||
label: $t('authentication.nickname'),
|
||||
rules: z.string().min(1, { message: $t('authentication.nicknameTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
passwordStrength: true,
|
||||
placeholder: $t('authentication.password'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
renderComponentContent() {
|
||||
return {
|
||||
strengthText: () => $t('authentication.passwordStrength'),
|
||||
};
|
||||
},
|
||||
rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.confirmPassword'),
|
||||
},
|
||||
dependencies: {
|
||||
rules(values) {
|
||||
const { password } = values;
|
||||
return z
|
||||
.string({ required_error: $t('authentication.passwordTip') })
|
||||
.min(1, { message: $t('authentication.passwordTip') })
|
||||
.refine((value) => value === password, {
|
||||
message: $t('authentication.confirmPasswordTip'),
|
||||
});
|
||||
},
|
||||
triggerFields: ['password'],
|
||||
},
|
||||
fieldName: 'confirmPassword',
|
||||
label: $t('authentication.confirmPassword'),
|
||||
},
|
||||
{
|
||||
component: 'VbenCheckbox',
|
||||
fieldName: 'agreePolicy',
|
||||
renderComponentContent: () => ({
|
||||
default: () =>
|
||||
h('span', [
|
||||
$t('authentication.agree'),
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
class: 'vben-link ml-1 ',
|
||||
href: '',
|
||||
},
|
||||
`${$t('authentication.privacyPolicy')} & ${$t('authentication.terms')}`,
|
||||
),
|
||||
]),
|
||||
}),
|
||||
rules: z.boolean().refine((value) => !!value, {
|
||||
message: $t('authentication.agreeTip'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AuthenticationRegister
|
||||
ref="registerRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="loading"
|
||||
@submit="handleRegister"
|
||||
/>
|
||||
<Verification
|
||||
ref="verifyRef"
|
||||
v-if="captchaEnable"
|
||||
:captcha-type="captchaType"
|
||||
:check-captcha-api="checkCaptcha"
|
||||
:get-captcha-api="getCaptcha"
|
||||
:img-size="{ width: '400px', height: '200px' }"
|
||||
mode="pop"
|
||||
@on-success="handleVerifySuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
210
apps/web-naive/src/views/_core/authentication/social-login.vue
Normal file
210
apps/web-naive/src/views/_core/authentication/social-login.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '@vben/common-ui';
|
||||
|
||||
import type { AuthApi } from '#/api/core/auth';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { AuthenticationLogin, Verification, z } from '@vben/common-ui';
|
||||
import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
import { getUrlValue } from '@vben/utils';
|
||||
|
||||
import {
|
||||
checkCaptcha,
|
||||
getCaptcha,
|
||||
getTenantByWebsite,
|
||||
getTenantSimpleList,
|
||||
} from '#/api/core/auth';
|
||||
import { useAuthStore } from '#/store';
|
||||
|
||||
defineOptions({ name: 'SocialLogin' });
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const accessStore = useAccessStore();
|
||||
const { query } = useRoute();
|
||||
const router = useRouter();
|
||||
const tenantEnable = isTenantEnable();
|
||||
const captchaEnable = isCaptchaEnable();
|
||||
|
||||
const loginRef = ref();
|
||||
const verifyRef = ref();
|
||||
|
||||
const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWord'
|
||||
|
||||
/** 获取租户列表,并默认选中 */
|
||||
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
||||
async function fetchTenantList() {
|
||||
if (!tenantEnable) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取租户列表、域名对应租户
|
||||
const websiteTenantPromise = getTenantByWebsite(window.location.hostname);
|
||||
tenantList.value = await getTenantSimpleList();
|
||||
|
||||
// 选中租户:域名 > store 中的租户 > 首个租户
|
||||
let tenantId: null | number = null;
|
||||
const websiteTenant = await websiteTenantPromise;
|
||||
if (websiteTenant?.id) {
|
||||
tenantId = websiteTenant.id;
|
||||
}
|
||||
// 如果没有从域名获取到租户,尝试从 store 中获取
|
||||
if (!tenantId && accessStore.tenantId) {
|
||||
tenantId = accessStore.tenantId;
|
||||
}
|
||||
// 如果还是没有租户,使用列表中的第一个
|
||||
if (!tenantId && tenantList.value?.[0]?.id) {
|
||||
tenantId = tenantList.value[0].id;
|
||||
}
|
||||
|
||||
// 设置选中的租户编号
|
||||
accessStore.setTenantId(tenantId);
|
||||
loginRef.value.getFormApi().setFieldValue('tenantId', tenantId);
|
||||
} catch (error) {
|
||||
console.error('获取租户列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 尝试登录:当账号已经绑定,socialLogin 会直接获得 token */
|
||||
const socialType = Number(getUrlValue('type'));
|
||||
const redirect = getUrlValue('redirect');
|
||||
const socialCode = query?.code as string;
|
||||
const socialState = query?.state as string;
|
||||
async function tryLogin() {
|
||||
// 用于登录后,基于 redirect 的重定向
|
||||
if (redirect) {
|
||||
await router.replace({
|
||||
query: {
|
||||
...query,
|
||||
redirect: encodeURIComponent(redirect),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 尝试登录
|
||||
await authStore.authLogin('social', {
|
||||
type: socialType,
|
||||
code: socialCode,
|
||||
state: socialState,
|
||||
});
|
||||
}
|
||||
|
||||
/** 处理登录 */
|
||||
async function handleLogin(values: any) {
|
||||
// 如果开启验证码,则先验证验证码
|
||||
if (captchaEnable) {
|
||||
verifyRef.value.show();
|
||||
return;
|
||||
}
|
||||
|
||||
// 无验证码,直接登录
|
||||
await authStore.authLogin('username', {
|
||||
...values,
|
||||
socialType,
|
||||
socialCode,
|
||||
socialState,
|
||||
});
|
||||
}
|
||||
|
||||
/** 验证码通过,执行登录 */
|
||||
async function handleVerifySuccess({ captchaVerification }: any) {
|
||||
try {
|
||||
await authStore.authLogin('username', {
|
||||
...(await loginRef.value.getFormApi().getValues()),
|
||||
captchaVerification,
|
||||
socialType,
|
||||
socialCode,
|
||||
socialState,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in handleLogin:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 组件挂载时获取租户信息 */
|
||||
onMounted(async () => {
|
||||
await fetchTenantList();
|
||||
|
||||
await tryLogin();
|
||||
});
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
componentProps: {
|
||||
options: tenantList.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id.toString(),
|
||||
})),
|
||||
placeholder: $t('authentication.tenantTip'),
|
||||
},
|
||||
fieldName: 'tenantId',
|
||||
label: $t('authentication.tenant'),
|
||||
rules: z.string().min(1, { message: $t('authentication.tenantTip') }),
|
||||
dependencies: {
|
||||
triggerFields: ['tenantId'],
|
||||
if: tenantEnable,
|
||||
trigger(values) {
|
||||
if (values.tenantId) {
|
||||
accessStore.setTenantId(Number(values.tenantId));
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.usernameTip'),
|
||||
},
|
||||
fieldName: 'username',
|
||||
label: $t('authentication.username'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.usernameTip') })
|
||||
.default(import.meta.env.VITE_APP_DEFAULT_USERNAME),
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
componentProps: {
|
||||
placeholder: $t('authentication.passwordTip'),
|
||||
},
|
||||
fieldName: 'password',
|
||||
label: $t('authentication.password'),
|
||||
rules: z
|
||||
.string()
|
||||
.min(1, { message: $t('authentication.passwordTip') })
|
||||
.default(import.meta.env.VITE_APP_DEFAULT_PASSWORD),
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AuthenticationLogin
|
||||
ref="loginRef"
|
||||
:form-schema="formSchema"
|
||||
:loading="authStore.loginLoading"
|
||||
:show-code-login="false"
|
||||
:show-qrcode-login="false"
|
||||
:show-third-party-login="false"
|
||||
:show-register="false"
|
||||
@submit="handleLogin"
|
||||
/>
|
||||
<Verification
|
||||
ref="verifyRef"
|
||||
v-if="captchaEnable"
|
||||
:captcha-type="captchaType"
|
||||
:check-captcha-api="checkCaptcha"
|
||||
:get-captcha-api="getCaptcha"
|
||||
:img-size="{ width: '400px', height: '200px' }"
|
||||
mode="pop"
|
||||
@on-success="handleVerifySuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
221
apps/web-naive/src/views/_core/authentication/sso-login.vue
Normal file
221
apps/web-naive/src/views/_core/authentication/sso-login.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<script lang="ts" setup>
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { AuthenticationAuthTitle, VbenButton } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { authorize, getAuthorize } from '#/api/system/oauth2/open';
|
||||
|
||||
defineOptions({ name: 'SSOLogin' });
|
||||
|
||||
const { query } = useRoute(); // 路由参数
|
||||
|
||||
const client = ref({
|
||||
name: '',
|
||||
logo: '',
|
||||
}); // 客户端信息
|
||||
|
||||
const queryParams = reactive({
|
||||
responseType: '',
|
||||
clientId: '',
|
||||
redirectUri: '',
|
||||
state: '',
|
||||
scopes: [] as string[], // 优先从 query 参数获取;如果未传递,从后端获取
|
||||
}); // URL 上的 client_id、scope 等参数
|
||||
|
||||
const loading = ref(false); // 表单是否提交中
|
||||
|
||||
/** 初始化授权信息 */
|
||||
async function init() {
|
||||
// 防止在没有登录的情况下循环弹窗
|
||||
if (query.client_id === undefined) {
|
||||
return;
|
||||
}
|
||||
// 解析参数
|
||||
// 例如说【自动授权不通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write
|
||||
// 例如说【自动授权通过】:client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read
|
||||
queryParams.responseType = query.response_type as string;
|
||||
queryParams.clientId = query.client_id as string;
|
||||
queryParams.redirectUri = query.redirect_uri as string;
|
||||
queryParams.state = query.state as string;
|
||||
if (query.scope) {
|
||||
queryParams.scopes = (query.scope as string).split(' ');
|
||||
}
|
||||
|
||||
// 如果有 scope 参数,先执行一次自动授权,看看是否之前都授权过了。
|
||||
if (queryParams.scopes.length > 0) {
|
||||
const data = await doAuthorize(true, queryParams.scopes, []);
|
||||
if (data) {
|
||||
location.href = data;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 1.1 获取授权页的基本信息
|
||||
const data = await getAuthorize(queryParams.clientId);
|
||||
client.value = data.client;
|
||||
// 1.2 解析 scope
|
||||
let scopes;
|
||||
// 如果 params.scope 非空,则过滤下返回的 scopes
|
||||
if (queryParams.scopes.length > 0) {
|
||||
scopes = data.scopes.filter((scope) =>
|
||||
queryParams.scopes.includes(scope.key),
|
||||
);
|
||||
// 如果 params.scope 为空,则使用返回的 scopes 设置它
|
||||
} else {
|
||||
scopes = data.scopes;
|
||||
queryParams.scopes = scopes.map((scope) => scope.key);
|
||||
}
|
||||
|
||||
// 2.设置表单的初始值
|
||||
formApi.setFieldValue(
|
||||
'scopes',
|
||||
scopes.filter((scope) => scope.value).map((scope) => scope.key),
|
||||
);
|
||||
}
|
||||
|
||||
/** 处理授权的提交 */
|
||||
async function handleSubmit(approved: boolean) {
|
||||
// 计算 checkedScopes + uncheckedScopes
|
||||
let checkedScopes: string[];
|
||||
let uncheckedScopes: string[];
|
||||
if (approved) {
|
||||
// 同意授权,按照用户的选择
|
||||
const res = await formApi.getValues();
|
||||
checkedScopes = res.scopes;
|
||||
uncheckedScopes = queryParams.scopes.filter(
|
||||
(item) => !checkedScopes.includes(item),
|
||||
);
|
||||
} else {
|
||||
// 拒绝,则都是取消
|
||||
checkedScopes = [];
|
||||
uncheckedScopes = queryParams.scopes;
|
||||
}
|
||||
|
||||
// 提交授权的请求
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await doAuthorize(false, checkedScopes, uncheckedScopes);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
// 跳转授权成功后的回调地址
|
||||
location.href = data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 调用授权 API 接口 */
|
||||
const doAuthorize = (
|
||||
autoApprove: boolean,
|
||||
checkedScopes: string[],
|
||||
uncheckedScopes: string[],
|
||||
) => {
|
||||
return authorize(
|
||||
queryParams.responseType,
|
||||
queryParams.clientId,
|
||||
queryParams.redirectUri,
|
||||
queryParams.state,
|
||||
autoApprove,
|
||||
checkedScopes,
|
||||
uncheckedScopes,
|
||||
);
|
||||
};
|
||||
|
||||
/** 格式化 scope 文本 */
|
||||
function formatScope(scope: string) {
|
||||
// 格式化 scope 授权范围,方便用户理解。
|
||||
// 这里仅仅是一个 demo,可以考虑录入到字典数据中,例如说字典类型 "system_oauth2_scope",它的每个 scope 都是一条字典数据。
|
||||
switch (scope) {
|
||||
case 'user.read': {
|
||||
return '访问你的个人信息';
|
||||
}
|
||||
case 'user.write': {
|
||||
return '修改你的个人信息';
|
||||
}
|
||||
default: {
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
{
|
||||
fieldName: 'scopes',
|
||||
label: '授权范围',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: queryParams.scopes.map((scope) => ({
|
||||
label: formatScope(scope),
|
||||
value: scope,
|
||||
})),
|
||||
class: 'flex flex-col gap-2',
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm(
|
||||
reactive({
|
||||
commonConfig: {
|
||||
hideLabel: true,
|
||||
hideRequiredMark: true,
|
||||
},
|
||||
schema: formSchema,
|
||||
showDefaultActions: false,
|
||||
}),
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @keydown.enter.prevent="handleSubmit(true)">
|
||||
<AuthenticationAuthTitle>
|
||||
<slot name="title">
|
||||
{{ `${client.name} 👋🏻` }}
|
||||
</slot>
|
||||
<template #desc>
|
||||
<span class="text-muted-foreground">
|
||||
此第三方应用请求获得以下权限:
|
||||
</span>
|
||||
</template>
|
||||
</AuthenticationAuthTitle>
|
||||
|
||||
<Form />
|
||||
|
||||
<div class="flex gap-2">
|
||||
<VbenButton
|
||||
:class="{
|
||||
'cursor-wait': loading,
|
||||
}"
|
||||
:loading="loading"
|
||||
aria-label="login"
|
||||
class="w-2/3"
|
||||
@click="handleSubmit(true)"
|
||||
>
|
||||
同意授权
|
||||
</VbenButton>
|
||||
<VbenButton
|
||||
:class="{
|
||||
'cursor-wait': loading,
|
||||
}"
|
||||
:loading="loading"
|
||||
aria-label="login"
|
||||
class="w-1/3"
|
||||
variant="outline"
|
||||
@click="handleSubmit(false)"
|
||||
>
|
||||
拒绝
|
||||
</VbenButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
7
apps/web-naive/src/views/_core/fallback/coming-soon.vue
Normal file
7
apps/web-naive/src/views/_core/fallback/coming-soon.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="coming-soon" />
|
||||
</template>
|
||||
9
apps/web-naive/src/views/_core/fallback/forbidden.vue
Normal file
9
apps/web-naive/src/views/_core/fallback/forbidden.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback403Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="403" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback500Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="500" />
|
||||
</template>
|
||||
9
apps/web-naive/src/views/_core/fallback/not-found.vue
Normal file
9
apps/web-naive/src/views/_core/fallback/not-found.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'Fallback404Demo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="404" />
|
||||
</template>
|
||||
9
apps/web-naive/src/views/_core/fallback/offline.vue
Normal file
9
apps/web-naive/src/views/_core/fallback/offline.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Fallback } from '@vben/common-ui';
|
||||
|
||||
defineOptions({ name: 'FallbackOfflineDemo' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fallback status="offline" />
|
||||
</template>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
111, 2000, 6000, 16_000, 33_333, 55_555, 64_000, 33_333, 18_000,
|
||||
36_000, 70_000, 42_444, 23_222, 13_000, 8000, 4000, 1200, 333, 222,
|
||||
111,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
{
|
||||
areaStyle: {},
|
||||
data: [
|
||||
33, 66, 88, 333, 3333, 6200, 20_000, 3000, 1200, 13_000, 22_000,
|
||||
11_000, 2221, 1201, 390, 198, 60, 30, 22, 11,
|
||||
],
|
||||
itemStyle: {
|
||||
color: '#019680',
|
||||
},
|
||||
smooth: true,
|
||||
type: 'line',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#019680',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
// xAxis: {
|
||||
// axisTick: {
|
||||
// show: false,
|
||||
// },
|
||||
// boundaryGap: false,
|
||||
// data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
// type: 'category',
|
||||
// },
|
||||
xAxis: {
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: Array.from({ length: 18 }).map((_item, index) => `${index + 6}:00`),
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'solid',
|
||||
width: 1,
|
||||
},
|
||||
show: true,
|
||||
},
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
max: 80_000,
|
||||
splitArea: {
|
||||
show: true,
|
||||
},
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: 0,
|
||||
data: ['访问', '趋势'],
|
||||
},
|
||||
radar: {
|
||||
indicator: [
|
||||
{
|
||||
name: '网页',
|
||||
},
|
||||
{
|
||||
name: '移动端',
|
||||
},
|
||||
{
|
||||
name: 'Ipad',
|
||||
},
|
||||
{
|
||||
name: '客户端',
|
||||
},
|
||||
{
|
||||
name: '第三方',
|
||||
},
|
||||
{
|
||||
name: '其它',
|
||||
},
|
||||
],
|
||||
radius: '60%',
|
||||
splitNumber: 8,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
areaStyle: {
|
||||
opacity: 1,
|
||||
shadowBlur: 0,
|
||||
shadowColor: 'rgba(0,0,0,.2)',
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 10,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#b6a2de',
|
||||
},
|
||||
name: '访问',
|
||||
value: [90, 50, 86, 40, 50, 20],
|
||||
},
|
||||
{
|
||||
itemStyle: {
|
||||
color: '#5ab1ef',
|
||||
},
|
||||
name: '趋势',
|
||||
value: [70, 75, 70, 76, 20, 85],
|
||||
},
|
||||
],
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
symbolSize: 0,
|
||||
type: 'radar',
|
||||
},
|
||||
],
|
||||
tooltip: {},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
legend: {
|
||||
bottom: '2%',
|
||||
left: 'center',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
animationDelay() {
|
||||
return Math.random() * 100;
|
||||
},
|
||||
animationEasing: 'exponentialInOut',
|
||||
animationType: 'scale',
|
||||
avoidLabelOverlap: false,
|
||||
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||
data: [
|
||||
{ name: '搜索引擎', value: 1048 },
|
||||
{ name: '直接访问', value: 735 },
|
||||
{ name: '邮件营销', value: 580 },
|
||||
{ name: '联盟广告', value: 484 },
|
||||
],
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold',
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
// borderColor: '#fff',
|
||||
borderRadius: 10,
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
position: 'center',
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
name: '访问来源',
|
||||
radius: ['40%', '65%'],
|
||||
type: 'pie',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
onMounted(() => {
|
||||
renderEcharts({
|
||||
grid: {
|
||||
bottom: 0,
|
||||
containLabel: true,
|
||||
left: '1%',
|
||||
right: '1%',
|
||||
top: '2 %',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
barMaxWidth: 80,
|
||||
// color: '#4f69fd',
|
||||
data: [
|
||||
3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000,
|
||||
3200, 4800,
|
||||
],
|
||||
type: 'bar',
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
// color: '#4f69fd',
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
data: Array.from({ length: 12 }).map((_item, index) => `${index + 1}月`),
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: {
|
||||
max: 8000,
|
||||
splitNumber: 4,
|
||||
type: 'value',
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" />
|
||||
</template>
|
||||
90
apps/web-naive/src/views/dashboard/analytics/index.vue
Normal file
90
apps/web-naive/src/views/dashboard/analytics/index.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||
import type { TabOption } from '@vben/types';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
AnalysisChartsTabs,
|
||||
AnalysisOverview,
|
||||
} from '@vben/common-ui';
|
||||
import {
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
SvgCardIcon,
|
||||
SvgDownloadIcon,
|
||||
} from '@vben/icons';
|
||||
|
||||
import AnalyticsTrends from './analytics-trends.vue';
|
||||
import AnalyticsVisitsData from './analytics-visits-data.vue';
|
||||
import AnalyticsVisitsSales from './analytics-visits-sales.vue';
|
||||
import AnalyticsVisitsSource from './analytics-visits-source.vue';
|
||||
import AnalyticsVisits from './analytics-visits.vue';
|
||||
|
||||
const overviewItems: AnalysisOverviewItem[] = [
|
||||
{
|
||||
icon: SvgCardIcon,
|
||||
title: '用户量',
|
||||
totalTitle: '总用户量',
|
||||
totalValue: 120_000,
|
||||
value: 2000,
|
||||
},
|
||||
{
|
||||
icon: SvgCakeIcon,
|
||||
title: '访问量',
|
||||
totalTitle: '总访问量',
|
||||
totalValue: 500_000,
|
||||
value: 20_000,
|
||||
},
|
||||
{
|
||||
icon: SvgDownloadIcon,
|
||||
title: '下载量',
|
||||
totalTitle: '总下载量',
|
||||
totalValue: 120_000,
|
||||
value: 8000,
|
||||
},
|
||||
{
|
||||
icon: SvgBellIcon,
|
||||
title: '使用量',
|
||||
totalTitle: '总使用量',
|
||||
totalValue: 50_000,
|
||||
value: 5000,
|
||||
},
|
||||
];
|
||||
|
||||
const chartTabs: TabOption[] = [
|
||||
{
|
||||
label: '流量趋势',
|
||||
value: 'trends',
|
||||
},
|
||||
{
|
||||
label: '月访问量',
|
||||
value: 'visits',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<AnalysisOverview :items="overviewItems" />
|
||||
<AnalysisChartsTabs :tabs="chartTabs" class="mt-5">
|
||||
<template #trends>
|
||||
<AnalyticsTrends />
|
||||
</template>
|
||||
<template #visits>
|
||||
<AnalyticsVisits />
|
||||
</template>
|
||||
</AnalysisChartsTabs>
|
||||
|
||||
<div class="mt-5 w-full md:flex">
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问数量">
|
||||
<AnalyticsVisitsData />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mr-4 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
<AnalysisChartCard class="mt-5 md:mt-0 md:w-1/3" title="访问来源">
|
||||
<AnalyticsVisitsSales />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
266
apps/web-naive/src/views/dashboard/workspace/index.vue
Normal file
266
apps/web-naive/src/views/dashboard/workspace/index.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
WorkbenchProjectItem,
|
||||
WorkbenchQuickNavItem,
|
||||
WorkbenchTodoItem,
|
||||
WorkbenchTrendItem,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {
|
||||
AnalysisChartCard,
|
||||
WorkbenchHeader,
|
||||
WorkbenchProject,
|
||||
WorkbenchQuickNav,
|
||||
WorkbenchTodo,
|
||||
WorkbenchTrends,
|
||||
} from '@vben/common-ui';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { openWindow } from '@vben/utils';
|
||||
|
||||
import AnalyticsVisitsSource from '../analytics/analytics-visits-source.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 这是一个示例数据,实际项目中需要根据实际情况进行调整
|
||||
// url 也可以是内部路由,在 navTo 方法中识别处理,进行内部跳转
|
||||
// 例如:url: /dashboard/workspace
|
||||
const projectItems: WorkbenchProjectItem[] = [
|
||||
{
|
||||
color: '',
|
||||
content: '不要等待机会,而要创造机会。',
|
||||
date: '2021-04-01',
|
||||
group: '开源组',
|
||||
icon: 'carbon:logo-github',
|
||||
title: 'Github',
|
||||
url: 'https://github.com',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
content: '现在的你决定将来的你。',
|
||||
date: '2021-04-01',
|
||||
group: '算法组',
|
||||
icon: 'ion:logo-vue',
|
||||
title: 'Vue',
|
||||
url: 'https://vuejs.org',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
content: '没有什么才能比努力更重要。',
|
||||
date: '2021-04-01',
|
||||
group: '上班摸鱼',
|
||||
icon: 'ion:logo-html5',
|
||||
title: 'Html5',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/HTML',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
content: '热情和欲望可以突破一切难关。',
|
||||
date: '2021-04-01',
|
||||
group: 'UI',
|
||||
icon: 'ion:logo-angular',
|
||||
title: 'Angular',
|
||||
url: 'https://angular.io',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
content: '健康的身体是实现目标的基石。',
|
||||
date: '2021-04-01',
|
||||
group: '技术牛',
|
||||
icon: 'bx:bxl-react',
|
||||
title: 'React',
|
||||
url: 'https://reactjs.org',
|
||||
},
|
||||
{
|
||||
color: '#EBD94E',
|
||||
content: '路是走出来的,而不是空想出来的。',
|
||||
date: '2021-04-01',
|
||||
group: '架构组',
|
||||
icon: 'ion:logo-javascript',
|
||||
title: 'Js',
|
||||
url: 'https://developer.mozilla.org/zh-CN/docs/Web/JavaScript',
|
||||
},
|
||||
];
|
||||
|
||||
// 同样,这里的 url 也可以使用以 http 开头的外部链接
|
||||
const quickNavItems: WorkbenchQuickNavItem[] = [
|
||||
{
|
||||
color: '#1fdaca',
|
||||
icon: 'ion:home-outline',
|
||||
title: '首页',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
color: '#bf0c2c',
|
||||
icon: 'ion:grid-outline',
|
||||
title: '仪表盘',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
color: '#e18525',
|
||||
icon: 'ion:layers-outline',
|
||||
title: '组件',
|
||||
url: '/demos/features/icons',
|
||||
},
|
||||
{
|
||||
color: '#3fb27f',
|
||||
icon: 'ion:settings-outline',
|
||||
title: '系统管理',
|
||||
url: '/demos/features/login-expired', // 这里的 URL 是示例,实际项目中需要根据实际情况进行调整
|
||||
},
|
||||
{
|
||||
color: '#4daf1bc9',
|
||||
icon: 'ion:key-outline',
|
||||
title: '权限管理',
|
||||
url: '/demos/access/page-control',
|
||||
},
|
||||
{
|
||||
color: '#00d8ff',
|
||||
icon: 'ion:bar-chart-outline',
|
||||
title: '图表',
|
||||
url: '/analytics',
|
||||
},
|
||||
];
|
||||
|
||||
const todoItems = ref<WorkbenchTodoItem[]>([
|
||||
{
|
||||
completed: false,
|
||||
content: `审查最近提交到Git仓库的前端代码,确保代码质量和规范。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '审查前端代码提交',
|
||||
},
|
||||
{
|
||||
completed: true,
|
||||
content: `检查并优化系统性能,降低CPU使用率。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '系统性能优化',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `进行系统安全检查,确保没有安全漏洞或未授权的访问。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '安全检查',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `更新项目中的所有npm依赖包,确保使用最新版本。`,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '更新项目依赖',
|
||||
},
|
||||
{
|
||||
completed: false,
|
||||
content: `修复用户报告的页面UI显示问题,确保在不同浏览器中显示一致。 `,
|
||||
date: '2024-07-30 11:00:00',
|
||||
title: '修复UI显示问题',
|
||||
},
|
||||
]);
|
||||
const trendItems: WorkbenchTrendItem[] = [
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `在 <a>开源组</a> 创建了项目 <a>Vue</a>`,
|
||||
date: '刚刚',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关注了 <a>威廉</a> `,
|
||||
date: '1个小时前',
|
||||
title: '艾文',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1天前',
|
||||
title: '克里斯',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写一个Vite插件</a> `,
|
||||
date: '2天前',
|
||||
title: 'Vben',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-1',
|
||||
content: `回复了 <a>杰克</a> 的问题 <a>如何进行项目优化?</a>`,
|
||||
date: '3天前',
|
||||
title: '皮特',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-2',
|
||||
content: `关闭了问题 <a>如何运行项目</a> `,
|
||||
date: '1周前',
|
||||
title: '杰克',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-3',
|
||||
content: `发布了 <a>个人动态</a> `,
|
||||
date: '1周前',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `推送了代码到 <a>Github</a>`,
|
||||
date: '2021-04-01 20:00',
|
||||
title: '威廉',
|
||||
},
|
||||
{
|
||||
avatar: 'svg:avatar-4',
|
||||
content: `发表文章 <a>如何编写使用 Admin Vben</a> `,
|
||||
date: '2021-03-01 20:00',
|
||||
title: 'Vben',
|
||||
},
|
||||
];
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 这是一个示例方法,实际项目中需要根据实际情况进行调整
|
||||
// This is a sample method, adjust according to the actual project requirements
|
||||
function navTo(nav: WorkbenchProjectItem | WorkbenchQuickNavItem) {
|
||||
if (nav.url?.startsWith('http')) {
|
||||
openWindow(nav.url);
|
||||
return;
|
||||
}
|
||||
if (nav.url?.startsWith('/')) {
|
||||
router.push(nav.url).catch((error) => {
|
||||
console.error('Navigation failed:', error);
|
||||
});
|
||||
} else {
|
||||
console.warn(`Unknown URL for navigation item: ${nav.title} -> ${nav.url}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<WorkbenchHeader
|
||||
:avatar="userStore.userInfo?.avatar || preferences.app.defaultAvatar"
|
||||
>
|
||||
<template #title>
|
||||
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
|
||||
</template>
|
||||
<template #description> 今日晴,20℃ - 32℃! </template>
|
||||
</WorkbenchHeader>
|
||||
|
||||
<div class="mt-5 flex flex-col lg:flex-row">
|
||||
<div class="mr-4 w-full lg:w-3/5">
|
||||
<WorkbenchProject :items="projectItems" title="项目" @click="navTo" />
|
||||
<WorkbenchTrends :items="trendItems" class="mt-5" title="最新动态" />
|
||||
</div>
|
||||
<div class="w-full lg:w-2/5">
|
||||
<WorkbenchQuickNav
|
||||
:items="quickNavItems"
|
||||
class="mt-5 lg:mt-0"
|
||||
title="快捷导航"
|
||||
@click="navTo"
|
||||
/>
|
||||
<WorkbenchTodo :items="todoItems" class="mt-5" title="待办事项" />
|
||||
<AnalysisChartCard class="mt-5" title="访问来源">
|
||||
<AnalyticsVisitsSource />
|
||||
</AnalysisChartCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts" setup>
|
||||
import type { InfraCodegenApi } from '#/api/infra/codegen';
|
||||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
import { useBasicInfoFormSchema } from '../data';
|
||||
|
||||
const props = defineProps<{
|
||||
table: InfraCodegenApi.CodegenTable;
|
||||
}>();
|
||||
|
||||
/** 表单实例 */
|
||||
const [Form, formApi] = useVbenForm({
|
||||
wrapperClass: 'grid grid-cols-1 md:grid-cols-2 gap-4', // 配置表单布局为两列
|
||||
schema: useBasicInfoFormSchema(),
|
||||
layout: 'horizontal',
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
/** 动态更新表单值 */
|
||||
watch(
|
||||
() => props.table,
|
||||
(val: any) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
formApi.setValues(val);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 暴露出表单校验方法和表单值获取方法 */
|
||||
defineExpose({
|
||||
validate: async () => {
|
||||
const { valid } = await formApi.validate();
|
||||
return valid;
|
||||
},
|
||||
getValues: formApi.getValues,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Form />
|
||||
</template>
|
||||
49
apps/web-naive/src/views/system/area/modules/form.vue
Normal file
49
apps/web-naive/src/views/system/area/modules/form.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { getAreaByIp } from '#/api/system/area';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const [Form, { setFieldValue, validate, getValues }] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await getValues();
|
||||
try {
|
||||
const result = await getAreaByIp(data.ip);
|
||||
// 设置结果
|
||||
await setFieldValue('result', result);
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="IP 查询">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,88 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SystemSmsTemplateApi } from '#/api/system/sms/template';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import {
|
||||
createSmsTemplate,
|
||||
getSmsTemplate,
|
||||
updateSmsTemplate,
|
||||
} from '#/api/system/sms/template';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<SystemSmsTemplateApi.SmsTemplate>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['短信模板'])
|
||||
: $t('ui.actionTitle.create', ['短信模板']);
|
||||
});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as SystemSmsTemplateApi.SmsTemplate;
|
||||
try {
|
||||
await (formData.value?.id
|
||||
? updateSmsTemplate(data)
|
||||
: createSmsTemplate(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<SystemSmsTemplateApi.SmsTemplate>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getSmsTemplate(data.id);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts" setup>
|
||||
import type { SystemUserApi } from '#/api/system/user';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { message } from '#/adapter/naive';
|
||||
import { resetUserPassword } from '#/api/system/user';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useResetPasswordFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useResetPasswordFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await formApi.getValues();
|
||||
try {
|
||||
await resetUserPassword(data.id, data.newPassword);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<SystemUserApi.User>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
// 设置到 values
|
||||
await formApi.setValues(data);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="重置密码">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
||||
1
apps/web-naive/tailwind.config.mjs
Normal file
1
apps/web-naive/tailwind.config.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from '@vben/tailwind-config';
|
||||
12
apps/web-naive/tsconfig.json
Normal file
12
apps/web-naive/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/web-app.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"#/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"references": [{ "path": "./tsconfig.node.json" }],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
10
apps/web-naive/tsconfig.node.json
Normal file
10
apps/web-naive/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@vben/tsconfig/node.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["vite.config.mts"]
|
||||
}
|
||||
20
apps/web-naive/vite.config.mts
Normal file
20
apps/web-naive/vite.config.mts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from '@vben/vite-config';
|
||||
|
||||
export default defineConfig(async () => {
|
||||
return {
|
||||
application: {},
|
||||
vite: {
|
||||
server: {
|
||||
proxy: {
|
||||
'/admin-api': {
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/admin-api/, ''),
|
||||
// mock代理目标地址
|
||||
target: 'http://localhost:48080/admin-api',
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user