feat(tenant): 登录后应用租户品牌(主色/标题/Logo/版权)+ 租户表单品牌字段

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 22:32:25 +08:00
parent 64bbb0a987
commit 3746198f19
5 changed files with 75 additions and 11 deletions

View File

@ -45,3 +45,16 @@ export function register(data: {
export function changePassword(oldPassword: string, newPassword: string): Promise<unknown> {
return http.patch('/auth/me/password', { oldPassword, newPassword })
}
export interface TenantBrand {
name: string
brandName: string | null
logoUrl: string | null
themeColor: string | null
copyright: string | null
}
/** 当前租户品牌。 */
export function fetchBrand(signal?: AbortSignal): Promise<TenantBrand> {
return http.get<TenantBrand>('/auth/brand', { signal })
}

View File

@ -6,6 +6,10 @@ export interface Tenant {
name: string
status: 'enabled' | 'disabled'
remark: string | null
brandName: string | null
logoUrl: string | null
themeColor: string | null
copyright: string | null
createdAt: number
}
@ -32,11 +36,23 @@ export function listTenants(query: TenantListQuery, signal?: AbortSignal): Promi
return http.get<TenantListResult>('/tenants', { params, signal })
}
export function createTenant(data: { code: string; name: string; status?: string; remark?: string }): Promise<Tenant> {
export function createTenant(data: {
code: string
name: string
status?: string
remark?: string
brandName?: string
logoUrl?: string
themeColor?: string
copyright?: string
}): Promise<Tenant> {
return http.post<Tenant>('/tenants', data)
}
export function updateTenant(id: string, data: { name?: string; status?: string; remark?: string }): Promise<Tenant> {
export function updateTenant(
id: string,
data: { name?: string; status?: string; remark?: string; brandName?: string; logoUrl?: string; themeColor?: string; copyright?: string },
): Promise<Tenant> {
return http.patch<Tenant>(`/tenants/${id}`, data)
}

View File

@ -63,7 +63,7 @@
</keep-alive>
</router-view>
</div>
<ty-footer :fixed="app.footerFixed" />
<ty-footer :fixed="app.footerFixed" :copyright="copyright" />
</main>
</div>
@ -81,7 +81,7 @@ import { useAuthStore } from '@/stores/modules/auth'
import { useMenuStore } from '@/stores/modules/menu'
import { useDictStore } from '@/stores/modules/dict'
import { usePermissionStore } from '@/stores/modules/permission'
import logo from '@/assets/images/logo.png'
import defaultLogo from '@/assets/images/logo.png'
import type { LayoutSettings } from '@/config/layout'
import type { ResourceNode } from '@/types/resource'
import type { BreadcrumbItem } from '@/components/ty-breadcrumb.vue'
@ -100,16 +100,24 @@ const route = useRoute()
const router = useRouter()
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
const title = '投云科技'
const title = computed(() => auth.brand?.brandName || auth.brand?.name || '投云科技')
const logo = computed(() => auth.brand?.logoUrl || defaultLogo)
const copyright = computed(() => auth.brand?.copyright || '云南投云科技有限公司')
const HEADER_HEIGHT = 56
const menus = computed(() => menuStore.menus)
const settingsOpen = ref(false)
// 登录后拉取菜单、字典与权限码
onMounted(() => {
// 登录后拉取菜单、字典、权限码与租户品牌
onMounted(async () => {
menuStore.fetch()
dictStore.fetch()
permissionStore.fetch()
try {
await auth.fetchBrandState()
if (auth.brand?.themeColor) app.themeColor = auth.brand.themeColor
} catch {
// 品牌拉取失败则用默认
}
})
const activePath = computed(() => route.path)

View File

@ -23,6 +23,7 @@ import { createTenant, deleteTenant, listTenants, updateTenant, type TenantListQ
const columns: TableColumn[] = [
{ prop: 'name', label: '租户名称', minWidth: 140 },
{ prop: 'code', label: '租户编码', minWidth: 140 },
{ prop: 'brandName', label: '品牌名', minWidth: 120 },
{ prop: 'status', label: '状态', width: 80, align: 'center', type: 'tag', dictCode: 'common_status' },
{ prop: 'remark', label: '备注', minWidth: 160 },
{ prop: 'createdAt', label: '创建时间', width: 160, type: 'datetime' },
@ -54,6 +55,10 @@ const formSchema: JsonSchema = {
default: 'enabled',
},
remark: { type: 'string', title: '备注', format: 'textarea', placeholder: '请输入备注' },
brandName: { type: 'string', title: '品牌名', placeholder: '如 投云科技' },
logoUrl: { type: 'string', title: 'Logo 地址', placeholder: '请输入 Logo URL' },
themeColor: { type: 'string', title: '主色', placeholder: '如 #2E7FA3' },
copyright: { type: 'string', title: '版权文案', placeholder: '如 云南投云科技有限公司' },
},
required: ['code', 'name'],
}
@ -71,8 +76,25 @@ function toListQuery(query: PageQuery): TenantListQuery {
const list = (query: PageQuery, signal?: AbortSignal) => listTenants(toListQuery(query), signal)
const create = (data: Record<string, any>) =>
createTenant({ code: data.code, name: data.name, status: data.status, remark: data.remark })
createTenant({
code: data.code,
name: data.name,
status: data.status,
remark: data.remark,
brandName: data.brandName,
logoUrl: data.logoUrl,
themeColor: data.themeColor,
copyright: data.copyright,
})
const update = (id: string, data: Record<string, any>) =>
updateTenant(id, { name: data.name, status: data.status, remark: data.remark })
updateTenant(id, {
name: data.name,
status: data.status,
remark: data.remark,
brandName: data.brandName,
logoUrl: data.logoUrl,
themeColor: data.themeColor,
copyright: data.copyright,
})
const remove = (id: string) => deleteTenant(id)
</script>

View File

@ -1,16 +1,21 @@
import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
import { http } from '@/api/http';
import { login as loginApi, type LoginResult } from '@/api/auth';
import { fetchBrand, login as loginApi, type LoginResult, type TenantBrand } from '@/api/auth';
import { clearTokens, getAccessToken, getMustChangePassword, getRefreshToken, setMustChangePassword, setTokens } from '@/utils/token';
export const useAuthStore = defineStore('authStore', () => {
const accessToken = ref<string | null>(getAccessToken());
const refreshToken = ref<string | null>(getRefreshToken());
const mustChangePassword = ref<boolean>(getMustChangePassword());
const brand = ref<TenantBrand | null>(null);
const isAuthenticated = computed(() => !!accessToken.value);
async function fetchBrandState(): Promise<void> {
brand.value = await fetchBrand();
}
async function login(username: string, password: string, tenantCode?: string): Promise<LoginResult> {
const data = await loginApi(username, password, tenantCode);
if ('accessToken' in data) {
@ -44,5 +49,5 @@ export const useAuthStore = defineStore('authStore', () => {
setMustChangePassword(false);
}
return { accessToken, refreshToken, isAuthenticated, mustChangePassword, login, logout, reset };
return { accessToken, refreshToken, isAuthenticated, mustChangePassword, brand, login, logout, reset, fetchBrandState };
});