Merge branch 'main' of https://github.com/cedar-v/license-manager
This commit is contained in:
commit
b5d0798ee9
17 files changed
+2547
-284
No files matched your search
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"WebFetch(domain:www.figma.com)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import Axios from './https'
|
||||
|
||||
// 授权接口类型定义
|
||||
export interface License {
|
||||
id: string;
|
||||
license_code: string;
|
||||
customer_id: string;
|
||||
customer_name: string;
|
||||
description: string;
|
||||
status: 'active' | 'inactive' | 'expired';
|
||||
status_display: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
expiry_date: string;
|
||||
activation_date?: string;
|
||||
license_type: string;
|
||||
license_type_display: string;
|
||||
max_users?: number;
|
||||
current_users?: number;
|
||||
features: string[];
|
||||
license_key: string;
|
||||
hardware_id?: string;
|
||||
ip_restrictions?: string[];
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
}
|
||||
|
||||
// 授权查询请求参数
|
||||
export interface LicenseQueryRequest {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
search?: string;
|
||||
customer_id?: string;
|
||||
status?: 'active' | 'inactive' | 'expired';
|
||||
license_type?: string;
|
||||
sort?: 'created_at' | 'updated_at' | 'expiry_date' | 'activation_date';
|
||||
order?: 'asc' | 'desc';
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}
|
||||
|
||||
// 创建授权请求参数
|
||||
export interface LicenseCreateRequest {
|
||||
customer_id: string;
|
||||
description: string;
|
||||
license_type: string;
|
||||
expiry_date: string;
|
||||
max_users?: number;
|
||||
features: string[];
|
||||
ip_restrictions?: string[];
|
||||
hardware_id?: string;
|
||||
}
|
||||
|
||||
// 更新授权请求参数
|
||||
export interface LicenseUpdateRequest {
|
||||
description?: string;
|
||||
status?: 'active' | 'inactive' | 'expired';
|
||||
expiry_date?: string;
|
||||
max_users?: number;
|
||||
features?: string[];
|
||||
ip_restrictions?: string[];
|
||||
hardware_id?: string;
|
||||
}
|
||||
|
||||
// 授权统计数据
|
||||
export interface LicenseStats {
|
||||
total: number;
|
||||
active: number;
|
||||
inactive: number;
|
||||
expired: number;
|
||||
expiring_soon: number;
|
||||
}
|
||||
|
||||
// 授权查询响应
|
||||
export interface LicenseQueryResponse {
|
||||
code: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
data: {
|
||||
list: License[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 通用响应 - 泛型类型
|
||||
export interface ApiResponse<T = any> {
|
||||
code: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询授权列表
|
||||
*/
|
||||
export function getLicenses(params?: LicenseQueryRequest): Promise<LicenseQueryResponse> {
|
||||
return Axios.get('/api/licenses', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取授权详情
|
||||
*/
|
||||
export function getLicenseDetail(id: string): Promise<ApiResponse<License>> {
|
||||
return Axios.get(`/api/licenses/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建授权
|
||||
*/
|
||||
export function createLicense(data: LicenseCreateRequest): Promise<ApiResponse<License>> {
|
||||
return Axios.post('/api/licenses', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新授权
|
||||
*/
|
||||
export function updateLicense(id: string, data: LicenseUpdateRequest): Promise<ApiResponse<License>> {
|
||||
return Axios.put(`/api/licenses/${id}`, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除授权
|
||||
*/
|
||||
export function deleteLicense(id: string): Promise<ApiResponse> {
|
||||
return Axios.delete(`/api/licenses/${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活授权
|
||||
*/
|
||||
export function activateLicense(id: string): Promise<ApiResponse<License>> {
|
||||
return Axios.patch(`/api/licenses/${id}/activate`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用授权
|
||||
*/
|
||||
export function deactivateLicense(id: string): Promise<ApiResponse<License>> {
|
||||
return Axios.patch(`/api/licenses/${id}/deactivate`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取授权统计数据
|
||||
*/
|
||||
export function getLicenseStats(): Promise<ApiResponse<LicenseStats>> {
|
||||
return Axios.get('/api/licenses/stats')
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据客户获取授权列表
|
||||
*/
|
||||
export function getLicensesByCustomer(customerId: string, params?: Omit<LicenseQueryRequest, 'customer_id'>): Promise<LicenseQueryResponse> {
|
||||
return Axios.get(`/api/customers/${customerId}/licenses`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 续期授权
|
||||
*/
|
||||
export function renewLicense(id: string, expiry_date: string): Promise<ApiResponse<License>> {
|
||||
return Axios.patch(`/api/licenses/${id}/renew`, { expiry_date })
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 3.1 MiB |
@@ -77,7 +77,7 @@ body {
|
||||
[data-theme="dark"] {
|
||||
--app-bg-color: #1a1a1a;
|
||||
--app-content-bg: #2d2d2d;
|
||||
--app-text-primary: #e5eaf3;
|
||||
--app-text-primary: rgba(255, 255, 255, 1);
|
||||
--app-text-regular: #cfd3dc;
|
||||
--app-text-secondary: #a3a6ad;
|
||||
--app-border-color: #414243;
|
||||
@@ -90,7 +90,7 @@ body {
|
||||
/* Element Plus Dark Theme Adjustments */
|
||||
--el-bg-color: #2d2d2d !important;
|
||||
--el-bg-color-page: #1a1a1a !important;
|
||||
--el-text-color-primary: #e5eaf3 !important;
|
||||
--el-text-color-primary: rgba(255, 255, 255, 1) !important;
|
||||
--el-text-color-regular: #cfd3dc !important;
|
||||
--el-text-color-secondary: #a3a6ad !important;
|
||||
--el-border-color: #414243 !important;
|
||||
|
||||
@@ -92,6 +92,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { use } from 'echarts/core'
|
||||
import { useDevice } from '@/utils/useDevice'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import MobileDateRange from '@/components/common/MobileDateRange.vue'
|
||||
import { Calendar } from '@element-plus/icons-vue'
|
||||
import { getAuthorizationTrend, type TrendDataItem } from '@/api/dashboard'
|
||||
@@ -114,8 +115,9 @@ use([
|
||||
LegendComponent
|
||||
])
|
||||
|
||||
// 使用国际化
|
||||
// 使用国际化和store
|
||||
const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 快捷选择选项
|
||||
const quickOptions = computed(() => [
|
||||
@@ -158,7 +160,7 @@ const chartOption = computed(() => {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'var(--app-text-primary)',
|
||||
color: appStore.isDark ? 'rgba(255, 255, 255, 1)' : '#303133',
|
||||
fontSize: 12,
|
||||
fontFamily: 'Inter',
|
||||
margin: 12
|
||||
@@ -176,26 +178,26 @@ const chartOption = computed(() => {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
color: 'var(--app-text-primary)',
|
||||
color: appStore.isDark ? 'rgba(255, 255, 255, 1)' : '#303133',
|
||||
fontSize: 12,
|
||||
fontFamily: 'Inter',
|
||||
align: 'right'
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'var(--app-border-color)',
|
||||
color: appStore.isDark ? 'rgba(255, 255, 255, 0.12)' : '#E4E7ED',
|
||||
width: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'var(--app-content-bg)',
|
||||
borderColor: 'var(--app-content-bg)',
|
||||
backgroundColor: appStore.isDark ? 'rgba(31, 41, 53, 1)' : '#FFFFFF',
|
||||
borderColor: appStore.isDark ? 'rgba(255, 255, 255, 0.12)' : '#E4E7ED',
|
||||
borderWidth: 0.5,
|
||||
borderRadius: 4,
|
||||
textStyle: {
|
||||
color: 'var(--app-text-primary)',
|
||||
color: appStore.isDark ? 'rgba(255, 255, 255, 1)' : '#303133',
|
||||
fontSize: 12
|
||||
},
|
||||
extraCssText: 'box-shadow: 0px 4px 12px 0px rgba(59, 210, 180, 0.2); backdrop-filter: blur(4px);',
|
||||
@@ -629,4 +631,73 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* 图表组件暗模式样式 - 完美还原设计图 */
|
||||
|
||||
/* 图表卡片背景 */
|
||||
[data-theme="dark"] .license-trend-chart {
|
||||
background: rgba(31, 41, 53, 1) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
/* 图表标题 */
|
||||
[data-theme="dark"] .chart-title {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
/* 时间选择器按钮暗模式 */
|
||||
[data-theme="dark"] .time-selector :deep(.el-button--default) {
|
||||
background: #4b5563 !important;
|
||||
border-color: #6b7280 !important;
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .time-selector :deep(.el-button--default:hover) {
|
||||
background: #6b7280 !important;
|
||||
border-color: #6b7280 !important;
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .time-selector :deep(.el-button--primary) {
|
||||
background: #10b981 !important;
|
||||
border-color: #10b981 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .time-selector :deep(.el-button--primary:hover) {
|
||||
background: #059669 !important;
|
||||
border-color: #059669 !important;
|
||||
}
|
||||
|
||||
/* 日期显示器暗模式 */
|
||||
[data-theme="dark"] .date-display {
|
||||
background: #4b5563 !important;
|
||||
border-color: #6b7280 !important;
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .date-display:hover {
|
||||
border-color: #6b7280 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .date-display:focus {
|
||||
border-color: #10b981 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .date-display .date-icon {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
/* 日期选择器暗模式 */
|
||||
[data-theme="dark"] .time-selector :deep(.el-date-editor .el-input__wrapper) {
|
||||
background: #4b5563 !important;
|
||||
border-color: #6b7280 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .time-selector :deep(.el-date-editor .el-input__inner) {
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
</style>
|
||||
@@ -80,4 +80,9 @@ const iconClass = computed(() => ({
|
||||
fill: #1D1D1D;
|
||||
transition: fill 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* 暗模式下图标颜色 */
|
||||
[data-theme="dark"] .nav-icon :deep(path) {
|
||||
fill: #f9fafb !important;
|
||||
}
|
||||
</style>
|
||||
@@ -481,4 +481,85 @@ const handleUserClick = () => {
|
||||
left: 64px !important; /* 侧边栏收起状态,使用!important确保生效 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* 顶部导航栏暗模式样式 - 完美还原设计图 */
|
||||
|
||||
/* 导航栏背景和边框 */
|
||||
[data-theme="dark"] .nav-content {
|
||||
background: rgba(31, 41, 53, 1) !important;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
/* 侧边栏切换按钮暗模式 */
|
||||
[data-theme="dark"] .sidebar-toggle-btn:hover {
|
||||
background: rgba(16, 185, 129, 0.15) !important;
|
||||
}
|
||||
|
||||
/* 面包屑导航暗模式 */
|
||||
[data-theme="dark"] .breadcrumb {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .breadcrumb-current.page-title,
|
||||
[data-theme="dark"] .page-title {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .breadcrumb-link {
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .breadcrumb-link:hover {
|
||||
color: #059669 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .breadcrumb-current {
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .breadcrumb-separator {
|
||||
color: rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
/* 分割线暗模式 */
|
||||
[data-theme="dark"] .divider {
|
||||
background: rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
/* 操作按钮暗模式 */
|
||||
[data-theme="dark"] .action-btn {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .action-btn:hover {
|
||||
background: rgba(16, 185, 129, 0.15) !important;
|
||||
}
|
||||
|
||||
/* 通知徽章暗模式 */
|
||||
[data-theme="dark"] .notification-badge {
|
||||
background: #10b981 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: rgba(31, 41, 53, 1) !important;
|
||||
}
|
||||
|
||||
/* 导航栏图标暗模式 */
|
||||
[data-theme="dark"] .sidebar-toggle-btn,
|
||||
[data-theme="dark"] .user-avatar,
|
||||
[data-theme="dark"] .action-btn {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sidebar-toggle-btn :deep(svg),
|
||||
[data-theme="dark"] .user-avatar :deep(svg),
|
||||
[data-theme="dark"] .action-btn :deep(svg) {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sidebar-toggle-btn :deep(.nav-icon),
|
||||
[data-theme="dark"] .user-avatar :deep(.nav-icon),
|
||||
[data-theme="dark"] .action-btn :deep(.nav-icon) {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
</style>
|
||||
@@ -405,4 +405,82 @@ const handleNavClick = (item: NavItem, event: Event) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* 侧边栏暗模式样式 - 完美还原设计图 */
|
||||
|
||||
/* 侧边栏背景和边框 */
|
||||
[data-theme="dark"] .sidebar {
|
||||
background: rgba(31, 41, 53, 1) !important;
|
||||
border-right-color: rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
/* 侧边栏头部分割线 */
|
||||
[data-theme="dark"] .sidebar__header {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
/* Logo文字颜色 */
|
||||
[data-theme="dark"] .logo-text {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
/* 导航链接暗模式 */
|
||||
[data-theme="dark"] .nav-link {
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-link:hover {
|
||||
background: rgba(16, 185, 129, 0.15) !important;
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-link--active {
|
||||
background: rgba(16, 185, 129, 0.2) !important;
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-link--active .nav-text {
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
/* 导航图标暗模式 */
|
||||
[data-theme="dark"] .nav-icon {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-link:hover .nav-icon,
|
||||
[data-theme="dark"] .nav-link--active .nav-icon {
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
/* 子菜单暗模式 */
|
||||
[data-theme="dark"] .nav-sublink {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-sublink:hover {
|
||||
background: rgba(16, 185, 129, 0.15) !important;
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-sublink--active {
|
||||
background: rgba(16, 185, 129, 0.15) !important;
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
/* 底部区域暗模式 */
|
||||
[data-theme="dark"] .sidebar__footer {
|
||||
border-top-color: rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
/* 滚动条暗模式 */
|
||||
[data-theme="dark"] .sidebar__nav::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sidebar__nav::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -112,7 +112,103 @@
|
||||
},
|
||||
"licenses": {
|
||||
"title": "授权管理",
|
||||
"welcome": "欢迎你来到授权管理页面"
|
||||
"welcome": "欢迎你来到授权管理页面",
|
||||
"platform": "授权管理平台",
|
||||
"actions": {
|
||||
"query": "查询",
|
||||
"createLicense": "创建授权",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"activate": "激活",
|
||||
"deactivate": "停用",
|
||||
"renew": "续期",
|
||||
"viewDetails": "查看详情",
|
||||
"export": "导出",
|
||||
"refresh": "刷新"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索授权码、客户名称、描述",
|
||||
"selectCustomer": "下拉选择客户",
|
||||
"allCustomers": "全部客户",
|
||||
"dateRange": "选择日期范围",
|
||||
"startDate": "开始日期",
|
||||
"endDate": "结束日期"
|
||||
},
|
||||
"table": {
|
||||
"licenseCode": "授权码",
|
||||
"customerName": "客户名称",
|
||||
"description": "描述",
|
||||
"status": "状态",
|
||||
"licenseType": "授权类型",
|
||||
"expiryDate": "到期时间",
|
||||
"activationDate": "激活时间",
|
||||
"createTime": "创建时间",
|
||||
"operation": "操作",
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"status": {
|
||||
"active": "有效",
|
||||
"inactive": "无效",
|
||||
"expired": "已过期",
|
||||
"expiring": "即将过期"
|
||||
},
|
||||
"form": {
|
||||
"basicInfo": "基本信息",
|
||||
"selectCustomer": "选择客户",
|
||||
"description": "授权描述",
|
||||
"licenseType": "授权类型",
|
||||
"expiryDate": "到期日期",
|
||||
"maxUsers": "最大用户数",
|
||||
"features": "授权功能",
|
||||
"ipRestrictions": "IP限制",
|
||||
"hardwareId": "硬件ID",
|
||||
"placeholder": {
|
||||
"selectCustomer": "请选择客户",
|
||||
"enterDescription": "请输入授权描述",
|
||||
"selectLicenseType": "请选择授权类型",
|
||||
"selectExpiryDate": "请选择到期日期",
|
||||
"enterMaxUsers": "请输入最大用户数",
|
||||
"enterIpRestrictions": "请输入IP限制(一行一个)",
|
||||
"enterHardwareId": "请输入硬件ID"
|
||||
}
|
||||
},
|
||||
"validation": {
|
||||
"customerRequired": "请选择客户",
|
||||
"descriptionRequired": "请输入授权描述",
|
||||
"licenseTypeRequired": "请选择授权类型",
|
||||
"expiryDateRequired": "请选择到期日期",
|
||||
"maxUsersMin": "最大用户数不能小于1",
|
||||
"ipFormatError": "IP地址格式错误"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteTitle": "确认删除",
|
||||
"deleteMessage": "确定要删除授权 \"{code}\" 吗?",
|
||||
"activateTitle": "确认激活",
|
||||
"activateMessage": "确定要激活授权 \"{code}\" 吗?",
|
||||
"deactivateTitle": "确认停用",
|
||||
"deactivateMessage": "确定要停用授权 \"{code}\" 吗?",
|
||||
"confirm": "确定",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"message": {
|
||||
"createSuccess": "创建授权成功",
|
||||
"updateSuccess": "更新授权成功",
|
||||
"deleteSuccess": "删除授权成功",
|
||||
"activateSuccess": "激活授权成功",
|
||||
"deactivateSuccess": "停用授权成功",
|
||||
"renewSuccess": "续期授权成功",
|
||||
"loadError": "加载授权列表失败",
|
||||
"createError": "创建授权失败",
|
||||
"updateError": "更新授权失败",
|
||||
"deleteError": "删除授权失败"
|
||||
},
|
||||
"stats": {
|
||||
"total": "总授权数",
|
||||
"active": "有效授权",
|
||||
"inactive": "无效授权",
|
||||
"expired": "过期授权",
|
||||
"expiringSoon": "即将过期"
|
||||
}
|
||||
}
|
||||
},
|
||||
"customers": {
|
||||
|
||||
@@ -31,7 +31,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
},
|
||||
{
|
||||
path: "/licenses",
|
||||
component: () => import("@/views/Licenses.vue"),
|
||||
component: () => import("@/views/Licenses/index.vue"),
|
||||
meta: { title: "授权管理", requiresAuth: true }
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: 13895237362 2205451508@qq.com
|
||||
* @Date: 2025-08-01 09:32:42
|
||||
* @LastEditors: 13895237362 2205451508@qq.com
|
||||
* @LastEditTime: 2025-09-24 15:22:03
|
||||
* @LastEditTime: 2025-09-29 13:17:41
|
||||
* @FilePath: /frontend/src/views/Dashboard.vue
|
||||
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||
-->
|
||||
@@ -12,14 +12,15 @@
|
||||
<div class="content-container dashboard">
|
||||
<!-- 统计卡片区域 -->
|
||||
<div class="stats-section">
|
||||
<div class="stats-card" :class="{ 'compact-card': stat.id === 6, 'large-card': stat.id === 7 }" v-for="stat in statsData" :key="stat.id">
|
||||
<div
|
||||
class="stats-card"
|
||||
:class="{ 'compact-card': stat.id === 6, 'large-card': stat.id === 7 }"
|
||||
v-for="stat in statsData"
|
||||
:key="stat.id"
|
||||
>
|
||||
<div class="stat-icon">
|
||||
<div class="icon-circle">
|
||||
<img
|
||||
:src="stat.icon"
|
||||
:alt="stat.label"
|
||||
class="stat-icon-img"
|
||||
/>
|
||||
<img :src="stat.icon" :alt="stat.label" class="stat-icon-img" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
@@ -51,29 +52,57 @@
|
||||
<el-table
|
||||
:data="recentData"
|
||||
:loading="loading"
|
||||
style="width: 100%; height: 100%;"
|
||||
style="width: 100%; height: 100%"
|
||||
:header-row-class-name="'table-header'"
|
||||
:row-class-name="(params: any) => params.rowIndex % 2 === 1 ? 'stripe-row' : ''"
|
||||
:row-class-name="(params: any) => (params.rowIndex % 2 === 1 ? 'stripe-row' : '')"
|
||||
>
|
||||
<el-table-column :label="t('dashboard.recentLicenses.columns.serialNumber')" min-width="90" align="center">
|
||||
<el-table-column
|
||||
:label="t('dashboard.recentLicenses.columns.serialNumber')"
|
||||
min-width="90"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ $index }">
|
||||
{{ $index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="customer_name" :label="t('dashboard.recentLicenses.columns.customerName')" min-width="200" />
|
||||
<el-table-column prop="description" :label="t('dashboard.recentLicenses.columns.description')" min-width="150" />
|
||||
<el-table-column :label="t('dashboard.recentLicenses.columns.status')" width="120" align="center">
|
||||
<el-table-column
|
||||
prop="customer_name"
|
||||
:label="t('dashboard.recentLicenses.columns.customerName')"
|
||||
min-width="200"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="description"
|
||||
:label="t('dashboard.recentLicenses.columns.description')"
|
||||
min-width="150"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="t('dashboard.recentLicenses.columns.status')"
|
||||
width="120"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span
|
||||
class="status-badge"
|
||||
<span
|
||||
class="status-badge"
|
||||
:class="row.status === 1 ? 'status-valid' : 'status-invalid'"
|
||||
>
|
||||
{{ row.status === 1 ? t('dashboard.recentLicenses.statusLabels.valid') : t('dashboard.recentLicenses.statusLabels.invalid') }}
|
||||
{{
|
||||
row.status === 1
|
||||
? t('dashboard.recentLicenses.statusLabels.valid')
|
||||
: t('dashboard.recentLicenses.statusLabels.invalid')
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="end_date" :label="t('dashboard.recentLicenses.columns.expiryTime')" width="301" />
|
||||
<el-table-column prop="created_at" :label="t('dashboard.recentLicenses.columns.createTime')" width="301" />
|
||||
<el-table-column
|
||||
prop="end_date"
|
||||
:label="t('dashboard.recentLicenses.columns.expiryTime')"
|
||||
width="301"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="created_at"
|
||||
:label="t('dashboard.recentLicenses.columns.createTime')"
|
||||
width="301"
|
||||
/>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,8 +114,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Layout from '@/components/common/layout/Layout.vue';
|
||||
import LicenseTrendChart from '@/components/charts/LicenseTrendChart.vue';
|
||||
import Layout from '@/components/common/layout/Layout.vue'
|
||||
import LicenseTrendChart from '@/components/charts/LicenseTrendChart.vue'
|
||||
import { getRecentAuthorizations, type RecentAuthorizationItem } from '@/api/dashboard'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
@@ -154,7 +183,7 @@ const statsData = computed(() => [
|
||||
trend: '',
|
||||
icon: icon7
|
||||
}
|
||||
]);
|
||||
])
|
||||
|
||||
// 最近授权数据 - 响应式数据
|
||||
const recentData = ref<RecentAuthorizationItem[]>([])
|
||||
@@ -166,7 +195,7 @@ const fetchRecentAuthorizations = async () => {
|
||||
loading.value = true
|
||||
const response = await getRecentAuthorizations()
|
||||
console.log('最近授权数据:', response.data)
|
||||
recentData.value = response.data.list;
|
||||
recentData.value = response.data.list
|
||||
} catch (error: any) {
|
||||
console.error('获取最近授权数据失败:', error)
|
||||
ElMessage.error(error?.backendMessage || '获取最近授权数据失败')
|
||||
@@ -207,10 +236,10 @@ onMounted(() => {
|
||||
gap: 1.04vw; /* 20px/1920 = 1.04vw */
|
||||
margin-bottom: 2.08vw; /* 40px/1920 = 2.08vw */
|
||||
padding: 1.25vw; /* 24px/1920 = 1.25vw */
|
||||
background: linear-gradient(135deg, #1DB584 0%, #5AD8A6 100%);
|
||||
background: linear-gradient(135deg, #1db584 0%, #5ad8a6 100%);
|
||||
border-radius: 0.42vw; /* 8px/1920 = 0.42vw */
|
||||
position: relative;
|
||||
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -218,12 +247,17 @@ onMounted(() => {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(45deg, rgba(255,255,255,0.08) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, rgba(255,255,255,0.08) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.08) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.08) 75%);
|
||||
background:
|
||||
linear-gradient(45deg, rgba(255, 255, 255, 0.08) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, rgba(255, 255, 255, 0.08) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, rgba(255, 255, 255, 0.08) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, rgba(255, 255, 255, 0.08) 75%);
|
||||
background-size: 1.04vw 1.04vw; /* 20px/1920 = 1.04vw */
|
||||
background-position: 0 0, 0 0.52vw, 0.52vw -0.52vw, -0.52vw 0vw; /* 10px/1920 = 0.52vw */
|
||||
background-position:
|
||||
0 0,
|
||||
0 0.52vw,
|
||||
0.52vw -0.52vw,
|
||||
-0.52vw 0vw; /* 10px/1920 = 0.52vw */
|
||||
opacity: 0.15;
|
||||
border-radius: 0.42vw; /* 8px/1920 = 0.42vw */
|
||||
}
|
||||
@@ -236,7 +270,7 @@ onMounted(() => {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-width: 0;
|
||||
|
||||
|
||||
&:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -245,10 +279,12 @@ onMounted(() => {
|
||||
transform: translateY(-50%);
|
||||
width: 1px;
|
||||
height: 3.33vw; /* 64px/1920 = 3.33vw */
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(255, 255, 255, 0.08) 0%,
|
||||
rgba(255, 255, 255, 0.6) 49%,
|
||||
rgba(255, 255, 255, 0.08) 100%);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0.08) 0%,
|
||||
rgba(255, 255, 255, 0.6) 49%,
|
||||
rgba(255, 255, 255, 0.08) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +297,7 @@ onMounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
|
||||
.stat-icon-img {
|
||||
width: 0.83vw; /* 16px/1920 = 0.83vw */
|
||||
height: 0.83vw; /* 16px/1920 = 0.83vw */
|
||||
@@ -274,15 +310,15 @@ onMounted(() => {
|
||||
.stat-content {
|
||||
flex: 0 1 auto;
|
||||
min-width: 4.17vw; /* 80px/1920 = 4.17vw */
|
||||
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.83vw; /* 16px/1920 = 0.83vw */
|
||||
font-weight: 500;
|
||||
color: #FFFFFF;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.stat-value-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -358,7 +394,7 @@ onMounted(() => {
|
||||
|
||||
.card-header {
|
||||
padding: 1.25vw 1.25vw 0; /* 24px/1920 = 1.25vw */
|
||||
|
||||
|
||||
.card-title {
|
||||
font-size: 1.04vw; /* 20px/1920 = 1.04vw */
|
||||
font-weight: 400;
|
||||
@@ -375,7 +411,7 @@ onMounted(() => {
|
||||
overflow: auto; /* 如果内容过多,允许滚动 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
// Element Plus 表格样式重写
|
||||
:deep(.el-table) {
|
||||
border: 1px solid var(--app-border-light);
|
||||
@@ -387,42 +423,43 @@ onMounted(() => {
|
||||
|
||||
.table-header th {
|
||||
background-color: var(--app-bg-color) !important;
|
||||
|
||||
color: var(--app-text-primary);
|
||||
font-weight: 500;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
font-family: 'Source Han Sans CN', sans-serif;
|
||||
height: 48px;
|
||||
padding: 13px 20px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
||||
.el-table__body tr {
|
||||
height: 48px;
|
||||
|
||||
td {
|
||||
padding: 13px 20px;
|
||||
border-bottom: 1px solid var(--app-border-light);
|
||||
font-size: 14px;
|
||||
font-size: 14px;
|
||||
font-family: 'Source Han Sans CN', sans-serif;
|
||||
font-weight: 350;
|
||||
color: var(--app-text-primary);
|
||||
}
|
||||
color: var(--app-text-primary);
|
||||
}
|
||||
|
||||
&.stripe-row {
|
||||
background-color: var(--app-bg-color);
|
||||
}
|
||||
background-color: var(--app-bg-color);
|
||||
}
|
||||
|
||||
&:hover > td {
|
||||
background-color: var(--app-bg-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 序号列和状态列居中
|
||||
.el-table__body tr td:nth-child(1),
|
||||
.el-table__body tr td:nth-child(4) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
// 状态列防止文本溢出
|
||||
.el-table__body tr td:nth-child(4) {
|
||||
overflow: visible;
|
||||
@@ -443,15 +480,15 @@ onMounted(() => {
|
||||
white-space: nowrap;
|
||||
width: 46px;
|
||||
height: 22px;
|
||||
|
||||
|
||||
&.status-valid {
|
||||
background:#F0F5FF;
|
||||
color:#4763FF;
|
||||
background: #f0f5ff;
|
||||
color: #4763ff;
|
||||
}
|
||||
|
||||
|
||||
&.status-invalid {
|
||||
background:#FFE5E5;
|
||||
color: #E90C0C;
|
||||
background: #ffe5e5;
|
||||
color: #e90c0c;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,22 +500,22 @@ onMounted(() => {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
|
||||
.stats-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 8px;
|
||||
padding: 16px 0;
|
||||
|
||||
|
||||
&:not(:last-child)::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.stat-icon .icon-circle {
|
||||
width: 36px; /* 移动端固定大小 */
|
||||
height: 36px;
|
||||
|
||||
|
||||
.stat-icon-img {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
@@ -486,7 +523,7 @@ onMounted(() => {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.stat-content {
|
||||
.stat-value-row {
|
||||
gap: 6px;
|
||||
@@ -503,28 +540,28 @@ onMounted(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.stat-trend .trend-value {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
.content-section {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
|
||||
.table-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--app-shadow);
|
||||
}
|
||||
|
||||
|
||||
.card-header {
|
||||
padding: 16px 16px 0;
|
||||
|
||||
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.table-container {
|
||||
padding: 20px 16px 16px;
|
||||
}
|
||||
@@ -536,19 +573,19 @@ onMounted(() => {
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
|
||||
.stat-content .stat-value {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
|
||||
.card-header {
|
||||
padding: 12px 12px 0;
|
||||
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.table-container {
|
||||
padding: 16px 12px 12px;
|
||||
}
|
||||
@@ -565,7 +602,7 @@ onMounted(() => {
|
||||
display: flex; /* 桌面端使用flex布局传递高度给子组件 */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.dashboard {
|
||||
/* vw单位确保内容在2K(2560px)和4K(3840px)下按比例缩放充满屏幕 */
|
||||
overflow-x: hidden; /* 防止水平滚动条 */
|
||||
@@ -590,14 +627,188 @@ onMounted(() => {
|
||||
.stat-content .stat-value {
|
||||
font-size: max(1.46vw, 28px); /* 设置最小字体大小 */
|
||||
}
|
||||
|
||||
|
||||
.stat-content .stat-label {
|
||||
font-size: max(0.73vw, 14px);
|
||||
}
|
||||
|
||||
|
||||
.card-title {
|
||||
font-size: max(1.04vw, 20px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* 整体布局和背景 */
|
||||
|
||||
[data-theme='dark'] .dashboard {
|
||||
background: rgba(22, 29, 38, 1) !important;
|
||||
}
|
||||
|
||||
/* 统计卡片区域暗模式优化 */
|
||||
[data-theme='dark'] .stats-section {
|
||||
// background: linear-gradient(135deg, #10b981 0%, #34d399 100%) !important;
|
||||
background: rgba(1, 156, 124, 1) !important;
|
||||
box-shadow: 0 4px 20px rgba(16, 185, 129, 0.15) !important;
|
||||
}
|
||||
|
||||
/* 表格卡片暗模式 */
|
||||
[data-theme='dark'] .table-card {
|
||||
background: rgba(31, 41, 53, 1) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .card-title {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
/* 表格暗模式样式 */
|
||||
[data-theme='dark'] .table-container :deep(.el-table) {
|
||||
background: rgba(46, 59, 75, 1) !important;
|
||||
border-color: rgba(255, 255, 255, 0.12) !important;
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .table-container :deep(.el-table .table-header th) {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
color: #f9fafb !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .table-container :deep(.el-table__body tr) {
|
||||
background: rgba(46, 59, 75, 1) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .table-container :deep(.el-table__body tr td) {
|
||||
background: rgba(46, 59, 75, 1) !important;
|
||||
color: #e5e7eb !important;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
/* 条纹行样式 - 使用更高优先级的选择器 */
|
||||
[data-theme='dark'] .dashboard .table-container :deep(.el-table .el-table__body .stripe-row) {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
border-top: 1px solid rgba(46, 59, 75, 1) !important;
|
||||
border-bottom: 1px solid rgba(46, 59, 75, 1) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .dashboard .table-container :deep(.el-table .el-table__body .stripe-row td) {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
border-top: 1px solid rgba(46, 59, 75, 1) !important;
|
||||
border-bottom: 1px solid rgba(46, 59, 75, 1) !important;
|
||||
}
|
||||
|
||||
/* 非条纹行保持统一背景 */
|
||||
[data-theme='dark'] .dashboard .table-container :deep(.el-table .el-table__body tr:not(.stripe-row)) {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .dashboard .table-container :deep(.el-table .el-table__body tr:not(.stripe-row) td) {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .table-container :deep(.el-table__body tr:hover > td) {
|
||||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
/* 状态标签暗模式 */
|
||||
[data-theme='dark'] .status-badge.status-valid {
|
||||
background: #1e40af !important;
|
||||
color: #93c5fd !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .status-badge.status-invalid {
|
||||
background: #dc2626 !important;
|
||||
color: #fecaca !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* 暗模式表格样式 - 非scoped确保优先级 */
|
||||
[data-theme='dark'] .el-table {
|
||||
background: rgba(46, 59, 75, 1) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .el-table .table-header th {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
color: #f9fafb !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .el-table__header-wrapper .el-table__header th {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
color: #f9fafb !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .el-table__header th {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
color: #f9fafb !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .el-table th {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .el-table th.is-leaf {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
/* 使用更通用的选择器强制覆盖所有表头样式 */
|
||||
[data-theme='dark'] .dashboard .el-table thead th,
|
||||
[data-theme='dark'] .dashboard .el-table .el-table__header th,
|
||||
[data-theme='dark'] .dashboard .el-table .el-table__header-wrapper th,
|
||||
[data-theme='dark'] .dashboard .el-table__header-wrapper .el-table__header thead th {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
background: rgba(46, 59, 75, 1) !important;
|
||||
color: #f9fafb !important;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .el-table__body tr {
|
||||
background: rgba(46, 59, 75, 1) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .el-table__body tr td {
|
||||
background: rgba(46, 59, 75, 1) !important;
|
||||
color: #e5e7eb !important;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .el-table__body .stripe-row {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
border-top: 1px solid rgba(46, 59, 75, 1) !important;
|
||||
border-bottom: 1px solid rgba(46, 59, 75, 1) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .el-table__body .stripe-row td {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
border-top: 1px solid rgba(46, 59, 75, 1) !important;
|
||||
border-bottom: 1px solid rgba(46, 59, 75, 1) !important;
|
||||
}
|
||||
|
||||
/* 最强力的表头样式覆盖 */
|
||||
[data-theme='dark'] th {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
background: rgba(46, 59, 75, 1) !important;
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .el-table__cell {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .dashboard th,
|
||||
[data-theme='dark'] .dashboard .el-table th,
|
||||
[data-theme='dark'] .dashboard .el-table .el-table__cell {
|
||||
background-color: rgba(46, 59, 75, 1) !important;
|
||||
background: rgba(46, 59, 75, 1) !important;
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,59 +0,0 @@
|
||||
<!--
|
||||
* @Author: 13895237362 2205451508@qq.com
|
||||
* @Date: 2025-08-12 00:00:00
|
||||
* @LastEditors: 13895237362 2205451508@qq.com
|
||||
* @LastEditTime: 2025-08-12 00:00:00
|
||||
* @FilePath: /frontend/src/views/Licenses.vue
|
||||
* @Description: 授权管理页面
|
||||
-->
|
||||
<template>
|
||||
<Layout app-name="Cedar-V" :page-title="t('pages.licenses.title')">
|
||||
<!-- 页面内容 -->
|
||||
<div class="content-container">
|
||||
<h1>{{ t('pages.licenses.welcome') }}</h1>
|
||||
</div>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Layout from '@/components/common/layout/Layout.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-container {
|
||||
min-height: calc(100vh - 80px);
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.content-container {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.content-container {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.content-container {
|
||||
height: calc(100vh - 4.17vw);
|
||||
padding: 1.25vw;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="isEdit ? '编辑授权' : t('pages.licenses.actions.createLicense')"
|
||||
width="800px"
|
||||
:before-close="handleClose"
|
||||
@update:model-value="$emit('update:visible', $event)"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
class="license-form"
|
||||
>
|
||||
<div class="form-section">
|
||||
<h3 class="section-title">{{ t('pages.licenses.form.basicInfo') }}</h3>
|
||||
|
||||
<el-form-item
|
||||
:label="t('pages.licenses.form.selectCustomer')"
|
||||
prop="customer_id"
|
||||
>
|
||||
<el-select
|
||||
v-model="formData.customer_id"
|
||||
:placeholder="t('pages.licenses.form.placeholder.selectCustomer')"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
:disabled="isEdit"
|
||||
>
|
||||
<el-option
|
||||
v-for="customer in customers"
|
||||
:key="customer.id"
|
||||
:label="customer.customer_name"
|
||||
:value="customer.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
:label="t('pages.licenses.form.description')"
|
||||
prop="description"
|
||||
>
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
:placeholder="t('pages.licenses.form.placeholder.enterDescription')"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
:label="t('pages.licenses.form.licenseType')"
|
||||
prop="license_type"
|
||||
>
|
||||
<el-select
|
||||
v-model="formData.license_type"
|
||||
:placeholder="t('pages.licenses.form.placeholder.selectLicenseType')"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="标准版" value="standard" />
|
||||
<el-option label="专业版" value="professional" />
|
||||
<el-option label="企业版" value="enterprise" />
|
||||
<el-option label="定制版" value="custom" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
:label="t('pages.licenses.form.expiryDate')"
|
||||
prop="expiry_date"
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="formData.expiry_date"
|
||||
type="datetime"
|
||||
:placeholder="t('pages.licenses.form.placeholder.selectExpiryDate')"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
:disabled-date="disabledDate"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
:label="t('pages.licenses.form.maxUsers')"
|
||||
prop="max_users"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="formData.max_users"
|
||||
:min="1"
|
||||
:max="10000"
|
||||
:placeholder="t('pages.licenses.form.placeholder.enterMaxUsers')"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h3 class="section-title">高级配置</h3>
|
||||
|
||||
<el-form-item
|
||||
:label="t('pages.licenses.form.features')"
|
||||
prop="features"
|
||||
>
|
||||
<el-checkbox-group v-model="formData.features">
|
||||
<el-checkbox label="basic" value="basic">基础功能</el-checkbox>
|
||||
<el-checkbox label="advanced" value="advanced">高级功能</el-checkbox>
|
||||
<el-checkbox label="api" value="api">API接口</el-checkbox>
|
||||
<el-checkbox label="export" value="export">数据导出</el-checkbox>
|
||||
<el-checkbox label="backup" value="backup">数据备份</el-checkbox>
|
||||
<el-checkbox label="analytics" value="analytics">数据分析</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
:label="t('pages.licenses.form.ipRestrictions')"
|
||||
prop="ip_restrictions"
|
||||
>
|
||||
<el-input
|
||||
v-model="ipRestrictionsText"
|
||||
:placeholder="t('pages.licenses.form.placeholder.enterIpRestrictions')"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
@blur="handleIpRestrictionsChange"
|
||||
/>
|
||||
<div class="form-tip">多个IP地址请用换行分隔,支持CIDR格式(如:192.168.1.0/24)</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
:label="t('pages.licenses.form.hardwareId')"
|
||||
prop="hardware_id"
|
||||
>
|
||||
<el-input
|
||||
v-model="formData.hardware_id"
|
||||
:placeholder="t('pages.licenses.form.placeholder.enterHardwareId')"
|
||||
maxlength="100"
|
||||
/>
|
||||
<div class="form-tip">硬件ID用于绑定特定设备,留空表示不限制设备</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">
|
||||
{{ t('customers.actions.cancel') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ t('customers.actions.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { createLicense, updateLicense, type License, type LicenseCreateRequest, type LicenseUpdateRequest } from '@/api/license'
|
||||
import { type Customer } from '@/api/customer'
|
||||
|
||||
// Props & Emits
|
||||
interface Props {
|
||||
visible: boolean
|
||||
licenseData?: License | null
|
||||
customers: Customer[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
licenseData: null
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Refs
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitting = ref(false)
|
||||
|
||||
// Computed
|
||||
const isEdit = computed(() => !!props.licenseData)
|
||||
|
||||
// Form Data
|
||||
const formData = reactive({
|
||||
customer_id: '',
|
||||
description: '',
|
||||
license_type: '',
|
||||
expiry_date: '',
|
||||
max_users: 1,
|
||||
features: [] as string[],
|
||||
ip_restrictions: [] as string[],
|
||||
hardware_id: ''
|
||||
})
|
||||
|
||||
// IP限制文本(用于显示和编辑)
|
||||
const ipRestrictionsText = ref('')
|
||||
|
||||
// Form Rules
|
||||
const formRules: FormRules = {
|
||||
customer_id: [
|
||||
{ required: true, message: t('pages.licenses.validation.customerRequired'), trigger: 'change' }
|
||||
],
|
||||
description: [
|
||||
{ required: true, message: t('pages.licenses.validation.descriptionRequired'), trigger: 'blur' }
|
||||
],
|
||||
license_type: [
|
||||
{ required: true, message: t('pages.licenses.validation.licenseTypeRequired'), trigger: 'change' }
|
||||
],
|
||||
expiry_date: [
|
||||
{ required: true, message: t('pages.licenses.validation.expiryDateRequired'), trigger: 'change' }
|
||||
],
|
||||
max_users: [
|
||||
{ required: true, message: t('pages.licenses.validation.maxUsersMin'), trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: t('pages.licenses.validation.maxUsersMin'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// Methods
|
||||
const resetForm = () => {
|
||||
formData.customer_id = ''
|
||||
formData.description = ''
|
||||
formData.license_type = ''
|
||||
formData.expiry_date = ''
|
||||
formData.max_users = 1
|
||||
formData.features = []
|
||||
formData.ip_restrictions = []
|
||||
formData.hardware_id = ''
|
||||
ipRestrictionsText.value = ''
|
||||
}
|
||||
|
||||
const loadFormData = () => {
|
||||
if (props.licenseData) {
|
||||
formData.customer_id = props.licenseData.customer_id
|
||||
formData.description = props.licenseData.description
|
||||
formData.license_type = props.licenseData.license_type
|
||||
formData.expiry_date = props.licenseData.expiry_date
|
||||
formData.max_users = props.licenseData.max_users || 1
|
||||
formData.features = [...(props.licenseData.features || [])]
|
||||
formData.ip_restrictions = [...(props.licenseData.ip_restrictions || [])]
|
||||
formData.hardware_id = props.licenseData.hardware_id || ''
|
||||
ipRestrictionsText.value = formData.ip_restrictions.join('\n')
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
|
||||
const handleIpRestrictionsChange = () => {
|
||||
formData.ip_restrictions = ipRestrictionsText.value
|
||||
.split('\n')
|
||||
.map(ip => ip.trim())
|
||||
.filter(ip => ip.length > 0)
|
||||
}
|
||||
|
||||
const disabledDate = (time: Date) => {
|
||||
return time.getTime() < Date.now() - 24 * 60 * 60 * 1000
|
||||
}
|
||||
|
||||
const validateForm = async (): Promise<boolean> => {
|
||||
if (!formRef.value) return false
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
// 验证IP地址格式
|
||||
if (formData.ip_restrictions.length > 0) {
|
||||
const ipRegex = /^((\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?|(\d{1,3}\.){3}\d{1,3})$/
|
||||
for (const ip of formData.ip_restrictions) {
|
||||
if (!ipRegex.test(ip)) {
|
||||
ElMessage.error(`${t('pages.licenses.validation.ipFormatError')}: ${ip}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!(await validateForm())) return
|
||||
|
||||
try {
|
||||
submitting.value = true
|
||||
|
||||
if (isEdit.value && props.licenseData) {
|
||||
// 编辑授权
|
||||
const updateData: LicenseUpdateRequest = {
|
||||
description: formData.description,
|
||||
expiry_date: formData.expiry_date,
|
||||
max_users: formData.max_users,
|
||||
features: formData.features,
|
||||
ip_restrictions: formData.ip_restrictions.length > 0 ? formData.ip_restrictions : undefined,
|
||||
hardware_id: formData.hardware_id || undefined
|
||||
}
|
||||
|
||||
await updateLicense(props.licenseData.id, updateData)
|
||||
ElMessage.success(t('pages.licenses.message.updateSuccess'))
|
||||
} else {
|
||||
// 创建授权
|
||||
const createData: LicenseCreateRequest = {
|
||||
customer_id: formData.customer_id,
|
||||
description: formData.description,
|
||||
license_type: formData.license_type,
|
||||
expiry_date: formData.expiry_date,
|
||||
max_users: formData.max_users,
|
||||
features: formData.features,
|
||||
ip_restrictions: formData.ip_restrictions.length > 0 ? formData.ip_restrictions : undefined,
|
||||
hardware_id: formData.hardware_id || undefined
|
||||
}
|
||||
|
||||
await createLicense(createData)
|
||||
ElMessage.success(t('pages.licenses.message.createSuccess'))
|
||||
}
|
||||
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('Failed to save license:', error)
|
||||
ElMessage.error(isEdit.value
|
||||
? t('pages.licenses.message.updateError')
|
||||
: t('pages.licenses.message.createError')
|
||||
)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
// 延迟重置表单,避免关闭动画期间表单闪烁
|
||||
setTimeout(() => {
|
||||
formRef.value?.clearValidate()
|
||||
resetForm()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// Watchers
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible) {
|
||||
loadFormData()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/styles/variables.scss';
|
||||
|
||||
.license-form {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 32px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: $text-color-primary;
|
||||
margin: 0 0 16px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid $border-color-lighter;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: $text-color-secondary;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox-group) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
.el-checkbox {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
box-shadow: 0 0 0 1px $border-color-base inset;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px $primary-color inset;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
box-shadow: 0 0 0 1px $primary-color inset;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
box-shadow: 0 0 0 1px $border-color-base inset;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px $primary-color inset;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1px $primary-color inset;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
:deep(.el-dialog) {
|
||||
width: 95% !important;
|
||||
margin: 2.5vh auto;
|
||||
}
|
||||
|
||||
.license-form {
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
text-align: left !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,798 @@
|
||||
<!--
|
||||
* @Author: 13895237362 2205451508@qq.com
|
||||
* @Date: 2025-08-12 00:00:00
|
||||
* @LastEditors: 13895237362 2205451508@qq.com
|
||||
* @LastEditTime: 2025-09-29 17:29:42
|
||||
* @FilePath: /frontend/src/views/Licenses.vue
|
||||
* @Description: 授权管理页面
|
||||
-->
|
||||
<template>
|
||||
<Layout app-name="Cedar-V" :page-title="t('pages.licenses.title')">
|
||||
<div class="license-container">
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="main-content">
|
||||
<!-- 背景图片 -->
|
||||
<div class="background-section">
|
||||
<!-- 中央标题 -->
|
||||
<div class="center-title">
|
||||
<h1 class="platform-title">{{ t('pages.licenses.platform') }}</h1>
|
||||
</div>
|
||||
|
||||
<!-- 操作区域 -->
|
||||
<div class="action-section">
|
||||
<!-- 客户选择下拉框和操作按钮 -->
|
||||
<div class="action-row">
|
||||
<!-- 客户选择下拉框 -->
|
||||
<div class="customer-select-wrapper">
|
||||
<el-select
|
||||
v-model="selectedCustomer"
|
||||
:placeholder="t('pages.licenses.search.selectCustomer')"
|
||||
clearable
|
||||
filterable
|
||||
size="large"
|
||||
class="customer-select"
|
||||
@change="handleCustomerChange"
|
||||
>
|
||||
<el-option
|
||||
:label="t('pages.licenses.search.allCustomers')"
|
||||
value=""
|
||||
/>
|
||||
<el-option
|
||||
v-for="customer in customers"
|
||||
:key="customer.id"
|
||||
:label="customer.customer_name"
|
||||
:value="customer.id"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="action-btn query-btn"
|
||||
@click="handleQuery"
|
||||
>
|
||||
{{ t('pages.licenses.actions.query') }}
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="action-btn create-btn"
|
||||
@click="handleCreateLicense"
|
||||
>
|
||||
{{ t('pages.licenses.actions.createLicense') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 授权列表区域 -->
|
||||
<div v-if="showLicenseList" class="license-list-section">
|
||||
<div class="list-header">
|
||||
<div class="search-controls">
|
||||
<el-input
|
||||
v-model="searchText"
|
||||
:placeholder="t('pages.licenses.search.placeholder')"
|
||||
prefix-icon="Search"
|
||||
clearable
|
||||
size="large"
|
||||
class="search-input"
|
||||
@input="handleSearch"
|
||||
/>
|
||||
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
:range-separator="t('pages.licenses.search.dateRange')"
|
||||
:start-placeholder="t('pages.licenses.search.startDate')"
|
||||
:end-placeholder="t('pages.licenses.search.endDate')"
|
||||
size="large"
|
||||
class="date-picker"
|
||||
@change="handleDateRangeChange"
|
||||
/>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="refreshLicenseList"
|
||||
>
|
||||
{{ t('pages.licenses.actions.refresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 授权表格 -->
|
||||
<div class="table-container">
|
||||
<el-table
|
||||
:data="licenseList"
|
||||
v-loading="loading"
|
||||
:element-loading-text="t('pages.licenses.table.loading')"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
class="license-table"
|
||||
>
|
||||
<el-table-column
|
||||
prop="license_code"
|
||||
:label="t('pages.licenses.table.licenseCode')"
|
||||
width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<el-table-column
|
||||
prop="customer_name"
|
||||
:label="t('pages.licenses.table.customerName')"
|
||||
width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<el-table-column
|
||||
prop="description"
|
||||
:label="t('pages.licenses.table.description')"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<el-table-column
|
||||
prop="status"
|
||||
:label="t('pages.licenses.table.status')"
|
||||
width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="getStatusType(row.status)"
|
||||
effect="plain"
|
||||
>
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="license_type_display"
|
||||
:label="t('pages.licenses.table.licenseType')"
|
||||
width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
|
||||
<el-table-column
|
||||
prop="expiry_date"
|
||||
:label="t('pages.licenses.table.expiryDate')"
|
||||
width="180"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.expiry_date) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="created_at"
|
||||
:label="t('pages.licenses.table.createTime')"
|
||||
width="180"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
:label="t('pages.licenses.table.operation')"
|
||||
width="200"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
{{ t('pages.licenses.actions.edit') }}
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="row.status === 'inactive'"
|
||||
type="success"
|
||||
size="small"
|
||||
@click="handleActivate(row)"
|
||||
>
|
||||
{{ t('pages.licenses.actions.activate') }}
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="row.status === 'active'"
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="handleDeactivate(row)"
|
||||
>
|
||||
{{ t('pages.licenses.actions.deactivate') }}
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
{{ t('pages.licenses.actions.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑授权对话框 -->
|
||||
<LicenseForm
|
||||
v-model:visible="formDialogVisible"
|
||||
:license-data="editingLicense"
|
||||
:customers="customers"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import Layout from '@/components/common/layout/Layout.vue'
|
||||
import LicenseForm from './LicenseForm.vue'
|
||||
import { getLicenses, deleteLicense, activateLicense, deactivateLicense, type License, type LicenseQueryRequest } from '@/api/license'
|
||||
import { getCustomers, type Customer } from '@/api/customer'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// 响应式数据
|
||||
const selectedCustomer = ref<string>('')
|
||||
const customers = ref<Customer[]>([])
|
||||
const showLicenseList = ref(false)
|
||||
const searchText = ref('')
|
||||
const dateRange = ref<[Date, Date] | null>(null)
|
||||
const loading = ref(false)
|
||||
const licenseList = ref<License[]>([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const formDialogVisible = ref(false)
|
||||
const editingLicense = ref<License | null>(null)
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive<LicenseQueryRequest>({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
search: '',
|
||||
customer_id: '',
|
||||
sort: 'created_at',
|
||||
order: 'desc'
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
// const formattedCustomers = computed(() => {
|
||||
// return customers.value.filter(customer => customer.status === 'active')
|
||||
// })
|
||||
|
||||
// 方法
|
||||
const loadCustomers = async () => {
|
||||
try {
|
||||
const response = await getCustomers({ status: 'active', page_size: 1000 })
|
||||
customers.value = response.data.list
|
||||
} catch (error) {
|
||||
console.error('Failed to load customers:', error)
|
||||
ElMessage.error('加载客户列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const loadLicenseList = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await getLicenses(queryParams)
|
||||
licenseList.value = response.data.list
|
||||
total.value = response.data.total
|
||||
currentPage.value = response.data.page
|
||||
} catch (error) {
|
||||
console.error('Failed to load licenses:', error)
|
||||
ElMessage.error(t('pages.licenses.message.loadError'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCustomerChange = (value: string) => {
|
||||
queryParams.customer_id = value
|
||||
}
|
||||
|
||||
const handleQuery = () => {
|
||||
showLicenseList.value = true
|
||||
queryParams.page = 1
|
||||
currentPage.value = 1
|
||||
loadLicenseList()
|
||||
}
|
||||
|
||||
const handleCreateLicense = () => {
|
||||
editingLicense.value = null
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
queryParams.search = searchText.value
|
||||
queryParams.page = 1
|
||||
currentPage.value = 1
|
||||
if (showLicenseList.value) {
|
||||
loadLicenseList()
|
||||
}
|
||||
}
|
||||
|
||||
const handleDateRangeChange = (value: [Date, Date] | null) => {
|
||||
if (value) {
|
||||
queryParams.start_date = value[0].toISOString().split('T')[0]
|
||||
queryParams.end_date = value[1].toISOString().split('T')[0]
|
||||
} else {
|
||||
queryParams.start_date = undefined
|
||||
queryParams.end_date = undefined
|
||||
}
|
||||
|
||||
if (showLicenseList.value) {
|
||||
queryParams.page = 1
|
||||
currentPage.value = 1
|
||||
loadLicenseList()
|
||||
}
|
||||
}
|
||||
|
||||
const refreshLicenseList = () => {
|
||||
if (showLicenseList.value) {
|
||||
loadLicenseList()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
queryParams.page_size = size
|
||||
queryParams.page = 1
|
||||
currentPage.value = 1
|
||||
loadLicenseList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
queryParams.page = page
|
||||
loadLicenseList()
|
||||
}
|
||||
|
||||
const handleEdit = (license: License) => {
|
||||
editingLicense.value = license
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleActivate = async (license: License) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('pages.licenses.confirm.activateMessage', { code: license.license_code }),
|
||||
t('pages.licenses.confirm.activateTitle'),
|
||||
{
|
||||
confirmButtonText: t('pages.licenses.confirm.confirm'),
|
||||
cancelButtonText: t('pages.licenses.confirm.cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await activateLicense(license.id)
|
||||
ElMessage.success(t('pages.licenses.message.activateSuccess'))
|
||||
loadLicenseList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('Failed to activate license:', error)
|
||||
ElMessage.error('激活授权失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeactivate = async (license: License) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('pages.licenses.confirm.deactivateMessage', { code: license.license_code }),
|
||||
t('pages.licenses.confirm.deactivateTitle'),
|
||||
{
|
||||
confirmButtonText: t('pages.licenses.confirm.confirm'),
|
||||
cancelButtonText: t('pages.licenses.confirm.cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await deactivateLicense(license.id)
|
||||
ElMessage.success(t('pages.licenses.message.deactivateSuccess'))
|
||||
loadLicenseList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('Failed to deactivate license:', error)
|
||||
ElMessage.error('停用授权失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (license: License) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('pages.licenses.confirm.deleteMessage', { code: license.license_code }),
|
||||
t('pages.licenses.confirm.deleteTitle'),
|
||||
{
|
||||
confirmButtonText: t('pages.licenses.confirm.confirm'),
|
||||
cancelButtonText: t('pages.licenses.confirm.cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
await deleteLicense(license.id)
|
||||
ElMessage.success(t('pages.licenses.message.deleteSuccess'))
|
||||
loadLicenseList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('Failed to delete license:', error)
|
||||
ElMessage.error(t('pages.licenses.message.deleteError'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleFormSuccess = () => {
|
||||
formDialogVisible.value = false
|
||||
if (showLicenseList.value) {
|
||||
loadLicenseList()
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'success'
|
||||
case 'inactive':
|
||||
return 'info'
|
||||
case 'expired':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
return t(`pages.licenses.status.${status}`)
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return '--'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadCustomers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/styles/variables.scss';
|
||||
|
||||
.license-container {
|
||||
height: calc(100vh - 80px);
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.background-section {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-image: url('/src/assets/images/license-bg.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: height 0.3s ease;
|
||||
|
||||
&.collapsed {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.background-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.center-title {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.platform-title {
|
||||
font-family: 'PangMenZhengDao', 'Source Han Sans CN', sans-serif;
|
||||
font-size: 56px;
|
||||
font-weight: 400;
|
||||
color: #1C1C28;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
letter-spacing: 0.06em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.action-section {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 24px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
|
||||
// 重置所有子元素的基线对齐
|
||||
> * {
|
||||
vertical-align: top;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.customer-select-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
max-width: 900px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.customer-select {
|
||||
width: 100%;
|
||||
|
||||
// 重置Element Plus的默认margin
|
||||
:deep(.el-select) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// 重置el-input的默认样式
|
||||
:deep(.el-input) {
|
||||
margin: 0;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
height: 60px !important;
|
||||
font-size: 20px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid #E3E3E3;
|
||||
border-radius: 8px;
|
||||
padding: 0 48px 0 16px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
|
||||
.el-input__inner {
|
||||
color: #A9A9AF;
|
||||
font-size: 20px;
|
||||
line-height: 1.5;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&::placeholder {
|
||||
color: #A9A9AF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// 确保选择器本身没有额外的margin或padding
|
||||
:deep(.el-select__wrapper) {
|
||||
margin: 0;
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
height: 40px !important;
|
||||
padding: 0 24px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.15em;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
vertical-align: top;
|
||||
|
||||
// 重置Element Plus按钮的默认样式
|
||||
&:deep(.el-button) {
|
||||
margin: 0;
|
||||
height: 40px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&.query-btn {
|
||||
min-width: 91px;
|
||||
background-color: $primary-color;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($primary-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&.create-btn {
|
||||
min-width: 137px;
|
||||
background-color: $primary-color;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($primary-color, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.license-list-section {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid $border-color-light;
|
||||
}
|
||||
|
||||
.search-controls {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
min-width: 300px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
padding: 0 24px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.license-table {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
padding: 16px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.platform-title {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.action-section {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.customer-select-wrapper {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
.search-input,
|
||||
.date-picker {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.table-container {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.platform-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
height: 32px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.customer-select {
|
||||
:deep(.el-input__wrapper) {
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+501
-140
@@ -1,18 +1,26 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<!-- 背景层 -->
|
||||
<div class="background-layer"></div>
|
||||
<div class="background-layer" :style="backgroundStyle"></div>
|
||||
|
||||
<!-- Logo -->
|
||||
<div class="logo-section">
|
||||
<div class="logo-icon">
|
||||
<svg width="54" height="52" viewBox="0 0 54 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
width="54"
|
||||
height="52"
|
||||
viewBox="0 0 54 52"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M26.3125 11.4814L22.25 19.5947V22.7148L27.1191 13.0576L29.7393 18.1777L18.7988 40H0L13.5938 22.8037H14L13.8125 23.1201L7.46875 33.541H17.8438V16.9111L7.1875 25.6475L18.0312 10.1406L11.625 14.4463V14.2588L20.4375 0L26.3125 11.4814Z"
|
||||
fill="#019C7C" />
|
||||
fill="#019C7C"
|
||||
/>
|
||||
<path
|
||||
d="M34.5498 39.9996H28.75L24.5938 32.8864L27.125 27.6246L34.5498 39.9996ZM41 39.9996H36.2705L27.9346 25.941L30.7188 20.1559L41 39.9996Z"
|
||||
fill="#146B59" />
|
||||
fill="#146B59"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="logo-text">Cedar-V</span>
|
||||
@@ -33,18 +41,37 @@
|
||||
<div class="login-card">
|
||||
<h1 class="title">{{ t('login.title') }}</h1>
|
||||
<p class="subtitle">{{ t('login.subtitle') }}</p>
|
||||
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" @submit.prevent="handleLogin"
|
||||
class="login-form" size="large">
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
@submit.prevent="handleLogin"
|
||||
class="login-form"
|
||||
size="large"
|
||||
>
|
||||
<!-- 用户名输入框 -->
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="loginForm.username" :placeholder="t('login.usernamePlaceholder')" :prefix-icon="User"
|
||||
clearable class="login-input" />
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
:placeholder="t('login.usernamePlaceholder')"
|
||||
:prefix-icon="User"
|
||||
clearable
|
||||
class="login-input"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="loginForm.password" type="password" :placeholder="t('login.passwordPlaceholder')"
|
||||
:prefix-icon="Lock" show-password clearable class="login-input" @keyup.enter="handleLogin" />
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
:placeholder="t('login.passwordPlaceholder')"
|
||||
:prefix-icon="Lock"
|
||||
show-password
|
||||
clearable
|
||||
class="login-input"
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 记住密码和忘记密码 -->
|
||||
@@ -52,14 +79,25 @@
|
||||
<el-checkbox v-model="rememberMe" size="default">
|
||||
{{ t('login.remember') }}
|
||||
</el-checkbox>
|
||||
<el-link type="info" @click="handleForgotPassword" :underline="false" class="forgot-link">
|
||||
<el-link
|
||||
type="info"
|
||||
@click="handleForgotPassword"
|
||||
:underline="false"
|
||||
class="forgot-link"
|
||||
>
|
||||
{{ t('login.forgotPassword') }}
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleLogin" :loading="loading" class="login-button" size="large">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleLogin"
|
||||
:loading="loading"
|
||||
class="login-button"
|
||||
size="large"
|
||||
>
|
||||
{{ t('login.submit') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
@@ -71,29 +109,48 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { User, Lock } from '@element-plus/icons-vue';
|
||||
import { Login, type LoginRequest } from "@/api/user";
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { User, Lock } from '@element-plus/icons-vue'
|
||||
import { Login, type LoginRequest } from '@/api/user'
|
||||
import { changeLanguage, type SupportedLocale } from '@/utils/language'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
// 导入背景图片
|
||||
import lightBgImg from '@/assets/images/login-background.png'
|
||||
import darkBgImg from '@/assets/images/login-background-dark.png'
|
||||
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
const loginFormRef = ref<FormInstance>();
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 动态背景图样式
|
||||
const backgroundStyle = computed(() => {
|
||||
const isDark = appStore.theme === 'dark'
|
||||
const backgroundImage = isDark ? `url(${darkBgImg})` : `url(${lightBgImg})`
|
||||
|
||||
return {
|
||||
backgroundImage,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}
|
||||
})
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const loginFormRef = ref<FormInstance>()
|
||||
|
||||
// 当前语言
|
||||
const currentLanguage = ref(locale.value);
|
||||
const loading = ref(false);
|
||||
const currentLanguage = ref(locale.value)
|
||||
const loading = ref(false)
|
||||
|
||||
// 登录表单数据
|
||||
const loginForm = reactive({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const rememberMe = ref(false);
|
||||
const rememberMe = ref(false)
|
||||
|
||||
// 表单验证规则
|
||||
const loginRules: FormRules = {
|
||||
@@ -105,82 +162,85 @@ const loginRules: FormRules = {
|
||||
{ required: true, message: () => t('login.error.passwordRequired'), trigger: 'blur' },
|
||||
{ min: 6, message: () => t('login.error.passwordMinLength'), trigger: 'blur' }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// 切换语言
|
||||
function handleLanguageChange(lang: string) {
|
||||
// 使用统一的语言管理器切换语言
|
||||
changeLanguage(lang as SupportedLocale);
|
||||
currentLanguage.value = lang;
|
||||
changeLanguage(lang as SupportedLocale)
|
||||
currentLanguage.value = lang
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 确保主题正确初始化
|
||||
appStore.initTheme()
|
||||
|
||||
// 记住密码功能
|
||||
const saved = localStorage.getItem("loginInfo");
|
||||
const saved = localStorage.getItem('loginInfo')
|
||||
if (saved) {
|
||||
const info = JSON.parse(saved);
|
||||
loginForm.username = info.username || "";
|
||||
loginForm.password = info.password || "";
|
||||
rememberMe.value = true;
|
||||
const info = JSON.parse(saved)
|
||||
loginForm.username = info.username || ''
|
||||
loginForm.password = info.password || ''
|
||||
rememberMe.value = true
|
||||
}
|
||||
|
||||
// 同步当前语言状态
|
||||
currentLanguage.value = locale.value;
|
||||
});
|
||||
currentLanguage.value = locale.value
|
||||
})
|
||||
|
||||
function handleForgotPassword() {
|
||||
ElMessage.info(t('login.forgotPasswordAlert'));
|
||||
ElMessage.info(t('login.forgotPasswordAlert'))
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
if (!loginFormRef.value) return;
|
||||
if (!loginFormRef.value) return
|
||||
|
||||
try {
|
||||
const valid = await loginFormRef.value.validate();
|
||||
if (!valid) return;
|
||||
const valid = await loginFormRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
loading.value = true;
|
||||
loading.value = true
|
||||
|
||||
const loginData: LoginRequest = {
|
||||
username: loginForm.username,
|
||||
password: loginForm.password
|
||||
};
|
||||
}
|
||||
|
||||
const response = await Login(loginData);
|
||||
console.log('登录响应:', response);
|
||||
const response = await Login(loginData)
|
||||
console.log('登录响应:', response)
|
||||
|
||||
// 检查响应是否成功 (支持 code 000000)
|
||||
if (response.code === '000000') {
|
||||
// 登录成功
|
||||
if (response.data && response.data.token) {
|
||||
localStorage.setItem("token", response.data.token);
|
||||
localStorage.setItem('token', response.data.token)
|
||||
}
|
||||
|
||||
if (rememberMe.value) {
|
||||
localStorage.setItem(
|
||||
"loginInfo",
|
||||
'loginInfo',
|
||||
JSON.stringify({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password,
|
||||
password: loginForm.password
|
||||
})
|
||||
);
|
||||
)
|
||||
} else {
|
||||
localStorage.removeItem("loginInfo");
|
||||
localStorage.removeItem('loginInfo')
|
||||
}
|
||||
|
||||
ElMessage.success(response.message);
|
||||
router.push("/dashboard");
|
||||
ElMessage.success(response.message)
|
||||
router.push('/dashboard')
|
||||
} else {
|
||||
ElMessage.error(response.message);
|
||||
ElMessage.error(response.message)
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 使用后端返回的错误信息
|
||||
let errorMessage = error.backendMessage || error.response?.data?.message || error.message;
|
||||
let errorMessage = error.backendMessage || error.response?.data?.message || error.message
|
||||
if (errorMessage) {
|
||||
ElMessage.error(errorMessage);
|
||||
ElMessage.error(errorMessage)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -197,7 +257,9 @@ async function handleLogin() {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
background-color: var(--app-bg-color);
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
@include mobile {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -210,14 +272,10 @@ async function handleLogin() {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('@/assets/images/login-background.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
z-index: 1;
|
||||
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@include mobile {
|
||||
background-image: url('@/assets/images/login-background.png');
|
||||
filter: blur(2px);
|
||||
position: fixed;
|
||||
}
|
||||
@@ -231,14 +289,14 @@ async function handleLogin() {
|
||||
@include flex-center-vertical;
|
||||
gap: $spacing-base;
|
||||
z-index: 100;
|
||||
|
||||
|
||||
@include tablet {
|
||||
position: fixed;
|
||||
top: $spacing-medium;
|
||||
left: $spacing-medium;
|
||||
z-index: 300;
|
||||
}
|
||||
|
||||
|
||||
@include mobile {
|
||||
position: fixed;
|
||||
top: $spacing-medium;
|
||||
@@ -251,21 +309,36 @@ async function handleLogin() {
|
||||
|
||||
.logo-icon {
|
||||
position: relative;
|
||||
width: 56px; // 改为56px符合8px栅格
|
||||
height: 56px; // 改为56px符合8px栅格
|
||||
width: 56px; // 改为56px符合8px栅格
|
||||
height: 56px; // 改为56px符合8px栅格
|
||||
flex-shrink: 0;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
// 暗模式下为Logo添加发光效果
|
||||
:global([data-theme='dark']) & {
|
||||
filter: drop-shadow(0 0 8px rgba(1, 156, 124, 0.4));
|
||||
|
||||
svg {
|
||||
path {
|
||||
&:first-child {
|
||||
filter: drop-shadow(0 0 2px rgba(1, 156, 124, 0.6));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-family: 'Swis721 BlkCn BT', Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 24px; // 改为24px符合8px栅格
|
||||
font-size: 24px; // 改为24px符合8px栅格
|
||||
line-height: 1.2;
|
||||
color: $text-color-primary;
|
||||
color: var(--app-text-primary);
|
||||
transition: color 0.3s ease;
|
||||
@include text-ellipsis;
|
||||
|
||||
|
||||
@include mobile {
|
||||
font-size: 24px; // 改为24px符合8px栅格
|
||||
font-size: 24px; // 改为24px符合8px栅格
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,44 +349,65 @@ async function handleLogin() {
|
||||
right: 0;
|
||||
width: min(900px, 50vw);
|
||||
height: 100vh;
|
||||
background: rgba($background-color-white, 0.6);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(30px);
|
||||
box-shadow: -8px 0px 40px 0px rgba(63, 139, 255, 0.08);
|
||||
box-shadow: var(--app-shadow);
|
||||
@include flex-center;
|
||||
z-index: 50;
|
||||
padding: $spacing-large;
|
||||
box-sizing: border-box;
|
||||
|
||||
transition: all 0.3s ease;
|
||||
|
||||
// 暗模式样式 - 使用30%透明度的黑色背景
|
||||
:global([data-theme='dark']) & {
|
||||
background: rgba(0, 0, 0, 0.3) !important;
|
||||
backdrop-filter: blur(40px) saturate(1.2) !important;
|
||||
box-shadow:
|
||||
-8px 0px 32px 0px rgba(0, 0, 0, 0.4),
|
||||
0 4px 12px 0px rgba(0, 0, 0, 0.2) !important;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
@include desktop {
|
||||
width: 60vw;
|
||||
}
|
||||
|
||||
|
||||
@include tablet {
|
||||
width: 100vw;
|
||||
right: 0;
|
||||
background: rgba($background-color-white, 0.95);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
position: relative;
|
||||
padding: $spacing-medium;
|
||||
min-height: 100vh;
|
||||
|
||||
:global([data-theme='dark']) & {
|
||||
background: rgba(0, 0, 0, 0.3) !important;
|
||||
backdrop-filter: blur(30px) saturate(1.2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@include mobile {
|
||||
width: 100vw;
|
||||
right: 0;
|
||||
background: rgba($background-color-white, 0.95);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
position: relative;
|
||||
padding: $spacing-medium $spacing-small;
|
||||
min-height: 100vh;
|
||||
|
||||
:global([data-theme='dark']) & {
|
||||
background: rgba(0, 0, 0, 0.3) !important;
|
||||
backdrop-filter: blur(30px) saturate(1.2) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 448px; // 改为448px (56*8)
|
||||
max-width: 448px; // 改为448px (56*8)
|
||||
margin: 0 auto;
|
||||
|
||||
|
||||
@include mobile {
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -321,7 +415,7 @@ async function handleLogin() {
|
||||
|
||||
.login-card {
|
||||
padding: $spacing-large;
|
||||
|
||||
|
||||
@include mobile {
|
||||
padding: $spacing-base;
|
||||
}
|
||||
@@ -331,13 +425,14 @@ async function handleLogin() {
|
||||
margin: 0 0 $spacing-medium 0;
|
||||
font-family: 'Source Han Sans CN', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 32px; // 改为32px符合8px栅格
|
||||
font-size: 32px; // 改为32px符合8px栅格
|
||||
line-height: 1.2;
|
||||
color: $text-color-primary;
|
||||
color: var(--app-text-primary);
|
||||
text-align: center;
|
||||
|
||||
transition: color 0.3s ease;
|
||||
|
||||
@include mobile {
|
||||
font-size: 24px; // 改为24px符合8px栅格
|
||||
font-size: 24px; // 改为24px符合8px栅格
|
||||
margin-bottom: $spacing-small;
|
||||
}
|
||||
}
|
||||
@@ -348,9 +443,10 @@ async function handleLogin() {
|
||||
font-weight: $font-weight-primary;
|
||||
font-size: $font-size-medium;
|
||||
line-height: 1.4;
|
||||
color: $text-color-secondary;
|
||||
color: var(--app-text-secondary);
|
||||
text-align: center;
|
||||
|
||||
transition: color 0.3s ease;
|
||||
|
||||
@include mobile {
|
||||
font-size: $font-size-base;
|
||||
margin-bottom: $spacing-base;
|
||||
@@ -363,24 +459,43 @@ async function handleLogin() {
|
||||
}
|
||||
|
||||
.login-input :deep(.el-input__wrapper) {
|
||||
height: 56px; // 改为56px符合8px栅格
|
||||
border-radius: 8px; // 改为8px符合8px栅格
|
||||
background-color: $background-color-white;
|
||||
height: 56px; // 改为56px符合8px栅格
|
||||
border-radius: 8px; // 改为8px符合8px栅格
|
||||
background-color: var(--app-content-bg);
|
||||
border: 1px solid var(--app-border-color);
|
||||
box-shadow: none;
|
||||
transition: all 0.3s ease;
|
||||
padding: 0 $spacing-base;
|
||||
|
||||
|
||||
@include input-style;
|
||||
|
||||
|
||||
// 暗模式下的特殊样式
|
||||
:global([data-theme='dark']) & {
|
||||
background-color: rgba(45, 45, 45, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(8px);
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
background-color: rgba(45, 45, 45, 0.7);
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
border-color: $primary-color !important;
|
||||
background-color: rgba(45, 45, 45, 0.8);
|
||||
box-shadow: 0 0 0 2px rgba($primary-color, 0.3) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
height: 48px;
|
||||
min-height: 44px; // 触摸目标大小
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
border-color: $border-color-base;
|
||||
border-color: var(--app-border-light);
|
||||
}
|
||||
|
||||
|
||||
&.is-focus {
|
||||
border-color: $primary-color !important;
|
||||
box-shadow: 0 0 0 2px rgba($primary-color, 0.2) !important;
|
||||
@@ -390,11 +505,16 @@ async function handleLogin() {
|
||||
.login-input :deep(.el-input__inner) {
|
||||
height: 100%;
|
||||
font-size: $font-size-base;
|
||||
color: $text-color-primary;
|
||||
color: var(--app-text-primary);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--app-text-secondary);
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
@@ -402,31 +522,86 @@ async function handleLogin() {
|
||||
|
||||
.login-input :deep(.el-input__prefix) {
|
||||
color: rgba($primary-color, 0.6);
|
||||
transition: color 0.3s ease;
|
||||
|
||||
:global([data-theme='dark']) & {
|
||||
color: rgba($primary-color, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
height: 64px; // 改为64px符合8px栅格
|
||||
height: 64px; // 改为64px符合8px栅格
|
||||
font-size: $font-size-medium;
|
||||
font-weight: 600;
|
||||
border-radius: 8px; // 改为8px符合8px栅格
|
||||
background: linear-gradient(135deg, $primary-color 0%, color.adjust($primary-color, $lightness: -5%) 100%);
|
||||
border-radius: 8px; // 改为8px符合8px栅格
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
$primary-color 0%,
|
||||
color.adjust($primary-color, $lightness: -5%) 100%
|
||||
);
|
||||
border: none;
|
||||
margin-top: $spacing-medium;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
@include button-primary;
|
||||
|
||||
|
||||
// 暗模式下的按钮增强效果
|
||||
:global([data-theme='dark']) & {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
$primary-color 0%,
|
||||
color.adjust($primary-color, $lightness: -8%) 100%
|
||||
);
|
||||
box-shadow:
|
||||
0 4px 20px rgba($primary-color, 0.4),
|
||||
0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
height: 48px; // 改为48px符合8px栅格
|
||||
height: 48px; // 改为48px符合8px栅格
|
||||
min-height: 44px; // 触摸目标大小
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
|
||||
|
||||
@include non-touch-device {
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, color.adjust($primary-color, $lightness: -5%) 0%, $primary-color 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color.adjust($primary-color, $lightness: -5%) 0%,
|
||||
$primary-color 100%
|
||||
);
|
||||
@include hover-effect;
|
||||
box-shadow: 0 8px 25px rgba($primary-color, 0.3);
|
||||
|
||||
:global([data-theme='dark']) & {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color.adjust($primary-color, $lightness: -3%) 0%,
|
||||
color.adjust($primary-color, $lightness: -10%) 100%
|
||||
);
|
||||
box-shadow:
|
||||
0 8px 32px rgba($primary-color, 0.5),
|
||||
0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -435,7 +610,7 @@ async function handleLogin() {
|
||||
@include flex-between;
|
||||
margin: $spacing-medium 0;
|
||||
font-size: $font-size-base;
|
||||
|
||||
|
||||
@include mobile {
|
||||
flex-direction: column;
|
||||
gap: $spacing-base;
|
||||
@@ -445,14 +620,15 @@ async function handleLogin() {
|
||||
|
||||
.forgot-link {
|
||||
font-size: $font-size-base;
|
||||
color: $text-color-secondary;
|
||||
|
||||
color: var(--app-text-secondary);
|
||||
transition: color 0.3s ease;
|
||||
|
||||
@include non-touch-device {
|
||||
&:hover {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@include mobile {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
@@ -464,14 +640,14 @@ async function handleLogin() {
|
||||
top: $spacing-extra-large;
|
||||
right: $spacing-extra-large;
|
||||
z-index: 200;
|
||||
|
||||
|
||||
@include tablet {
|
||||
position: fixed;
|
||||
top: $spacing-medium;
|
||||
right: $spacing-medium;
|
||||
z-index: 300;
|
||||
}
|
||||
|
||||
|
||||
@include mobile {
|
||||
position: fixed;
|
||||
top: $spacing-medium;
|
||||
@@ -483,29 +659,47 @@ async function handleLogin() {
|
||||
}
|
||||
|
||||
.language-switcher :deep(.el-select) {
|
||||
width: 152px; // 改为152px (19*8)
|
||||
|
||||
width: 152px; // 改为152px (19*8)
|
||||
|
||||
@include mobile {
|
||||
width: 96px; // 改为96px (12*8)
|
||||
width: 96px; // 改为96px (12*8)
|
||||
}
|
||||
}
|
||||
|
||||
.language-switcher :deep(.el-input__inner) {
|
||||
background: rgba($background-color-white, 0.9);
|
||||
background: var(--app-content-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(156, 167, 199, 0.3);
|
||||
border: 1px solid var(--app-border-color);
|
||||
border-radius: $border-radius-base;
|
||||
font-size: $font-size-base;
|
||||
color: var(--app-text-primary);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
|
||||
// 暗模式下的特殊样式
|
||||
:global([data-theme='dark']) & {
|
||||
background: rgba(45, 45, 45, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(15px);
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
background: rgba(45, 45, 45, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
|
||||
@include non-touch-device {
|
||||
&:hover {
|
||||
border-color: $primary-color;
|
||||
box-shadow: 0 0 0 2px rgba($primary-color, 0.2);
|
||||
|
||||
:global([data-theme='dark']) & {
|
||||
border-color: $primary-color;
|
||||
box-shadow: 0 0 0 2px rgba($primary-color, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,23 +709,23 @@ async function handleLogin() {
|
||||
.login-section {
|
||||
padding: $spacing-medium;
|
||||
align-items: flex-start;
|
||||
padding-top: 80px; // 符合8px栅格
|
||||
padding-top: 80px; // 符合8px栅格
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 600px) {
|
||||
.login-section {
|
||||
padding-top: 48px; // 符合8px栅格
|
||||
padding-top: 48px; // 符合8px栅格
|
||||
}
|
||||
|
||||
|
||||
.title {
|
||||
margin-bottom: $spacing-small;
|
||||
}
|
||||
|
||||
|
||||
.subtitle {
|
||||
margin-bottom: $spacing-base;
|
||||
}
|
||||
|
||||
|
||||
.form-options {
|
||||
margin: $spacing-base 0;
|
||||
}
|
||||
@@ -547,36 +741,203 @@ async function handleLogin() {
|
||||
font-size: $font-size-base;
|
||||
line-height: 1;
|
||||
padding-left: $spacing-small;
|
||||
color: var(--app-text-regular);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* 覆盖语言切换器背景色 - 使用深度选择器 */
|
||||
/* 语言切换器下拉菜单暗模式适配 */
|
||||
.language-switcher :deep(.el-select-dropdown) {
|
||||
background-color: white !important;
|
||||
background-color: var(--app-content-bg) !important;
|
||||
border: 1px solid var(--app-border-color) !important;
|
||||
box-shadow: var(--app-shadow) !important;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
:global([data-theme='dark']) & {
|
||||
background: rgba(45, 45, 45, 0.95) !important;
|
||||
backdrop-filter: blur(20px) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 4px 16px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.language-switcher :deep(.el-popper) {
|
||||
background-color: white !important;
|
||||
background-color: var(--app-content-bg) !important;
|
||||
|
||||
:global([data-theme='dark']) & {
|
||||
background: rgba(45, 45, 45, 0.95) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 更具体的选择器 */
|
||||
:global(body) {
|
||||
.el-select-dropdown {
|
||||
background-color: white !important;
|
||||
.language-switcher :deep(.el-select-dropdown__item) {
|
||||
color: var(--app-text-primary) !important;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--app-action-btn-bg) !important;
|
||||
|
||||
:global([data-theme='dark']) & {
|
||||
background-color: rgba(1, 156, 124, 0.15) !important;
|
||||
color: rgba(1, 156, 124, 1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-popper {
|
||||
background-color: white !important;
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(1, 156, 124, 0.1) !important;
|
||||
color: $primary-color !important;
|
||||
|
||||
:global([data-theme='dark']) & {
|
||||
background-color: rgba(1, 156, 124, 0.2) !important;
|
||||
color: rgba(1, 156, 124, 1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
[class*="el-popper-container"] {
|
||||
background-color: white !important;
|
||||
|
||||
:global([data-theme='dark']) & {
|
||||
color: #e5eaf3 !important;
|
||||
}
|
||||
}
|
||||
:global([class*="el-popper-container"] .language-popper) {
|
||||
background-color: white !important;
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* 暗模式登录区域样式 - 完美还原截图设计 */
|
||||
[data-theme='dark'] .login-section {
|
||||
background: rgba(0, 0, 0, 0.3) !important;
|
||||
opacity: 0.8;
|
||||
backdrop-filter: blur(40px) !important;
|
||||
box-shadow: -6px 0px 30px 0px rgba(63, 139, 255, 0.05) !important;
|
||||
}
|
||||
|
||||
:global(.language-popper) {
|
||||
background-color: white !important;
|
||||
/* 暗模式下登录卡片 - 简洁设计 */
|
||||
[data-theme='dark'] .login-card {
|
||||
background: transparent !important;
|
||||
backdrop-filter: none !important;
|
||||
border-radius: 0 !important;
|
||||
padding: 40px !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
/* 暗模式下文字样式 - 还原截图 */
|
||||
[data-theme='dark'] .title {
|
||||
color: #ffffff !important;
|
||||
text-shadow: none !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .subtitle {
|
||||
color: #9ca3af !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
/* 暗模式下输入框样式 - 还原截图 */
|
||||
[data-theme='dark'] .login-input .el-input__wrapper {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
border-bottom: 1px solid #4b5563 !important;
|
||||
border-radius: 0 !important;
|
||||
backdrop-filter: none !important;
|
||||
box-shadow: none !important;
|
||||
padding: 8px 0 !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .login-input .el-input__wrapper:hover {
|
||||
border-bottom-color: #6b7280 !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .login-input .el-input__wrapper.is-focus {
|
||||
border-bottom-color: #00c896 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .login-input .el-input__inner {
|
||||
color: #ffffff !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .login-input .el-input__inner::placeholder {
|
||||
color: #6b7280 !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .login-input .el-input__prefix {
|
||||
color: #6b7280 !important;
|
||||
}
|
||||
|
||||
/* 暗模式下记住密码和忘记密码链接 - 还原截图 */
|
||||
[data-theme='dark'] .form-options .el-checkbox__label {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .forgot-link {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .forgot-link:hover {
|
||||
color: #00c896 !important;
|
||||
}
|
||||
|
||||
/* 暗模式下登录按钮 - 还原截图样式 */
|
||||
[data-theme='dark'] .login-button {
|
||||
background: #00c896 !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
border-radius: 6px !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .login-button:hover {
|
||||
background: #00b085 !important;
|
||||
box-shadow: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .login-button:active {
|
||||
background: #009973 !important;
|
||||
}
|
||||
|
||||
/* 暗模式下语言选择器 */
|
||||
[data-theme='dark'] .language-switcher .el-input__inner {
|
||||
background: rgba(25, 25, 25, 0.8) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15) !important;
|
||||
color: #e5eaf3 !important;
|
||||
backdrop-filter: blur(15px) !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .language-switcher .el-input__inner:hover {
|
||||
border-color: rgba(255, 255, 255, 0.3) !important;
|
||||
background: rgba(25, 25, 25, 0.9) !important;
|
||||
}
|
||||
|
||||
/* 暗模式下复选框 - 还原截图样式 */
|
||||
[data-theme='dark'] .el-checkbox__input.is-checked .el-checkbox__inner {
|
||||
background-color: #00c896 !important;
|
||||
border-color: #00c896 !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .el-checkbox__inner {
|
||||
background-color: transparent !important;
|
||||
border-color: #6b7280 !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .el-checkbox__inner:hover {
|
||||
border-color: #00c896 !important;
|
||||
}
|
||||
|
||||
/* 响应式暗模式样式 - 保持一致性 */
|
||||
@media (max-width: 1024px) {
|
||||
[data-theme='dark'] .login-section {
|
||||
background: #1a2332 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
[data-theme='dark'] .login-section {
|
||||
background: #1a2332 !important;
|
||||
}
|
||||
|
||||
[data-theme='dark'] .login-card {
|
||||
padding: 32px 24px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user