create-simpleadmin-ui 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/bin/create-simpleadmin-ui.mjs +2 -0
- package/package.json +35 -0
- package/src/index.mjs +557 -0
- package/src/sync-template.mjs +64 -0
- package/template/.env.development +4 -0
- package/template/.env.production.example +4 -0
- package/template/.vscode/extensions.json +3 -0
- package/template/README.md +26 -0
- package/template/index.html +19 -0
- package/template/package.json +30 -0
- package/template/public/vite.svg +1 -0
- package/template/src/App.vue +3 -0
- package/template/src/components/AppBreadcrumb.vue +132 -0
- package/template/src/components/AppPage.vue +23 -0
- package/template/src/components/ChangePasswordDialog.vue +97 -0
- package/template/src/components/ProfileEditDialog.vue +142 -0
- package/template/src/components/RichTextEditor.vue +121 -0
- package/template/src/components/SidebarMenuItem.vue +56 -0
- package/template/src/components/TagsView.vue +172 -0
- package/template/src/constants/menuIcons.ts +205 -0
- package/template/src/directives/permission.ts +13 -0
- package/template/src/env.d.ts +22 -0
- package/template/src/layouts/MainLayout.vue +410 -0
- package/template/src/main.ts +27 -0
- package/template/src/router/index.ts +198 -0
- package/template/src/stores/tagsView.ts +161 -0
- package/template/src/stores/theme.ts +68 -0
- package/template/src/stores/user.ts +191 -0
- package/template/src/styles/global.css +314 -0
- package/template/src/utils/datetime.ts +34 -0
- package/template/src/utils/request.ts +324 -0
- package/template/src/utils/requestSign.ts +162 -0
- package/template/src/views/error/NotFound.vue +49 -0
- package/template/src/views/home/HomeView.vue +1177 -0
- package/template/src/views/login/LoginView.vue +576 -0
- package/template/src/views/monitor/loginlog/index.vue +366 -0
- package/template/src/views/monitor/online/index.vue +298 -0
- package/template/src/views/monitor/operlog/index.vue +492 -0
- package/template/src/views/system/config/index.vue +407 -0
- package/template/src/views/system/dept/index.vue +517 -0
- package/template/src/views/system/dict/index.vue +747 -0
- package/template/src/views/system/file/index.vue +1031 -0
- package/template/src/views/system/menu/index.vue +822 -0
- package/template/src/views/system/message/index.vue +422 -0
- package/template/src/views/system/notice/index.vue +578 -0
- package/template/src/views/system/openApp/index.vue +596 -0
- package/template/src/views/system/post/index.vue +495 -0
- package/template/src/views/system/role/index.vue +546 -0
- package/template/src/views/system/user/index.vue +373 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/tsconfig.app.json +30 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +24 -0
- package/template/vite.config.ts +22 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 请求签名(HMAC-SHA256),与后端 RequestSignCanonical 规则一致。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** 空 body 的 SHA-256 小写 hex */
|
|
6
|
+
export const EMPTY_BODY_SHA256 =
|
|
7
|
+
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
|
|
8
|
+
|
|
9
|
+
const HEX = '0123456789abcdef'
|
|
10
|
+
const textEncoder = new TextEncoder()
|
|
11
|
+
|
|
12
|
+
/** 缓存 HMAC CryptoKey,避免每次请求 importKey */
|
|
13
|
+
let cachedHmacKey: CryptoKey | null = null
|
|
14
|
+
let cachedHmacSecret = ''
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 规范化路径:小写、去掉末尾斜杠(根除外)。
|
|
18
|
+
* @param path 原始路径
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeSignPath(path: string): string {
|
|
21
|
+
let p = (path || '/').trim()
|
|
22
|
+
if (!p.startsWith('/')) p = `/${p}`
|
|
23
|
+
p = p.toLowerCase()
|
|
24
|
+
while (p.length > 1 && p.endsWith('/')) p = p.slice(0, -1)
|
|
25
|
+
return p
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 拼接完整 API 路径(含 /api/v1 前缀)。
|
|
30
|
+
* @param baseURL axios baseURL
|
|
31
|
+
* @param url 相对或绝对路径
|
|
32
|
+
*/
|
|
33
|
+
export function resolveApiPath(baseURL: string | undefined, url: string): string {
|
|
34
|
+
const raw = String(url || '')
|
|
35
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
36
|
+
try {
|
|
37
|
+
return normalizeSignPath(new URL(raw).pathname)
|
|
38
|
+
} catch {
|
|
39
|
+
return normalizeSignPath(raw)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const base = String(baseURL || '').replace(/\/+$/, '')
|
|
44
|
+
const path = raw.startsWith('/') ? raw : `/${raw}`
|
|
45
|
+
const full = path.toLowerCase().startsWith('/api') ? path : `${base}${path}`
|
|
46
|
+
return normalizeSignPath(full)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 将 params 规范为排序 QUERY 串。
|
|
51
|
+
* @param params axios params
|
|
52
|
+
*/
|
|
53
|
+
export function buildSignQuery(params: unknown): string {
|
|
54
|
+
if (params == null || typeof params !== 'object') return ''
|
|
55
|
+
|
|
56
|
+
const map = new Map<string, string[]>()
|
|
57
|
+
for (const [key, value] of Object.entries(params as Record<string, unknown>)) {
|
|
58
|
+
if (!key || value === undefined || value === null) continue
|
|
59
|
+
const list = map.get(key) ?? []
|
|
60
|
+
if (Array.isArray(value)) {
|
|
61
|
+
for (const item of value) list.push(String(item))
|
|
62
|
+
} else {
|
|
63
|
+
list.push(String(value))
|
|
64
|
+
}
|
|
65
|
+
map.set(key, list)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (map.size === 0) return ''
|
|
69
|
+
|
|
70
|
+
const keys = [...map.keys()].sort()
|
|
71
|
+
return keys
|
|
72
|
+
.map((k) => {
|
|
73
|
+
const values = map.get(k)!
|
|
74
|
+
if (values.length > 1) values.sort()
|
|
75
|
+
return `${k}=${values.join(',')}`
|
|
76
|
+
})
|
|
77
|
+
.join('&')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 拼接待签字符串(path 应已规范化)。
|
|
82
|
+
*/
|
|
83
|
+
export function buildCanonicalString(parts: {
|
|
84
|
+
method: string
|
|
85
|
+
path: string
|
|
86
|
+
query: string
|
|
87
|
+
timestamp: string
|
|
88
|
+
nonce: string
|
|
89
|
+
bodySha256: string
|
|
90
|
+
idempotencyKey: string
|
|
91
|
+
}): string {
|
|
92
|
+
return `${parts.method}\n${parts.path}\n${parts.query || ''}\n${parts.timestamp}\n${parts.nonce}\n${parts.bodySha256}\n${parts.idempotencyKey || ''}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function toHex(buf: ArrayBuffer): string {
|
|
96
|
+
const bytes = new Uint8Array(buf)
|
|
97
|
+
let out = ''
|
|
98
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
99
|
+
const b = bytes[i]
|
|
100
|
+
out += HEX[b >>> 4] + HEX[b & 0xf]
|
|
101
|
+
}
|
|
102
|
+
return out
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* SHA-256 小写 hex。
|
|
107
|
+
* @param text UTF-8 文本
|
|
108
|
+
*/
|
|
109
|
+
export async function sha256Hex(text: string): Promise<string> {
|
|
110
|
+
if (!text) return EMPTY_BODY_SHA256
|
|
111
|
+
const buf = await crypto.subtle.digest('SHA-256', textEncoder.encode(text))
|
|
112
|
+
return toHex(buf)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function getHmacKey(secret: string): Promise<CryptoKey> {
|
|
116
|
+
if (cachedHmacKey && cachedHmacSecret === secret) return cachedHmacKey
|
|
117
|
+
cachedHmacKey = await crypto.subtle.importKey(
|
|
118
|
+
'raw',
|
|
119
|
+
textEncoder.encode(secret),
|
|
120
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
121
|
+
false,
|
|
122
|
+
['sign'],
|
|
123
|
+
)
|
|
124
|
+
cachedHmacSecret = secret
|
|
125
|
+
return cachedHmacKey
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* HMAC-SHA256 小写 hex(缓存 CryptoKey)。
|
|
130
|
+
* @param secret AppSecret
|
|
131
|
+
* @param message 待签串
|
|
132
|
+
*/
|
|
133
|
+
export async function hmacSha256Hex(secret: string, message: string): Promise<string> {
|
|
134
|
+
const key = await getHmacKey(secret)
|
|
135
|
+
const sig = await crypto.subtle.sign('HMAC', key, textEncoder.encode(message))
|
|
136
|
+
return toHex(sig)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 生成 Nonce(16–64 字符)。
|
|
141
|
+
*/
|
|
142
|
+
export function createNonce(): string {
|
|
143
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
144
|
+
const bytes = new Uint8Array(16)
|
|
145
|
+
crypto.getRandomValues(bytes)
|
|
146
|
+
let out = ''
|
|
147
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
148
|
+
out += HEX[bytes[i] >>> 4] + HEX[bytes[i] & 0xf]
|
|
149
|
+
}
|
|
150
|
+
return out
|
|
151
|
+
}
|
|
152
|
+
return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 14)}`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 是否为签名失败类 401(不应清登录态)。
|
|
157
|
+
* @param message 错误文案
|
|
158
|
+
*/
|
|
159
|
+
export function isRequestSignFailureMessage(message: string | undefined): boolean {
|
|
160
|
+
if (!message) return false
|
|
161
|
+
return /签名|Nonce|AppId|时间戳|缺少签名/.test(message)
|
|
162
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useRouter } from 'vue-router'
|
|
3
|
+
const router = useRouter()
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div class="not-found sa-page">
|
|
8
|
+
<div class="not-found__card">
|
|
9
|
+
<p class="not-found__code">404</p>
|
|
10
|
+
<h1>页面不存在</h1>
|
|
11
|
+
<p>请检查菜单路由配置,或返回工作台继续操作。</p>
|
|
12
|
+
<el-button type="primary" @click="router.push('/index')">返回首页</el-button>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<style scoped lang="scss">
|
|
18
|
+
.not-found {
|
|
19
|
+
min-height: 60vh;
|
|
20
|
+
display: grid;
|
|
21
|
+
place-items: center;
|
|
22
|
+
}
|
|
23
|
+
.not-found__card {
|
|
24
|
+
text-align: center;
|
|
25
|
+
padding: 40px 36px;
|
|
26
|
+
background: #fff;
|
|
27
|
+
border: 1px solid var(--sa-border);
|
|
28
|
+
border-radius: 20px;
|
|
29
|
+
box-shadow: var(--sa-shadow);
|
|
30
|
+
max-width: 420px;
|
|
31
|
+
}
|
|
32
|
+
.not-found__code {
|
|
33
|
+
margin: 0;
|
|
34
|
+
font-size: 64px;
|
|
35
|
+
font-weight: 800;
|
|
36
|
+
letter-spacing: -0.06em;
|
|
37
|
+
color: var(--sa-accent);
|
|
38
|
+
line-height: 1;
|
|
39
|
+
}
|
|
40
|
+
h1 {
|
|
41
|
+
margin: 8px 0 0;
|
|
42
|
+
font-size: 24px;
|
|
43
|
+
}
|
|
44
|
+
p {
|
|
45
|
+
margin: 10px 0 22px;
|
|
46
|
+
color: var(--sa-ink-secondary);
|
|
47
|
+
font-size: 14px;
|
|
48
|
+
}
|
|
49
|
+
</style>
|