feat:新增客户管理页面多语言国际化

This commit is contained in:
13895237362 committed 2025-09-17 17:26:31 +08:00
1 parent 5c086a32f5
commit 114c630035
12 files changed
+582 -149

No files matched your search

+4
View File
@@ -5,6 +5,10 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes, minimum-scale=0.5, maximum-scale=3.0" />
<title>Vite + Vue3.0 + TS项目模板</title>
<!-- Source Han Sans CN 字体 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@100;300;400;500;700;900&display=swap" rel="stylesheet">
</head>
<body>
<div id="app"></div>
+3 -2
View File
@@ -25,14 +25,14 @@ html {
body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
font-family: $font-family-primary;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: $font-size-base;
line-height: 1.6;
color: $text-color-primary;
background-color: $background-color-base;
@include mobile {
font-size: $font-size-small;
line-height: 1.5;
@@ -55,6 +55,7 @@ body {
--el-color-primary-dark-2: #0e8a71 !important;
--el-border-radius-base: 4px;
--el-font-size-base: 14px;
--el-font-family: #{$font-family-primary};
}
/* 工具类样式 */
@@ -29,6 +29,9 @@ $font-size-base: 14px; // 保持14px(UI常用)
$font-size-small: 12px; // 改为12px
$font-size-extra-small: 12px;
// 字体系列
$font-family-primary: 'Noto Sans SC', 'Source Han Sans CN', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
// 字体粗细
$font-weight-primary: 500;
$font-weight-secondary: 400;
+132
View File
@@ -99,5 +99,137 @@
"month": "This Month"
}
}
},
"pages": {
"users": {
"title": "System Users",
"welcome": "Welcome to the System Users page"
},
"roles": {
"title": "Roles & Permissions",
"welcome": "Welcome to the Roles & Permissions page"
},
"licenses": {
"title": "License Management",
"welcome": "Welcome to the License Management page"
}
},
"customers": {
"title": "Customer Management",
"viewCustomer": "View Customer",
"addCustomer": "Add New Customer",
"editCustomer": "Edit Customer",
"actions": {
"add": "Add New Customer",
"edit": "Edit",
"delete": "Delete",
"enable": "Enable",
"disable": "Disable",
"viewLicense": "View License",
"save": "Save",
"cancel": "Cancel"
},
"table": {
"customerCode": "Customer Code",
"customerName": "Customer Name",
"customerType": "Customer Type",
"contactPerson": "Contact Person",
"email": "Email",
"customerLevel": "Customer Level",
"status": "Status",
"createTime": "Create Time",
"operation": "Operation",
"loading": "Loading..."
},
"form": {
"basicInfo": "Basic Information",
"businessInfo": "Business Information",
"customerName": "Customer Name",
"customerType": "Customer Type",
"customerLevel": "Customer Level",
"contactPerson": "Contact Person",
"email": "Email",
"status": "Status",
"phone": "Phone",
"address": "Address",
"industry": "Industry",
"companySize": "Company Size",
"businessScope": "Business Scope",
"establishedDate": "Established Date",
"registrationNumber": "Registration Number",
"taxNumber": "Tax Number",
"legalRepresentative": "Legal Representative",
"website": "Website",
"description": "Description",
"preferredLicense": "Preferred License",
"placeholder": {
"enter": "Please enter",
"select": "Please select"
}
},
"filter": {
"label": "Filter:",
"customerType": "Customer Type",
"customerLevel": "Customer Level",
"status": "Status"
},
"search": {
"placeholder": "Search customer code, name, contact, email"
},
"breadcrumb": {
"customerManagement": "Customer Management",
"addCustomer": "Add New Customer",
"editCustomer": "Edit Customer"
},
"confirm": {
"deleteTitle": "Confirm Delete",
"deleteMessage": "Are you sure you want to delete customer \"{name}\"?",
"disableTitle": "Confirm Disable",
"disableMessage": "Are you sure you want to disable customer \"{name}\"?",
"enableTitle": "Confirm Enable",
"enableMessage": "Are you sure you want to enable customer \"{name}\"?",
"confirm": "Confirm",
"cancel": "Cancel"
},
"message": {
"deleteSuccess": "Delete successful",
"deleteCancel": "Delete cancelled",
"deleteLoading": "Deleting...",
"disableLoading": "Disabling...",
"enableLoading": "Enabling...",
"statusChangeCancel": "{action} cancelled",
"loadEnumError": "Failed to load enum data",
"getDetailError": "Failed to get customer detail: "
},
"validation": {
"nameRequired": "Please enter customer name",
"typeRequired": "Please select customer type",
"levelRequired": "Please select customer level",
"contactRequired": "Please enter contact person",
"statusRequired": "Please select status",
"companySizeRequired": "Please select company size",
"emailFormat": "Please enter valid email format",
"phoneFormat": "Please enter valid phone format",
"websiteFormat": "Please enter valid website format"
},
"view": {
"back": "Back",
"statusInfo": "Status Information",
"licenseStats": "License Statistics",
"creator": "Creator",
"createTime": "Create Time",
"updater": "Updater",
"updateTime": "Update Time",
"totalLicenses": "Total Licenses",
"activeLicenses": "Active Licenses",
"recentLicense": "Recent License",
"expiringSoon": "Expiring Soon",
"expiringSoonTime": "Expiring Soon Time",
"expiredLicenses": "Expired Licenses",
"expiredTime": "Expired Time",
"noData": "--",
"onlineLicense": "Online License",
"customerNotExist": "Customer data does not exist"
}
}
}
+132
View File
@@ -99,5 +99,137 @@
"month": "今月"
}
}
},
"pages": {
"users": {
"title": "システムユーザー",
"welcome": "システムユーザーページへようこそ"
},
"roles": {
"title": "ロールと権限",
"welcome": "ロールと権限ページへようこそ"
},
"licenses": {
"title": "ライセンス管理",
"welcome": "ライセンス管理ページへようこそ"
}
},
"customers": {
"title": "顧客管理",
"viewCustomer": "顧客表示",
"addCustomer": "新規顧客追加",
"editCustomer": "顧客編集",
"actions": {
"add": "新規顧客追加",
"edit": "編集",
"delete": "削除",
"enable": "有効化",
"disable": "無効化",
"viewLicense": "ライセンス表示",
"save": "保存",
"cancel": "キャンセル"
},
"table": {
"customerCode": "顧客コード",
"customerName": "顧客名",
"customerType": "顧客タイプ",
"contactPerson": "担当者",
"email": "メール",
"customerLevel": "顧客レベル",
"status": "ステータス",
"createTime": "作成日時",
"operation": "操作",
"loading": "読み込み中..."
},
"form": {
"basicInfo": "基本情報",
"businessInfo": "ビジネス情報",
"customerName": "顧客名",
"customerType": "顧客タイプ",
"customerLevel": "顧客レベル",
"contactPerson": "担当者",
"email": "メール",
"status": "ステータス",
"phone": "電話番号",
"address": "住所",
"industry": "業界",
"companySize": "会社規模",
"businessScope": "事業範囲",
"establishedDate": "設立日",
"registrationNumber": "登録番号",
"taxNumber": "税務番号",
"legalRepresentative": "法定代表者",
"website": "ウェブサイト",
"description": "説明",
"preferredLicense": "優先ライセンス",
"placeholder": {
"enter": "入力してください",
"select": "選択してください"
}
},
"filter": {
"label": "フィルター:",
"customerType": "顧客タイプ",
"customerLevel": "顧客レベル",
"status": "ステータス"
},
"search": {
"placeholder": "顧客コード、顧客名、担当者、メールで検索"
},
"breadcrumb": {
"customerManagement": "顧客管理",
"addCustomer": "新規顧客追加",
"editCustomer": "顧客編集"
},
"confirm": {
"deleteTitle": "削除確認",
"deleteMessage": "顧客 \"{name}\" を削除してもよろしいですか?",
"disableTitle": "無効化確認",
"disableMessage": "顧客 \"{name}\" を無効化してもよろしいですか?",
"enableTitle": "有効化確認",
"enableMessage": "顧客 \"{name}\" を有効化してもよろしいですか?",
"confirm": "確認",
"cancel": "キャンセル"
},
"message": {
"deleteSuccess": "削除しました",
"deleteCancel": "削除をキャンセルしました",
"deleteLoading": "削除中...",
"disableLoading": "無効化中...",
"enableLoading": "有効化中...",
"statusChangeCancel": "{action}をキャンセルしました",
"loadEnumError": "列挙データの読み込みに失敗しました",
"getDetailError": "顧客詳細の取得に失敗しました:"
},
"validation": {
"nameRequired": "顧客名を入力してください",
"typeRequired": "顧客タイプを選択してください",
"levelRequired": "顧客レベルを選択してください",
"contactRequired": "担当者を入力してください",
"statusRequired": "ステータスを選択してください",
"companySizeRequired": "会社規模を選択してください",
"emailFormat": "正しいメール形式を入力してください",
"phoneFormat": "正しい電話番号形式を入力してください",
"websiteFormat": "正しいウェブサイト形式を入力してください"
},
"view": {
"back": "戻る",
"statusInfo": "ステータス情報",
"licenseStats": "ライセンス統計",
"creator": "作成者",
"createTime": "作成日時",
"updater": "更新者",
"updateTime": "更新日時",
"totalLicenses": "総ライセンス数",
"activeLicenses": "アクティブライセンス",
"recentLicense": "最近のライセンス",
"expiringSoon": "期限近し",
"expiringSoonTime": "期限近し日時",
"expiredLicenses": "期限切れライセンス",
"expiredTime": "期限切れ日時",
"noData": "--",
"onlineLicense": "オンラインライセンス",
"customerNotExist": "顧客データが存在しません"
}
}
}
+132
View File
@@ -99,5 +99,137 @@
"month": "本月"
}
}
},
"pages": {
"users": {
"title": "系统用户",
"welcome": "欢迎你来到系统用户页面"
},
"roles": {
"title": "角色权限",
"welcome": "欢迎你来到角色权限页面"
},
"licenses": {
"title": "授权管理",
"welcome": "欢迎你来到授权管理页面"
}
},
"customers": {
"title": "客户管理",
"viewCustomer": "查看客户",
"addCustomer": "添加新客户",
"editCustomer": "编辑客户",
"actions": {
"add": "添加新客户",
"edit": "编辑",
"delete": "删除",
"enable": "启用",
"disable": "禁用",
"viewLicense": "查看授权",
"save": "保 存",
"cancel": "取 消"
},
"table": {
"customerCode": "客户编码",
"customerName": "客户名称",
"customerType": "客户类型",
"contactPerson": "联系人",
"email": "邮箱",
"customerLevel": "客户等级",
"status": "状态",
"createTime": "创建时间",
"operation": "操作",
"loading": "加载中..."
},
"form": {
"basicInfo": "基本信息",
"businessInfo": "商业信息",
"customerName": "客户名称",
"customerType": "客户类型",
"customerLevel": "客户等级",
"contactPerson": "联系人",
"email": "邮箱",
"status": "状态",
"phone": "联系电话",
"address": "地址",
"industry": "所属行业",
"companySize": "公司规模",
"businessScope": "经营范围",
"establishedDate": "成立时间",
"registrationNumber": "工商注册号",
"taxNumber": "税务登记号",
"legalRepresentative": "法定代表人",
"website": "公司网址",
"description": "客户描述",
"preferredLicense": "首选授权",
"placeholder": {
"enter": "请输入",
"select": "请选择"
}
},
"filter": {
"label": "筛选:",
"customerType": "客户类型",
"customerLevel": "客户等级",
"status": "状态"
},
"search": {
"placeholder": "搜索客户编码、客户名称、联系人、邮箱"
},
"breadcrumb": {
"customerManagement": "客户管理",
"addCustomer": "添加新客户",
"editCustomer": "编辑客户"
},
"confirm": {
"deleteTitle": "确认删除",
"deleteMessage": "确定要删除客户 \"{name}\" 吗?",
"disableTitle": "确认禁用",
"disableMessage": "确定要禁用客户 \"{name}\" 吗?",
"enableTitle": "确认启用",
"enableMessage": "确定要启用客户 \"{name}\" 吗?",
"confirm": "确定",
"cancel": "取消"
},
"message": {
"deleteSuccess": "删除成功",
"deleteCancel": "已取消删除",
"deleteLoading": "删除中...",
"disableLoading": "禁用中...",
"enableLoading": "启用中...",
"statusChangeCancel": "已取消{action}",
"loadEnumError": "加载枚举数据失败",
"getDetailError": "获取客户详情失败:"
},
"validation": {
"nameRequired": "请输入客户名称",
"typeRequired": "请选择客户类型",
"levelRequired": "请选择客户等级",
"contactRequired": "请输入联系人",
"statusRequired": "请选择状态",
"companySizeRequired": "请选择企业规模",
"emailFormat": "请输入正确的邮箱格式",
"phoneFormat": "请输入正确的手机号码",
"websiteFormat": "请输入正确的网址格式"
},
"view": {
"back": "返 回",
"statusInfo": "状态信息",
"licenseStats": "授权统计",
"creator": "创建人",
"createTime": "创建时间",
"updater": "更新人",
"updateTime": "更新时间",
"totalLicenses": "总授权数",
"activeLicenses": "活跃授权",
"recentLicense": "最近授权",
"expiringSoon": "即将过期",
"expiringSoonTime": "即将过期时间",
"expiredLicenses": "过期授权",
"expiredTime": "过期时间",
"noData": "--",
"onlineLicense": "在线授权",
"customerNotExist": "客户数据不存在"
}
}
}
+41 -38
View File
@@ -11,15 +11,15 @@
<div class="top-section">
<!-- 面包屑导航 -->
<div class="breadcrumb">
<span class="breadcrumb-item">客户管理</span>
<span class="breadcrumb-item">{{ t('customers.breadcrumb.customerManagement') }}</span>
<span class="breadcrumb-separator">></span>
<span class="breadcrumb-current">{{ isEdit ? '编辑客户' : '添加新客户' }}</span>
<span class="breadcrumb-current">{{ isEdit ? t('customers.breadcrumb.editCustomer') : t('customers.breadcrumb.addCustomer') }}</span>
</div>
<!-- 操作按钮 -->
<div class="form-actions">
<el-button @click="handleCancel">取 消</el-button>
<el-button type="primary" @click="handleSave" :loading="loading">保 存</el-button>
<el-button @click="handleCancel">{{ t('customers.actions.cancel') }}</el-button>
<el-button type="primary" @click="handleSave" :loading="loading">{{ t('customers.actions.save') }}</el-button>
</div>
</div>
@@ -28,16 +28,16 @@
<el-form :model="formData" :rules="formRules" ref="formRef" label-position="top">
<!-- 基本信息 -->
<div class="customer-form">
<h3 class="section-title">基本信息</h3>
<h3 class="section-title">{{ t('customers.form.basicInfo') }}</h3>
<!-- Flex布局 - 7个字段横向排列 -->
<div class="fields-row-flex">
<el-form-item label="客户名称" prop="name" required class="field-item">
<el-input v-model="formData.name" placeholder="请输入" />
<el-form-item :label="t('customers.form.customerName')" prop="name" required class="field-item">
<el-input v-model="formData.name" :placeholder="t('customers.form.placeholder.enter')" />
</el-form-item>
<el-form-item label="客户类型" prop="type" required class="field-item">
<el-select v-model="formData.type" placeholder="请选择" style="width: 100%">
<el-form-item :label="t('customers.form.customerType')" prop="type" required class="field-item">
<el-select v-model="formData.type" :placeholder="t('customers.form.placeholder.select')" style="width: 100%">
<el-option
v-for="option in customerTypeOptions"
:key="option.key"
@@ -47,8 +47,8 @@
</el-select>
</el-form-item>
<el-form-item label="客户等级" prop="level" required class="field-item">
<el-select v-model="formData.level" placeholder="请选择" style="width: 100%">
<el-form-item :label="t('customers.form.customerLevel')" prop="level" required class="field-item">
<el-select v-model="formData.level" :placeholder="t('customers.form.placeholder.select')" style="width: 100%">
<el-option
v-for="option in customerLevelOptions"
:key="option.key"
@@ -58,15 +58,15 @@
</el-select>
</el-form-item>
<el-form-item label="联系人" prop="contact" required class="field-item">
<el-input v-model="formData.contact" placeholder="请输入" />
<el-form-item :label="t('customers.form.contactPerson')" prop="contact" required class="field-item">
<el-input v-model="formData.contact" :placeholder="t('customers.form.placeholder.enter')" />
</el-form-item>
<el-form-item label="邮箱" prop="email" class="field-item">
<el-input v-model="formData.email" placeholder="请输入" />
<el-form-item :label="t('customers.form.email')" prop="email" class="field-item">
<el-input v-model="formData.email" :placeholder="t('customers.form.placeholder.enter')" />
</el-form-item>
<el-form-item label="状态" prop="status" required class="field-item status-field">
<el-form-item :label="t('customers.form.status')" prop="status" required class="field-item status-field">
<el-radio-group v-model="formData.status">
<el-radio
v-for="option in statusOptions"
@@ -78,26 +78,26 @@
</el-radio-group>
</el-form-item>
<el-form-item label="联系电话" prop="phone" class="field-item">
<el-input v-model="formData.phone" placeholder="请输入" />
<el-form-item :label="t('customers.form.phone')" prop="phone" class="field-item">
<el-input v-model="formData.phone" :placeholder="t('customers.form.placeholder.enter')" />
</el-form-item>
</div>
<!-- Grid布局 - 地址跨列 -->
<div class="fields-row-grid">
<el-form-item label="地址" prop="address" class="address-field">
<el-input v-model="formData.address" placeholder="请输入" />
<el-form-item :label="t('customers.form.address')" prop="address" class="address-field">
<el-input v-model="formData.address" :placeholder="t('customers.form.placeholder.enter')" />
</el-form-item>
</div>
</div>
<!-- 商业信息 -->
<div class="customer-form">
<h3 class="section-title">商业信息</h3>
<h3 class="section-title">{{ t('customers.form.businessInfo') }}</h3>
<div class="business-fields">
<el-form-item label="企业规模" prop="companySize" required class="company-size-field">
<el-select v-model="formData.companySize" placeholder="请选择" style="width: 100%">
<el-form-item :label="t('customers.form.companySize')" prop="companySize" required class="company-size-field">
<el-select v-model="formData.companySize" :placeholder="t('customers.form.placeholder.select')" style="width: 100%">
<el-option
v-for="option in companySizeOptions"
:key="option.key"
@@ -107,12 +107,12 @@
</el-select>
</el-form-item>
<el-form-item label="客户描述" prop="description" class="description-field">
<el-form-item :label="t('customers.form.description')" prop="description" class="description-field">
<el-input
v-model="formData.description"
type="textarea"
:rows="4"
placeholder="请输入"
:placeholder="t('customers.form.placeholder.enter')"
:maxlength="500"
show-word-limit
/>
@@ -126,6 +126,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { useI18n } from 'vue-i18n'
import { createCustomer, updateCustomer, getCustomerDetail } from '@/api/customer'
import {
getCustomerTypeEnums,
@@ -159,6 +160,8 @@ const emit = defineEmits<{
cancel: []
}>()
const { t } = useI18n()
const formRef = ref<FormInstance>()
const loading = ref(false)
@@ -195,7 +198,7 @@ const loadEnums = async () => {
companySizeOptions.value = sizeRes.data.items
}
} catch (error) {
console.error('加载枚举数据失败:', error)
console.error(t('customers.message.loadEnumError'), error)
}
}
@@ -216,28 +219,28 @@ const formData = reactive<CustomerFormData>({
// 表单验证规则
const formRules: FormRules = {
name: [
{ required: true, message: '请输入客户名称', trigger: 'blur' }
{ required: true, message: t('customers.validation.nameRequired'), trigger: 'blur' }
],
contact: [
{ required: true, message: '请输入联系人', trigger: 'blur' }
{ required: true, message: t('customers.validation.contactRequired'), trigger: 'blur' }
],
type: [
{ required: true, message: '请选择客户类型', trigger: 'change' }
{ required: true, message: t('customers.validation.typeRequired'), trigger: 'change' }
],
level: [
{ required: true, message: '请选择客户等级', trigger: 'change' }
{ required: true, message: t('customers.validation.levelRequired'), trigger: 'change' }
],
status: [
{ required: true, message: '请选择状态', trigger: 'change' }
{ required: true, message: t('customers.validation.statusRequired'), trigger: 'change' }
],
companySize: [
{ required: true, message: '请选择企业规模', trigger: 'change' }
{ required: true, message: t('customers.validation.companySizeRequired'), trigger: 'change' }
],
email: [
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
{ type: 'email', message: t('customers.validation.emailFormat'), trigger: 'blur' }
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
{ pattern: /^1[3-9]\d{9}$/, message: t('customers.validation.phoneFormat'), trigger: 'blur' }
]
}
@@ -266,13 +269,13 @@ const loadCustomerData = async () => {
description: customerData.description || ''
})
} else {
ElMessage.error('获取客户详情失败:' + response.message)
ElMessage.error(t('customers.message.getDetailError') + response.message)
}
} catch (error: any) {
console.error('获取客户详情失败:', error)
console.error('Get customer detail error:', error)
let errorMessage = error.backendMessage || error.response?.data?.message || error.message
if (errorMessage) {
ElMessage.error('获取客户详情失败:' + errorMessage)
ElMessage.error(t('customers.message.getDetailError') + errorMessage)
}
} finally {
loading.value = false
@@ -321,7 +324,7 @@ const handleSave = async () => {
ElMessage.error(response.message)
}
} catch (error: any) {
console.error('保存失败:', error)
console.error('Save failed:', error)
// 使用后端返回的错误信息
let errorMessage = error.backendMessage || error.response?.data?.message || error.message
if (errorMessage) {
+53 -51
View File
@@ -11,14 +11,14 @@
<div class="top-section">
<!-- 面包屑导航 -->
<div class="breadcrumb">
<span class="breadcrumb-item">客户管理</span>
<span class="breadcrumb-item">{{ t('customers.breadcrumb.customerManagement') }}</span>
<span class="breadcrumb-separator">></span>
<span class="breadcrumb-current">查看客户</span>
<span class="breadcrumb-current">{{ t('customers.viewCustomer') }}</span>
</div>
<!-- 返回按钮 -->
<div class="form-actions">
<el-button @click="handleBack">返 回</el-button>
<el-button @click="handleBack">{{ t('customers.view.back') }}</el-button>
</div>
</div>
@@ -26,83 +26,83 @@
<div class="customer-view" v-loading="loading">
<!-- 基本信息卡片 -->
<div class="info-card basic-info">
<h3 class="section-title">基本信息</h3>
<h3 class="section-title">{{ t('customers.form.basicInfo') }}</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">客户名称:</span>
<span class="value">{{ customerData.name || '--' }}</span>
<span class="label">{{ t('customers.form.customerName') }}:</span>
<span class="value">{{ customerData.name || t('customers.view.noData') }}</span>
</div>
<div class="info-item">
<span class="label">客户类型:</span>
<span class="label">{{ t('customers.form.customerType') }}:</span>
<span class="value">{{ getCustomerTypeLabel(customerData.type) }}</span>
</div>
<div class="info-item">
<span class="label">客户等级:</span>
<span class="label">{{ t('customers.form.customerLevel') }}:</span>
<span class="value">{{ getCustomerLevelLabel(customerData.level) }}</span>
</div>
<div class="info-item">
<span class="label">联系人:</span>
<span class="value">{{ customerData.contact || '--' }}</span>
<span class="label">{{ t('customers.form.contactPerson') }}:</span>
<span class="value">{{ customerData.contact || t('customers.view.noData') }}</span>
</div>
<div class="info-item">
<span class="label">邮箱:</span>
<span class="value">{{ customerData.email || '--' }}</span>
<span class="label">{{ t('customers.form.email') }}:</span>
<span class="value">{{ customerData.email || t('customers.view.noData') }}</span>
</div>
<div class="info-item">
<span class="label">状态:</span>
<span class="label">{{ t('customers.form.status') }}:</span>
<span class="value">{{ getStatusLabel(customerData.status) }}</span>
</div>
<div class="info-item">
<span class="label">联系电话:</span>
<span class="value">{{ customerData.phone || '--' }}</span>
<span class="label">{{ t('customers.form.phone') }}:</span>
<span class="value">{{ customerData.phone || t('customers.view.noData') }}</span>
</div>
<div class="info-item full-width">
<span class="label">地址:</span>
<span class="value">{{ customerData.address || '--' }}</span>
<span class="label">{{ t('customers.form.address') }}:</span>
<span class="value">{{ customerData.address || t('customers.view.noData') }}</span>
</div>
</div>
</div>
<!-- 商业信息卡片 -->
<div class="info-card business-info">
<h3 class="section-title">商业信息</h3>
<h3 class="section-title">{{ t('customers.form.businessInfo') }}</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">企业规模:</span>
<span class="value">{{ enumLabels.companySize || '--' }}</span>
<span class="label">{{ t('customers.form.companySize') }}:</span>
<span class="value">{{ enumLabels.companySize || t('customers.view.noData') }}</span>
</div>
<div class="info-item full-width">
<span class="label">首选授权:</span>
<span class="value">{{ customerData.preferredLicense || '--' }}</span>
<span class="label">{{ t('customers.form.preferredLicense') }}:</span>
<span class="value">{{ customerData.preferredLicense || t('customers.view.noData') }}</span>
</div>
<div class="info-item full-width">
<span class="label">客户描述:</span>
<span class="value description">{{ customerData.description || '--' }}</span>
<span class="label">{{ t('customers.form.description') }}:</span>
<span class="value description">{{ customerData.description || t('customers.view.noData') }}</span>
</div>
</div>
</div>
<!-- 状态信息卡片 -->
<div class="info-card status-info">
<h3 class="section-title">状态信息</h3>
<h3 class="section-title">{{ t('customers.view.statusInfo') }}</h3>
<div class="info-grid">
<div class="info-item inline-group full-width">
<div class="inline-pair">
<span class="label">创建人:</span>
<span class="value">{{ customerData.statusRecords?.[0]?.user || '--' }}</span>
<span class="label">{{ t('customers.view.creator') }}:</span>
<span class="value">{{ customerData.statusRecords?.[0]?.user || t('customers.view.noData') }}</span>
</div>
<div class="inline-pair">
<span class="label">创建时间:</span>
<span class="label">{{ t('customers.view.createTime') }}:</span>
<span class="value">{{ formatDate(customerData.statusRecords?.[0]?.time) }}</span>
</div>
</div>
<div class="info-item inline-group full-width">
<div class="inline-pair">
<span class="label">更新人:</span>
<span class="value">{{ customerData.statusRecords?.[1]?.user || '--' }}</span>
<span class="label">{{ t('customers.view.updater') }}:</span>
<span class="value">{{ customerData.statusRecords?.[1]?.user || t('customers.view.noData') }}</span>
</div>
<div class="inline-pair">
<span class="label">更新时间:</span>
<span class="label">{{ t('customers.view.updateTime') }}:</span>
<span class="value">{{ formatDate(customerData.statusRecords?.[1]?.time) }}</span>
</div>
</div>
@@ -111,37 +111,37 @@
<!-- 授权统计卡片 -->
<div class="info-card license-stats">
<h3 class="section-title">授权统计</h3>
<h3 class="section-title">{{ t('customers.view.licenseStats') }}</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">总授权数:</span>
<span class="label">{{ t('customers.view.totalLicenses') }}:</span>
<span class="value">{{ customerData.totalLicenses || 0 }}</span>
</div>
<div class="info-item">
<span class="label">活跃授权:</span>
<span class="label">{{ t('customers.view.activeLicenses') }}:</span>
<span class="value">{{ customerData.activeLicenses || 0 }}</span>
</div>
<div class="info-item">
<span class="label">最近授权:</span>
<span class="label">{{ t('customers.view.recentLicense') }}:</span>
<span class="value">{{ formatDate(customerData.latestLicenseTime) }}</span>
</div>
<div class="info-item inline-group full-width">
<div class="inline-pair">
<span class="label">即将过期:</span>
<span class="label">{{ t('customers.view.expiringSoon') }}:</span>
<span class="value">{{ customerData.expiringSoonLicenses || 0 }}</span>
</div>
<div class="inline-pair">
<span class="label">即将过期时间:</span>
<span class="label">{{ t('customers.view.expiringSoonTime') }}:</span>
<span class="value">{{ formatDate(customerData.expiringSoonTime) }}</span>
</div>
</div>
<div class="info-item inline-group full-width">
<div class="inline-pair">
<span class="label">过期授权:</span>
<span class="label">{{ t('customers.view.expiredLicenses') }}:</span>
<span class="value">{{ customerData.expiredLicenses || 0 }}</span>
</div>
<div class="inline-pair">
<span class="label">过期时间:</span>
<span class="label">{{ t('customers.view.expiredTime') }}:</span>
<span class="value">{{ formatDate(customerData.expiredTime) }}</span>
</div>
</div>
@@ -154,6 +154,7 @@
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getCustomerDetail } from '@/api/customer'
import { useI18n } from 'vue-i18n'
interface StatusRecord {
action: string
@@ -191,6 +192,7 @@ const emit = defineEmits<{
back: []
}>()
const { t } = useI18n()
const loading = ref(false)
const customerData = ref<Partial<CustomerViewData>>({})
@@ -213,7 +215,7 @@ const fetchCustomerDetail = async () => {
const customer = response.data
if (!customer) {
ElMessage.error('客户数据不存在')
ElMessage.error(t('customers.view.customerNotExist'))
return
}
@@ -236,16 +238,16 @@ const fetchCustomerDetail = async () => {
level: customer.customer_level,
status: customer.status,
companySize: customer.company_size || '',
preferredLicense: '在线授权', // 暂时写死,等后端添加字段
preferredLicense: t('customers.view.onlineLicense'), // 暂时写死,等后端添加字段
description: customer.description || '',
statusRecords: [
{
action: '创建人',
action: t('customers.view.creator'),
user: customer.created_by,
time: customer.created_at
},
{
action: '更新人',
action: t('customers.view.updater'),
user: customer.updated_by,
time: customer.updated_at
}
@@ -260,7 +262,7 @@ const fetchCustomerDetail = async () => {
expiredTime: undefined
}
} catch (error: any) {
console.error('获取客户详情失败:', error)
console.error('Get customer detail failed:', error)
// 使用后端返回的错误信息
let errorMessage = error.backendMessage || error.response?.data?.message || error.message
if (errorMessage) {
@@ -278,26 +280,26 @@ const handleBack = () => {
// 获取客户类型标签
const getCustomerTypeLabel = (type: string | undefined) => {
if (!type) return '--'
return enumLabels.value.customerType || '--'
if (!type) return t('customers.view.noData')
return enumLabels.value.customerType || t('customers.view.noData')
}
// 获取客户等级标签
const getCustomerLevelLabel = (level: string | undefined) => {
if (!level) return '--'
return enumLabels.value.customerLevel || '--'
if (!level) return t('customers.view.noData')
return enumLabels.value.customerLevel || t('customers.view.noData')
}
// 获取状态标签
const getStatusLabel = (status: string | undefined) => {
if (!status) return '--'
return enumLabels.value.status || '--'
if (!status) return t('customers.view.noData')
return enumLabels.value.status || t('customers.view.noData')
}
// 格式化日期
const formatDate = (date: string | undefined) => {
if (!date) return '--'
if (!date) return t('customers.view.noData')
return new Date(date).toLocaleString('zh-CN')
}
+67 -52
View File
@@ -15,7 +15,7 @@
<!-- 左侧操作区域 -->
<div class="left-actions">
<el-button type="primary" class="add-customer-btn" @click="handleAddCustomer">
添加新客户
{{ t('customers.actions.add') }}
</el-button>
</div>
@@ -23,10 +23,10 @@
<div class="right-actions">
<!-- 筛选区域 -->
<div class="filter-section">
<span class="filter-label">筛选:</span>
<span class="filter-label">{{ t('customers.filter.label') }}</span>
<el-select
v-model="filterCustomerType"
placeholder="客户类型"
:placeholder="t('customers.filter.customerType')"
class="filter-select"
clearable
@change="handleFilterChange"
@@ -40,7 +40,7 @@
</el-select>
<el-select
v-model="filterCustomerLevel"
placeholder="客户等级"
:placeholder="t('customers.filter.customerLevel')"
class="filter-select"
clearable
@change="handleFilterChange"
@@ -54,7 +54,7 @@
</el-select>
<el-select
v-model="filterStatus"
placeholder="状态"
:placeholder="t('customers.filter.status')"
class="filter-select"
clearable
@change="handleFilterChange"
@@ -72,7 +72,7 @@
<div class="search-section">
<el-input
v-model="searchKeyword"
placeholder="搜索客户编码、客户名称、联系人、邮箱"
:placeholder="t('customers.search.placeholder')"
class="search-input"
>
<template #append>
@@ -89,7 +89,7 @@
<el-table
:data="tableData"
v-loading="loading"
element-loading-text="加载中..."
:element-loading-text="t('customers.table.loading')"
stripe
border
style="width: 100%;"
@@ -97,51 +97,51 @@
:row-style="getRowStyle"
:max-height="'calc(100vh - 280px)'"
>
<el-table-column prop="customer_code" label="客户编码" :width="170" show-overflow-tooltip align="left">
<el-table-column prop="customer_code" :label="t('customers.table.customerCode')" :width="170" show-overflow-tooltip align="left">
<template #default="scope">
<span class="customer-code" style="white-space: nowrap; overflow: visible; text-overflow: initial;">{{ scope.row.customer_code }}</span>
</template>
</el-table-column>
<el-table-column prop="customer_name" label="客户名称" :width="180" show-overflow-tooltip align="center">
<el-table-column prop="customer_name" :label="t('customers.table.customerName')" :width="180" show-overflow-tooltip align="center">
<template #default="scope">
<span class="ellipsis-text">{{ scope.row.customer_name }}</span>
</template>
</el-table-column>
<el-table-column prop="customer_type_display" label="客户类型" :width="130" align="center">
<el-table-column prop="customer_type_display" :label="t('customers.table.customerType')" :width="130" align="center">
</el-table-column>
<el-table-column prop="contact_person" label="联系人" :width="140" align="center" />
<el-table-column prop="email" label="邮箱" :width="160" show-overflow-tooltip align="center">
<el-table-column prop="contact_person" :label="t('customers.table.contactPerson')" :width="140" align="center" />
<el-table-column prop="email" :label="t('customers.table.email')" :width="160" show-overflow-tooltip align="center">
<template #default="scope">
<span class="ellipsis-text">{{ scope.row.email }}</span>
</template>
</el-table-column>
<el-table-column prop="customer_level_display" label="客户等级" :width="130" align="center">
<el-table-column prop="customer_level_display" :label="t('customers.table.customerLevel')" :width="130" align="center">
</el-table-column>
<el-table-column prop="status_display" label="状态" :width="100" align="center">
<el-table-column prop="status_display" :label="t('customers.table.status')" :width="100" align="center">
<template #default="scope">
<div class="status-tag" :class="getStatusClass(scope.row.status)">
{{scope.row.status_display}}
</div>
</template>
</el-table-column>
<el-table-column label="创建时间" :width="135" align="center">
<el-table-column :label="t('customers.table.createTime')" :width="140" align="center">
<template #default="scope">
{{ formatDate(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" :width="340" fixed="right" class-name="action-column" align="center">
<el-table-column :label="t('customers.table.operation')" fixed="right" class-name="action-column" align="center">
<template #default="scope">
<div class="action-buttons">
<button class="action-btn primary" @click="handleViewLicense(scope.row)">查看授权</button>
<button class="action-btn primary" @click="handleEdit(scope.row)">编辑</button>
<button class="action-btn primary" @click="handleViewLicense(scope.row)">{{ t('customers.actions.viewLicense') }}</button>
<button class="action-btn primary" @click="handleEdit(scope.row)">{{ t('customers.actions.edit') }}</button>
<button
class="action-btn"
:class="scope.row.status === 'active' ? 'warning' : 'success'"
@click="handleDisable(scope.row)"
>
{{ scope.row.status === 'active' ? '禁用' : '启用' }}
{{ scope.row.status === 'active' ? t('customers.actions.disable') : t('customers.actions.enable') }}
</button>
<button class="action-btn danger" @click="handleDelete(scope.row)">删除</button>
<button class="action-btn danger" @click="handleDelete(scope.row)">{{ t('customers.actions.delete') }}</button>
</div>
</template>
</el-table-column>
@@ -191,6 +191,7 @@ import CustomerForm from './CustomerForm.vue'
import CustomerView from './CustomerView.vue'
import { Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox, ElLoading } from 'element-plus'
import { useI18n } from 'vue-i18n'
import {
getCustomers,
deleteCustomer,
@@ -207,6 +208,7 @@ import {
// Customer类型已从API文件导入
const { t } = useI18n()
const searchKeyword = ref('')
const filterCustomerType = ref('')
const filterCustomerLevel = ref('')
@@ -248,15 +250,15 @@ const loadEnums = async () => {
statusOptions.value = statusRes.data.items
}
} catch (error) {
console.error('加载枚举数据失败:', error)
console.error(t('customers.message.loadEnumError'), error)
}
}
// 页面标题
const getPageTitle = () => {
if (showCustomerView.value) return '查看客户'
if (showCustomerForm.value) return isEditMode.value ? '编辑客户' : '添加新客户'
return '客户管理'
if (showCustomerView.value) return t('customers.viewCustomer')
if (showCustomerForm.value) return isEditMode.value ? t('customers.editCustomer') : t('customers.addCustomer')
return t('customers.title')
}
// 时间格式化函数
@@ -302,7 +304,7 @@ const loadData = async () => {
total.value = 0
}
} catch (error: any) {
console.error('加载数据错误:', error)
console.error('Load data error:', error)
// 使用后端返回的错误信息
let errorMessage = error.backendMessage || error.response?.data?.message || error.message
@@ -347,22 +349,28 @@ const handleEdit = (row: Customer) => {
const handleDisable = async (row: Customer) => {
const newStatus = row.status === 'active' ? 'disabled' : 'active'
const actionText = newStatus === 'disabled' ? '禁用' : '启用'
const actionText = newStatus === 'disabled' ? t('customers.actions.disable') : t('customers.actions.enable')
try {
await ElMessageBox.confirm(
`确定要${actionText}客户 "${row.customer_name}" 吗?`,
`确认${actionText}`,
newStatus === 'disabled'
? t('customers.confirm.disableMessage', { name: row.customer_name })
: t('customers.confirm.enableMessage', { name: row.customer_name }),
newStatus === 'disabled'
? t('customers.confirm.disableTitle')
: t('customers.confirm.enableTitle'),
{
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmButtonText: t('customers.confirm.confirm'),
cancelButtonText: t('customers.confirm.cancel'),
type: 'warning'
}
)
const loadingInstance = ElLoading.service({
lock: true,
text: `${actionText}中...`,
text: newStatus === 'disabled'
? t('customers.message.disableLoading')
: t('customers.message.enableLoading'),
background: 'rgba(0, 0, 0, 0.7)'
})
@@ -377,7 +385,7 @@ const handleDisable = async (row: Customer) => {
ElMessage.error(response.message)
}
} catch (error: any) {
console.error(`${actionText}错误:`, error)
console.error(`${actionText} error:`, error)
// 使用后端返回的错误信息
let errorMessage = error.backendMessage || error.response?.data?.message || error.message
@@ -388,7 +396,7 @@ const handleDisable = async (row: Customer) => {
loadingInstance.close()
}
} catch {
ElMessage.info(`已取消${actionText}`)
ElMessage.info(t('customers.message.statusChangeCancel', { action: actionText }))
}
}
@@ -414,18 +422,18 @@ const handleViewLicense = (row: Customer) => {
const handleDelete = async (row: Customer) => {
try {
await ElMessageBox.confirm(
`确定要删除客户 "${row.customer_name}" 吗?`,
'确认删除',
t('customers.confirm.deleteMessage', { name: row.customer_name }),
t('customers.confirm.deleteTitle'),
{
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmButtonText: t('customers.confirm.confirm'),
cancelButtonText: t('customers.confirm.cancel'),
type: 'warning'
}
)
const loadingInstance = ElLoading.service({
lock: true,
text: '删除中...',
text: t('customers.message.deleteLoading'),
background: 'rgba(0, 0, 0, 0.7)'
})
@@ -440,7 +448,7 @@ const handleDelete = async (row: Customer) => {
ElMessage.error(response.message)
}
} catch (error: any) {
console.error('删除错误:', error)
console.error('Delete error:', error)
// 使用后端返回的错误信息
let errorMessage = error.backendMessage || error.response?.data?.message || error.message
@@ -451,14 +459,14 @@ const handleDelete = async (row: Customer) => {
loadingInstance.close()
}
} catch {
ElMessage.info('已取消删除')
ElMessage.info(t('customers.message.deleteCancel'))
}
}
// 处理客户表单操作
const handleFormSave = async (data: any) => {
// 这里处理保存逻辑
console.log('保存客户数据:', data)
console.log('Save customer data:', data)
showCustomerForm.value = false
await loadData() // 重新加载数据
}
@@ -645,8 +653,9 @@ onMounted(async () => {
width: 100%;
height: 100%; // 占满表格容器高度
position: relative;
overflow: auto; // 启用滚动以支持固定列
@include smooth-scroll;
@include mobile {
-webkit-overflow-scrolling: touch;
}
@@ -662,7 +671,7 @@ onMounted(async () => {
.el-table__header-wrapper,
.el-table__body-wrapper {
width: 100% !important;
overflow-x: hidden !important; /* 防止水平滚动条影响对齐 */
overflow-x: auto !important; /* 允许水平滚动以支持固定列 */
}
.el-table__body-wrapper {
@@ -810,6 +819,8 @@ onMounted(async () => {
.action-buttons {
gap: 8px; // 改为8px符合8px栅格
flex-wrap: nowrap;
justify-content: center;
width: auto;
}
.action-btn {
@@ -914,6 +925,8 @@ onMounted(async () => {
.action-buttons {
gap: 0.42vw; /* 8px/1920 = 0.42vw */
justify-content: center;
width: auto;
}
.action-btn {
@@ -958,10 +971,10 @@ onMounted(async () => {
box-sizing: border-box !important;
}
/* 确保操作列在桌面端的正确宽度 */
/* 操作列自适应宽度 */
.action-column {
width: 17.708vw !important; /* 300px/1920 = 15.625vw */
min-width: 17.708vw !important;
width: auto !important;
min-width: max-content !important;
}
}
@@ -1057,10 +1070,10 @@ onMounted(async () => {
padding: 8px 12px !important;
}
/* 移动端操作列宽度修复 */
/* 移动端操作列自适应宽度 */
.action-column {
width: 160px !important;
min-width: 160px !important;
width: auto !important;
min-width: max-content !important;
}
}
/* 设置表格容器 */
@@ -1441,10 +1454,11 @@ onMounted(async () => {
.action-buttons {
gap: $spacing-small;
padding: $spacing-base 0;
width: 100%;
width: auto;
min-width: max-content;
@include flex-center-vertical;
justify-content: flex-start;
justify-content: center;
flex-wrap: nowrap;
overflow: visible;
@@ -1473,10 +1487,11 @@ onMounted(async () => {
font-weight: $font-weight-primary;
line-height: 20px; // 改为20px符合8px栅格
flex-shrink: 0;
width: auto;
min-width: fit-content;
white-space: nowrap;
@include button-base;
@include text-ellipsis;
@include mobile {
flex: 0 0 calc(50% - #{math.div($spacing-extra-small, 2)}) !important;
+5 -2
View File
@@ -7,16 +7,19 @@
* @Description: 授权管理页面
-->
<template>
<Layout app-name="Cedar-V" page-title="授权管理">
<Layout app-name="Cedar-V" :page-title="t('pages.licenses.title')">
<!-- 页面内容 -->
<div class="content-container">
<h1>欢迎你来到授权管理页面</h1>
<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>
+5 -2
View File
@@ -7,16 +7,19 @@
* @Description: 角色权限页面
-->
<template>
<Layout app-name="Cedar-V" page-title="角色权限">
<Layout app-name="Cedar-V" :page-title="t('pages.roles.title')">
<!-- 页面内容 -->
<div class="content-container">
<h1>欢迎你来到角色权限页面</h1>
<h1>{{ t('pages.roles.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>
+5 -2
View File
@@ -7,16 +7,19 @@
* @Description: 系统用户页面
-->
<template>
<Layout app-name="Cedar-V" page-title="系统用户">
<Layout app-name="Cedar-V" :page-title="t('pages.users.title')">
<!-- 页面内容 -->
<div class="content-container">
<h1>欢迎你来到系统用户页面</h1>
<h1>{{ t('pages.users.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>