国际化,管理后台修改.
This commit is contained in:
parent
33bb1bbd56
commit
5c2469137c
|
|
@ -16,7 +16,7 @@
|
|||
### 小程序 (miniprogram/)
|
||||
- **框架**: uni-app + Vue 3 + Vite
|
||||
- **UI库**: uView Plus 3.6
|
||||
- **国际化**: vue-i18n (中文/英文/葡萄牙语)
|
||||
- **国际化**: vue-i18n (中文/英文/西班牙语)
|
||||
|
||||
### 管理后台 (admin/)
|
||||
- **框架**: Vue 3 + Vite
|
||||
|
|
@ -96,7 +96,7 @@ docker-compose up -d
|
|||
|
||||
- **基础路径**: `/api/v1/`
|
||||
- **认证方式**: Bearer Token (JWT)
|
||||
- **多语言**: `Accept-Language` 头或 `?language=zh|en|pt`
|
||||
- **多语言**: `Accept-Language` 头或 `?language=zh|en|es`
|
||||
|
||||
### 主要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
|
||||
3. **预约编号格式**: `APT` + `YYYYMMDD` + 6位数字
|
||||
4. **订单编号格式**: `ORD` + `YYYYMMDD` + 6位数字
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@
|
|||
<el-dialog
|
||||
v-model="detailsDialogVisible"
|
||||
title="订单详情"
|
||||
width="700px"
|
||||
width="800px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-loading="detailsLoading" class="appointment-details">
|
||||
|
|
@ -185,10 +185,13 @@
|
|||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="服务项目">
|
||||
{{ appointmentDetails.service?.titleZh || '-' }}
|
||||
{{ appointmentDetails.service?.titleZh || appointmentDetails.hotService?.name_zh || '-' }}
|
||||
</el-descriptions-item>
|
||||
<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 label="订单金额">
|
||||
<span class="amount" v-if="appointmentDetails.amount">
|
||||
|
|
@ -199,11 +202,11 @@
|
|||
<el-descriptions-item label="支付时间">
|
||||
{{ appointmentDetails.paidAt ? formatDate(appointmentDetails.paidAt) : '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="预约日期">
|
||||
{{ appointmentDetails.appointmentDate || '-' }}
|
||||
<el-descriptions-item label="预约日期" v-if="appointmentDetails.appointmentDate">
|
||||
{{ appointmentDetails.appointmentDate }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="预约时间">
|
||||
{{ appointmentDetails.appointmentTime || '-' }}
|
||||
<el-descriptions-item label="预约时间" v-if="appointmentDetails.appointmentTime">
|
||||
{{ appointmentDetails.appointmentTime }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ formatDate(appointmentDetails.createdAt) }}
|
||||
|
|
@ -216,14 +219,292 @@
|
|||
<!-- Contact Info -->
|
||||
<el-descriptions title="联系信息" :column="2" border class="mt-20">
|
||||
<el-descriptions-item label="预约人姓名">
|
||||
{{ appointmentDetails.realName }}
|
||||
{{ appointmentDetails.realName || '-' }}
|
||||
</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">
|
||||
{{ getContactMethodLabel(appointmentDetails.contactMethod) }}
|
||||
</el-tag>
|
||||
{{ appointmentDetails.contactValue }}
|
||||
</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>
|
||||
|
||||
<!-- User Info -->
|
||||
|
|
@ -585,11 +866,120 @@ function getLanguageLabel(lang) {
|
|||
const labels = {
|
||||
zh: '中文',
|
||||
en: 'English',
|
||||
pt: 'Português'
|
||||
es: 'Español'
|
||||
}
|
||||
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
|
||||
onMounted(() => {
|
||||
fetchCategories()
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<el-table-column prop="key" label="Key" width="150" />
|
||||
<el-table-column prop="name_zh" 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 label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
<el-form-item label="英文名称" prop="name_en">
|
||||
<el-input v-model="form.name_en" />
|
||||
</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-form-item>
|
||||
<el-form-item label="排序" prop="sort_order">
|
||||
|
|
@ -91,7 +91,7 @@ const rules = {
|
|||
{ required: true, message: '请输入英文名称', trigger: 'blur' }
|
||||
],
|
||||
name_pt: [
|
||||
{ required: true, message: '请输入葡语名称', trigger: 'blur' }
|
||||
{ required: true, message: '请输入西语名称', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@
|
|||
</el-table-column>
|
||||
<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_pt" label="葡语名称" min-width="120" />
|
||||
<el-table-column prop="name_pt" label="西语名称" min-width="120" />
|
||||
<el-table-column label="服务类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getServiceTypeTag(row.service_type)" size="small">
|
||||
|
|
@ -147,8 +147,8 @@
|
|||
<el-form-item label="英文名称" required>
|
||||
<el-input v-model="hotServiceForm.name_en" placeholder="Please enter English name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="葡语名称" required>
|
||||
<el-input v-model="hotServiceForm.name_pt" placeholder="Por favor, insira o nome" />
|
||||
<el-form-item label="西语名称" required>
|
||||
<el-input v-model="hotServiceForm.name_pt" placeholder="Por favor, ingrese el nombre" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务类型" required>
|
||||
<el-select v-model="hotServiceForm.service_type" placeholder="请选择服务类型" filterable style="width: 100%">
|
||||
|
|
|
|||
|
|
@ -130,12 +130,12 @@
|
|||
<el-form-item label="英文内容">
|
||||
<el-input v-model="sendForm.contentEn" type="textarea" :rows="2" placeholder="English content (optional)" maxlength="500" />
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">葡语内容(选填)</el-divider>
|
||||
<el-form-item label="葡语标题">
|
||||
<el-input v-model="sendForm.titlePt" placeholder="Título em português (opcional)" maxlength="100" />
|
||||
<el-divider content-position="left">西语内容(选填)</el-divider>
|
||||
<el-form-item label="西语标题">
|
||||
<el-input v-model="sendForm.titlePt" placeholder="Título en español (opcional)" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="葡语内容">
|
||||
<el-input v-model="sendForm.contentPt" type="textarea" :rows="2" placeholder="Conteúdo em português (opcional)" maxlength="500" />
|
||||
<el-form-item label="西语内容">
|
||||
<el-input v-model="sendForm.contentPt" type="textarea" :rows="2" placeholder="Contenido en español (opcional)" maxlength="500" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@
|
|||
</el-table-column>
|
||||
<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="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">
|
||||
<template #default="{ row }">
|
||||
<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-input v-model="form.titleEn" placeholder="Please enter English title" />
|
||||
</el-form-item>
|
||||
<el-form-item label="葡语标题" prop="titlePt">
|
||||
<el-input v-model="form.titlePt" placeholder="Por favor, insira o título em português" />
|
||||
<el-form-item label="西语标题" prop="titlePt">
|
||||
<el-input v-model="form.titlePt" placeholder="Por favor, ingrese el título en español" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">其他信息</el-divider>
|
||||
|
|
@ -299,7 +299,7 @@ const formRules = {
|
|||
serviceType: [{ required: true, message: '请选择服务具体类型', trigger: 'change' }],
|
||||
titleZh: [{ required: true, message: '请输入中文标题', trigger: 'blur' }],
|
||||
titleEn: [{ required: true, message: '请输入英文标题', trigger: 'blur' }],
|
||||
titlePt: [{ required: true, message: '请输入葡语标题', trigger: 'blur' }]
|
||||
titlePt: [{ required: true, message: '请输入西语标题', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 获取服务类型标签
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
<el-select v-model="filters.language" placeholder="全部" clearable style="width: 120px">
|
||||
<el-option label="中文" value="zh" />
|
||||
<el-option label="English" value="en" />
|
||||
<el-option label="Português" value="pt" />
|
||||
<el-option label="Español" value="es" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
|
|
@ -494,7 +494,7 @@ function getLanguageLabel(lang) {
|
|||
const labels = {
|
||||
zh: '中文',
|
||||
en: 'English',
|
||||
pt: 'Português'
|
||||
es: 'Español'
|
||||
}
|
||||
return labels[lang] || lang
|
||||
}
|
||||
|
|
@ -503,7 +503,7 @@ function getLanguageTagType(lang) {
|
|||
const types = {
|
||||
zh: '',
|
||||
en: 'success',
|
||||
pt: 'warning'
|
||||
es: 'warning'
|
||||
}
|
||||
return types[lang] || 'info'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ Authorization: Bearer <your_token>
|
|||
|
||||
## 多语言支持
|
||||
|
||||
系统支持三种语言: 中文(zh)、英文(en)、葡萄牙语(pt)
|
||||
系统支持三种语言: 中文(zh)、英文(en)、西班牙语(es)
|
||||
|
||||
可通过以下方式指定语言:
|
||||
1. 请求头: \`Accept-Language: zh\`
|
||||
|
|
@ -140,7 +140,7 @@ Authorization: Bearer <your_token>
|
|||
phone: { type: 'string', example: '+86 13800138000' },
|
||||
whatsapp: { type: 'string', example: '+1 234 567 8900' },
|
||||
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 },
|
||||
invitationCode: { type: 'string', example: 'ABC123' },
|
||||
status: { type: 'string', enum: ['active', 'suspended'], example: 'active' },
|
||||
|
|
@ -156,7 +156,7 @@ Authorization: Bearer <your_token>
|
|||
key: { type: 'string', example: 'airport' },
|
||||
nameZh: { type: 'string', example: '机场接送' },
|
||||
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' },
|
||||
sortOrder: { type: 'integer', example: 1 },
|
||||
},
|
||||
|
|
@ -175,10 +175,10 @@ Authorization: Bearer <your_token>
|
|||
},
|
||||
titleZh: { type: 'string', example: '机场接机服务' },
|
||||
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' },
|
||||
descriptionEn: { type: 'string' },
|
||||
descriptionPt: { type: 'string' },
|
||||
descriptionEs: { type: 'string' },
|
||||
image: { type: 'string', example: '/uploads/service.jpg' },
|
||||
price: { type: 'number', format: 'decimal', example: 299.00 },
|
||||
status: { type: 'string', enum: ['active', 'inactive'], example: 'active' },
|
||||
|
|
@ -229,7 +229,7 @@ Authorization: Bearer <your_token>
|
|||
id: { type: 'integer', example: 1 },
|
||||
name_zh: { type: 'string', example: '全球机票代理' },
|
||||
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: {
|
||||
type: 'string',
|
||||
description: '服务具体类型',
|
||||
|
|
@ -275,10 +275,10 @@ Authorization: Bearer <your_token>
|
|||
type: { type: 'string', enum: ['system', 'activity', 'service'], example: 'system' },
|
||||
titleZh: { type: 'string', example: '系统通知' },
|
||||
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' },
|
||||
contentEn: { type: 'string' },
|
||||
contentPt: { type: 'string' },
|
||||
contentEs: { type: 'string' },
|
||||
isRead: { type: 'boolean', example: false },
|
||||
readAt: { type: 'string', format: 'date-time' },
|
||||
createdAt: { type: 'string', format: 'date-time' },
|
||||
|
|
|
|||
71
backend/src/migrations/012-rename-pt-to-es.js
Normal file
71
backend/src/migrations/012-rename-pt-to-es.js
Normal 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 };
|
||||
|
|
@ -26,10 +26,10 @@ const Category = sequelize.define('Category', {
|
|||
allowNull: false,
|
||||
field: 'name_en',
|
||||
},
|
||||
namePt: {
|
||||
nameEs: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
field: 'name_pt',
|
||||
field: 'name_es',
|
||||
},
|
||||
icon: {
|
||||
type: DataTypes.STRING(500),
|
||||
|
|
@ -60,14 +60,14 @@ Category.prototype.toJSON = function () {
|
|||
// snake_case (for backward compatibility)
|
||||
name_zh: values.nameZh,
|
||||
name_en: values.nameEn,
|
||||
name_pt: values.namePt,
|
||||
name_es: values.nameEs,
|
||||
sort_order: values.sortOrder,
|
||||
created_at: values.createdAt,
|
||||
updated_at: values.updatedAt,
|
||||
// camelCase (for consistency)
|
||||
nameZh: values.nameZh,
|
||||
nameEn: values.nameEn,
|
||||
namePt: values.namePt,
|
||||
nameEs: values.nameEs,
|
||||
icon: values.icon,
|
||||
sortOrder: values.sortOrder,
|
||||
createdAt: values.createdAt,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const HotService = sequelize.define(
|
|||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
},
|
||||
name_pt: {
|
||||
name_es: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ const Notification = sequelize.define('Notification', {
|
|||
allowNull: false,
|
||||
field: 'title_en',
|
||||
},
|
||||
titlePt: {
|
||||
titleEs: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: false,
|
||||
field: 'title_pt',
|
||||
field: 'title_es',
|
||||
},
|
||||
contentZh: {
|
||||
type: DataTypes.TEXT,
|
||||
|
|
@ -49,10 +49,10 @@ const Notification = sequelize.define('Notification', {
|
|||
allowNull: false,
|
||||
field: 'content_en',
|
||||
},
|
||||
contentPt: {
|
||||
contentEs: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
field: 'content_pt',
|
||||
field: 'content_es',
|
||||
},
|
||||
isRead: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ const Service = sequelize.define('Service', {
|
|||
allowNull: false,
|
||||
field: 'title_en',
|
||||
},
|
||||
titlePt: {
|
||||
titleEs: {
|
||||
type: DataTypes.STRING(200),
|
||||
allowNull: false,
|
||||
field: 'title_pt',
|
||||
field: 'title_es',
|
||||
},
|
||||
descriptionZh: {
|
||||
type: DataTypes.TEXT,
|
||||
|
|
@ -51,10 +51,10 @@ const Service = sequelize.define('Service', {
|
|||
allowNull: true,
|
||||
field: 'description_en',
|
||||
},
|
||||
descriptionPt: {
|
||||
descriptionEs: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'description_pt',
|
||||
field: 'description_es',
|
||||
},
|
||||
image: {
|
||||
type: DataTypes.STRING(500),
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ router.post(
|
|||
body('categoryId').isUUID().withMessage('Category ID must be a valid UUID'),
|
||||
body('titleZh').notEmpty().withMessage('Chinese 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) => {
|
||||
if (value === null || value === undefined) return true;
|
||||
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');
|
||||
return true;
|
||||
}),
|
||||
body('descriptionPt').optional({ nullable: true }).custom((value) => {
|
||||
body('descriptionEs').optional({ nullable: true }).custom((value) => {
|
||||
if (value === null || value === undefined) return true;
|
||||
if (typeof value !== 'string') throw new Error('Description must be a string');
|
||||
return true;
|
||||
|
|
@ -108,7 +108,7 @@ router.put(
|
|||
body('categoryId').optional().isUUID().withMessage('Category ID must be a valid UUID'),
|
||||
body('titleZh').optional().notEmpty().withMessage('Chinese 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) => {
|
||||
if (value === null || value === undefined) return true;
|
||||
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');
|
||||
return true;
|
||||
}),
|
||||
body('descriptionPt').optional({ nullable: true }).custom((value) => {
|
||||
body('descriptionEs').optional({ nullable: true }).custom((value) => {
|
||||
if (value === null || value === undefined) return true;
|
||||
if (typeof value !== 'string') throw new Error('Description must be a string');
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ router.put(
|
|||
'/language',
|
||||
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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -61,13 +61,13 @@ const getAppointmentList = async (options = {}) => {
|
|||
{
|
||||
model: Service,
|
||||
as: 'service',
|
||||
attributes: ['id', 'titleZh', 'titleEn', 'titlePt', 'image', 'price', 'categoryId'],
|
||||
attributes: ['id', 'titleZh', 'titleEn', 'titleEs', 'image', 'price', 'categoryId'],
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
model: Category,
|
||||
as: 'category',
|
||||
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt'],
|
||||
attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs'],
|
||||
...(categoryId && { where: { id: categoryId } }),
|
||||
},
|
||||
],
|
||||
|
|
@ -75,7 +75,7 @@ const getAppointmentList = async (options = {}) => {
|
|||
{
|
||||
model: 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,
|
||||
},
|
||||
{
|
||||
|
|
@ -141,15 +141,20 @@ const getAppointmentDetails = async (appointmentId) => {
|
|||
{
|
||||
model: Service,
|
||||
as: 'service',
|
||||
attributes: ['id', 'titleZh', 'titleEn', 'titlePt', 'descriptionZh', 'descriptionEn', 'descriptionPt', 'image', 'price'],
|
||||
attributes: ['id', 'titleZh', 'titleEn', 'titleEs', 'descriptionZh', 'descriptionEn', 'descriptionEs', 'image', 'price'],
|
||||
include: [
|
||||
{
|
||||
model: 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,
|
||||
as: 'user',
|
||||
|
|
@ -287,7 +292,7 @@ const getAppointmentStatistics = async (options = {}) => {
|
|||
{
|
||||
model: Service,
|
||||
as: 'service',
|
||||
attributes: ['id', 'titleZh', 'titleEn', 'titlePt'],
|
||||
attributes: ['id', 'titleZh', 'titleEn', 'titleEs'],
|
||||
...(categoryId && {
|
||||
include: [
|
||||
{
|
||||
|
|
@ -304,7 +309,7 @@ const getAppointmentStatistics = async (options = {}) => {
|
|||
'serviceId',
|
||||
[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']],
|
||||
limit: 10,
|
||||
raw: true,
|
||||
|
|
@ -320,7 +325,7 @@ const getAppointmentStatistics = async (options = {}) => {
|
|||
serviceName: {
|
||||
zh: item['service.titleZh'],
|
||||
en: item['service.titleEn'],
|
||||
pt: item['service.titlePt'],
|
||||
es: item['service.titleEs'],
|
||||
},
|
||||
count: parseInt(item.count),
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -62,10 +62,10 @@ class AdminNotificationService {
|
|||
type: notificationData.type || 'system',
|
||||
titleZh: notificationData.titleZh,
|
||||
titleEn: notificationData.titleEn || notificationData.titleZh,
|
||||
titlePt: notificationData.titlePt || notificationData.titleZh,
|
||||
titleEs: notificationData.titleEs || notificationData.titleZh,
|
||||
contentZh: notificationData.contentZh,
|
||||
contentEn: notificationData.contentEn || notificationData.contentZh,
|
||||
contentPt: notificationData.contentPt || notificationData.contentZh,
|
||||
contentEs: notificationData.contentEs || notificationData.contentZh,
|
||||
isRead: false,
|
||||
});
|
||||
|
||||
|
|
@ -88,10 +88,10 @@ class AdminNotificationService {
|
|||
type: notificationData.type || 'system',
|
||||
titleZh: notificationData.titleZh,
|
||||
titleEn: notificationData.titleEn || notificationData.titleZh,
|
||||
titlePt: notificationData.titlePt || notificationData.titleZh,
|
||||
titleEs: notificationData.titleEs || notificationData.titleZh,
|
||||
contentZh: notificationData.contentZh,
|
||||
contentEn: notificationData.contentEn || notificationData.contentZh,
|
||||
contentPt: notificationData.contentPt || notificationData.contentZh,
|
||||
contentEs: notificationData.contentEs || notificationData.contentZh,
|
||||
isRead: false,
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class AdminServiceService {
|
|||
where[Op.or] = [
|
||||
{ titleZh: { [Op.like]: `%${search}%` } },
|
||||
{ titleEn: { [Op.like]: `%${search}%` } },
|
||||
{ titlePt: { [Op.like]: `%${search}%` } },
|
||||
{ titleEs: { [Op.like]: `%${search}%` } },
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ class AdminServiceService {
|
|||
{
|
||||
model: Category,
|
||||
as: 'category',
|
||||
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt', 'icon'],
|
||||
attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs', 'icon'],
|
||||
},
|
||||
],
|
||||
order: [['sortOrder', 'ASC'], ['createdAt', 'DESC']],
|
||||
|
|
@ -79,7 +79,7 @@ class AdminServiceService {
|
|||
}
|
||||
|
||||
// Validate required fields
|
||||
const requiredFields = ['categoryId', 'titleZh', 'titleEn', 'titlePt'];
|
||||
const requiredFields = ['categoryId', 'titleZh', 'titleEn', 'titleEs'];
|
||||
for (const field of requiredFields) {
|
||||
if (!serviceData[field]) {
|
||||
const error = new Error(`Missing required field: ${field}`);
|
||||
|
|
@ -95,10 +95,10 @@ class AdminServiceService {
|
|||
serviceType: serviceData.serviceType || null,
|
||||
titleZh: serviceData.titleZh,
|
||||
titleEn: serviceData.titleEn,
|
||||
titlePt: serviceData.titlePt,
|
||||
titleEs: serviceData.titleEs,
|
||||
descriptionZh: serviceData.descriptionZh || null,
|
||||
descriptionEn: serviceData.descriptionEn || null,
|
||||
descriptionPt: serviceData.descriptionPt || null,
|
||||
descriptionEs: serviceData.descriptionEs || null,
|
||||
image: serviceData.image || null,
|
||||
price: serviceData.price || null,
|
||||
status: serviceData.status || 'active',
|
||||
|
|
@ -150,10 +150,10 @@ class AdminServiceService {
|
|||
'serviceType',
|
||||
'titleZh',
|
||||
'titleEn',
|
||||
'titlePt',
|
||||
'titleEs',
|
||||
'descriptionZh',
|
||||
'descriptionEn',
|
||||
'descriptionPt',
|
||||
'descriptionEs',
|
||||
'image',
|
||||
'price',
|
||||
'status',
|
||||
|
|
@ -237,7 +237,7 @@ class AdminServiceService {
|
|||
{
|
||||
model: Category,
|
||||
as: 'category',
|
||||
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt', 'icon'],
|
||||
attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs', 'icon'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -161,12 +161,12 @@ const getServicePopularityRankings = async (options = {}) => {
|
|||
{
|
||||
model: Service,
|
||||
as: 'service',
|
||||
attributes: ['id', 'titleZh', 'titleEn', 'titlePt', 'image', 'price'],
|
||||
attributes: ['id', 'titleZh', 'titleEn', 'titleEs', 'image', 'price'],
|
||||
include: [
|
||||
{
|
||||
model: 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.title_zh',
|
||||
'service.title_en',
|
||||
'service.title_pt',
|
||||
'service.title_es',
|
||||
'service.image',
|
||||
'service.price',
|
||||
'service.category.id',
|
||||
'service.category.key',
|
||||
'service.category.name_zh',
|
||||
'service.category.name_en',
|
||||
'service.category.name_pt',
|
||||
'service.category.name_es',
|
||||
],
|
||||
order: [[literal('appointmentCount'), 'DESC']],
|
||||
limit: parseInt(limit),
|
||||
|
|
@ -201,7 +201,7 @@ const getServicePopularityRankings = async (options = {}) => {
|
|||
serviceName: {
|
||||
zh: item['service.titleZh'],
|
||||
en: item['service.titleEn'],
|
||||
pt: item['service.titlePt'],
|
||||
es: item['service.titleEs'],
|
||||
},
|
||||
category: {
|
||||
id: item['service.category.id'],
|
||||
|
|
@ -209,7 +209,7 @@ const getServicePopularityRankings = async (options = {}) => {
|
|||
name: {
|
||||
zh: item['service.category.nameZh'],
|
||||
en: item['service.category.nameEn'],
|
||||
pt: item['service.category.namePt'],
|
||||
es: item['service.category.nameEs'],
|
||||
},
|
||||
},
|
||||
appointmentCount: parseInt(item.appointmentCount),
|
||||
|
|
|
|||
|
|
@ -256,19 +256,19 @@ class AppointmentService {
|
|||
{
|
||||
model: Service,
|
||||
as: 'service',
|
||||
attributes: ['id', 'titleZh', 'titleEn', 'titlePt', 'image', 'price'],
|
||||
attributes: ['id', 'titleZh', 'titleEn', 'titleEs', 'image', 'price'],
|
||||
include: [
|
||||
{
|
||||
model: 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_pt', 'service_type', 'image_url'],
|
||||
attributes: ['id', 'name_zh', 'name_en', 'name_es', 'service_type', 'image_url'],
|
||||
},
|
||||
],
|
||||
order: [['createdAt', 'DESC']],
|
||||
|
|
@ -308,12 +308,12 @@ class AppointmentService {
|
|||
{
|
||||
model: Service,
|
||||
as: 'service',
|
||||
attributes: ['id', 'titleZh', 'titleEn', 'titlePt', 'descriptionZh', 'descriptionEn', 'descriptionPt', 'image', 'price'],
|
||||
attributes: ['id', 'titleZh', 'titleEn', 'titleEs', 'descriptionZh', 'descriptionEn', 'descriptionEs', 'image', 'price'],
|
||||
include: [
|
||||
{
|
||||
model: Category,
|
||||
as: 'category',
|
||||
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt'],
|
||||
attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -475,17 +475,17 @@ class AppointmentService {
|
|||
created: {
|
||||
zh: '预约创建成功',
|
||||
en: 'Appointment Created',
|
||||
pt: 'Agendamento Criado',
|
||||
es: 'Cita Creada',
|
||||
},
|
||||
updated: {
|
||||
zh: '预约状态更新',
|
||||
en: 'Appointment Updated',
|
||||
pt: 'Agendamento Atualizado',
|
||||
es: 'Cita Actualizada',
|
||||
},
|
||||
cancelled: {
|
||||
zh: '预约已取消',
|
||||
en: 'Appointment Cancelled',
|
||||
pt: 'Agendamento Cancelado',
|
||||
es: 'Cita Cancelada',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -493,17 +493,17 @@ class AppointmentService {
|
|||
created: {
|
||||
zh: `您的预约 ${appointment.appointmentNo} 已创建成功`,
|
||||
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: {
|
||||
zh: `您的预约 ${appointment.appointmentNo} 状态已更新为 ${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: {
|
||||
zh: `您的预约 ${appointment.appointmentNo} 已取消`,
|
||||
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',
|
||||
titleZh: titleMap[action].zh,
|
||||
titleEn: titleMap[action].en,
|
||||
titlePt: titleMap[action].pt,
|
||||
titleEs: titleMap[action].es,
|
||||
contentZh: contentMap[action].zh,
|
||||
contentEn: contentMap[action].en,
|
||||
contentPt: contentMap[action].pt,
|
||||
contentEs: contentMap[action].es,
|
||||
isRead: false,
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ exports.getActiveBanners = async () => {
|
|||
exports.getActiveHotServices = async (lang = 'zh') => {
|
||||
const services = await HotService.findAll({
|
||||
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: [
|
||||
['sort_order', 'ASC'],
|
||||
['id', 'ASC'],
|
||||
|
|
@ -35,7 +35,7 @@ exports.getActiveHotServices = async (lang = 'zh') => {
|
|||
|
||||
// Map name based on language
|
||||
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 {
|
||||
id: service.id,
|
||||
name: service[nameField],
|
||||
|
|
|
|||
|
|
@ -48,10 +48,10 @@ class NotificationService {
|
|||
type: notificationData.type,
|
||||
titleZh: notificationData.titleZh,
|
||||
titleEn: notificationData.titleEn,
|
||||
titlePt: notificationData.titlePt,
|
||||
titleEs: notificationData.titleEs,
|
||||
contentZh: notificationData.contentZh,
|
||||
contentEn: notificationData.contentEn,
|
||||
contentPt: notificationData.contentPt,
|
||||
contentEs: notificationData.contentEs,
|
||||
isRead: false,
|
||||
});
|
||||
|
||||
|
|
@ -195,7 +195,7 @@ class NotificationService {
|
|||
/**
|
||||
* Get localized notification content
|
||||
* @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
|
||||
*/
|
||||
static getLocalizedNotification(notification, language = 'zh') {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const logger = require('../config/logger');
|
|||
class ServiceService {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
static async getCategories(language = 'zh') {
|
||||
|
|
@ -68,7 +68,7 @@ class ServiceService {
|
|||
{
|
||||
model: Category,
|
||||
as: 'category',
|
||||
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt', 'icon'],
|
||||
attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs', 'icon'],
|
||||
...(categoryKey ? { where: { key: categoryKey }, required: true } : {}),
|
||||
},
|
||||
];
|
||||
|
|
@ -127,7 +127,7 @@ class ServiceService {
|
|||
{
|
||||
model: Category,
|
||||
as: 'category',
|
||||
attributes: ['id', 'key', 'nameZh', 'nameEn', 'namePt', 'icon'],
|
||||
attributes: ['id', 'key', 'nameZh', 'nameEn', 'nameEs', 'icon'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ const updateUserProfile = async (userId, updateData) => {
|
|||
/**
|
||||
* Set user language preference
|
||||
* @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
|
||||
*/
|
||||
const setUserLanguage = async (userId, language) => {
|
||||
|
|
@ -95,9 +95,9 @@ const setUserLanguage = async (userId, language) => {
|
|||
}
|
||||
|
||||
// Validate language code
|
||||
const validLanguages = ['zh', 'en', 'pt'];
|
||||
const validLanguages = ['zh', 'en', 'es'];
|
||||
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.code = 'INVALID_LANGUAGE';
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export default {
|
|||
languages: [
|
||||
{ code: 'zh', name: '中文' },
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'pt', name: 'Português' }
|
||||
{ code: 'es', name: 'Español' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export default {
|
|||
language: {
|
||||
chinese: '中文',
|
||||
english: 'English',
|
||||
portuguese: 'Português'
|
||||
spanish: 'Español'
|
||||
},
|
||||
notification: {
|
||||
all: 'All',
|
||||
|
|
|
|||
254
miniprogram/src/locale/es.js
Normal file
254
miniprogram/src/locale/es.js
Normal 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'
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import { createI18n } from 'vue-i18n'
|
|||
// 导入语言包
|
||||
import zh from './zh.js'
|
||||
import en from './en.js'
|
||||
import pt from './pt.js'
|
||||
import es from './es.js'
|
||||
|
||||
// 获取系统语言
|
||||
const getSystemLanguage = () => {
|
||||
|
|
@ -23,8 +23,8 @@ const getSystemLanguage = () => {
|
|||
language = 'zh'
|
||||
} else if (language.indexOf('en') !== -1) {
|
||||
language = 'en'
|
||||
} else if (language.indexOf('pt') !== -1) {
|
||||
language = 'pt'
|
||||
} else if (language.indexOf('es') !== -1) {
|
||||
language = 'es'
|
||||
} else {
|
||||
language = 'zh' // 默认中文
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ const i18n = createI18n({
|
|||
messages: {
|
||||
zh,
|
||||
en,
|
||||
pt
|
||||
es
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
@ -115,7 +115,7 @@ export default {
|
|||
language: {
|
||||
chinese: '中文',
|
||||
english: 'English',
|
||||
portuguese: 'Português'
|
||||
spanish: 'Español'
|
||||
},
|
||||
notification: {
|
||||
all: '全部',
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@
|
|||
languages: [
|
||||
{ code: 'zh', name: '中文' },
|
||||
{ code: 'en', name: 'English' },
|
||||
{ code: 'pt', name: 'Português' }
|
||||
{ code: 'es', name: 'Español' }
|
||||
],
|
||||
user: null,
|
||||
isLogin: false,
|
||||
|
|
|
|||
|
|
@ -414,12 +414,12 @@
|
|||
const locale = this.$i18n.locale
|
||||
if (item.service) {
|
||||
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 || '-'
|
||||
}
|
||||
if (item.hotService) {
|
||||
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 || '-'
|
||||
}
|
||||
return this.getServiceTypeText(item.serviceType) || '-'
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ export const i18nUtils = {
|
|||
language = 'zh'
|
||||
} else if (language.indexOf('en') !== -1) {
|
||||
language = 'en'
|
||||
} else if (language.indexOf('pt') !== -1) {
|
||||
language = 'pt'
|
||||
} else if (language.indexOf('es') !== -1) {
|
||||
language = 'es'
|
||||
} else {
|
||||
language = 'zh' // 默认中文
|
||||
}
|
||||
|
|
@ -56,7 +56,7 @@ export const i18nUtils = {
|
|||
const languageMap = {
|
||||
'zh': '中文',
|
||||
'en': 'English',
|
||||
'pt': 'Português'
|
||||
'es': 'Español'
|
||||
}
|
||||
return languageMap[code] || '中文'
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user