This commit is contained in:
wood911010 committed 2025-10-10 21:53:21 +08:00
commit b5d0798ee9
17 files changed
+2457 -194

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"WebFetch(domain:www.figma.com)"
],
"deny": [],
"ask": []
}
}
+164
View File
@@ -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

+2 -2
View File
@@ -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);',
@@ -630,3 +632,72 @@ 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>
@@ -482,3 +482,84 @@ const handleUserClick = () => {
}
}
</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>
@@ -406,3 +406,81 @@ 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>
+97 -1
View File
@@ -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": {
+1 -1
View File
@@ -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 }
},
{
+241 -30
View File
@@ -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"
: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,7 +236,7 @@ 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;
@@ -218,12 +247,17 @@ onMounted(() => {
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, rgba(255,255,255,0.08) 25%, transparent 25%),
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 */
}
@@ -245,10 +279,12 @@ onMounted(() => {
transform: translateY(-50%);
width: 1px;
height: 3.33vw; /* 64px/1920 = 3.33vw */
background: linear-gradient(to bottom,
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%);
rgba(255, 255, 255, 0.08) 100%
);
}
}
@@ -278,7 +314,7 @@ onMounted(() => {
.stat-label {
font-size: 0.83vw; /* 16px/1920 = 0.83vw */
font-weight: 500;
color: #FFFFFF;
color: #ffffff;
line-height: 1;
white-space: nowrap;
}
@@ -387,6 +423,7 @@ onMounted(() => {
.table-header th {
background-color: var(--app-bg-color) !important;
color: var(--app-text-primary);
font-weight: 500;
font-size: 16px;
@@ -445,13 +482,13 @@ onMounted(() => {
height: 22px;
&.status-valid {
background:#F0F5FF;
color:#4763FF;
background: #f0f5ff;
color: #4763ff;
}
&.status-invalid {
background:#FFE5E5;
color: #E90C0C;
background: #ffe5e5;
color: #e90c0c;
}
}
@@ -601,3 +638,177 @@ onMounted(() => {
}
}
</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>
-59
View File
@@ -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>
+448
View File
@@ -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>
+798
View File
@@ -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>
+455 -94
View File
@@ -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,6 +257,8 @@ 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;
}
@@ -254,6 +312,20 @@ async function handleLogin() {
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 {
@@ -261,7 +333,8 @@ async function handleLogin() {
font-weight: 400;
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 {
@@ -276,13 +349,24 @@ 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;
@@ -291,21 +375,31 @@ async function handleLogin() {
@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;
}
}
}
@@ -333,8 +427,9 @@ async function handleLogin() {
font-weight: 700;
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栅格
@@ -348,8 +443,9 @@ 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;
@@ -365,20 +461,39 @@ async function handleLogin() {
.login-input :deep(.el-input__wrapper) {
height: 56px; // 改为56px符合8px栅格
border-radius: 8px; // 改为8px符合8px栅格
background-color: $background-color-white;
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 {
@@ -390,10 +505,15 @@ 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,6 +522,11 @@ 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 {
@@ -410,12 +535,46 @@ async function handleLogin() {
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%);
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栅格
min-height: 44px; // 触摸目标大小
@@ -424,9 +583,25 @@ async function handleLogin() {
@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);
}
}
}
}
@@ -445,7 +620,8 @@ 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 {
@@ -491,13 +667,26 @@ async function handleLogin() {
}
.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;
}
@@ -506,6 +695,11 @@ async function handleLogin() {
&: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);
}
}
}
}
@@ -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;
}
}
</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;
}
/* 暗模式下登录卡片 - 简洁设计 */
[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;
}
/* 暗模式下文字样式 - 还原截图 */
[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;
}
}
:global([class*="el-popper-container"] .language-popper) {
background-color: white !important;
}
:global(.language-popper) {
background-color: white !important;
}
</style>