feat(app): 初始化项目配置和页面结构

- 添加 .dockerignore 和 .env.example 配置文件
- 添加 .gitignore 忽略规则配置
- 创建服务端代理API路由(_file、_modules、_server)
- 集成 Ant Design Vue 组件库并配置SSR样式提取
- 定义API响应类型封装
- 创建基础布局组件(blank、console)
- 实现应用中心页面和组件(AppsCenter)
- 添加文章列表测试页面
- 配置控制台导航菜单结构
- 实现控制台头部组件
- 创建联系页面表单
This commit is contained in:
2026-01-17 18:23:37 +08:00
commit 5e26fdc7fb
439 changed files with 56219 additions and 0 deletions

9
app/utils/domain.ts Normal file
View File

@@ -0,0 +1,9 @@
export function getTenantId(defaultTenantId = '10398') {
if (!import.meta.client) return defaultTenantId
try {
return localStorage.getItem('TenantId') || defaultTenantId
} catch {
return defaultTenantId
}
}

247
app/utils/permission.ts Normal file
View File

@@ -0,0 +1,247 @@
/**
* 按钮级权限控制
*
* 参考 mp-vue/src/utils/permission.ts
* 当前项目未引入 Pinia 的 user store因此改为从 Nuxt state / localStorage 读取。
*/
import type { App } from 'vue'
import { useState } from '#imports'
import type { User } from '@/api/system/user/model'
import type { Role } from '@/api/system/role/model'
import type { Menu } from '@/api/system/menu/model'
type AuthzState = {
roles: string[]
authorities: string[]
}
const AUTHZ_STORAGE_KEY = 'Authz'
function uniqNonEmpty(values: Array<string | undefined | null>) {
const seen = new Set<string>()
for (const v of values) {
const s = typeof v === 'string' ? v.trim() : ''
if (!s || s === 'null' || s === 'undefined') continue
seen.add(s)
}
return Array.from(seen)
}
function safeJsonParse(value: string): unknown {
try {
return JSON.parse(value)
} catch {
return undefined
}
}
function normalizeStringArray(value: unknown): string[] {
if (Array.isArray(value)) {
return uniqNonEmpty(value.filter((v): v is string => typeof v === 'string'))
}
if (typeof value === 'string') {
return uniqNonEmpty(
value
.split(',')
.map((v) => v.trim())
.filter(Boolean)
)
}
return []
}
function readAuthzFromStorage(): AuthzState {
if (!import.meta.client) return { roles: [], authorities: [] }
try {
const raw = localStorage.getItem(AUTHZ_STORAGE_KEY)
if (!raw) return { roles: [], authorities: [] }
const parsed = safeJsonParse(raw)
if (!parsed || typeof parsed !== 'object') return { roles: [], authorities: [] }
const obj = parsed as Record<string, unknown>
return {
roles: normalizeStringArray(obj.roles),
authorities: normalizeStringArray(obj.authorities)
}
} catch {
return { roles: [], authorities: [] }
}
}
function writeAuthzToStorage(next: AuthzState) {
if (!import.meta.client) return
try {
localStorage.setItem(AUTHZ_STORAGE_KEY, JSON.stringify(next))
} catch {
// ignore
}
}
function getAuthzStateRef() {
try {
return useState<AuthzState>('authz', () => readAuthzFromStorage())
} catch {
return null
}
}
function getAuthzSnapshot(): AuthzState {
const state = getAuthzStateRef()
return state?.value ?? readAuthzFromStorage()
}
export function setAuthz(next: Partial<AuthzState>) {
const current = getAuthzSnapshot()
const merged: AuthzState = {
roles: normalizeStringArray(next.roles ?? current.roles),
authorities: normalizeStringArray(next.authorities ?? current.authorities)
}
const state = getAuthzStateRef()
if (state) state.value = merged
writeAuthzToStorage(merged)
}
export function clearAuthz() {
const state = getAuthzStateRef()
if (state) state.value = { roles: [], authorities: [] }
if (!import.meta.client) return
try {
localStorage.removeItem(AUTHZ_STORAGE_KEY)
} catch {
// ignore
}
}
function getRoleCodesFromUser(user?: User | null) {
const roleCodes: string[] = []
const fromUserRoleCode = normalizeStringArray(user?.roleCode)
roleCodes.push(...fromUserRoleCode)
const roles: Role[] | undefined = user?.roles
if (Array.isArray(roles)) {
for (const role of roles) {
if (!role) continue
roleCodes.push(role.roleCode)
}
}
return uniqNonEmpty(roleCodes)
}
function getAuthoritiesFromUser(user?: User | null) {
const authorities: string[] = []
const list: Menu[] | undefined = user?.authorities
if (Array.isArray(list)) {
for (const item of list) {
if (!item) continue
authorities.push(item.authority)
}
}
return uniqNonEmpty(authorities)
}
export function setAuthzFromUser(user?: User | null) {
setAuthz({
roles: getRoleCodesFromUser(user),
authorities: getAuthoritiesFromUser(user)
})
}
/* 判断数组是否有某些值(全包含) */
function normalizeNeedles(value: string | string[]) {
if (Array.isArray(value)) return uniqNonEmpty(value)
const s = typeof value === 'string' ? value.trim() : ''
return s ? [s] : []
}
function normalizeHaystack(array: (string | undefined)[]) {
return uniqNonEmpty(array)
}
function arrayHas(array: (string | undefined)[], value: string | string[]): boolean {
if (!value) return true
if (!array) return false
const needles = normalizeNeedles(value)
if (needles.length === 0) return true
const haystack = new Set(normalizeHaystack(array))
for (let i = 0; i < needles.length; i++) {
if (!haystack.has(needles[i])) return false
}
return true
}
/* 判断数组是否有任意值(任一包含) */
function arrayHasAny(array: (string | undefined)[], value: string | string[]): boolean {
if (!value) return true
if (!array) return false
const needles = normalizeNeedles(value)
if (needles.length === 0) return true
const haystack = new Set(normalizeHaystack(array))
for (let i = 0; i < needles.length; i++) {
if (haystack.has(needles[i])) return true
}
return false
}
/**
* 是否有某些角色
* @param value 角色字符或字符数组
*/
export function hasRole(value: string | string[]): boolean {
const { roles } = getAuthzSnapshot()
return arrayHas(roles, value)
}
/**
* 是否有任意角色
* @param value 角色字符或字符数组
*/
export function hasAnyRole(value: string | string[]): boolean {
const { roles } = getAuthzSnapshot()
return arrayHasAny(roles, value)
}
/**
* 是否有某些权限
* @param value 权限字符或字符数组
*/
export function hasPermission(value: string | string[]): boolean {
const { authorities } = getAuthzSnapshot()
return arrayHas(authorities, value)
}
/**
* 是否有任意权限
* @param value 权限字符或字符数组
*/
export function hasAnyPermission(value: string | string[]): boolean {
const { authorities } = getAuthzSnapshot()
return arrayHasAny(authorities, value)
}
export default {
install(app: App) {
// 添加自定义指令
app.directive('role', {
mounted: (el, binding) => {
if (!hasRole(binding.value)) el.parentNode?.removeChild(el)
}
})
app.directive('any-role', {
mounted: (el, binding) => {
if (!hasAnyRole(binding.value)) el.parentNode?.removeChild(el)
}
})
app.directive('permission', {
mounted: (el, binding) => {
if (!hasPermission(binding.value)) el.parentNode?.removeChild(el)
}
})
app.directive('any-permission', {
mounted: (el, binding) => {
if (!hasAnyPermission(binding.value)) el.parentNode?.removeChild(el)
}
})
}
}

101
app/utils/request.ts Normal file
View File

@@ -0,0 +1,101 @@
import { useRequestFetch, useRuntimeConfig } from '#imports'
import { MODULES_API_URL, SERVER_API_URL } from '@/config/setting'
import { getTenantId } from '@/utils/domain'
import { getToken } from '@/utils/token-util'
type RequestConfig = {
params?: Record<string, any>
data?: any
headers?: Record<string, any>
responseType?: 'json' | 'blob' | 'text' | 'arrayBuffer'
}
type AxiosLikeResponse<T> = {
data: T
}
type NuxtFetch = ReturnType<typeof useRequestFetch>
type NuxtFetchOptions = NonNullable<Parameters<NuxtFetch>[1]>
type NuxtFetchMethod = NonNullable<NuxtFetchOptions['method']>
function getFetch(): NuxtFetch {
try {
return useRequestFetch()
} catch {
return globalThis.$fetch as unknown as NuxtFetch
}
}
function normalizeUrl(url: string) {
const config = useRuntimeConfig()
const serverBase = config.public.serverApiBase
const modulesBase = config.public.modulesApiBase
if (url.startsWith(serverBase)) {
return SERVER_API_URL + url.slice(serverBase.length)
}
if (url.startsWith(modulesBase)) {
return MODULES_API_URL + url.slice(modulesBase.length)
}
return url
}
function mergeHeaders(headers?: Record<string, any>) {
const config = useRuntimeConfig()
const tenantId = headers?.TenantId ?? headers?.tenantId ?? getTenantId(String(config.public.tenantId))
const token = getToken()
const merged: Record<string, string> = {
TenantId: String(tenantId)
}
if (token) {
merged.Authorization = token
}
if (headers) {
for (const [key, value] of Object.entries(headers)) {
if (value === undefined || value === null) continue
merged[key] = String(value)
}
}
return merged
}
function withDefaultModuleBase(url: string) {
if (url.startsWith('http://') || url.startsWith('https://')) return url
if (url.startsWith('/api/')) return url
if (url.startsWith(SERVER_API_URL) || url.startsWith(MODULES_API_URL)) return url
if (!url.startsWith('/')) return MODULES_API_URL + '/' + url
return MODULES_API_URL + url
}
async function request<T>(
method: NuxtFetchMethod,
url: string,
body?: any,
config: RequestConfig = {}
): Promise<AxiosLikeResponse<T>> {
const $fetch = getFetch()
const normalized = withDefaultModuleBase(normalizeUrl(url))
const data = await $fetch<T>(normalized, {
method,
query: config.params,
body: body ?? config.data,
headers: mergeHeaders(config.headers),
responseType: config.responseType as any
})
return { data }
}
const requestClient = {
get: <T>(url: string, config?: RequestConfig) => request<T>('GET', url, undefined, config),
delete: <T>(url: string, config?: RequestConfig) => request<T>('DELETE', url, undefined, config),
post: <T>(url: string, data?: any, config?: RequestConfig) => request<T>('POST', url, data, config),
put: <T>(url: string, data?: any, config?: RequestConfig) => request<T>('PUT', url, data, config)
}
export default requestClient

42
app/utils/token-util.ts Normal file
View File

@@ -0,0 +1,42 @@
const TOKEN_KEY = 'AccessToken'
const TOKEN_EVENT = 'auth-token-changed'
function notifyTokenChange() {
if (!import.meta.client) return
try {
window.dispatchEvent(new Event(TOKEN_EVENT))
} catch {
// ignore
}
}
export function setToken(token?: string, remember?: boolean) {
if (!token || !import.meta.client) return
try {
const storage = remember ? localStorage : sessionStorage
storage.setItem(TOKEN_KEY, token)
notifyTokenChange()
} catch {
// ignore storage errors
}
}
export function getToken() {
if (!import.meta.client) return ''
try {
return localStorage.getItem(TOKEN_KEY) || sessionStorage.getItem(TOKEN_KEY) || ''
} catch {
return ''
}
}
export function removeToken() {
if (!import.meta.client) return
try {
localStorage.removeItem(TOKEN_KEY)
sessionStorage.removeItem(TOKEN_KEY)
notifyTokenChange()
} catch {
// ignore storage errors
}
}