国际化,管理后台修改.

This commit is contained in:
18631081161 2025-12-21 17:22:48 +08:00
parent 33bb1bbd56
commit 5c2469137c
33 changed files with 840 additions and 374 deletions

View File

@ -16,7 +16,7 @@
### 小程序 (miniprogram/) ### 小程序 (miniprogram/)
- **框架**: uni-app + Vue 3 + Vite - **框架**: uni-app + Vue 3 + Vite
- **UI库**: uView Plus 3.6 - **UI库**: uView Plus 3.6
- **国际化**: vue-i18n (中文/英文/葡萄牙语) - **国际化**: vue-i18n (中文/英文/西班牙语)
### 管理后台 (admin/) ### 管理后台 (admin/)
- **框架**: Vue 3 + Vite - **框架**: Vue 3 + Vite
@ -96,7 +96,7 @@ docker-compose up -d
- **基础路径**: `/api/v1/` - **基础路径**: `/api/v1/`
- **认证方式**: Bearer Token (JWT) - **认证方式**: Bearer Token (JWT)
- **多语言**: `Accept-Language` 头或 `?language=zh|en|pt` - **多语言**: `Accept-Language` 头或 `?language=zh|en|es`
### 主要API端点 ### 主要API端点
@ -160,7 +160,7 @@ GET /api/v1/withdrawals # 提现记录
## 注意事项 ## 注意事项
1. **多语言字段**: Service、Category、Notification等模型包含 `_zh`、`_en`、`_pt` 后缀的字段 1. **多语言字段**: Service、Category、Notification等模型包含 `_zh`、`_en`、`_es` 后缀的字段
2. **UUID主键**: 所有主表使用UUID而非自增ID 2. **UUID主键**: 所有主表使用UUID而非自增ID
3. **预约编号格式**: `APT` + `YYYYMMDD` + 6位数字 3. **预约编号格式**: `APT` + `YYYYMMDD` + 6位数字
4. **订单编号格式**: `ORD` + `YYYYMMDD` + 6位数字 4. **订单编号格式**: `ORD` + `YYYYMMDD` + 6位数字

View File

@ -169,7 +169,7 @@
<el-dialog <el-dialog
v-model="detailsDialogVisible" v-model="detailsDialogVisible"
title="订单详情" title="订单详情"
width="700px" width="800px"
destroy-on-close destroy-on-close
> >
<div v-loading="detailsLoading" class="appointment-details"> <div v-loading="detailsLoading" class="appointment-details">
@ -185,10 +185,13 @@
</el-tag> </el-tag>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="服务项目"> <el-descriptions-item label="服务项目">
{{ appointmentDetails.service?.titleZh || '-' }} {{ appointmentDetails.service?.titleZh || appointmentDetails.hotService?.name_zh || '-' }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="服务分类"> <el-descriptions-item label="服务分类">
{{ appointmentDetails.service?.category?.nameZh || '-' }} {{ appointmentDetails.service?.category?.nameZh || (appointmentDetails.hotService ? '热门服务' : '-') }}
</el-descriptions-item>
<el-descriptions-item label="服务类型" v-if="appointmentDetails.serviceType">
{{ getServiceTypeLabel(appointmentDetails.serviceType) }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="订单金额"> <el-descriptions-item label="订单金额">
<span class="amount" v-if="appointmentDetails.amount"> <span class="amount" v-if="appointmentDetails.amount">
@ -199,11 +202,11 @@
<el-descriptions-item label="支付时间"> <el-descriptions-item label="支付时间">
{{ appointmentDetails.paidAt ? formatDate(appointmentDetails.paidAt) : '-' }} {{ appointmentDetails.paidAt ? formatDate(appointmentDetails.paidAt) : '-' }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="预约日期"> <el-descriptions-item label="预约日期" v-if="appointmentDetails.appointmentDate">
{{ appointmentDetails.appointmentDate || '-' }} {{ appointmentDetails.appointmentDate }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="预约时间"> <el-descriptions-item label="预约时间" v-if="appointmentDetails.appointmentTime">
{{ appointmentDetails.appointmentTime || '-' }} {{ appointmentDetails.appointmentTime }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="创建时间"> <el-descriptions-item label="创建时间">
{{ formatDate(appointmentDetails.createdAt) }} {{ formatDate(appointmentDetails.createdAt) }}
@ -216,14 +219,292 @@
<!-- Contact Info --> <!-- Contact Info -->
<el-descriptions title="联系信息" :column="2" border class="mt-20"> <el-descriptions title="联系信息" :column="2" border class="mt-20">
<el-descriptions-item label="预约人姓名"> <el-descriptions-item label="预约人姓名">
{{ appointmentDetails.realName }} {{ appointmentDetails.realName || '-' }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="联系方式"> <el-descriptions-item label="联系方式" v-if="appointmentDetails.contactMethod && appointmentDetails.contactValue">
<el-tag :type="getContactMethodType(appointmentDetails.contactMethod)" size="small"> <el-tag :type="getContactMethodType(appointmentDetails.contactMethod)" size="small">
{{ getContactMethodLabel(appointmentDetails.contactMethod) }} {{ getContactMethodLabel(appointmentDetails.contactMethod) }}
</el-tag> </el-tag>
{{ appointmentDetails.contactValue }} {{ appointmentDetails.contactValue }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="手机号" v-if="appointmentDetails.phone">
{{ appointmentDetails.phoneCountryCode ? `+${appointmentDetails.phoneCountryCode} ` : '' }}{{ appointmentDetails.phone }}
</el-descriptions-item>
<el-descriptions-item label="WhatsApp" v-if="appointmentDetails.whatsapp">
{{ appointmentDetails.whatsapp }}
</el-descriptions-item>
<el-descriptions-item label="微信号" v-if="appointmentDetails.wechatId">
{{ appointmentDetails.wechatId }}
</el-descriptions-item>
</el-descriptions>
<!-- Flight Booking Fields -->
<el-descriptions
title="机票预约信息"
:column="2"
border
class="mt-20"
v-if="hasFlightFields(appointmentDetails)"
>
<el-descriptions-item label="行程类型" v-if="appointmentDetails.tripType">
{{ appointmentDetails.tripType === 'single' ? '单程' : '往返' }}
</el-descriptions-item>
<el-descriptions-item label="舱位类型" v-if="appointmentDetails.cabinType">
{{ getCabinTypeLabel(appointmentDetails.cabinType) }}
</el-descriptions-item>
<el-descriptions-item label="出发城市" v-if="appointmentDetails.departureCity">
{{ appointmentDetails.departureCity }}
</el-descriptions-item>
<el-descriptions-item label="到达城市" v-if="appointmentDetails.arrivalCity">
{{ appointmentDetails.arrivalCity }}
</el-descriptions-item>
<el-descriptions-item label="出发日期" v-if="appointmentDetails.departureDate">
{{ appointmentDetails.departureDate }}
</el-descriptions-item>
<el-descriptions-item label="返程日期" v-if="appointmentDetails.returnDate">
{{ appointmentDetails.returnDate }}
</el-descriptions-item>
<el-descriptions-item label="成人人数" v-if="appointmentDetails.adultCount">
{{ appointmentDetails.adultCount }}
</el-descriptions-item>
<el-descriptions-item label="儿童人数" v-if="appointmentDetails.childCount">
{{ appointmentDetails.childCount }}
</el-descriptions-item>
<el-descriptions-item label="婴儿人数" v-if="appointmentDetails.infantCount">
{{ appointmentDetails.infantCount }}
</el-descriptions-item>
<el-descriptions-item label="行李件数" v-if="appointmentDetails.luggageCount">
{{ appointmentDetails.luggageCount }}
</el-descriptions-item>
</el-descriptions>
<!-- Hotel Booking Fields -->
<el-descriptions
title="酒店预订信息"
:column="2"
border
class="mt-20"
v-if="hasHotelFields(appointmentDetails)"
>
<el-descriptions-item label="入住日期" v-if="appointmentDetails.checkInDate">
{{ appointmentDetails.checkInDate }}
</el-descriptions-item>
<el-descriptions-item label="退房日期" v-if="appointmentDetails.checkOutDate">
{{ appointmentDetails.checkOutDate }}
</el-descriptions-item>
<el-descriptions-item label="国家/城市" v-if="appointmentDetails.countryCity">
{{ appointmentDetails.countryCity }}
</el-descriptions-item>
<el-descriptions-item label="酒店名称" v-if="appointmentDetails.hotelName">
{{ appointmentDetails.hotelName }}
</el-descriptions-item>
<el-descriptions-item label="房间数量" v-if="appointmentDetails.roomCount">
{{ appointmentDetails.roomCount }}
</el-descriptions-item>
<el-descriptions-item label="房间类型" v-if="appointmentDetails.roomType">
{{ appointmentDetails.roomType }}
</el-descriptions-item>
<el-descriptions-item label="成人人数" v-if="appointmentDetails.adultCount">
{{ appointmentDetails.adultCount }}
</el-descriptions-item>
<el-descriptions-item label="儿童人数" v-if="appointmentDetails.childCount">
{{ appointmentDetails.childCount }}
</el-descriptions-item>
<el-descriptions-item label="是否需要餐食" v-if="appointmentDetails.needMeal !== null && appointmentDetails.needMeal !== undefined">
{{ appointmentDetails.needMeal ? '是' : '否' }}
</el-descriptions-item>
<el-descriptions-item label="餐食计划" v-if="appointmentDetails.mealPlan">
{{ getMealPlanLabel(appointmentDetails.mealPlan) }}
</el-descriptions-item>
</el-descriptions>
<!-- VIP Lounge / Airport Transfer Fields -->
<el-descriptions
title="VIP贵宾室/接送机信息"
:column="2"
border
class="mt-20"
v-if="hasVipFields(appointmentDetails)"
>
<el-descriptions-item label="到达航班号" v-if="appointmentDetails.arrivalFlightNo">
{{ appointmentDetails.arrivalFlightNo }}
</el-descriptions-item>
<el-descriptions-item label="出发航班号" v-if="appointmentDetails.departureFlightNo">
{{ appointmentDetails.departureFlightNo }}
</el-descriptions-item>
<el-descriptions-item label="机场/航站楼" v-if="appointmentDetails.airportTerminal">
{{ appointmentDetails.airportTerminal }}
</el-descriptions-item>
<el-descriptions-item label="送达地址" v-if="appointmentDetails.deliveryAddress" :span="2">
{{ appointmentDetails.deliveryAddress }}
</el-descriptions-item>
</el-descriptions>
<!-- Unaccompanied Minor Fields -->
<el-descriptions
title="无成人陪伴儿童信息"
:column="2"
border
class="mt-20"
v-if="hasUnaccompaniedMinorFields(appointmentDetails)"
>
<el-descriptions-item label="儿童年龄" v-if="appointmentDetails.childAge">
{{ appointmentDetails.childAge }}
</el-descriptions-item>
<el-descriptions-item label="男孩数量" v-if="appointmentDetails.boyCount">
{{ appointmentDetails.boyCount }}
</el-descriptions-item>
<el-descriptions-item label="女孩数量" v-if="appointmentDetails.girlCount">
{{ appointmentDetails.girlCount }}
</el-descriptions-item>
<el-descriptions-item label="行程" v-if="appointmentDetails.itinerary" :span="2">
{{ appointmentDetails.itinerary }}
</el-descriptions-item>
</el-descriptions>
<!-- Train Ticket Fields -->
<el-descriptions
title="高铁票预订信息"
:column="2"
border
class="mt-20"
v-if="hasTrainFields(appointmentDetails)"
>
<el-descriptions-item label="出发站" v-if="appointmentDetails.originStation">
{{ appointmentDetails.originStation }}
</el-descriptions-item>
<el-descriptions-item label="到达站" v-if="appointmentDetails.destinationStation">
{{ appointmentDetails.destinationStation }}
</el-descriptions-item>
<el-descriptions-item label="出发日期" v-if="appointmentDetails.departureDate">
{{ appointmentDetails.departureDate }}
</el-descriptions-item>
<el-descriptions-item label="座位等级" v-if="appointmentDetails.seatClass">
{{ getSeatClassLabel(appointmentDetails.seatClass) }}
</el-descriptions-item>
<el-descriptions-item label="乘客数量" v-if="appointmentDetails.passengerCount">
{{ appointmentDetails.passengerCount }}
</el-descriptions-item>
</el-descriptions>
<!-- Medical / Special Assistance Fields -->
<el-descriptions
title="医疗/特殊需求信息"
:column="2"
border
class="mt-20"
v-if="hasMedicalFields(appointmentDetails)"
>
<el-descriptions-item label="医院名称" v-if="appointmentDetails.hospitalName">
{{ appointmentDetails.hospitalName }}
</el-descriptions-item>
<el-descriptions-item label="病情描述" v-if="appointmentDetails.conditionDescription" :span="2">
{{ appointmentDetails.conditionDescription }}
</el-descriptions-item>
<el-descriptions-item label="特殊协助原因" v-if="appointmentDetails.specialAssistanceReason" :span="2">
{{ appointmentDetails.specialAssistanceReason }}
</el-descriptions-item>
</el-descriptions>
<!-- Pet Transport Fields -->
<el-descriptions
title="宠物托运信息"
:column="2"
border
class="mt-20"
v-if="hasPetFields(appointmentDetails)"
>
<el-descriptions-item label="出发地" v-if="appointmentDetails.origin">
{{ appointmentDetails.origin }}
</el-descriptions-item>
<el-descriptions-item label="目的地" v-if="appointmentDetails.destination">
{{ appointmentDetails.destination }}
</el-descriptions-item>
<el-descriptions-item label="航班号" v-if="appointmentDetails.flightNo">
{{ appointmentDetails.flightNo }}
</el-descriptions-item>
<el-descriptions-item label="宠物类型" v-if="appointmentDetails.petType">
{{ appointmentDetails.petType }}
</el-descriptions-item>
<el-descriptions-item label="宠物名称" v-if="appointmentDetails.petName">
{{ appointmentDetails.petName }}
</el-descriptions-item>
<el-descriptions-item label="检疫证明" v-if="appointmentDetails.hasQuarantineCert !== null && appointmentDetails.hasQuarantineCert !== undefined">
{{ appointmentDetails.hasQuarantineCert ? '有' : '无' }}
</el-descriptions-item>
</el-descriptions>
<!-- Guide / Translation Service Fields -->
<el-descriptions
title="导游/翻译服务信息"
:column="2"
border
class="mt-20"
v-if="appointmentDetails.serviceDays"
>
<el-descriptions-item label="服务天数">
{{ appointmentDetails.serviceDays }}
</el-descriptions-item>
</el-descriptions>
<!-- Logistics Fields -->
<el-descriptions
title="物流信息"
:column="2"
border
class="mt-20"
v-if="hasLogisticsFields(appointmentDetails)"
>
<el-descriptions-item label="物品名称" v-if="appointmentDetails.itemName">
{{ appointmentDetails.itemName }}
</el-descriptions-item>
<el-descriptions-item label="物品数量" v-if="appointmentDetails.itemQuantity">
{{ appointmentDetails.itemQuantity }}
</el-descriptions-item>
<el-descriptions-item label="起运港" v-if="appointmentDetails.originPort">
{{ appointmentDetails.originPort }}
</el-descriptions-item>
<el-descriptions-item label="目的港" v-if="appointmentDetails.destinationPort">
{{ appointmentDetails.destinationPort }}
</el-descriptions-item>
<el-descriptions-item label="货物名称" v-if="appointmentDetails.cargoName">
{{ appointmentDetails.cargoName }}
</el-descriptions-item>
<el-descriptions-item label="货物数量" v-if="appointmentDetails.cargoQuantity">
{{ appointmentDetails.cargoQuantity }}
</el-descriptions-item>
</el-descriptions>
<!-- Travel Planning Fields -->
<el-descriptions
title="旅游规划信息"
:column="2"
border
class="mt-20"
v-if="hasTravelFields(appointmentDetails)"
>
<el-descriptions-item label="出行日期" v-if="appointmentDetails.travelDate">
{{ appointmentDetails.travelDate }}
</el-descriptions-item>
<el-descriptions-item label="旅游目的地" v-if="appointmentDetails.travelDestination">
{{ appointmentDetails.travelDestination }}
</el-descriptions-item>
<el-descriptions-item label="旅游天数" v-if="appointmentDetails.travelDays">
{{ appointmentDetails.travelDays }}
</el-descriptions-item>
</el-descriptions>
<!-- Consulting Service Fields -->
<el-descriptions
title="咨询服务信息"
:column="1"
border
class="mt-20"
v-if="appointmentDetails.specificRequirements"
>
<el-descriptions-item label="具体需求">
{{ appointmentDetails.specificRequirements }}
</el-descriptions-item>
</el-descriptions> </el-descriptions>
<!-- User Info --> <!-- User Info -->
@ -585,11 +866,120 @@ function getLanguageLabel(lang) {
const labels = { const labels = {
zh: '中文', zh: '中文',
en: 'English', en: 'English',
pt: 'Português' es: 'Español'
} }
return labels[lang] || lang return labels[lang] || lang
} }
function getServiceTypeLabel(type) {
const labels = {
travel: '旅行服务',
accommodation: '住宿服务',
guide: '导游服务',
translation: '翻译服务',
consulting: '咨询服务',
flight: '机票预订',
hotel: '酒店预订',
train: '高铁票预订',
vip_lounge: 'VIP贵宾室',
airport_transfer: '接送机',
pet_transport: '宠物托运',
medical: '医疗服务',
logistics: '物流服务',
other: '其他服务'
}
return labels[type] || type
}
function getCabinTypeLabel(type) {
const labels = {
economy: '经济舱',
premium_economy: '超级经济舱',
business: '商务舱'
}
return labels[type] || type
}
function getMealPlanLabel(plan) {
const labels = {
breakfast: '早餐',
three_meals: '三餐',
all_inclusive: '全包'
}
return labels[plan] || plan
}
function getSeatClassLabel(seatClass) {
const labels = {
first: '一等座',
second: '二等座',
third: '三等座'
}
return labels[seatClass] || seatClass
}
// Field detection helpers
function hasFlightFields(data) {
return data && (
data.tripType || data.cabinType || data.departureCity || data.arrivalCity ||
data.departureDate || data.returnDate || data.luggageCount ||
(data.adultCount && !data.checkInDate && !data.originStation)
)
}
function hasHotelFields(data) {
return data && (
data.checkInDate || data.checkOutDate || data.countryCity ||
data.hotelName || data.roomCount || data.roomType ||
data.needMeal !== null || data.mealPlan
)
}
function hasVipFields(data) {
return data && (
data.arrivalFlightNo || data.departureFlightNo ||
data.airportTerminal || data.deliveryAddress
)
}
function hasUnaccompaniedMinorFields(data) {
return data && (
data.childAge || data.boyCount || data.girlCount || data.itinerary
)
}
function hasTrainFields(data) {
return data && (
data.originStation || data.destinationStation || data.seatClass || data.passengerCount
)
}
function hasMedicalFields(data) {
return data && (
data.hospitalName || data.conditionDescription || data.specialAssistanceReason
)
}
function hasPetFields(data) {
return data && (
data.petType || data.petName || data.hasQuarantineCert !== null ||
(data.origin && data.destination && data.flightNo)
)
}
function hasLogisticsFields(data) {
return data && (
data.itemName || data.itemQuantity || data.originPort ||
data.destinationPort || data.cargoName || data.cargoQuantity
)
}
function hasTravelFields(data) {
return data && (
data.travelDate || data.travelDestination || data.travelDays
)
}
// Initialize // Initialize
onMounted(() => { onMounted(() => {
fetchCategories() fetchCategories()

View File

@ -15,7 +15,7 @@
<el-table-column prop="key" label="Key" width="150" /> <el-table-column prop="key" label="Key" width="150" />
<el-table-column prop="name_zh" label="中文名称" /> <el-table-column prop="name_zh" label="中文名称" />
<el-table-column prop="name_en" label="英文名称" /> <el-table-column prop="name_en" label="英文名称" />
<el-table-column prop="name_pt" label="语名称" /> <el-table-column prop="name_pt" label="西语名称" />
<el-table-column prop="sort_order" label="排序" width="80" /> <el-table-column prop="sort_order" label="排序" width="80" />
<el-table-column label="操作" width="180" fixed="right"> <el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
@ -42,7 +42,7 @@
<el-form-item label="英文名称" prop="name_en"> <el-form-item label="英文名称" prop="name_en">
<el-input v-model="form.name_en" /> <el-input v-model="form.name_en" />
</el-form-item> </el-form-item>
<el-form-item label="语名称" prop="name_pt"> <el-form-item label="西语名称" prop="name_pt">
<el-input v-model="form.name_pt" /> <el-input v-model="form.name_pt" />
</el-form-item> </el-form-item>
<el-form-item label="排序" prop="sort_order"> <el-form-item label="排序" prop="sort_order">
@ -91,7 +91,7 @@ const rules = {
{ required: true, message: '请输入英文名称', trigger: 'blur' } { required: true, message: '请输入英文名称', trigger: 'blur' }
], ],
name_pt: [ name_pt: [
{ required: true, message: '请输入语名称', trigger: 'blur' } { required: true, message: '请输入西语名称', trigger: 'blur' }
] ]
} }

View File

@ -77,7 +77,7 @@
</el-table-column> </el-table-column>
<el-table-column prop="name_zh" label="中文名称" min-width="120" /> <el-table-column prop="name_zh" label="中文名称" min-width="120" />
<el-table-column prop="name_en" label="英文名称" min-width="120" /> <el-table-column prop="name_en" label="英文名称" min-width="120" />
<el-table-column prop="name_pt" label="语名称" min-width="120" /> <el-table-column prop="name_pt" label="西语名称" min-width="120" />
<el-table-column label="服务类型" width="100" align="center"> <el-table-column label="服务类型" width="100" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="getServiceTypeTag(row.service_type)" size="small"> <el-tag :type="getServiceTypeTag(row.service_type)" size="small">
@ -147,8 +147,8 @@
<el-form-item label="英文名称" required> <el-form-item label="英文名称" required>
<el-input v-model="hotServiceForm.name_en" placeholder="Please enter English name" /> <el-input v-model="hotServiceForm.name_en" placeholder="Please enter English name" />
</el-form-item> </el-form-item>
<el-form-item label="语名称" required> <el-form-item label="西语名称" required>
<el-input v-model="hotServiceForm.name_pt" placeholder="Por favor, insira o nome" /> <el-input v-model="hotServiceForm.name_pt" placeholder="Por favor, ingrese el nombre" />
</el-form-item> </el-form-item>
<el-form-item label="服务类型" required> <el-form-item label="服务类型" required>
<el-select v-model="hotServiceForm.service_type" placeholder="请选择服务类型" filterable style="width: 100%"> <el-select v-model="hotServiceForm.service_type" placeholder="请选择服务类型" filterable style="width: 100%">

View File

@ -130,12 +130,12 @@
<el-form-item label="英文内容"> <el-form-item label="英文内容">
<el-input v-model="sendForm.contentEn" type="textarea" :rows="2" placeholder="English content (optional)" maxlength="500" /> <el-input v-model="sendForm.contentEn" type="textarea" :rows="2" placeholder="English content (optional)" maxlength="500" />
</el-form-item> </el-form-item>
<el-divider content-position="left">语内容选填</el-divider> <el-divider content-position="left">西语内容选填</el-divider>
<el-form-item label="语标题"> <el-form-item label="西语标题">
<el-input v-model="sendForm.titlePt" placeholder="Título em português (opcional)" maxlength="100" /> <el-input v-model="sendForm.titlePt" placeholder="Título en español (opcional)" maxlength="100" />
</el-form-item> </el-form-item>
<el-form-item label="语内容"> <el-form-item label="西语内容">
<el-input v-model="sendForm.contentPt" type="textarea" :rows="2" placeholder="Conteúdo em português (opcional)" maxlength="500" /> <el-input v-model="sendForm.contentPt" type="textarea" :rows="2" placeholder="Contenido en español (opcional)" maxlength="500" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>

View File

@ -72,7 +72,7 @@
</el-table-column> </el-table-column>
<el-table-column prop="titleZh" label="中文名称" min-width="150" show-overflow-tooltip /> <el-table-column prop="titleZh" label="中文名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="titleEn" label="英文名称" min-width="150" show-overflow-tooltip /> <el-table-column prop="titleEn" label="英文名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="titlePt" label="语名称" min-width="150" show-overflow-tooltip /> <el-table-column prop="titlePt" label="西语名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="category" label="分类" width="120" align="center"> <el-table-column prop="category" label="分类" width="120" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.category" size="small">{{ row.category.nameZh || row.category.name_zh || '-' }}</el-tag> <el-tag v-if="row.category" size="small">{{ row.category.nameZh || row.category.name_zh || '-' }}</el-tag>
@ -172,8 +172,8 @@
<el-form-item label="英文标题" prop="titleEn"> <el-form-item label="英文标题" prop="titleEn">
<el-input v-model="form.titleEn" placeholder="Please enter English title" /> <el-input v-model="form.titleEn" placeholder="Please enter English title" />
</el-form-item> </el-form-item>
<el-form-item label="语标题" prop="titlePt"> <el-form-item label="西语标题" prop="titlePt">
<el-input v-model="form.titlePt" placeholder="Por favor, insira o título em português" /> <el-input v-model="form.titlePt" placeholder="Por favor, ingrese el título en español" />
</el-form-item> </el-form-item>
<el-divider content-position="left">其他信息</el-divider> <el-divider content-position="left">其他信息</el-divider>
@ -299,7 +299,7 @@ const formRules = {
serviceType: [{ required: true, message: '请选择服务具体类型', trigger: 'change' }], serviceType: [{ required: true, message: '请选择服务具体类型', trigger: 'change' }],
titleZh: [{ required: true, message: '请输入中文标题', trigger: 'blur' }], titleZh: [{ required: true, message: '请输入中文标题', trigger: 'blur' }],
titleEn: [{ required: true, message: '请输入英文标题', trigger: 'blur' }], titleEn: [{ required: true, message: '请输入英文标题', trigger: 'blur' }],
titlePt: [{ required: true, message: '请输入语标题', trigger: 'blur' }] titlePt: [{ required: true, message: '请输入西语标题', trigger: 'blur' }]
} }
// //

View File

@ -22,7 +22,7 @@
<el-select v-model="filters.language" placeholder="全部" clearable style="width: 120px"> <el-select v-model="filters.language" placeholder="全部" clearable style="width: 120px">
<el-option label="中文" value="zh" /> <el-option label="中文" value="zh" />
<el-option label="English" value="en" /> <el-option label="English" value="en" />
<el-option label="Português" value="pt" /> <el-option label="Español" value="es" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@ -494,7 +494,7 @@ function getLanguageLabel(lang) {
const labels = { const labels = {
zh: '中文', zh: '中文',
en: 'English', en: 'English',
pt: 'Português' es: 'Español'
} }
return labels[lang] || lang return labels[lang] || lang
} }
@ -503,7 +503,7 @@ function getLanguageTagType(lang) {
const types = { const types = {
zh: '', zh: '',
en: 'success', en: 'success',
pt: 'warning' es: 'warning'
} }
return types[lang] || 'info' return types[lang] || 'info'
} }

View File

@ -38,7 +38,7 @@ Authorization: Bearer <your_token>
## 多语言支持 ## 多语言支持
系统支持三种语言: 中文(zh)英文(en)葡萄牙语(pt) 系统支持三种语言: 中文(zh)英文(en)西班牙语(es)
可通过以下方式指定语言: 可通过以下方式指定语言:
1. 请求头: \`Accept-Language: zh\` 1. 请求头: \`Accept-Language: zh\`
@ -140,7 +140,7 @@ Authorization: Bearer <your_token>
phone: { type: 'string', example: '+86 13800138000' }, phone: { type: 'string', example: '+86 13800138000' },
whatsapp: { type: 'string', example: '+1 234 567 8900' }, whatsapp: { type: 'string', example: '+1 234 567 8900' },
wechatId: { type: 'string', example: 'wxid_xxxxx' }, wechatId: { type: 'string', example: 'wxid_xxxxx' },
language: { type: 'string', enum: ['zh', 'en', 'pt'], example: 'zh' }, language: { type: 'string', enum: ['zh', 'en', 'es'], example: 'zh' },
balance: { type: 'number', format: 'decimal', example: 100.50 }, balance: { type: 'number', format: 'decimal', example: 100.50 },
invitationCode: { type: 'string', example: 'ABC123' }, invitationCode: { type: 'string', example: 'ABC123' },
status: { type: 'string', enum: ['active', 'suspended'], example: 'active' }, status: { type: 'string', enum: ['active', 'suspended'], example: 'active' },
@ -156,7 +156,7 @@ Authorization: Bearer <your_token>
key: { type: 'string', example: 'airport' }, key: { type: 'string', example: 'airport' },
nameZh: { type: 'string', example: '机场接送' }, nameZh: { type: 'string', example: '机场接送' },
nameEn: { type: 'string', example: 'Airport Transfer' }, nameEn: { type: 'string', example: 'Airport Transfer' },
namePt: { type: 'string', example: 'Transferência do Aeroporto' }, nameEs: { type: 'string', example: 'Transferencia del Aeropuerto' },
icon: { type: 'string', example: 'airport-icon.png' }, icon: { type: 'string', example: 'airport-icon.png' },
sortOrder: { type: 'integer', example: 1 }, sortOrder: { type: 'integer', example: 1 },
}, },
@ -175,10 +175,10 @@ Authorization: Bearer <your_token>
}, },
titleZh: { type: 'string', example: '机场接机服务' }, titleZh: { type: 'string', example: '机场接机服务' },
titleEn: { type: 'string', example: 'Airport Pickup Service' }, titleEn: { type: 'string', example: 'Airport Pickup Service' },
titlePt: { type: 'string', example: 'Serviço de Busca no Aeroporto' }, titleEs: { type: 'string', example: 'Servicio de Recogida en el Aeropuerto' },
descriptionZh: { type: 'string' }, descriptionZh: { type: 'string' },
descriptionEn: { type: 'string' }, descriptionEn: { type: 'string' },
descriptionPt: { type: 'string' }, descriptionEs: { type: 'string' },
image: { type: 'string', example: '/uploads/service.jpg' }, image: { type: 'string', example: '/uploads/service.jpg' },
price: { type: 'number', format: 'decimal', example: 299.00 }, price: { type: 'number', format: 'decimal', example: 299.00 },
status: { type: 'string', enum: ['active', 'inactive'], example: 'active' }, status: { type: 'string', enum: ['active', 'inactive'], example: 'active' },
@ -229,7 +229,7 @@ Authorization: Bearer <your_token>
id: { type: 'integer', example: 1 }, id: { type: 'integer', example: 1 },
name_zh: { type: 'string', example: '全球机票代理' }, name_zh: { type: 'string', example: '全球机票代理' },
name_en: { type: 'string', example: 'Global Flight Booking' }, name_en: { type: 'string', example: 'Global Flight Booking' },
name_pt: { type: 'string', example: 'Reserva de Voos Global' }, name_es: { type: 'string', example: 'Reserva de Vuelos Global' },
service_type: { service_type: {
type: 'string', type: 'string',
description: '服务具体类型', description: '服务具体类型',
@ -275,10 +275,10 @@ Authorization: Bearer <your_token>
type: { type: 'string', enum: ['system', 'activity', 'service'], example: 'system' }, type: { type: 'string', enum: ['system', 'activity', 'service'], example: 'system' },
titleZh: { type: 'string', example: '系统通知' }, titleZh: { type: 'string', example: '系统通知' },
titleEn: { type: 'string', example: 'System Notification' }, titleEn: { type: 'string', example: 'System Notification' },
titlePt: { type: 'string', example: 'Notificação do Sistema' }, titleEs: { type: 'string', example: 'Notificación del Sistema' },
contentZh: { type: 'string' }, contentZh: { type: 'string' },
contentEn: { type: 'string' }, contentEn: { type: 'string' },
contentPt: { type: 'string' }, contentEs: { type: 'string' },
isRead: { type: 'boolean', example: false }, isRead: { type: 'boolean', example: false },
readAt: { type: 'string', format: 'date-time' }, readAt: { type: 'string', format: 'date-time' },
createdAt: { type: 'string', format: 'date-time' }, createdAt: { type: 'string', format: 'date-time' },

View File

@ -0,0 +1,71 @@
/**
* Migration: Rename Portuguese (pt) fields to Spanish (es)
* 将葡萄牙语字段重命名为西班牙语字段
*/
const { sequelize } = require('../config/database');
async function up() {
const queryInterface = sequelize.getQueryInterface();
console.log('Starting migration: Rename pt fields to es...');
try {
// 1. Rename category table fields
console.log('Renaming category.name_pt to name_es...');
await queryInterface.renameColumn('category', 'name_pt', 'name_es');
// 2. Rename service table fields
console.log('Renaming service.title_pt to title_es...');
await queryInterface.renameColumn('service', 'title_pt', 'title_es');
console.log('Renaming service.description_pt to description_es...');
await queryInterface.renameColumn('service', 'description_pt', 'description_es');
// 3. Rename notification table fields
console.log('Renaming notification.title_pt to title_es...');
await queryInterface.renameColumn('notification', 'title_pt', 'title_es');
console.log('Renaming notification.content_pt to content_es...');
await queryInterface.renameColumn('notification', 'content_pt', 'content_es');
// 4. Rename hot_services table fields
console.log('Renaming hot_services.name_pt to name_es...');
await queryInterface.renameColumn('hot_services', 'name_pt', 'name_es');
// 5. Update user language preferences from 'pt' to 'es'
console.log('Updating user language preferences from pt to es...');
await sequelize.query("UPDATE `user` SET language = 'es' WHERE language = 'pt'");
console.log('Migration completed successfully!');
} catch (error) {
console.error('Migration failed:', error.message);
throw error;
}
}
async function down() {
const queryInterface = sequelize.getQueryInterface();
console.log('Starting rollback: Rename es fields back to pt...');
try {
// Reverse all the renames
await queryInterface.renameColumn('category', 'name_es', 'name_pt');
await queryInterface.renameColumn('service', 'title_es', 'title_pt');
await queryInterface.renameColumn('service', 'description_es', 'description_pt');
await queryInterface.renameColumn('notification', 'title_es', 'title_pt');
await queryInterface.renameColumn('notification', 'content_es', 'content_pt');
await queryInterface.renameColumn('hot_services', 'name_es', 'name_pt');
// Revert user language preferences
await sequelize.query("UPDATE `user` SET language = 'pt' WHERE language = 'es'");
console.log('Rollback completed successfully!');
} catch (error) {
console.error('Rollback failed:', error.message);
throw error;
}
}
module.exports = { up, down };

View File

@ -26,10 +26,10 @@ const Category = sequelize.define('Category', {
allowNull: false, allowNull: false,
field: 'name_en', field: 'name_en',
}, },
namePt: { nameEs: {
type: DataTypes.STRING(100), type: DataTypes.STRING(100),
allowNull: false, allowNull: false,
field: 'name_pt', field: 'name_es',
}, },
icon: { icon: {
type: DataTypes.STRING(500), type: DataTypes.STRING(500),
@ -60,14 +60,14 @@ Category.prototype.toJSON = function () {
// snake_case (for backward compatibility) // snake_case (for backward compatibility)
name_zh: values.nameZh, name_zh: values.nameZh,
name_en: values.nameEn, name_en: values.nameEn,
name_pt: values.namePt, name_es: values.nameEs,
sort_order: values.sortOrder, sort_order: values.sortOrder,
created_at: values.createdAt, created_at: values.createdAt,
updated_at: values.updatedAt, updated_at: values.updatedAt,
// camelCase (for consistency) // camelCase (for consistency)
nameZh: values.nameZh, nameZh: values.nameZh,
nameEn: values.nameEn, nameEn: values.nameEn,
namePt: values.namePt, nameEs: values.nameEs,
icon: values.icon, icon: values.icon,
sortOrder: values.sortOrder, sortOrder: values.sortOrder,
createdAt: values.createdAt, createdAt: values.createdAt,

View File

@ -17,7 +17,7 @@ const HotService = sequelize.define(
type: DataTypes.STRING(100), type: DataTypes.STRING(100),
allowNull: false, allowNull: false,
}, },
name_pt: { name_es: {
type: DataTypes.STRING(100), type: DataTypes.STRING(100),
allowNull: false, allowNull: false,
}, },

View File

@ -34,10 +34,10 @@ const Notification = sequelize.define('Notification', {
allowNull: false, allowNull: false,
field: 'title_en', field: 'title_en',
}, },
titlePt: { titleEs: {
type: DataTypes.STRING(200), type: DataTypes.STRING(200),
allowNull: false, allowNull: false,
field: 'title_pt', field: 'title_es',
}, },
contentZh: { contentZh: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
@ -49,10 +49,10 @@ const Notification = sequelize.define('Notification', {
allowNull: false, allowNull: false,
field: 'content_en', field: 'content_en',
}, },
contentPt: { contentEs: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: false, allowNull: false,
field: 'content_pt', field: 'content_es',
}, },
isRead: { isRead: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,

View File

@ -36,10 +36,10 @@ const Service = sequelize.define('Service', {
allowNull: false, allowNull: false,
field: 'title_en', field: 'title_en',
}, },
titlePt: { titleEs: {
type: DataTypes.STRING(200), type: DataTypes.STRING(200),
allowNull: false, allowNull: false,
field: 'title_pt', field: 'title_es',
}, },
descriptionZh: { descriptionZh: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
@ -51,10 +51,10 @@ const Service = sequelize.define('Service', {
allowNull: true, allowNull: true,
field: 'description_en', field: 'description_en',
}, },
descriptionPt: { descriptionEs: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: true, allowNull: true,
field: 'description_pt', field: 'description_es',
}, },
image: { image: {
type: DataTypes.STRING(500), type: DataTypes.STRING(500),

View File

@ -61,7 +61,7 @@ router.post(
body('categoryId').isUUID().withMessage('Category ID must be a valid UUID'), body('categoryId').isUUID().withMessage('Category ID must be a valid UUID'),
body('titleZh').notEmpty().withMessage('Chinese title is required'), body('titleZh').notEmpty().withMessage('Chinese title is required'),
body('titleEn').notEmpty().withMessage('English title is required'), body('titleEn').notEmpty().withMessage('English title is required'),
body('titlePt').notEmpty().withMessage('Portuguese title is required'), body('titleEs').notEmpty().withMessage('Spanish title is required'),
body('descriptionZh').optional({ nullable: true }).custom((value) => { body('descriptionZh').optional({ nullable: true }).custom((value) => {
if (value === null || value === undefined) return true; if (value === null || value === undefined) return true;
if (typeof value !== 'string') throw new Error('Description must be a string'); if (typeof value !== 'string') throw new Error('Description must be a string');
@ -72,7 +72,7 @@ router.post(
if (typeof value !== 'string') throw new Error('Description must be a string'); if (typeof value !== 'string') throw new Error('Description must be a string');
return true; return true;
}), }),
body('descriptionPt').optional({ nullable: true }).custom((value) => { body('descriptionEs').optional({ nullable: true }).custom((value) => {
if (value === null || value === undefined) return true; if (value === null || value === undefined) return true;
if (typeof value !== 'string') throw new Error('Description must be a string'); if (typeof value !== 'string') throw new Error('Description must be a string');
return true; return true;
@ -108,7 +108,7 @@ router.put(
body('categoryId').optional().isUUID().withMessage('Category ID must be a valid UUID'), body('categoryId').optional().isUUID().withMessage('Category ID must be a valid UUID'),
body('titleZh').optional().notEmpty().withMessage('Chinese title cannot be empty'), body('titleZh').optional().notEmpty().withMessage('Chinese title cannot be empty'),
body('titleEn').optional().notEmpty().withMessage('English title cannot be empty'), body('titleEn').optional().notEmpty().withMessage('English title cannot be empty'),
body('titlePt').optional().notEmpty().withMessage('Portuguese title cannot be empty'), body('titleEs').optional().notEmpty().withMessage('Spanish title cannot be empty'),
body('descriptionZh').optional({ nullable: true }).custom((value) => { body('descriptionZh').optional({ nullable: true }).custom((value) => {
if (value === null || value === undefined) return true; if (value === null || value === undefined) return true;
if (typeof value !== 'string') throw new Error('Description must be a string'); if (typeof value !== 'string') throw new Error('Description must be a string');
@ -119,7 +119,7 @@ router.put(
if (typeof value !== 'string') throw new Error('Description must be a string'); if (typeof value !== 'string') throw new Error('Description must be a string');
return true; return true;
}), }),
body('descriptionPt').optional({ nullable: true }).custom((value) => { body('descriptionEs').optional({ nullable: true }).custom((value) => {
if (value === null || value === undefined) return true; if (value === null || value === undefined) return true;
if (typeof value !== 'string') throw new Error('Description must be a string'); if (typeof value !== 'string') throw new Error('Description must be a string');
return true; return true;

View File

@ -45,7 +45,7 @@ router.put(
'/language', '/language',
authenticateUser, authenticateUser,
[ [
body('language').notEmpty().isIn(['zh', 'en', 'pt']).withMessage('Language must be one of: zh, en, pt'), body('language').notEmpty().isIn(['zh', 'en', 'es']).withMessage('Language must be one of: zh, en, es'),
], ],
userController.setLanguage userController.setLanguage
); );

View File

@ -61,13 +61,13 @@ const getAppointmentList = async (options = {}) => {
{ {
model: Service, model: Service,
as: 'service', as: 'service',
attributes: ['id', 'titleZh', 'titleEn', 'titlePt', 'image', 'price', 'categoryId'], attributes: ['id', 'titleZh', 'titleEn', 'titleEs', 'image', 'price', 'categoryId'],
required: false, required: false,
include: [ include: [
{ {
model: Category, model: Category,
as: 'category', as: 'category',
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt'], attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs'],
...(categoryId && { where: { id: categoryId } }), ...(categoryId && { where: { id: categoryId } }),
}, },
], ],
@ -75,7 +75,7 @@ const getAppointmentList = async (options = {}) => {
{ {
model: HotService, model: HotService,
as: 'hotService', as: 'hotService',
attributes: ['id', 'name_zh', 'name_en', 'name_pt', 'image_url', 'service_type'], attributes: ['id', 'name_zh', 'name_en', 'name_es', 'image_url', 'service_type'],
required: false, required: false,
}, },
{ {
@ -141,15 +141,20 @@ const getAppointmentDetails = async (appointmentId) => {
{ {
model: Service, model: Service,
as: 'service', as: 'service',
attributes: ['id', 'titleZh', 'titleEn', 'titlePt', 'descriptionZh', 'descriptionEn', 'descriptionPt', 'image', 'price'], attributes: ['id', 'titleZh', 'titleEn', 'titleEs', 'descriptionZh', 'descriptionEn', 'descriptionEs', 'image', 'price'],
include: [ include: [
{ {
model: Category, model: Category,
as: 'category', as: 'category',
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt'], attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs'],
}, },
], ],
}, },
{
model: HotService,
as: 'hotService',
attributes: ['id', 'name_zh', 'name_en', 'name_es', 'image_url', 'service_type'],
},
{ {
model: User, model: User,
as: 'user', as: 'user',
@ -287,7 +292,7 @@ const getAppointmentStatistics = async (options = {}) => {
{ {
model: Service, model: Service,
as: 'service', as: 'service',
attributes: ['id', 'titleZh', 'titleEn', 'titlePt'], attributes: ['id', 'titleZh', 'titleEn', 'titleEs'],
...(categoryId && { ...(categoryId && {
include: [ include: [
{ {
@ -304,7 +309,7 @@ const getAppointmentStatistics = async (options = {}) => {
'serviceId', 'serviceId',
[require('sequelize').fn('COUNT', require('sequelize').col('Appointment.id')), 'count'], [require('sequelize').fn('COUNT', require('sequelize').col('Appointment.id')), 'count'],
], ],
group: ['serviceId', 'service.id', 'service.titleZh', 'service.titleEn', 'service.titlePt'], group: ['serviceId', 'service.id', 'service.titleZh', 'service.titleEn', 'service.titleEs'],
order: [[require('sequelize').literal('count'), 'DESC']], order: [[require('sequelize').literal('count'), 'DESC']],
limit: 10, limit: 10,
raw: true, raw: true,
@ -320,7 +325,7 @@ const getAppointmentStatistics = async (options = {}) => {
serviceName: { serviceName: {
zh: item['service.titleZh'], zh: item['service.titleZh'],
en: item['service.titleEn'], en: item['service.titleEn'],
pt: item['service.titlePt'], es: item['service.titleEs'],
}, },
count: parseInt(item.count), count: parseInt(item.count),
})), })),

View File

@ -62,10 +62,10 @@ class AdminNotificationService {
type: notificationData.type || 'system', type: notificationData.type || 'system',
titleZh: notificationData.titleZh, titleZh: notificationData.titleZh,
titleEn: notificationData.titleEn || notificationData.titleZh, titleEn: notificationData.titleEn || notificationData.titleZh,
titlePt: notificationData.titlePt || notificationData.titleZh, titleEs: notificationData.titleEs || notificationData.titleZh,
contentZh: notificationData.contentZh, contentZh: notificationData.contentZh,
contentEn: notificationData.contentEn || notificationData.contentZh, contentEn: notificationData.contentEn || notificationData.contentZh,
contentPt: notificationData.contentPt || notificationData.contentZh, contentEs: notificationData.contentEs || notificationData.contentZh,
isRead: false, isRead: false,
}); });
@ -88,10 +88,10 @@ class AdminNotificationService {
type: notificationData.type || 'system', type: notificationData.type || 'system',
titleZh: notificationData.titleZh, titleZh: notificationData.titleZh,
titleEn: notificationData.titleEn || notificationData.titleZh, titleEn: notificationData.titleEn || notificationData.titleZh,
titlePt: notificationData.titlePt || notificationData.titleZh, titleEs: notificationData.titleEs || notificationData.titleZh,
contentZh: notificationData.contentZh, contentZh: notificationData.contentZh,
contentEn: notificationData.contentEn || notificationData.contentZh, contentEn: notificationData.contentEn || notificationData.contentZh,
contentPt: notificationData.contentPt || notificationData.contentZh, contentEs: notificationData.contentEs || notificationData.contentZh,
isRead: false, isRead: false,
})); }));

View File

@ -28,7 +28,7 @@ class AdminServiceService {
where[Op.or] = [ where[Op.or] = [
{ titleZh: { [Op.like]: `%${search}%` } }, { titleZh: { [Op.like]: `%${search}%` } },
{ titleEn: { [Op.like]: `%${search}%` } }, { titleEn: { [Op.like]: `%${search}%` } },
{ titlePt: { [Op.like]: `%${search}%` } }, { titleEs: { [Op.like]: `%${search}%` } },
]; ];
} }
@ -39,7 +39,7 @@ class AdminServiceService {
{ {
model: Category, model: Category,
as: 'category', as: 'category',
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt', 'icon'], attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs', 'icon'],
}, },
], ],
order: [['sortOrder', 'ASC'], ['createdAt', 'DESC']], order: [['sortOrder', 'ASC'], ['createdAt', 'DESC']],
@ -79,7 +79,7 @@ class AdminServiceService {
} }
// Validate required fields // Validate required fields
const requiredFields = ['categoryId', 'titleZh', 'titleEn', 'titlePt']; const requiredFields = ['categoryId', 'titleZh', 'titleEn', 'titleEs'];
for (const field of requiredFields) { for (const field of requiredFields) {
if (!serviceData[field]) { if (!serviceData[field]) {
const error = new Error(`Missing required field: ${field}`); const error = new Error(`Missing required field: ${field}`);
@ -95,10 +95,10 @@ class AdminServiceService {
serviceType: serviceData.serviceType || null, serviceType: serviceData.serviceType || null,
titleZh: serviceData.titleZh, titleZh: serviceData.titleZh,
titleEn: serviceData.titleEn, titleEn: serviceData.titleEn,
titlePt: serviceData.titlePt, titleEs: serviceData.titleEs,
descriptionZh: serviceData.descriptionZh || null, descriptionZh: serviceData.descriptionZh || null,
descriptionEn: serviceData.descriptionEn || null, descriptionEn: serviceData.descriptionEn || null,
descriptionPt: serviceData.descriptionPt || null, descriptionEs: serviceData.descriptionEs || null,
image: serviceData.image || null, image: serviceData.image || null,
price: serviceData.price || null, price: serviceData.price || null,
status: serviceData.status || 'active', status: serviceData.status || 'active',
@ -150,10 +150,10 @@ class AdminServiceService {
'serviceType', 'serviceType',
'titleZh', 'titleZh',
'titleEn', 'titleEn',
'titlePt', 'titleEs',
'descriptionZh', 'descriptionZh',
'descriptionEn', 'descriptionEn',
'descriptionPt', 'descriptionEs',
'image', 'image',
'price', 'price',
'status', 'status',
@ -237,7 +237,7 @@ class AdminServiceService {
{ {
model: Category, model: Category,
as: 'category', as: 'category',
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt', 'icon'], attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs', 'icon'],
}, },
], ],
}); });

View File

@ -161,12 +161,12 @@ const getServicePopularityRankings = async (options = {}) => {
{ {
model: Service, model: Service,
as: 'service', as: 'service',
attributes: ['id', 'titleZh', 'titleEn', 'titlePt', 'image', 'price'], attributes: ['id', 'titleZh', 'titleEn', 'titleEs', 'image', 'price'],
include: [ include: [
{ {
model: Category, model: Category,
as: 'category', as: 'category',
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt'], attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs'],
}, },
], ],
}, },
@ -181,14 +181,14 @@ const getServicePopularityRankings = async (options = {}) => {
'service.id', 'service.id',
'service.title_zh', 'service.title_zh',
'service.title_en', 'service.title_en',
'service.title_pt', 'service.title_es',
'service.image', 'service.image',
'service.price', 'service.price',
'service.category.id', 'service.category.id',
'service.category.key', 'service.category.key',
'service.category.name_zh', 'service.category.name_zh',
'service.category.name_en', 'service.category.name_en',
'service.category.name_pt', 'service.category.name_es',
], ],
order: [[literal('appointmentCount'), 'DESC']], order: [[literal('appointmentCount'), 'DESC']],
limit: parseInt(limit), limit: parseInt(limit),
@ -201,7 +201,7 @@ const getServicePopularityRankings = async (options = {}) => {
serviceName: { serviceName: {
zh: item['service.titleZh'], zh: item['service.titleZh'],
en: item['service.titleEn'], en: item['service.titleEn'],
pt: item['service.titlePt'], es: item['service.titleEs'],
}, },
category: { category: {
id: item['service.category.id'], id: item['service.category.id'],
@ -209,7 +209,7 @@ const getServicePopularityRankings = async (options = {}) => {
name: { name: {
zh: item['service.category.nameZh'], zh: item['service.category.nameZh'],
en: item['service.category.nameEn'], en: item['service.category.nameEn'],
pt: item['service.category.namePt'], es: item['service.category.nameEs'],
}, },
}, },
appointmentCount: parseInt(item.appointmentCount), appointmentCount: parseInt(item.appointmentCount),

View File

@ -256,19 +256,19 @@ class AppointmentService {
{ {
model: Service, model: Service,
as: 'service', as: 'service',
attributes: ['id', 'titleZh', 'titleEn', 'titlePt', 'image', 'price'], attributes: ['id', 'titleZh', 'titleEn', 'titleEs', 'image', 'price'],
include: [ include: [
{ {
model: Category, model: Category,
as: 'category', as: 'category',
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt'], attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs'],
}, },
], ],
}, },
{ {
model: HotService, model: HotService,
as: 'hotService', as: 'hotService',
attributes: ['id', 'name_zh', 'name_en', 'name_pt', 'service_type', 'image_url'], attributes: ['id', 'name_zh', 'name_en', 'name_es', 'service_type', 'image_url'],
}, },
], ],
order: [['createdAt', 'DESC']], order: [['createdAt', 'DESC']],
@ -308,12 +308,12 @@ class AppointmentService {
{ {
model: Service, model: Service,
as: 'service', as: 'service',
attributes: ['id', 'titleZh', 'titleEn', 'titlePt', 'descriptionZh', 'descriptionEn', 'descriptionPt', 'image', 'price'], attributes: ['id', 'titleZh', 'titleEn', 'titleEs', 'descriptionZh', 'descriptionEn', 'descriptionEs', 'image', 'price'],
include: [ include: [
{ {
model: Category, model: Category,
as: 'category', as: 'category',
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt'], attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs'],
}, },
], ],
}, },
@ -475,17 +475,17 @@ class AppointmentService {
created: { created: {
zh: '预约创建成功', zh: '预约创建成功',
en: 'Appointment Created', en: 'Appointment Created',
pt: 'Agendamento Criado', es: 'Cita Creada',
}, },
updated: { updated: {
zh: '预约状态更新', zh: '预约状态更新',
en: 'Appointment Updated', en: 'Appointment Updated',
pt: 'Agendamento Atualizado', es: 'Cita Actualizada',
}, },
cancelled: { cancelled: {
zh: '预约已取消', zh: '预约已取消',
en: 'Appointment Cancelled', en: 'Appointment Cancelled',
pt: 'Agendamento Cancelado', es: 'Cita Cancelada',
}, },
}; };
@ -493,17 +493,17 @@ class AppointmentService {
created: { created: {
zh: `您的预约 ${appointment.appointmentNo} 已创建成功`, zh: `您的预约 ${appointment.appointmentNo} 已创建成功`,
en: `Your appointment ${appointment.appointmentNo} has been created successfully`, en: `Your appointment ${appointment.appointmentNo} has been created successfully`,
pt: `Seu agendamento ${appointment.appointmentNo} foi criado com sucesso`, es: `Su cita ${appointment.appointmentNo} ha sido creada exitosamente`,
}, },
updated: { updated: {
zh: `您的预约 ${appointment.appointmentNo} 状态已更新为 ${appointment.status}`, zh: `您的预约 ${appointment.appointmentNo} 状态已更新为 ${appointment.status}`,
en: `Your appointment ${appointment.appointmentNo} status has been updated to ${appointment.status}`, en: `Your appointment ${appointment.appointmentNo} status has been updated to ${appointment.status}`,
pt: `O status do seu agendamento ${appointment.appointmentNo} foi atualizado para ${appointment.status}`, es: `El estado de su cita ${appointment.appointmentNo} ha sido actualizado a ${appointment.status}`,
}, },
cancelled: { cancelled: {
zh: `您的预约 ${appointment.appointmentNo} 已取消`, zh: `您的预约 ${appointment.appointmentNo} 已取消`,
en: `Your appointment ${appointment.appointmentNo} has been cancelled`, en: `Your appointment ${appointment.appointmentNo} has been cancelled`,
pt: `Seu agendamento ${appointment.appointmentNo} foi cancelado`, es: `Su cita ${appointment.appointmentNo} ha sido cancelada`,
}, },
}; };
@ -512,10 +512,10 @@ class AppointmentService {
type: 'service', type: 'service',
titleZh: titleMap[action].zh, titleZh: titleMap[action].zh,
titleEn: titleMap[action].en, titleEn: titleMap[action].en,
titlePt: titleMap[action].pt, titleEs: titleMap[action].es,
contentZh: contentMap[action].zh, contentZh: contentMap[action].zh,
contentEn: contentMap[action].en, contentEn: contentMap[action].en,
contentPt: contentMap[action].pt, contentEs: contentMap[action].es,
isRead: false, isRead: false,
}); });
} catch (error) { } catch (error) {

View File

@ -26,7 +26,7 @@ exports.getActiveBanners = async () => {
exports.getActiveHotServices = async (lang = 'zh') => { exports.getActiveHotServices = async (lang = 'zh') => {
const services = await HotService.findAll({ const services = await HotService.findAll({
where: { is_active: true }, where: { is_active: true },
attributes: ['id', 'name_zh', 'name_en', 'name_pt', 'service_type', 'image_url', 'detail_image', 'link_url', 'sort_order'], attributes: ['id', 'name_zh', 'name_en', 'name_es', 'service_type', 'image_url', 'detail_image', 'link_url', 'sort_order'],
order: [ order: [
['sort_order', 'ASC'], ['sort_order', 'ASC'],
['id', 'ASC'], ['id', 'ASC'],
@ -35,7 +35,7 @@ exports.getActiveHotServices = async (lang = 'zh') => {
// Map name based on language // Map name based on language
return services.map((service) => { return services.map((service) => {
const nameField = lang === 'en' ? 'name_en' : lang === 'pt' ? 'name_pt' : 'name_zh'; const nameField = lang === 'en' ? 'name_en' : lang === 'es' ? 'name_es' : 'name_zh';
return { return {
id: service.id, id: service.id,
name: service[nameField], name: service[nameField],

View File

@ -48,10 +48,10 @@ class NotificationService {
type: notificationData.type, type: notificationData.type,
titleZh: notificationData.titleZh, titleZh: notificationData.titleZh,
titleEn: notificationData.titleEn, titleEn: notificationData.titleEn,
titlePt: notificationData.titlePt, titleEs: notificationData.titleEs,
contentZh: notificationData.contentZh, contentZh: notificationData.contentZh,
contentEn: notificationData.contentEn, contentEn: notificationData.contentEn,
contentPt: notificationData.contentPt, contentEs: notificationData.contentEs,
isRead: false, isRead: false,
}); });
@ -195,7 +195,7 @@ class NotificationService {
/** /**
* Get localized notification content * Get localized notification content
* @param {Object} notification - Notification object * @param {Object} notification - Notification object
* @param {string} language - Language code (zh, en, pt) * @param {string} language - Language code (zh, en, es)
* @returns {Object} - Localized notification * @returns {Object} - Localized notification
*/ */
static getLocalizedNotification(notification, language = 'zh') { static getLocalizedNotification(notification, language = 'zh') {

View File

@ -8,7 +8,7 @@ const logger = require('../config/logger');
class ServiceService { class ServiceService {
/** /**
* Get all categories * Get all categories
* @param {string} language - Language preference (zh, en, pt) * @param {string} language - Language preference (zh, en, es)
* @returns {Promise<Array>} - List of categories * @returns {Promise<Array>} - List of categories
*/ */
static async getCategories(language = 'zh') { static async getCategories(language = 'zh') {
@ -68,7 +68,7 @@ class ServiceService {
{ {
model: Category, model: Category,
as: 'category', as: 'category',
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt', 'icon'], attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs', 'icon'],
...(categoryKey ? { where: { key: categoryKey }, required: true } : {}), ...(categoryKey ? { where: { key: categoryKey }, required: true } : {}),
}, },
]; ];
@ -127,7 +127,7 @@ class ServiceService {
{ {
model: Category, model: Category,
as: 'category', as: 'category',
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt', 'icon'], attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs', 'icon'],
}, },
], ],
}); });

View File

@ -81,7 +81,7 @@ const updateUserProfile = async (userId, updateData) => {
/** /**
* Set user language preference * Set user language preference
* @param {string} userId - User ID * @param {string} userId - User ID
* @param {string} language - Language code (zh, en, pt) * @param {string} language - Language code (zh, en, es)
* @returns {Promise<Object>} Updated user profile * @returns {Promise<Object>} Updated user profile
*/ */
const setUserLanguage = async (userId, language) => { const setUserLanguage = async (userId, language) => {
@ -95,9 +95,9 @@ const setUserLanguage = async (userId, language) => {
} }
// Validate language code // Validate language code
const validLanguages = ['zh', 'en', 'pt']; const validLanguages = ['zh', 'en', 'es'];
if (!validLanguages.includes(language)) { if (!validLanguages.includes(language)) {
const error = new Error('Invalid language code. Must be one of: zh, en, pt'); const error = new Error('Invalid language code. Must be one of: zh, en, es');
error.statusCode = 400; error.statusCode = 400;
error.code = 'INVALID_LANGUAGE'; error.code = 'INVALID_LANGUAGE';
throw error; throw error;

View File

@ -37,7 +37,7 @@ export default {
languages: [ languages: [
{ code: 'zh', name: '中文' }, { code: 'zh', name: '中文' },
{ code: 'en', name: 'English' }, { code: 'en', name: 'English' },
{ code: 'pt', name: 'Português' } { code: 'es', name: 'Español' }
] ]
} }
}, },

View File

@ -99,7 +99,7 @@ export default {
language: { language: {
chinese: '中文', chinese: '中文',
english: 'English', english: 'English',
portuguese: 'Português' spanish: 'Español'
}, },
notification: { notification: {
all: 'All', all: 'All',

View File

@ -0,0 +1,254 @@
// Spanish language pack
export default {
common: {
confirm: 'Confirmar',
cancel: 'Cancelar',
save: 'Guardar',
delete: 'Eliminar',
edit: 'Editar',
back: 'Volver',
loading: 'Cargando...',
noData: 'Sin Datos',
noMore: 'No hay más',
networkError: 'Error de Red',
success: 'Éxito',
failed: 'Fallido',
submit: 'Enviar',
reserve: 'Reservar',
currency: '¥'
},
tabbar: {
home: 'Inicio',
appointment: 'Cita',
me: 'Yo'
},
home: {
hotServices: 'Servicios Populares',
aboutMe: 'Sobre Nosotros',
domesticTickets: 'Vuelos Nacionales/Internacionales',
globalHotels: 'Hoteles Globales',
vipLounge: 'Sala VIP del Aeropuerto',
customService: 'Servicio Especial para Pasajeros'
},
appointment: {
title: 'Servicio de Citas',
allServices: 'Todos los Servicios de Citas',
selectService: 'Seleccionar Servicio',
selectTime: 'Seleccionar Hora',
contactInfo: 'Información de Contacto',
submit: 'Enviar Cita',
categories: {
airport: 'Aeropuerto',
train: 'Tren',
highSpeedRail: 'Tren de Alta Velocidad',
bus: 'Autobús',
hotel: 'Hotel',
homestay: 'Casa de Familia'
},
serviceItem: {
domesticTickets: 'Reserva de Vuelos Nacionales/Internacionales'
}
},
reserveDetails: {
reserve: 'Reservar'
},
infoEntry: {
title: 'Información de Cita',
personalInfo: 'Información Personal',
realName: 'Nombre Real',
realNamePlaceholder: 'Por favor, ingrese su nombre real',
wechat: 'ID de WeChat',
wechatPlaceholder: 'Por favor, ingrese su ID de WeChat',
phone: 'Número de Teléfono',
phonePlaceholder: 'Por favor, ingrese su número de teléfono',
whatsapp: 'WhatsApp',
whatsappPlaceholder: 'Por favor, ingrese su número de WhatsApp',
contactMethod: 'Elija un método de contacto',
required: 'Obligatorio',
pleaseEnterName: 'Por favor, ingrese su nombre',
hasData: 'Datos disponibles',
selectCountry: 'Seleccionar País/Región',
remark: 'Observación',
remarkPlaceholder: 'Por favor, ingrese observación',
serviceInfo: 'Información del Servicio',
departureDate: 'Fecha de Salida',
departureDatePlaceholder: 'Por favor, seleccione la fecha de salida',
year: '',
month: '',
day: ''
},
me: {
title: 'Perfil',
profile: 'Información Personal',
settings: 'Configuración',
language: 'Idioma',
about: 'Sobre Nosotros',
logout: 'Cerrar Sesión',
appointment: 'Cita',
inProgress: 'En Progreso',
completed: 'Completado',
notification: 'Notificación',
customerService: 'Atención al Cliente',
contactUs: 'Contáctenos',
inviteReward: 'Invita Amigos y Gana Recompensas',
userAgreement: 'Acuerdo de Usuario',
privacyPolicy: 'Política de Privacidad',
general: 'General',
other: 'Otro'
},
language: {
chinese: '中文',
english: 'English',
spanish: 'Español'
},
notification: {
all: 'Todos',
system: 'Sistema',
activity: 'Actividad',
service: 'Servicio',
markAllRead: 'Marcar Todo como Leído',
delete: 'Eliminar',
noNotification: 'Sin Notificaciones',
justNow: 'Ahora mismo',
minutesAgo: 'hace {n} min',
hoursAgo: 'hace {n} horas',
daysAgo: 'hace {n} días'
},
myAppointment: {
title: 'Mis Citas',
all: 'Todos',
inProgress: 'En Progreso',
completed: 'Completado',
viewDetail: 'Ver Detalles',
serviceDetail: 'Detalles del Servicio',
serviceType: 'Tipo de Servicio',
name: 'Nombre',
submitTime: 'Fecha de Envío',
contact: 'Contacto',
appointmentDate: 'Fecha de Cita',
notes: 'Observaciones',
status: 'Estado',
amount: 'Monto',
noAppointment: 'Sin citas',
statusPending: 'Pendiente',
statusConfirmed: 'Confirmado',
statusInProgress: 'En Progreso',
statusCompleted: 'Completado',
statusCancelled: 'Cancelado'
},
login: {
title: 'Iniciar Sesión',
oneClickLogin: 'Registro/Inicio de Sesión con Un Clic',
agreeToTerms: 'He leído y acepto',
userAgreement: '《Acuerdo de Usuario》',
and: 'y',
privacyPolicy: '《Política de Privacidad》',
agree: 'Aceptar',
mustAgreeToTerms: 'Por favor, acepte el Acuerdo de Usuario y la Política de Privacidad',
loginSuccess: 'Inicio de Sesión Exitoso',
loginFailed: 'Error de inicio de sesión, intente de nuevo',
wechatLoginFailed: 'Error de inicio de sesión de WeChat',
loginError: 'Error al iniciar sesión, intente de nuevo',
userAgreementContent: `Acuerdo de Usuario
Bienvenido al uso de esta aplicación. Este acuerdo especifica los términos y condiciones para el uso de esta aplicación.
1. Términos de Servicio
El usuario acepta cumplir con todos los términos y condiciones de este acuerdo.
2. Responsabilidad del Usuario
El usuario es responsable de la seguridad de su cuenta y acepta no compartir credenciales de inicio de sesión con otras personas.
3. Comportamiento Prohibido
El usuario no debe participar en ninguna actividad ilegal o perjudicial.
4. Exención de Responsabilidad
Esta aplicación se proporciona "tal cual" sin ninguna garantía expresa o implícita.
5. Derecho de Modificación
Nos reservamos el derecho de modificar este acuerdo en cualquier momento.`,
privacyPolicyContent: `Política de Privacidad
Valoramos su privacidad. Esta política explica cómo recopilamos, usamos y protegemos su información.
1. Recopilación de Información
Recopilamos información que usted proporciona al usar esta aplicación, incluyendo información de cuenta y datos de uso.
2. Uso de Información
Usamos la información recopilada para mejorar el servicio, realizar análisis y proporcionar experiencia personalizada.
3. Protección de Información
Tomamos medidas de seguridad apropiadas para proteger su información personal.
4. Compartir con Terceros
No vendemos su información personal a terceros.
5. Contáctenos
Si tiene preguntas sobre privacidad, contáctenos a través de la aplicación.`
},
invite: {
title: 'Invitar Nuevos Usuarios',
rewardTitle: 'Invita Amigos, Gana Dinero',
rewardDesc: 'Invita nuevos usuarios con éxito y recibe reembolso después de que realicen compras',
stepsTitle: 'Pasos',
step1: '1. Invita nuevos usuarios a registrarse',
step2: '2. Nuevos usuarios completan el pago',
step3: '3. Recibe recompensas',
viewDetail: 'Ver Detalles',
generateQRCode: 'Generar Código QR',
shareToFriend: 'Compartir con Amigo',
withdrawRecord: 'Registro de Retiro',
withdrawPeriod: 'Detalles del Retiro',
applyWithdraw: 'Solicitar Retiro',
withdrawDetail: 'Detalles del Retiro',
withdrawApplication: 'Solicitud de Retiro',
enterAmount: 'Por favor, ingrese el monto del retiro',
enterPlaceholder: 'Por favor, ingrese',
amountHint: 'Mínimo 1 yuan por vez, disponible 99 yuan',
nextStep: 'Siguiente Paso',
selectPaymentMethod: 'Por favor, seleccione el método de pago',
wechat: 'WeChat',
alipay: 'Alipay',
bankCard: 'Tarjeta Bancaria',
uploadQRCode: 'Por favor, cargue el código QR de WeChat',
enterBankInfo: 'Por favor, ingrese la información de la tarjeta bancaria',
bankCardNumber: 'Número de Tarjeta Bancaria',
enterBankCardNumber: 'Por favor, ingrese el número de tarjeta bancaria',
cardholderName: 'Nombre del Titular',
enterCardholderName: 'Por favor, ingrese el nombre del titular',
bankName: 'Nombre del Banco',
enterBankName: 'Por favor, ingrese el nombre del banco',
swiftCode: 'Código Swift',
enterSwiftCode: 'Por favor, ingrese el código Swift',
optional: 'Opcional',
enterAmountError: 'Por favor, ingrese el monto correcto',
amountTooLow: 'Monto inferior a 1 yuan, no es posible solicitar retiro',
amountExceedsBalance: 'Monto excede el saldo disponible',
uploadQRCodeError: 'Por favor, cargue el código QR',
bankInfoError: 'Por favor, complete la información de la tarjeta bancaria',
time: 'Tiempo',
amount: 'Cantidad',
status: 'Estado',
inviteRecord: 'Registro de Invitaciones',
username: 'Nombre de Usuario',
uid: 'UID',
inviteTime: 'Tiempo de Invitación',
paid: 'Pagado',
paidYes: '¥ 12',
paidNo: '¥ 4',
statusWaiting: 'Esperando',
statusProcessing: 'Procesando',
statusCompleted: 'Completado',
ruleTitle: 'Reglas',
ruleContent: '1. Invita nuevos usuarios a registrarse y completar la primera compra\n2. Recibe recompensa en efectivo por cada invitación exitosa\n3. Las recompensas se acreditarán dentro de 24 horas después del pago\n4. El monto mínimo de retiro es de 100 yuan',
qrcodeGenerated: 'Código QR Generado',
shareTitle: 'Te invito a participar',
shareSuccess: 'Compartido con Éxito',
applyWithdrawConfirm: '¿Confirmar solicitud de retiro?',
applySuccess: 'Solicitud Exitosa',
qrcodeTitle: 'Invita Amigos, Gana Dinero',
invitationCode: 'Código de Invitación',
saveImage: 'Guardar Imagen',
saveSuccess: 'Guardado con Éxito'
}
}

View File

@ -4,7 +4,7 @@ import { createI18n } from 'vue-i18n'
// 导入语言包 // 导入语言包
import zh from './zh.js' import zh from './zh.js'
import en from './en.js' import en from './en.js'
import pt from './pt.js' import es from './es.js'
// 获取系统语言 // 获取系统语言
const getSystemLanguage = () => { const getSystemLanguage = () => {
@ -23,8 +23,8 @@ const getSystemLanguage = () => {
language = 'zh' language = 'zh'
} else if (language.indexOf('en') !== -1) { } else if (language.indexOf('en') !== -1) {
language = 'en' language = 'en'
} else if (language.indexOf('pt') !== -1) { } else if (language.indexOf('es') !== -1) {
language = 'pt' language = 'es'
} else { } else {
language = 'zh' // 默认中文 language = 'zh' // 默认中文
} }
@ -47,7 +47,7 @@ const i18n = createI18n({
messages: { messages: {
zh, zh,
en, en,
pt es
} }
}) })

View File

@ -1,254 +0,0 @@
// Portuguese language pack
export default {
common: {
confirm: 'Confirmar',
cancel: 'Cancelar',
save: 'Salvar',
delete: 'Excluir',
edit: 'Editar',
back: 'Voltar',
loading: 'Carregando...',
noData: 'Sem Dados',
noMore: 'Não há mais',
networkError: 'Erro de Rede',
success: 'Sucesso',
failed: 'Falhou',
submit: 'Enviar',
reserve: 'Reservar',
currency: '¥'
},
tabbar: {
home: 'Início',
appointment: 'Agendamento',
me: 'Eu'
},
home: {
hotServices: 'Serviços Populares',
aboutMe: 'Sobre Nós',
domesticTickets: 'Voos Domésticos/Internacionais',
globalHotels: 'Hotéis Globais',
vipLounge: 'Sala VIP do Aeroporto',
customService: 'Serviço Especial para Passageiros'
},
appointment: {
title: 'Serviço de Agendamento',
allServices: 'Todos os Serviços de Agendamento',
selectService: 'Selecionar Serviço',
selectTime: 'Selecionar Horário',
contactInfo: 'Informações de Contato',
submit: 'Enviar Agendamento',
categories: {
airport: 'Aeroporto',
train: 'Trem',
highSpeedRail: 'Trem de Alta Velocidade',
bus: 'Ônibus',
hotel: 'Hotel',
homestay: 'Casa de Família'
},
serviceItem: {
domesticTickets: 'Reserva de Voos Domésticos/Internacionais'
}
},
reserveDetails: {
reserve: 'Reservar'
},
infoEntry: {
title: 'Informações de Agendamento',
personalInfo: 'Informações Pessoais',
realName: 'Nome Real',
realNamePlaceholder: 'Por favor, insira seu nome real',
wechat: 'ID do WeChat',
wechatPlaceholder: 'Por favor, insira seu ID do WeChat',
phone: 'Número de Telefone',
phonePlaceholder: 'Por favor, insira seu número de telefone',
whatsapp: 'WhatsApp',
whatsappPlaceholder: 'Por favor, insira seu número do WhatsApp',
contactMethod: 'Escolha um método de contato',
required: 'Obrigatório',
pleaseEnterName: 'Por favor, insira seu nome',
hasData: 'Dados disponíveis',
selectCountry: 'Selecionar País/Região',
remark: 'Observação',
remarkPlaceholder: 'Por favor, insira observação',
serviceInfo: 'Informações do Serviço',
departureDate: 'Data de Partida',
departureDatePlaceholder: 'Por favor, selecione a data de partida',
year: '',
month: '',
day: ''
},
me: {
title: 'Perfil',
profile: 'Informações Pessoais',
settings: 'Configurações',
language: 'Idioma',
about: 'Sobre Nós',
logout: 'Sair',
appointment: 'Agendamento',
inProgress: 'Em Andamento',
completed: 'Concluído',
notification: 'Notificação',
customerService: 'Atendimento ao Cliente',
contactUs: 'Fale Conosco',
inviteReward: 'Convide Amigos e Ganhe Recompensas',
userAgreement: 'Acordo do Usuário',
privacyPolicy: 'Política de Privacidade',
general: 'Geral',
other: 'Outro'
},
language: {
chinese: '中文',
english: 'English',
portuguese: 'Português'
},
notification: {
all: 'Todos',
system: 'Sistema',
activity: 'Atividade',
service: 'Serviço',
markAllRead: 'Marcar Tudo como Lido',
delete: 'Excluir',
noNotification: 'Sem Notificações',
justNow: 'Agora mesmo',
minutesAgo: '{n} min atrás',
hoursAgo: '{n} horas atrás',
daysAgo: '{n} dias atrás'
},
myAppointment: {
title: 'Meus Agendamentos',
all: 'Todos',
inProgress: 'Em Andamento',
completed: 'Concluído',
viewDetail: 'Ver Detalhes',
serviceDetail: 'Detalhes do Serviço',
serviceType: 'Tipo de Serviço',
name: 'Nome',
submitTime: 'Data de Envio',
contact: 'Contato',
appointmentDate: 'Data do Agendamento',
notes: 'Observações',
status: 'Status',
amount: 'Valor',
noAppointment: 'Sem agendamentos',
statusPending: 'Pendente',
statusConfirmed: 'Confirmado',
statusInProgress: 'Em Andamento',
statusCompleted: 'Concluído',
statusCancelled: 'Cancelado'
},
login: {
title: 'Entrar',
oneClickLogin: 'Registro/Login com Um Clique',
agreeToTerms: 'Li e concordo com',
userAgreement: '《Acordo do Usuário》',
and: 'e',
privacyPolicy: '《Política de Privacidade》',
agree: 'Concordar',
mustAgreeToTerms: 'Por favor, concorde com o Acordo do Usuário e a Política de Privacidade',
loginSuccess: 'Login Bem-sucedido',
loginFailed: 'Falha no login, tente novamente',
wechatLoginFailed: 'Falha no login do WeChat',
loginError: 'Erro ao fazer login, tente novamente',
userAgreementContent: `Acordo do Usuário
Bem-vindo ao uso deste aplicativo. Este acordo especifica os termos e condições para o uso deste aplicativo.
1. Termos de Serviço
O usuário concorda em cumprir todos os termos e condições deste acordo.
2. Responsabilidade do Usuário
O usuário é responsável pela segurança de sua conta e concorda em não compartilhar credenciais de login com outras pessoas.
3. Comportamento Proibido
O usuário não deve se envolver em nenhuma atividade ilegal ou prejudicial.
4. Isenção de Responsabilidade
Este aplicativo é fornecido "como está" sem nenhuma garantia expressa ou implícita.
5. Direito de Modificação
Reservamos o direito de modificar este acordo a qualquer momento.`,
privacyPolicyContent: `Política de Privacidade
Valorizamos sua privacidade. Esta política explica como coletamos, usamos e protegemos suas informações.
1. Coleta de Informações
Coletamos informações que você fornece ao usar este aplicativo, incluindo informações de conta e dados de uso.
2. Uso de Informações
Usamos as informações coletadas para melhorar o serviço, realizar análises e fornecer experiência personalizada.
3. Proteção de Informações
Tomamos medidas de segurança apropriadas para proteger suas informações pessoais.
4. Compartilhamento com Terceiros
Não vendemos suas informações pessoais para terceiros.
5. Entre em Contato Conosco
Se tiver dúvidas sobre privacidade, entre em contato conosco através do aplicativo.`
},
invite: {
title: 'Convidar Novos Usuários',
rewardTitle: 'Convide Amigos, Ganhe Dinheiro',
rewardDesc: 'Convide novos usuários com sucesso e receba cashback após fazerem compras',
stepsTitle: 'Passos',
step1: '1. Convide novos usuários para se registrar',
step2: '2. Novos usuários completam o pagamento',
step3: '3. Receba recompensas',
viewDetail: 'Ver Detalhes',
generateQRCode: 'Gerar Código QR',
shareToFriend: 'Compartilhar com Amigo',
withdrawRecord: 'Registro de Saque',
withdrawPeriod: 'Detalhes do Saque',
applyWithdraw: 'Solicitar Saque',
withdrawDetail: 'Detalhes do Saque',
withdrawApplication: 'Solicitação de Saque',
enterAmount: 'Por favor, insira o valor do saque',
enterPlaceholder: 'Por favor, insira',
amountHint: 'Mínimo 1 yuan por vez, disponível 99 yuan',
nextStep: 'Próximo Passo',
selectPaymentMethod: 'Por favor, selecione o método de pagamento',
wechat: 'WeChat',
alipay: 'Alipay',
bankCard: 'Cartão Bancário',
uploadQRCode: 'Por favor, carregue o código QR do WeChat',
enterBankInfo: 'Por favor, insira as informações do cartão bancário',
bankCardNumber: 'Número do Cartão Bancário',
enterBankCardNumber: 'Por favor, insira o número do cartão bancário',
cardholderName: 'Nome do Titular',
enterCardholderName: 'Por favor, insira o nome do titular',
bankName: 'Nome do Banco',
enterBankName: 'Por favor, insira o nome do banco',
swiftCode: 'Código Swift',
enterSwiftCode: 'Por favor, insira o código Swift',
optional: 'Opcional',
enterAmountError: 'Por favor, insira o valor correto',
amountTooLow: 'Valor inferior a 1 yuan, não é possível solicitar saque',
amountExceedsBalance: 'Valor excede o saldo disponível',
uploadQRCodeError: 'Por favor, carregue o código QR',
bankInfoError: 'Por favor, complete as informações do cartão bancário',
time: 'Tempo',
amount: 'Quantia',
status: 'Status',
inviteRecord: 'Registro de Convites',
username: 'Nome de Usuário',
uid: 'UID',
inviteTime: 'Tempo de Convite',
paid: 'Pago',
paidYes: '¥ 12',
paidNo: '¥ 4',
statusWaiting: 'Aguardando',
statusProcessing: 'Processando',
statusCompleted: 'Concluído',
ruleTitle: 'Regras',
ruleContent: '1. Convide novos usuários para se registrar e completar a primeira compra\n2. Receba recompensa em dinheiro para cada convite bem-sucedido\n3. As recompensas serão creditadas dentro de 24 horas após o pagamento\n4. O valor mínimo de saque é de 100 yuan',
qrcodeGenerated: 'Código QR Gerado',
shareTitle: 'Convido você para participar',
shareSuccess: 'Compartilhado com Sucesso',
applyWithdrawConfirm: 'Confirmar solicitação de saque?',
applySuccess: 'Solicitação Bem-sucedida',
qrcodeTitle: 'Convide Amigos, Ganhe Dinheiro',
invitationCode: 'Código de Convite',
saveImage: 'Salvar Imagem',
saveSuccess: 'Salvo com Sucesso'
}
}

View File

@ -115,7 +115,7 @@ export default {
language: { language: {
chinese: '中文', chinese: '中文',
english: 'English', english: 'English',
portuguese: 'Português' spanish: 'Español'
}, },
notification: { notification: {
all: '全部', all: '全部',

View File

@ -245,7 +245,7 @@
languages: [ languages: [
{ code: 'zh', name: '中文' }, { code: 'zh', name: '中文' },
{ code: 'en', name: 'English' }, { code: 'en', name: 'English' },
{ code: 'pt', name: 'Português' } { code: 'es', name: 'Español' }
], ],
user: null, user: null,
isLogin: false, isLogin: false,

View File

@ -414,12 +414,12 @@
const locale = this.$i18n.locale const locale = this.$i18n.locale
if (item.service) { if (item.service) {
if (locale === 'zh') return item.service.titleZh || item.service.titleEn || '-' if (locale === 'zh') return item.service.titleZh || item.service.titleEn || '-'
else if (locale === 'pt') return item.service.titlePt || item.service.titleEn || '-' else if (locale === 'es') return item.service.titleEs || item.service.titleEn || '-'
else return item.service.titleEn || item.service.titleZh || '-' else return item.service.titleEn || item.service.titleZh || '-'
} }
if (item.hotService) { if (item.hotService) {
if (locale === 'zh') return item.hotService.name_zh || item.hotService.name_en || '-' if (locale === 'zh') return item.hotService.name_zh || item.hotService.name_en || '-'
else if (locale === 'pt') return item.hotService.name_pt || item.hotService.name_en || '-' else if (locale === 'es') return item.hotService.name_es || item.hotService.name_en || '-'
else return item.hotService.name_en || item.hotService.name_zh || '-' else return item.hotService.name_en || item.hotService.name_zh || '-'
} }
return this.getServiceTypeText(item.serviceType) || '-' return this.getServiceTypeText(item.serviceType) || '-'

View File

@ -39,8 +39,8 @@ export const i18nUtils = {
language = 'zh' language = 'zh'
} else if (language.indexOf('en') !== -1) { } else if (language.indexOf('en') !== -1) {
language = 'en' language = 'en'
} else if (language.indexOf('pt') !== -1) { } else if (language.indexOf('es') !== -1) {
language = 'pt' language = 'es'
} else { } else {
language = 'zh' // 默认中文 language = 'zh' // 默认中文
} }
@ -56,7 +56,7 @@ export const i18nUtils = {
const languageMap = { const languageMap = {
'zh': '中文', 'zh': '中文',
'en': 'English', 'en': 'English',
'pt': 'Português' 'es': 'Español'
} }
return languageMap[code] || '中文' return languageMap[code] || '中文'
} }