新增h5qrcode依赖

This commit is contained in:
2026-02-05 17:38:19 +08:00
parent 5a13803743
commit f476cee76d
16 changed files with 2036 additions and 559 deletions
+4
View File
@@ -4,6 +4,10 @@
{ {
"playground" : "standard", "playground" : "standard",
"type" : "uni-app:app-android" "type" : "uni-app:app-android"
},
{
"playground" : "standard",
"type" : "uni-app:app-ios"
} }
] ]
} }
+1 -1
View File
@@ -6,7 +6,7 @@
// 配置项:true 表示打印日志,false 表示不打印日志 // 配置项:true 表示打印日志,false 表示不打印日志
export const CONSOLE_CONFIG = { export const CONSOLE_CONFIG = {
// 是否启用 console.log // 是否启用 console.log
enableLog: false, enableLog: true,
// 是否启用 console.warn // 是否启用 console.warn
enableWarn: true, enableWarn: true,
// 是否启用 console.error // 是否启用 console.error
+22 -1
View File
@@ -469,6 +469,7 @@ export default {
languageSetting: 'Language Setting', languageSetting: 'Language Setting',
chinese: '简体中文', chinese: '简体中文',
english: 'English', english: 'English',
indonesian: 'Bahasa Indonesia',
languageSwitched: 'Language switched, refreshing...', languageSwitched: 'Language switched, refreshing...',
notification: 'Notification', notification: 'Notification',
privacy: 'Privacy', privacy: 'Privacy',
@@ -572,10 +573,30 @@ export default {
agreement: 'User Agreement', agreement: 'User Agreement',
privacy: 'Privacy Policy', privacy: 'Privacy Policy',
termsOfService: 'Terms of Service', termsOfService: 'Terms of Service',
termsAndConditions: 'Terms & Conditions',
lastUpdate: 'Last Update', lastUpdate: 'Last Update',
applicableToService: 'Applicable to "FengDianZhe" shared fan rental service', applicableToService: 'Applicable to "FengDianZhe" shared fan rental service',
footerNotice: 'If you have questions about this agreement, please go to "My-Customer Service"', footerNotice: 'If you have questions about this agreement, please go to "My-Customer Service"',
footerNoticePolicy: 'If you have questions about this policy, please go to "My-Customer Service"' footerNoticePolicy: 'If you have questions about this policy, please go to "My-Customer Service"',
// Terms and Conditions Content
applicableLaw: 'Applicable Law',
applicableLawContent: 'These Terms of Service are governed by the laws of the People\'s Republic of China. By using this service, you agree to be bound by Chinese law. Any disputes arising from this service shall first be resolved through friendly negotiation; if negotiation fails, either party may file a lawsuit with the People\'s Court having jurisdiction over the location of the service provider.',
paymentMethods: 'Payment Methods',
paymentMethodsContent: 'We support multiple payment methods, including but not limited to: WeChat Pay, Alipay, WeChat Pay Score deposit-free, etc. Users need to complete the payment process before using the service. After successful payment, the system will automatically unlock the device for user access. All payment transactions are conducted through secure encrypted channels to ensure user fund security.',
refundPolicy: 'Refund Policy',
refundPolicyContent: '1. Deposit Refund: After returning the device, the deposit will be automatically refunded to the original payment account after deducting the corresponding rental fee, expected to arrive within 0-7 business days.\n2. Order Cancellation: Unused orders can be cancelled before use begins, and the deposit will be fully refunded.\n3. Exception Refund: In case of special circumstances such as device failure, users can apply for a refund, which we will process within 3-5 business days after verification.\n4. Membership Cards/Coupons: Purchased membership cards and coupons generally do not support refunds. Please contact customer service for special cases.',
serviceTerms: 'Service Terms',
serviceTermsContent: 'When using this service, users should comply with the following regulations: 1) Take good care of the rented equipment and do not intentionally damage or privately occupy it; 2) Return the equipment on time to avoid additional charges; 3) Do not use the equipment for illegal purposes; 4) If equipment failure is found, contact customer service promptly. Violation of the above regulations may result in service termination and liability.',
liabilityLimitation: 'Liability Limitation',
liabilityLimitationContent: 'To the maximum extent permitted by law, we are not liable for any indirect, incidental, special, or consequential damages arising from the use or inability to use this service. Our total liability shall not exceed the fees paid by users for using this service. We are not responsible for service interruptions or delays caused by force majeure, network failures, third-party reasons, etc.',
disputeResolution: 'Dispute Resolution',
disputeResolutionContent: 'If users have any questions or disputes about the service, please first contact us through customer service channels. We will respond within 24 hours of receiving feedback and negotiate a resolution as soon as possible. If negotiation fails, both parties agree to submit the dispute to the People\'s Court with jurisdiction over the location of the service provider for resolution through litigation. During the dispute resolution period, both parties should continue to perform the undisputed terms of this agreement.'
}, },
search: { search: {
+843
View File
@@ -0,0 +1,843 @@
export default {
common: {
confirm: 'Konfirmasi',
cancel: 'Batal',
and: 'dan',
submit: 'Kirim',
processing: 'Memproses',
submitting: 'Mengirim',
uploading: 'Mengunggah...',
getting: 'Mengambil...',
filling: 'Mengisi...',
save: 'Simpan',
loadFailed: 'Gagal memuat',
invalidUrl: 'Tautan tidak valid',
statusCode: 'Kode Status',
message: 'Pesan',
none: 'Tidak ada',
unexpectedError: 'Kesalahan tidak terduga',
processException: 'Terjadi pengecualian dalam proses',
errorInfo: 'Informasi Kesalahan',
edit: 'Edit',
delete: 'Hapus',
search: 'Cari',
loading: 'Memuat...',
loadingData: 'Mengambil data...',
loadingLocation: 'Mengambil informasi lokasi...',
loadingMap: 'Memuat peta...',
loadingPosition: 'Mengambil informasi lokasi...',
noData: 'Tidak ada data',
success: 'Berhasil',
failed: 'Gagal',
retry: 'Coba lagi',
back: 'Kembali',
next: 'Selanjutnya',
complete: 'Selesai',
more: 'Lebih banyak',
close: 'Tutup',
yes: 'Ya',
no: 'Tidak',
all: 'Semua',
tips: 'Tips',
notice: 'Pemberitahuan',
warning: 'Peringatan',
error: 'Kesalahan',
networkError: 'Kesalahan jaringan',
systemError: 'Kesalahan sistem',
authFailed: 'Autentikasi gagal',
unauthorized: 'Tidak diizinkan',
loginRequired: 'Harap login terlebih dahulu',
operationSuccess: 'Operasi berhasil',
operationFailed: 'Operasi gagal',
sending: 'Mengirim...',
loggingIn: 'Masuk...',
refresh: 'Muat ulang',
pull: 'Tarik untuk muat ulang',
release: 'Lepas untuk muat ulang',
noMore: 'Tidak ada lagi',
functionDeveloping: 'Fungsi sedang dikembangkan'
},
nav: {
home: 'Beranda',
my: 'Saya',
orders: 'Pesanan',
settings: 'Pengaturan',
back: 'Kembali',
title: 'Kipas Angin & Power Bank Berbagi FengDianZhe'
},
app: {
name: 'FengDianZhe',
slogan: 'Kipas Angin & Power Bank Berbagi',
fullName: 'FengDianZhe - Kipas Angin & Power Bank Berbagi',
welcome: 'Selamat datang menggunakan FengDianZhe'
},
home: {
title: 'Kipas Angin & Power Bank Berbagi FengDianZhe',
nearbyDevices: 'Perangkat Terdekat',
scanToUse: 'Pindai untuk Menggunakan',
personalCenter: 'Pusat Pribadi',
useGuide: 'Panduan Penggunaan',
buyDevice: 'Kustomisasi Power Bank',
navigate: 'Navigasi',
relocate: 'Lokasi Ulang',
search: 'Cari',
service: 'Layanan Pelanggan',
searchPlaceholder: 'Cari lokasi terdekat',
nearbyDeviceLocation: 'Lokasi Perangkat Terdekat',
noNearbyDevice: 'Tidak ada perangkat terdekat',
relocating: 'Melokalisasi ulang...',
locateSuccess: 'Lokasi berhasil',
locateFailed: 'Lokasi gagal, harap periksa izin lokasi',
invalidQRCode: 'Kode QR perangkat tidak valid',
scanFailed: 'Pemindaian gagal',
noticeTitle: 'Pemberitahuan',
getLocationFailed: 'Gagal mendapatkan lokasi, menampilkan peta default'
},
guide: {
title: 'Panduan Penggunaan',
step1Title: 'Pindai untuk Menggunakan',
step1Desc: 'Temukan perangkat terdekat, pindai kode QR pada perangkat',
step2Title: 'Pembayaran Tanpa Deposit',
step2Desc: 'Tidak perlu membayar deposit, gunakan skor pembayaran tanpa deposit untuk menyelesaikan penyewaan',
step3Title: 'Mulai Menggunakan',
step3Desc: 'Perangkat akan terbuka secara otomatis, ambil kipas angin setelah muncul dan mulai gunakan',
step4Title: 'Kembalikan Perangkat',
step4Desc: 'Setelah selesai digunakan, kembalikan kipas angin sesuai spesifikasi perangkat untuk mengakhiri pesanan'
},
location: {
rent: 'Dapat Disewa',
return: 'Dapat Dikembalikan',
navigate: 'Navigasi',
distance: 'Jarak',
businessHours: 'Jam Operasional:',
navigateHere: 'Navigasi ke Sini',
coordinateError: 'Informasi koordinat lokasi ini abnormal',
notExist: 'Lokasi tidak ada',
supportCouponOrMember: 'Dapat menggunakan kupon, kartu anggota'
},
device: {
reportError: 'Laporkan Kesalahan Perangkat',
scanToUse: 'Pindai untuk Menggunakan',
deviceInfo: 'Informasi Perangkat',
deviceNo: 'Nomor Perangkat',
location: 'Lokasi',
businessHours: 'Jam Operasional',
pricing: 'Penagihan',
pricingText: 'Rp5/jam, Rp36/24 jam, total ¥899',
getDeviceInfoFailed: 'Gagal mendapatkan informasi perangkat',
available: 'Tersedia',
offline: 'Offline',
pricingRules: 'Aturan Penagihan',
capLimit: 'Maksimum',
usageInstructions: 'Instruksi Penggunaan',
checkBeforeUse: 'Harap periksa apakah perangkat dalam kondisi baik sebelum digunakan',
autoChargeOvertime: 'Melebihi waktu penggunaan akan dikenakan biaya per jam secara otomatis',
useInDesignatedArea: 'Harap gunakan perangkat di area yang ditentukan',
rentDepositFree: 'Sewa Tanpa Deposit',
wxPayScoreDesc: 'Skor Pembayaran WeChat | Nikmati dengan 550 poin atau lebih',
checking: 'Memeriksa',
deviceNoNotRecognized: 'Nomor perangkat tidak dikenali',
processFailed: 'Pemrosesan gagal, harap coba lagi nanti',
sharedFan: 'Kipas Angin Berbagi',
deviceNoRequired: 'Nomor perangkat tidak boleh kosong',
rentFailed: 'Penyewaan perangkat gagal',
rentSuccess: 'Penyewaan berhasil',
rentFailedRetry: 'Penyewaan gagal, harap coba lagi',
getPayParamsFailed: 'Gagal mendapatkan parameter pembayaran',
payScoreFailedCancelled: 'Panggilan skor pembayaran gagal, pesanan telah dibatalkan',
canUsePromotion: 'Tips: Dapat menggunakan kupon, kartu anggota',
goToBuy: 'Beli Sekarang'
},
order: {
myOrders: 'Pesanan Saya',
myDeviceOrders: 'Kustomisasi Saya',
noOrderRecord: 'Tidak ada catatan pesanan',
getOrderListFailed: 'Gagal mendapatkan daftar pesanan',
confirmCancelContent: 'Apakah Anda yakin ingin membatalkan pesanan ini?',
orderDetail: 'Detail Pesanan',
orderNo: 'Nomor Pesanan',
orderStatus: 'Status Pesanan',
deviceNo: 'Nomor Perangkat',
rentLocation: 'Lokasi Penyewaan',
rentTime: 'Waktu Penyewaan',
returnTime: 'Waktu Pengembalian',
startTime: 'Waktu Mulai',
endTime: 'Waktu Berakhir',
duration: 'Durasi Penggunaan',
amount: 'Jumlah',
totalAmount: 'Jumlah Total',
payAmount: 'Jumlah Pembayaran',
deposit: 'Deposit',
rentFee: 'Biaya Sewa',
myCards: 'Diskon Kartu Anggota',
myCoupons: 'Diskon Kupon',
payNow: 'Bayar Sekarang',
cancelOrder: 'Batalkan Pesanan',
quickReturn: 'Pengembalian Cepat',
returnDevice: 'Kembalikan Perangkat',
viewDetails: 'Lihat Detail',
orderCompleted: 'Pesanan Selesai',
orderCancelled: 'Pesanan Dibatalkan',
waitingForPayment: 'Menunggu Pembayaran',
inUse: 'Sedang Digunakan',
finished: 'Selesai',
cancelled: 'Dibatalkan',
renting: 'Menyewa',
rentFan: 'Sewa Kipas Angin',
noOrder: 'Tidak ada pesanan yang sedang digunakan',
getOrderFailed: 'Gagal mendapatkan pesanan',
paymentSuccess: 'Pembayaran berhasil',
paymentFailed: 'Pembayaran gagal',
cancelSuccess: 'Pembatalan berhasil',
cancelFailed: 'Pembatalan gagal',
returnSuccess: 'Pengembalian berhasil',
returnFailed: 'Pengembalian gagal',
confirmCancel: 'Konfirmasi membatalkan pesanan?',
confirmReturn: 'Konfirmasi mengembalikan perangkat?',
wxPayScore: 'Skor Pembayaran WeChat',
depositFree: 'Sewa Tanpa Deposit',
whitelistOrder: 'Pesanan Whitelist',
memberOrder: 'Pesanan Anggota',
wxPay: 'Pembayaran WeChat',
depositPay: 'Sewa dengan Deposit',
paymentInProgress: 'Sedang membayar',
paymentFailedRetry: 'Pembayaran gagal, harap bayar lagi',
pleasePaySoon: 'Harap selesaikan pembayaran segera',
pleaseReturnInTime: 'Harap simpan perangkat dengan baik dan kembalikan tepat waktu setelah digunakan',
returnedThankYou: 'Kipas angin Anda telah dikembalikan, terima kasih telah menggunakan',
used: 'Digunakan',
rentInfo: 'Informasi Penyewaan',
fanNo: 'Nomor Kipas Angin',
rentMethod: 'Metode Penyewaan',
returnLocation: 'Lokasi Pengembalian',
paid: 'Dibayar',
canExpressReturn: 'Dapat dikembalikan melalui ekspres',
pauseBilling: 'Jeda Penagihan',
rentAgain: 'Sewa Lagi',
backToHome: 'Kembali ke Beranda',
feeAppeal: 'Banding Biaya',
orderIdRequired: 'ID Pesanan tidak boleh kosong',
refundSuccess: 'Permohonan pengembalian dana berhasil',
refundFailed: 'Permohonan pengembalian dana gagal',
orderNotExist: 'Informasi pesanan tidak ada',
currentFee: 'Biaya Saat Ini',
returnInstructions: 'Instruksi Pengembalian',
ensureDeviceIntact: 'Harap pastikan perangkat dalam kondisi baik',
insertFanBack: 'Masukkan kipas angin ke posisi asli atau soket kosong',
autoDetectReturn: 'Sistem akan secara otomatis mendeteksi pengembalian dan memproses pengembalian dana',
autoJumpAfterReturn: 'Setelah pengembalian berhasil akan otomatis melompat ke halaman sukses',
refreshStatus: 'Muat Ulang Status',
countdown: 'Hitungan Mundur',
pauseAndExpress: 'Jeda penagihan, kembalikan melalui ekspres',
orderInfoMissing: 'Informasi pesanan kurang',
returnSuccessMessage: 'Kipas angin telah dikembalikan dengan sukses, sisa deposit akan dikembalikan ke akun Anda',
noOrderInUse: 'Tidak ditemukan pesanan yang sedang digunakan',
pleaseRefreshManually: 'Harap muat ulang secara manual untuk melihat status pengembalian',
cancelling: 'Membatalkan pesanan',
cancelFailedContactService: 'Pembatalan pesanan gagal, harap hubungi layanan pelanggan',
getOrderStatusFailed: 'Pemeriksaan status pesanan gagal',
syncSuccess: 'Sinkronisasi status berhasil',
syncFailed: 'Sinkronisasi status gagal',
freeRentTime: 'Waktu Gratis',
pricingRule: 'Aturan Penagihan',
paymentMethod: 'Metode Pembayaran',
perHour: 'Per Jam',
perMinute: 'Per Menit',
perHalfHour: 'Per Setengah Jam',
deviceNoEject: 'Power Bank Tidak Muncul',
returnReminder: 'Pengingat Pengembalian',
canUsePromotion: 'Dapat menggunakan kupon, kartu anggota',
usedPromotion: 'Jenis Diskon',
convertToOwn: 'Tidak ingin mengembalikan? Klik untuk mengubah menjadi milik sendiri',
convertToOwnTitle: 'Ubah menjadi Milik Sendiri',
convertToOwnConfirm: 'Hanya perlu membayar ¥99, dapat diubah menjadi milik sendiri, power bank akan menjadi milik Anda, konfirmasi operasi?',
convertToOwnSuccess: 'Berhasil diubah menjadi milik sendiri',
convertToOwnFailed: 'Operasi gagal, harap coba lagi nanti',
convertToOwnConfirmBtn: 'Beli untuk Milik Sendiri',
convertToOwnCancelBtn: 'Lanjutkan Menyewa',
convertToOwnWithMaxFee: 'Tidak ingin mengembalikan? Ubah menjadi milik sendiri',
convertToOwnWithMaxFeeTitle: 'Beli dan Bawa Pulang!',
convertToOwnWithMaxFeeConfirm: 'Karena sudah nyaman digunakan, langsung beli dan bawa pulang! Hanya ¥99, perangkat selamanya milik Anda, tidak perlu dikembalikan~\n✅Mendukung pengisian Type-C, sangat nyaman digunakan di rumah~\n✅Setelah dibeli, tidak ada batasan penggunaan, gunakan sesuka hati!',
convertToOwnWithMaxFeeSuccess: 'Pembelian berhasil',
convertToOwnWithMaxFeeFailed: 'Pembelian gagal, harap coba lagi nanti',
deviceNoEjectTitle: 'Power Bank Tidak Muncul',
deviceNoEjectConfirm: 'Apakah power bank Anda tidak muncul? Kami akan segera menanganinya untuk Anda, diperkirakan akan diselesaikan dalam 5 menit.',
deviceNoEjectSuccess: 'Umpan balik telah diterima, akan diproses dalam 5 menit',
deviceNoEjectFailed: 'Pengiriman umpan balik gagal, harap coba lagi nanti',
returnProblemTip: 'Setelah produk dikembalikan ke gudang, pesanan masih belum berakhir, harap pergi ke',
contactStaff: 'Hubungi staf.'
},
user: {
clickToLogin: 'Klik untuk Login',
loginPrompt: 'Setelah otorisasi login, Anda dapat melihat pesanan dan aset',
personalCenter: 'Pusat Pribadi',
depositBalance: 'Saldo Deposit',
withdraw: 'Tarik',
commonServices: 'Layanan Umum',
quickReturn: 'Pengembalian Cepat',
quickReturnDesc: '(Langsung lihat pesanan yang sedang digunakan)',
expressReturn: 'Catatan Pengembalian Ekspres',
myOrders: 'Pesanan Saya',
myCards: 'Kartu Anggota Saya',
myCoupons: 'Kupon Saya',
customerService: 'Pusat Layanan Pelanggan',
feedback: 'Keluhan dan Saran',
businessLicense: 'Lisensi Bisnis',
cooperation: 'Kerja Sama dan Keanggotaan',
settings: 'Pengaturan',
userAgreement: '《Perjanjian Pengguna》',
privacyPolicy: '《Kebijakan Privasi》',
version: 'v',
logout: 'Keluar',
confirmLogout: 'Konfirmasi keluar?',
logoutSuccess: 'Keluar berhasil',
getUserInfoFailed: 'Gagal mendapatkan informasi pengguna',
updateSuccess: 'Informasi berhasil diperbarui',
updateFailed: 'Gagal memperbarui informasi pengguna',
avatarUpdated: 'Avatar telah diperbarui',
avatarUploadFailed: 'Gagal memperbarui avatar',
noAvatar: 'Avatar tidak dipilih',
noAvatarUrl: 'Alamat avatar tidak diperoleh',
avatarDownloadFailed: 'Gagal mengunduh avatar',
notLoggedIn: 'Tidak login',
phoneNotBound: 'Nomor telepon tidak terikat',
balanceDesc: 'Dapat digunakan untuk menyewa perangkat',
feedbackRecord: 'Catatan Keluhan'
},
auth: {
authTitle: 'Otorisasi Mendapatkan Nomor Telepon',
authDesc: 'Untuk memberikan layanan yang lebih baik dan kontak darurat, perlu otorisasi untuk mendapatkan nomor telepon Anda',
getPhoneNumber: 'Login Cepat Nomor Telepon',
notNow: 'Tidak Sekarang',
authRequired: 'Perlu Otorisasi',
authSuccess: 'Otorisasi berhasil',
authFailed: 'Otorisasi gagal',
loginTitle: 'Login',
loginDesc: 'Untuk memastikan pengalaman penggunaan, harap selesaikan login terlebih dahulu',
getUserInfoSuccess: 'Berhasil mendapatkan informasi pengguna',
getUserInfoFailed: 'Gagal mendapatkan informasi pengguna',
pleaseUseInWechat: 'Harap gunakan fungsi ini di WeChat Mini Program',
agreeToTerms: 'Saya telah membaca dan menyetujui',
pleaseAgreeToTerms: 'Harap baca dan setujui 《Perjanjian Pengguna》 dan 《Kebijakan Privasi》 terlebih dahulu',
loginSuccess: 'Login berhasil',
loginFailed: 'Login gagal',
phoneCancelled: 'Otorisasi nomor telepon dibatalkan',
goToLogin: 'Pergi ke Login',
authDescShort: 'Untuk memberikan layanan yang lebih baik, perlu otorisasi untuk mendapatkan nomor telepon Anda',
phoneRequired: 'Perlu otorisasi nomor telepon untuk menggunakan perangkat',
getting: 'Mengambil...',
phoneSuccess: 'Berhasil mendapatkan nomor telepon',
phoneError: 'Kesalahan mendapatkan nomor telepon',
phoneGetFailed: 'Gagal mendapatkan nomor telepon',
authCodeFailed: 'Gagal mendapatkan kode otorisasi',
phoneLogin: 'Login Nomor Telepon',
phonePlaceholder: 'Harap masukkan nomor telepon',
codePlaceholder: 'Harap masukkan kode verifikasi',
getCode: 'Dapatkan Kode Verifikasi',
resend: 'Kirim Ulang',
loginBtn: 'Login Sekarang',
phoneRequired: 'Harap masukkan nomor telepon',
phoneInvalid: 'Harap masukkan nomor telepon yang benar',
codeRequired: 'Harap masukkan kode verifikasi',
codeSent: 'Kode verifikasi telah dikirim',
sendCodeFailed: 'Gagal mengirim kode verifikasi',
regionNotSupported: 'Pengguna di luar Tiongkok Daratan, Hong Kong, dan Makau harap login melalui otorisasi nomor telepon platform',
onlyMainlandSupported: 'Saat ini hanya mendukung wilayah Tiongkok Daratan',
getServicePhoneFailed: 'Gagal mendapatkan nomor telepon layanan pelanggan',
noAuthToken: 'Login berhasil tetapi tidak mendapatkan token otorisasi'
},
permission: {
locationTitle: 'Otorisasi Informasi Lokasi',
locationNeed: 'Perlu mendapatkan informasi lokasi Anda untuk menampilkan perangkat terdekat, harap aktifkan izin lokasi di "Pengaturan-Manajemen Izin".',
locationDenied: 'Lokasi tidak diotorisasi, tidak dapat menampilkan perangkat terdekat. Anda dapat mengaktifkan kembali izin lokasi di "Pengaturan-Manajemen Izin" nanti.',
goToSettings: 'Pergi ke Pengaturan',
later: 'Tidak Sekarang',
gotIt: 'Mengerti'
},
payment: {
paymentAmount: 'Jumlah Pembayaran',
paymentMethod: 'Metode Pembayaran',
wechatPay: 'Pembayaran WeChat',
alipay: 'Alipay',
balance: 'Pembayaran Saldo',
payNow: 'Bayar Sekarang',
paying: 'Sedang membayar...',
paymentSuccess: 'Pembayaran berhasil',
paymentFailed: 'Pembayaran gagal',
paymentCancelled: 'Pembayaran dibatalkan',
orderPayment: 'Pembayaran Pesanan',
waitingForPayment: 'Menunggu Pembayaran',
pleasePayIn15Min: 'Harap selesaikan pembayaran dalam 15 menit',
orderInfo: 'Informasi Pesanan',
createTime: 'Waktu Pembuatan',
contactPhone: 'Nomor Telepon Kontak',
feeInfo: 'Informasi Biaya',
deposit: 'Deposit',
package: 'Paket',
total: 'Total',
paymentFailedRetry: 'Pembayaran gagal, harap coba lagi',
createPayOrderFailed: 'Gagal membuat pesanan pembayaran',
subscriptionSuccess: 'Berlangganan berhasil',
subscriptionFailed: 'Berlangganan gagal, harap coba lagi nanti'
},
feedback: {
uploading: 'Mengunggah...',
title: 'Keluhan dan Saran',
placeholder: 'Harap jelaskan secara detail masalah yang Anda hadapi, agar kami dapat menyelesaikannya dengan lebih baik',
submit: 'Kirim Umpan Balik',
submitSuccess: 'Umpan balik berhasil',
submitFailed: 'Umpan balik gagal',
contentRequired: 'Harap masukkan konten',
issueType: 'Jenis Masalah',
issueDescription: 'Deskripsi Masalah',
imageUpload: 'Unggah Gambar (Opsional)',
uploadImage: 'Unggah Gambar',
contactInfo: 'Informasi Kontak',
contactPlaceholder: 'Harap tinggalkan nomor telepon Anda, agar kami dapat menghubungi Anda',
pleaseSelectType: 'Harap pilih jenis masalah',
pleaseDescribe: 'Harap jelaskan masalah Anda',
pleaseContact: 'Harap tinggalkan informasi kontak',
imageUploadFailed: 'Gagal mengunggah gambar, harap coba lagi',
deviceFault: 'Kesalahan Perangkat',
chargingIssue: 'Masalah Biaya',
usageSuggestion: 'Saran Penggunaan',
other: 'Lainnya',
recordList: 'Catatan Keluhan',
detail: 'Detail Keluhan',
noRecord: 'Tidak ada catatan keluhan',
getListFailed: 'Gagal mendapatkan daftar',
getDetailFailed: 'Gagal mendapatkan detail',
processing: 'Memproses',
completed: 'Selesai',
pending: 'Menunggu Diproses',
complain: 'Keluhan',
suggestion: 'Saran',
contactPhone: 'Nomor Telepon Kontak',
initialSubmit: 'Pengiriman Pertama',
submitTime: 'Waktu Pengiriman',
uploadedImages: 'Gambar yang Diunggah',
platformReplies: 'Balasan Platform',
userReplies: 'Balasan Pengguna',
platform: 'Layanan Pelanggan Platform',
me: 'Saya',
replyPlaceholder: 'Harap masukkan balasan Anda...',
submitReply: 'Kirim Balasan',
replySuccess: 'Balasan berhasil',
replyFailed: 'Balasan gagal',
pleaseEnterReply: 'Harap masukkan konten balasan',
idRequired: 'ID keluhan tidak boleh kosong',
viewRecords: 'Lihat Catatan',
replyHistory: 'Riwayat Balasan'
},
help: {
title: 'Pusat Layanan Pelanggan',
commonQuestions: 'Pertanyaan Umum',
contactUs: 'Hubungi Kami',
phone: 'Telepon',
email: 'Email',
workingHours: 'Jam Kerja',
workingHoursValue: 'Senin hingga Minggu 09:00-22:00',
functionDeveloping: 'Fungsi sedang dikembangkan',
faq1Question: 'Bagaimana cara menyewa kipas angin?',
faq1Answer: 'Klik tombol "Pindai untuk Menyewa" di beranda, gunakan WeChat untuk memindai kode QR pada perangkat, selesaikan pembayaran sesuai petunjuk untuk menggunakan.',
faq2Question: 'Bagaimana tarifnya?',
faq2Answer: 'Produk ini menyewakan kipas angin dalam bentuk sewa tanpa deposit, tidak perlu membayar deposit, metode penagihan spesifik mengikuti petunjuk pemindaian kode QR kabinet lokasi.',
faq3Question: 'Bagaimana cara mengembalikan kipas angin?',
faq3Answer: 'Bawa kipas angin ke titik pengembalian mana pun, klik tombol "Pindai untuk Mengembalikan" di beranda, pindai kode QR titik pengembalian untuk menyelesaikan pengembalian.',
faq4Question: 'Berapa lama deposit akan dikembalikan?',
faq4Answer: 'Setelah perangkat dikembalikan, deposit akan secara otomatis memulai pengembalian dana, diperkirakan akan diterima dalam 0-7 hari kerja.',
faq5Question: 'Apa yang harus dilakukan jika perangkat tidak dapat digunakan secara normal?',
faq5Answer: 'Anda dapat mengirimkan umpan balik kesalahan melalui "Saya-Keluhan dan Saran", atau langsung menghubungi nomor telepon layanan pelanggan untuk menanganinya.'
},
settings: {
title: 'Pengaturan',
language: 'Bahasa',
languageSetting: 'Pengaturan Bahasa',
chinese: '简体中文',
english: 'English',
indonesian: 'Bahasa Indonesia',
languageSwitched: 'Bahasa telah diubah, sedang memuat ulang...',
notification: 'Notifikasi',
privacy: 'Privasi',
about: 'Tentang',
clearCache: 'Hapus Cache',
cacheCleared: 'Cache telah dihapus',
logout: 'Keluar',
confirmLogout: 'Konfirmasi keluar?',
logoutSuccess: 'Keluar berhasil'
},
express: {
title: 'Pengembalian Ekspres',
addReturn: 'Tambah Pengembalian',
returnRecord: 'Catatan Pengembalian Ekspres',
expressNo: 'Nomor Ekspres',
expressCompany: 'Perusahaan Ekspres',
sendTime: 'Waktu Pengiriman',
receivedTime: 'Waktu Penerimaan',
status: 'Status',
pending: 'Menunggu Diproses',
shipped: 'Telah Dikirim',
received: 'Telah Diterima',
detail: 'Detail',
recipientInfo: 'Informasi Penerima',
recipientName: 'FengDianZhe 18163601305',
recipientAddress: 'Gedung A2, Lantai 623, Taman Sains dan Teknologi Xinchanghaijian, Jalan Lugu, Distrik Yuelu, Changsha, Provinsi Hunan',
copyAllInfo: 'Salin Semua Informasi',
recipient: 'Penerima',
recipientAddressLabel: 'Alamat Penerima',
copySuccess: 'Semua informasi telah disalin',
copyFailed: 'Gagal menyalin',
noReturnRecord: 'Tidak ada catatan pengembalian',
toFill: 'Menunggu Diisi',
userPhone: 'Nomor Telepon Pengguna',
billingPaused: 'Penagihan Dijeda',
completed: 'Selesai',
processing: 'Memproses',
getListFailed: 'Gagal mendapatkan daftar',
loadFailed: 'Gagal memuat',
returnCompleted: 'Pengembalian Selesai',
returnCompletedDesc: 'Ekspres Anda telah berhasil dikembalikan',
processingDesc: 'Sedang memproses permintaan pengembalian Anda',
pendingDesc: 'Menunggu pemrosesan aplikasi pengembalian',
expressInfo: 'Informasi Ekspres',
trackingNo: 'Nomor Pelacakan',
packageType: 'Jenis Paket',
packageWeight: 'Berat Paket',
returnInfo: 'Informasi Pengembalian',
returnAddress: 'Alamat Pengembalian',
returnTime: 'Waktu Pengembalian',
processTime: 'Waktu Pemrosesan',
completeTime: 'Waktu Penyelesaian',
remarkInfo: 'Informasi Catatan',
copyTrackingNo: 'Salin Nomor Pelacakan',
trackingNoCopied: 'Nomor pelacakan telah disalin',
workingHours: 'Senin hingga Minggu 09:00-22:00',
call: 'Hubungi',
returnDetail: 'Detail Pengembalian',
getDetailFailed: 'Gagal mendapatkan detail',
fillExpress: 'Pengembalian Ekspres',
openTime: 'Waktu Mulai',
fillExpressInfo: 'Isi Informasi Pengembalian Ekspres',
contactPhone: 'Nomor Telepon Kontak',
fillTrackingPlaceholder: 'Harap masukkan nomor ekspres yang perlu diisi',
trackingPlaceholder: 'Harap masukkan nomor ekspres (dapat dikosongkan terlebih dahulu)',
confirmFill: 'Konfirmasi Isi',
submitInfo: 'Kirim Informasi',
orderNoMissing: 'Nomor pesanan kurang',
getRecordFailed: 'Gagal mendapatkan catatan',
existingReturnNotice: 'Sudah ada aplikasi pengembalian ekspres, apakah akan pergi untuk mengisi nomor ekspres?',
goToFill: 'Pergi untuk Mengisi',
alreadyHasRecord: 'Sudah ada catatan pengembalian',
pleaseEnterValidPhone: 'Harap isi nomor telepon kontak yang valid',
pleaseEnterTrackingNo: 'Harap isi nomor ekspres',
filling: 'Mengisi...',
fillSuccess: 'Pengisian berhasil',
fillFailed: 'Pengisian gagal',
submitSuccess: 'Pengiriman berhasil',
submitFailed: 'Pengiriman gagal'
},
join: {
title: 'Kerja Sama dan Keanggotaan',
cooperationTitle: 'Metode Kerja Sama',
contactUs: 'Hubungi Kami',
phone: 'Nomor Telepon Kontak',
email: 'Email Kontak',
submit: 'Kirim Aplikasi',
name: 'Nama',
contactPhone: 'Informasi Kontak',
city: 'Kota',
intention: 'Niat Kerja Sama',
placeholder: 'Harap jelaskan secara singkat niat kerja sama Anda...',
submitSuccess: 'Pengiriman berhasil, kami akan segera menghubungi Anda',
submitFailed: 'Pengiriman gagal, harap coba lagi nanti',
pageLoadFailed: 'Gagal memuat halaman'
},
legal: {
agreement: 'Perjanjian Pengguna',
privacy: 'Kebijakan Privasi',
termsOfService: 'Ketentuan Layanan',
termsAndConditions: 'Syarat & Ketentuan',
lastUpdate: 'Pembaruan Terakhir',
applicableToService: 'Berlaku untuk layanan sewa kipas angin berbagi "FengDianZhe"',
footerNotice: 'Jika ada pertanyaan tentang perjanjian ini, harap pergi ke "Saya-Layanan Pelanggan" untuk konsultasi',
footerNoticePolicy: 'Jika ada pertanyaan tentang kebijakan ini, harap pergi ke "Saya-Layanan Pelanggan" untuk konsultasi',
// Konten Syarat dan Ketentuan
applicableLaw: 'Hukum yang Berlaku',
applicableLawContent: 'Ketentuan Layanan ini diatur oleh hukum Republik Rakyat Tiongkok. Dengan menggunakan layanan ini, Anda setuju untuk terikat oleh hukum Tiongkok. Setiap perselisihan yang timbul dari layanan ini harus diselesaikan terlebih dahulu melalui negosiasi bersahabat; jika negosiasi gagal, salah satu pihak dapat mengajukan gugatan ke Pengadilan Rakyat yang memiliki yurisdiksi atas lokasi penyedia layanan.',
paymentMethods: 'Metode Pembayaran',
paymentMethodsContent: 'Kami mendukung berbagai metode pembayaran, termasuk namun tidak terbatas pada: WeChat Pay, Alipay, WeChat Pay Score tanpa deposit, dll. Pengguna perlu menyelesaikan proses pembayaran sebelum menggunakan layanan. Setelah pembayaran berhasil, sistem akan secara otomatis membuka kunci perangkat untuk akses pengguna. Semua transaksi pembayaran dilakukan melalui saluran terenkripsi yang aman untuk memastikan keamanan dana pengguna.',
refundPolicy: 'Kebijakan Pengembalian Dana',
refundPolicyContent: '1. Pengembalian Deposit: Setelah mengembalikan perangkat, deposit akan secara otomatis dikembalikan ke akun pembayaran asli setelah dikurangi biaya sewa yang sesuai, diperkirakan tiba dalam 0-7 hari kerja.\n2. Pembatalan Pesanan: Pesanan yang tidak digunakan dapat dibatalkan sebelum penggunaan dimulai, dan deposit akan dikembalikan sepenuhnya.\n3. Pengembalian Dana Pengecualian: Dalam kasus keadaan khusus seperti kegagalan perangkat, pengguna dapat mengajukan pengembalian dana, yang akan kami proses dalam 3-5 hari kerja setelah verifikasi.\n4. Kartu Keanggotaan/Kupon: Kartu keanggotaan dan kupon yang dibeli umumnya tidak mendukung pengembalian dana. Silakan hubungi layanan pelanggan untuk kasus khusus.',
serviceTerms: 'Ketentuan Layanan',
serviceTermsContent: 'Saat menggunakan layanan ini, pengguna harus mematuhi peraturan berikut: 1) Jaga peralatan yang disewa dengan baik dan jangan sengaja merusak atau memilikinya secara pribadi; 2) Kembalikan peralatan tepat waktu untuk menghindari biaya tambahan; 3) Jangan gunakan peralatan untuk tujuan ilegal; 4) Jika ditemukan kegagalan peralatan, hubungi layanan pelanggan segera. Pelanggaran terhadap peraturan di atas dapat mengakibatkan penghentian layanan dan tanggung jawab.',
liabilityLimitation: 'Batasan Tanggung Jawab',
liabilityLimitationContent: 'Sejauh diizinkan oleh hukum, kami tidak bertanggung jawab atas kerusakan tidak langsung, insidental, khusus, atau konsekuensial yang timbul dari penggunaan atau ketidakmampuan menggunakan layanan ini. Total tanggung jawab kami tidak akan melebihi biaya yang dibayarkan oleh pengguna untuk menggunakan layanan ini. Kami tidak bertanggung jawab atas gangguan atau penundaan layanan yang disebabkan oleh force majeure, kegagalan jaringan, alasan pihak ketiga, dll.',
disputeResolution: 'Penyelesaian Sengketa',
disputeResolutionContent: 'Jika pengguna memiliki pertanyaan atau perselisihan tentang layanan, silakan hubungi kami terlebih dahulu melalui saluran layanan pelanggan. Kami akan merespons dalam 24 jam setelah menerima umpan balik dan bernegosiasi untuk penyelesaian sesegera mungkin. Jika negosiasi gagal, kedua belah pihak setuju untuk menyerahkan perselisihan ke Pengadilan Rakyat dengan yurisdiksi atas lokasi penyedia layanan untuk penyelesaian melalui litigasi. Selama periode penyelesaian sengketa, kedua belah pihak harus terus melaksanakan ketentuan yang tidak dipersengketakan dari perjanjian ini.'
},
search: {
title: 'Cari Perangkat',
placeholder: 'Harap masukkan nama lokasi atau alamat',
history: 'Riwayat Pencarian',
clear: 'Hapus Riwayat',
noResult: 'Tidak ada hasil pencarian',
searching: 'Mencari...',
invalidCoordinate: 'Koordinat lokasi ini tidak valid',
positionInfoError: 'Informasi lokasi abnormal'
},
share: {
title: 'FengDianZhe - Kipas Angin & Power Bank Berbagi',
path: '/pages/index/index'
},
error: {
networkError: 'Koneksi jaringan gagal',
serverError: 'Kesalahan server',
timeout: 'Permintaan timeout',
unknown: 'Kesalahan tidak diketahui',
tryAgain: 'Harap coba lagi nanti'
},
time: {
hour: 'jam',
minute: 'menit',
second: 'detik',
day: 'hari',
week: 'minggu',
month: 'bulan',
year: 'tahun',
justNow: 'Baru saja',
minutesAgo: 'menit yang lalu',
hoursAgo: 'jam yang lalu',
daysAgo: 'hari yang lalu',
yesterday: 'Kemarin',
today: 'Hari ini',
tomorrow: 'Besok',
hours: 'jam',
minutes: 'menit',
halfHours: 'setengah jam'
},
unit: {
yuan: 'Yuan',
meter: 'meter',
km: 'kilometer',
piece: 'buah',
times: 'kali'
},
waiting: {
title: 'Perangkat Sedang Muncul',
preparing: 'Sedang menyiapkan perangkat untuk Anda',
longTimeNotice: 'Jika tidak muncul dalam waktu lama, harap hubungi staf di lokasi atau coba lagi nanti',
deviceEjecting: 'Perangkat sedang muncul, harap tunggu',
rentFailed: 'Penyewaan perangkat gagal, pesanan telah dibatalkan',
timeout: 'Waktu tunggu habis, harap coba lagi nanti'
},
success: {
paymentSuccess: 'Pembayaran berhasil',
paymentSuccessDesc: 'Pesanan Anda telah berhasil dibayar',
orderInfo: 'Informasi Pesanan',
paymentAmount: 'Jumlah Pembayaran',
paymentTime: 'Waktu Pembayaran',
deviceStatus: 'Status Perangkat',
preparingDevice: 'Sedang menyiapkan perangkat Anda, harap tunggu...',
deviceReady: 'Perangkat telah muncul, harap ambil kipas angin Anda',
deviceFailed: 'Gagal mengeluarkan perangkat, harap hubungi layanan pelanggan',
backToHome: 'Kembali ke Beranda',
viewOrder: 'Lihat Pesanan',
returnSuccess: 'Pengembalian berhasil',
returnSuccessDesc: 'Kipas angin Anda telah dikembalikan, biaya telah dipotong dari deposit',
usedTime: 'Durasi Penggunaan',
packageTime: 'Durasi Paket',
extraTime: 'Waktu Tambahan',
returnTime: 'Waktu Pengembalian',
packageFee: 'Biaya Paket',
extraFee: 'Biaya Tambahan',
totalFee: 'Total Biaya',
depositAmount: 'Deposit',
refundAmount: 'Jumlah Pengembalian',
refundStatus: 'Status Pengembalian',
refundNotice: 'Penjelasan Pengembalian Dana',
refundNotice1: 'Jumlah sisa deposit perlu Anda ajukan penarikan secara manual',
refundNotice2: 'Setelah aplikasi penarikan diajukan, akan dikembalikan ke akun pembayaran asli dalam 1-3 hari kerja',
refundNotice3: 'Jika ada pertanyaan, harap hubungi layanan pelanggan',
applyRefund: 'Ajukan Pengembalian Dana',
refundWaiting: 'Menunggu Aplikasi',
refundProcessing: 'Memproses',
refundSuccess: 'Telah Dikembalikan',
refundFailed: 'Pengembalian dana gagal'
},
deposit: {
title: 'Manajemen Deposit',
depositBalance: 'Saldo Deposit',
withdraw: 'Tarik',
withdrawRecord: 'Catatan Penarikan',
withdrawAmount: 'Jumlah Penarikan',
withdrawStatus: 'Status Penarikan',
applyWithdraw: 'Ajukan Penarikan',
withdrawSuccess: 'Penarikan berhasil',
withdrawFailed: 'Penarikan gagal',
noBalance: 'Tidak ada saldo yang dapat ditarik',
confirmWithdraw: 'Konfirmasi Penarikan',
withdrawDesc: 'Deposit akan dikembalikan ke jalur asli, diperkirakan akan diterima dalam 0-7 hari kerja',
withdrawing: 'Sedang menarik...',
withdrawSubmitted: 'Aplikasi penarikan telah diajukan',
withdrawNotice: 'Penjelasan Penarikan',
withdrawNotice1: 'Jumlah penarikan akan dikembalikan ke akun pembayaran asli',
withdrawNotice2: 'Setelah aplikasi penarikan diajukan, diperkirakan akan diterima dalam 0-7 hari kerja',
withdrawNotice3: 'Jika tidak diterima setelah waktu habis, harap hubungi layanan pelanggan untuk menanganinya',
depositRecord: 'Catatan Deposit',
payRecord: 'Catatan Pembayaran',
refundRecord: 'Catatan Pengembalian',
orderNotReturned: 'Pesanan saat ini belum dikembalikan, harap kembalikan terlebih dahulu sebelum menarik',
alreadyRefunded: 'Deposit telah dikembalikan, tidak perlu menarik ulang',
refundProcessing: 'Pengembalian deposit sedang diproses, harap tunggu dengan sabar'
},
userProfile: {
title: 'Informasi Pribadi',
avatar: 'Avatar',
nickname: 'Nama Panggilan',
phone: 'Nomor Telepon',
edit: 'Edit',
save: 'Simpan',
cancel: 'Batal',
clickToChange: 'Klik avatar untuk mengubah',
notSet: 'Tidak diatur',
notBound: 'Tidak terikat',
balance: 'Saldo',
enterNickname: 'Harap masukkan nama panggilan baru',
nicknameRequired: 'Nama panggilan tidak boleh kosong',
saving: 'Menyimpan...',
nicknameUpdated: 'Nama panggilan berhasil diubah',
updateFailed: 'Gagal mengubah',
uploading: 'Mengunggah...'
},
purchase: {
title: 'Area Diskon',
memberCard: 'Kartu Anggota',
coupon: 'Kupon',
buyNow: 'Beli Sekarang',
myCards: 'Kartu Anggota Saya',
myCoupons: 'Kupon Saya',
cardDescription: 'Penjelasan Kartu Anggota',
couponDescription: 'Penjelasan Kupon',
pleaseSelect: 'Harap pilih produk yang ingin dibeli',
noCards: 'Tidak ada kartu anggota yang tersedia',
noCoupons: 'Tidak ada kupon yang tersedia',
cardUseInstruction: 'Instruksi Penggunaan',
cardValidityPeriod: 'Periode Validitas',
cardRefundPolicy: 'Penjelasan Pengembalian Dana',
cardUseDescription: 'Kartu anggota berlaku segera setelah pembelian dan dapat digunakan di lokasi yang ditentukan. Kartu kali dihitung berdasarkan jumlah penggunaan, kartu durasi dihitung berdasarkan durasi penggunaan, harap pilih jenis kartu yang sesuai dengan kebutuhan aktual Anda.',
cardValidityDescription: 'Kartu anggota berlaku sejak tanggal pembelian, periode validitas berbeda sesuai dengan jenis kartu. Kartu kali akan kedaluwarsa setelah digunakan dalam periode validitas, kartu durasi akan kedaluwarsa setelah durasi penggunaan kumulatif tercapai dalam periode validitas.',
cardRefundDescription: 'Kartu anggota tidak mendukung pengembalian dana setelah pembelian, bagian yang tidak digunakan dapat terus digunakan dalam periode validitas. Jika perlu pengembalian dana dalam situasi khusus, harap hubungi layanan pelanggan untuk menanganinya.',
couponUseInstruction: 'Instruksi Penggunaan',
couponValidityPeriod: 'Periode Validitas',
couponUsageScope: 'Cakupan Penggunaan',
couponUseDescription: 'Kupon berlaku segera setelah pembelian dan dapat digunakan saat penyelesaian pesanan. Setiap pesanan hanya dapat menggunakan satu kupon, kupon tidak dapat digunakan bersamaan dengan aktivitas diskon lainnya.',
couponValidityDescription: 'Kupon berlaku sejak tanggal pembelian, harap gunakan dalam periode validitas. Setelah kedaluwarsa, kupon akan otomatis tidak berlaku dan tidak dapat diperpanjang.',
couponUsageDescription: 'Kupon dapat digunakan di lokasi yang ditentukan, untuk lokasi yang tersedia harap lihat detail kupon. Beberapa kupon memiliki persyaratan minimum konsumsi, harap perhatikan kondisi penggunaan.'
},
myCard: {
type: 'Jenis',
timesCard: 'Kartu Kali',
durationCard: 'Kartu Durasi',
remainingTimes: 'Sisa Kali:',
remainingDuration: 'Sisa Durasi',
hours: 'jam',
validPeriod: 'Periode Validitas',
active: 'Sedang Digunakan',
expired: 'Tidak Berlaku',
used: 'Habis',
position: 'Lokasi Penggunaan',
price: 'Harga Pembelian',
noCards: 'Tidak ada kartu anggota',
buyNow: 'Beli Sekarang',
getListFailed: 'Gagal mendapatkan daftar kartu anggota',
dailyLimit: 'Batas Harian',
singleTimeLimit: 'Batas Waktu Per Kali',
unlimited: 'Tidak Terbatas',
times: 'kali',
minutes: 'menit',
validWithinDays: 'hari berlaku',
validFromPurchase: 'Dari waktu pembelian',
daysValid: 'hari berlaku',
currentCycleUsed: 'Digunakan dalam siklus ini',
totalCount: 'Total Kali',
expire: 'Kedaluwarsa',
expiredOn: 'Kedaluwarsa pada',
renew: 'Perpanjang Kartu',
toUse: 'Gunakan',
onlyForRegionBefore: 'Hanya untuk',
onlyForRegionAfter: 'menggunakan'
},
myCoupon: {
available: 'Tersedia',
used: 'Digunakan',
expired: 'Kedaluwarsa',
useNow: 'Gunakan',
usedStatus: 'Digunakan',
expiredStatus: 'Kedaluwarsa',
refundedStatus: 'Telah Dikembalikan',
noAvailableCoupons: 'Tidak ada kupon yang tersedia',
noUsedCoupons: 'Tidak ada kupon yang telah digunakan',
noExpiredCoupons: 'Tidak ada kupon yang kedaluwarsa',
buyNow: 'Beli Sekarang',
getListFailed: 'Gagal mendapatkan daftar kupon',
onlyForRegionBefore: 'Hanya untuk',
onlyForRegionAfter: 'menggunakan'
},
goods: {
title: 'Detail Produk',
goodsTitle: 'Detail Kustomisasi',
productName: 'FengDianZhe Kipas Angin Berbagi + Power Bank + Seri Hand Warmer - Pink Sakura',
perUnit: '/buah',
buyNow: 'Beli Sekarang',
productDetail: 'Detail Kustomisasi',
features: {
battery: '8000Ahm',
batteryDesc: 'Baterai Kapasitas Besar',
wind: 'Kipas Angin Efisien',
temp: 'Kontrol Suhu Pintar',
charge: 'Pengisian Cepat'
},
description: 'FengDianZhe kipas angin berbagi, mengintegrasikan tiga fungsi dalam satu: kipas angin, power bank, dan hand warmer. Menggunakan baterai kapasitas besar 8000mAh, daya tahan lama. Desain kipas angin efisien, tiga tingkat angin dapat disesuaikan. Hand warmer kontrol suhu pintar, hangat di musim dingin dan sejuk di musim panas. Teknologi pengisian cepat, mendukung pengisian multi-perangkat. Warna pink sakura, modis dan indah, adalah teman perjalanan terbaik Anda.',
confirmPurchase: 'Konfirmasi Pembelian',
confirmPurchaseContent: 'Konfirmasi membeli produk ini, perlu membayar ¥{price}?',
purchaseSuccess: 'Pembelian berhasil',
purchaseFailed: 'Pembelian gagal',
processing: 'Sedang memproses...'
}
}
+3 -1
View File
@@ -1,8 +1,10 @@
import zhCN from './zh-CN.js' import zhCN from './zh-CN.js'
import enUS from './en-US.js' import enUS from './en-US.js'
import idID from './id-ID.js'
export default { export default {
'zh-CN': zhCN, 'zh-CN': zhCN,
'en-US': enUS 'en-US': enUS,
'id-ID': idID
} }
+22 -1
View File
@@ -469,6 +469,7 @@ export default {
languageSetting: '语言设置', languageSetting: '语言设置',
chinese: '简体中文', chinese: '简体中文',
english: 'English', english: 'English',
indonesian: 'Bahasa Indonesia',
languageSwitched: '语言已切换,正在刷新...', languageSwitched: '语言已切换,正在刷新...',
notification: '通知', notification: '通知',
privacy: '隐私', privacy: '隐私',
@@ -572,10 +573,30 @@ export default {
agreement: '用户协议', agreement: '用户协议',
privacy: '隐私政策', privacy: '隐私政策',
termsOfService: '服务条款', termsOfService: '服务条款',
termsAndConditions: '条款与细则',
lastUpdate: '最后更新', lastUpdate: '最后更新',
applicableToService: '适用于"风电者"共享风扇租借服务', applicableToService: '适用于"风电者"共享风扇租借服务',
footerNotice: '如对本协议有疑问,请前往"我的-客服"咨询', footerNotice: '如对本协议有疑问,请前往"我的-客服"咨询',
footerNoticePolicy: '如对本政策有疑问,请前往"我的-客服"咨询' footerNoticePolicy: '如对本政策有疑问,请前往"我的-客服"咨询',
// 条款与细则内容
applicableLaw: '适用法律',
applicableLawContent: '本服务条款受中华人民共和国法律管辖。用户使用本服务即表示同意接受中国法律的约束。任何因本服务引起的争议,应首先通过友好协商解决;协商不成的,任何一方均可向服务提供方所在地有管辖权的人民法院提起诉讼。',
paymentMethods: '支付方式',
paymentMethodsContent: '我们支持多种支付方式,包括但不限于:微信支付、支付宝、微信支付分免押金等。用户在使用服务前需完成支付流程。支付成功后,系统将自动开启设备供用户使用。所有支付交易均通过安全加密通道进行,确保用户资金安全。',
refundPolicy: '退款介绍',
refundPolicyContent: '1. 押金退款:归还设备后,押金将在扣除相应租金后自动退还至原支付账户,预计0-7个工作日到账。\n2. 订单取消:未使用的订单可在开始使用前取消,押金将全额退还。\n3. 异常退款:如遇设备故障等特殊情况,用户可申请退款,我们将在核实后3-5个工作日内处理。\n4. 会员卡/优惠券:已购买的会员卡和优惠券一般不支持退款,特殊情况请联系客服处理。',
serviceTerms: '服务条款',
serviceTermsContent: '用户在使用本服务时,应遵守以下规定:1) 妥善保管租借的设备,不得故意损坏或私自占有;2) 按时归还设备,避免产生额外费用;3) 不得将设备用于非法用途;4) 如发现设备故障,应及时联系客服处理。违反上述规定的,我们有权终止服务并追究相应责任。',
liabilityLimitation: '责任限制',
liabilityLimitationContent: '在法律允许的最大范围内,我们对因使用或无法使用本服务而导致的任何间接、偶然、特殊或后果性损害不承担责任。我们的总责任不超过用户为使用本服务所支付的费用。对于因不可抗力、网络故障、第三方原因等导致的服务中断或延迟,我们不承担责任。',
disputeResolution: '争议解决',
disputeResolutionContent: '如用户对服务有任何疑问或争议,请首先通过客服渠道联系我们,我们将在收到反馈后24小时内响应,并尽快协商解决。如协商不成,双方同意将争议提交至服务提供方所在地有管辖权的人民法院通过诉讼方式解决。在争议解决期间,双方应继续履行本协议中无争议的条款。'
}, },
search: { search: {
+25 -2
View File
@@ -4,14 +4,33 @@ import { createSSRApp } from 'vue'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import zhCN from './locale/zh-CN.js' import zhCN from './locale/zh-CN.js'
import enUS from './locale/en-US.js' import enUS from './locale/en-US.js'
import idID from './locale/id-ID.js'
import uView from '@climblee/uv-ui' import uView from '@climblee/uv-ui'
import { initConsoleControl } from './config/console.js' import { initConsoleControl } from './config/console.js'
// 初始化 console 控制 // 初始化 console 控制
initConsoleControl() initConsoleControl()
// 检测是否为 H5 环境
const isH5Platform = () => {
try {
const systemInfo = uni.getSystemInfoSync()
return systemInfo.platform === 'web' || systemInfo.uniPlatform === 'web' ||
(typeof window !== 'undefined' && typeof document !== 'undefined')
} catch (e) {
// 如果获取系统信息失败,尝试通过全局对象判断
return typeof window !== 'undefined' && typeof document !== 'undefined'
}
}
// 获取系统语言 // 获取系统语言
const getSystemLanguage = () => { const getSystemLanguage = () => {
// H5 环境默认使用印尼语
if (isH5Platform()) {
return 'id-ID'
}
// 非 H5 环境根据系统语言判断
let language = 'en-US' let language = 'en-US'
try { try {
const systemInfo = uni.getSystemInfoSync() const systemInfo = uni.getSystemInfoSync()
@@ -22,6 +41,8 @@ const getSystemLanguage = () => {
} }
} catch (e) { } catch (e) {
console.error('获取系统语言失败:', e) console.error('获取系统语言失败:', e)
// 默认使用中文
language = 'zh-CN'
} }
return language return language
} }
@@ -38,7 +59,8 @@ const getSavedLanguage = () => {
return systemLang return systemLang
} catch (e) { } catch (e) {
console.error('语言设置出错:', e) console.error('语言设置出错:', e)
return 'zh-CN' // 出错时根据平台返回默认语言
return isH5Platform() ? 'id-ID' : 'zh-CN'
} }
} }
@@ -81,7 +103,8 @@ function getI18nInstance() {
fallbackLocale: 'zh-CN', fallbackLocale: 'zh-CN',
messages: { messages: {
'zh-CN': zhCN, 'zh-CN': zhCN,
'en-US': enUS 'en-US': enUS,
'id-ID': idID
}, },
silentTranslationWarn: true, silentTranslationWarn: true,
silentFallbackWarn: true silentFallbackWarn: true
+1
View File
@@ -3,6 +3,7 @@
"@climblee/uv-ui": "^1.1.20", "@climblee/uv-ui": "^1.1.20",
"axios": "^1.7.9", "axios": "^1.7.9",
"axios-miniprogram-adapter": "0.3.4", "axios-miniprogram-adapter": "0.3.4",
"html5-qrcode": "^2.3.8",
"uniapp-axios-adapter": "^0.3.2", "uniapp-axios-adapter": "^0.3.2",
"uview-ui": "1.8.8", "uview-ui": "1.8.8",
"vue-i18n": "9" "vue-i18n": "9"
+8
View File
@@ -87,6 +87,14 @@
"navigationBarTextStyle": "black" "navigationBarTextStyle": "black"
} }
}, },
{
"path": "pages/legal/terms",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{ {
"path": "pages/my/index", "path": "pages/my/index",
"style": { "style": {
+107 -22
View File
@@ -989,21 +989,39 @@
const processScanResult = async (scanResult) => { const processScanResult = async (scanResult) => {
try { try {
console.log('===== 处理扫码结果 =====');
console.log('扫码结果对象:', scanResult);
console.log('scanType:', scanResult.scanType);
console.log('result:', scanResult.result);
console.log('path:', scanResult.path);
let deviceNo; let deviceNo;
if (scanResult.scanType == 'MANUAL') { if (scanResult.scanType == 'MANUAL') {
deviceNo = scanResult.result; deviceNo = scanResult.result;
} else if (scanResult.scanType == '"QR_CODE"') { console.log('手动输入模式,设备号:', deviceNo);
deviceNo = getQueryString(scanResult.result, 'deviceNo') } else if (scanResult.scanType == 'QR_CODE') {
// 修复:移除多余的引号
deviceNo = getQueryString(scanResult.result, 'deviceNo');
console.log('二维码扫描模式,提取设备号:', deviceNo);
} else { } else {
deviceNo = getQueryString(scanResult.path || scanResult.result, 'deviceNo') deviceNo = getQueryString(scanResult.path || scanResult.result, 'deviceNo');
console.log('其他模式,提取设备号:', deviceNo);
} }
console.log('最终设备号:', deviceNo);
if (!deviceNo) { if (!deviceNo) {
console.warn('未能提取到设备号');
uni.showToast({ uni.showToast({
title: t('home.invalidQRCode'), title: t('home.invalidQRCode'),
icon: 'none' icon: 'none'
}) });
return // 关闭扫码页面
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
}
return;
} }
// 检查是否有使用中的订单 // 检查是否有使用中的订单
@@ -1011,10 +1029,24 @@
if (inUseRes && inUseRes.code === 200 && inUseRes.data) { if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
const inUseOrder = inUseRes.data const inUseOrder = inUseRes.data
// 先关闭扫码页,再跳转
const pages = getCurrentPages();
if (pages.length > 1 && pages[pages.length - 1].route.includes('scan')) {
uni.navigateBack({
success: () => {
setTimeout(() => {
uni.reLaunch({ uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}` url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
}) });
return }, 100);
}
});
} else {
uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
});
}
return;
} }
// 检查是否有待支付订单 // 检查是否有待支付订单
@@ -1022,9 +1054,23 @@
if (orderRes && orderRes.code === 200 && orderRes.data) { if (orderRes && orderRes.code === 200 && orderRes.data) {
const unpaidOrder = orderRes.data const unpaidOrder = orderRes.data
// 先关闭扫码页,再跳转
const pages = getCurrentPages();
if (pages.length > 1 && pages[pages.length - 1].route.includes('scan')) {
uni.navigateBack({
success: () => {
setTimeout(() => {
uni.navigateTo({ uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}` url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
}) });
}, 100);
}
});
} else {
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
});
}
} else { } else {
try { try {
const deviceInfoRes = await getDeviceInfo(deviceNo) const deviceInfoRes = await getDeviceInfo(deviceNo)
@@ -1032,40 +1078,79 @@
if (deviceInfoRes.code == 200 && deviceInfoRes.data && deviceInfoRes.data.device) { if (deviceInfoRes.code == 200 && deviceInfoRes.data && deviceInfoRes.data.device) {
const deviceInfo = deviceInfoRes.data.device const deviceInfo = deviceInfoRes.data.device
// 先关闭扫码页,再跳转
const pages = getCurrentPages();
const closeScanPageAndNavigate = (url) => {
if (pages.length > 1 && pages[pages.length - 1].route.includes('scan')) {
uni.navigateBack({
success: () => {
setTimeout(() => {
uni.navigateTo({ url });
}, 100);
}
});
} else {
uni.navigateTo({ url });
}
};
if (deviceInfo.feeConfig) { if (deviceInfo.feeConfig) {
try { try {
const feeConfig = JSON.parse(deviceInfo.feeConfig) const feeConfig = JSON.parse(deviceInfo.feeConfig)
uni.navigateTo({ closeScanPageAndNavigate(`/pages/device/detail?deviceNo=${deviceNo}&feeConfig=${encodeURIComponent(deviceInfo.feeConfig)}`);
url: `/pages/device/detail?deviceNo=${deviceNo}&feeConfig=${encodeURIComponent(deviceInfo.feeConfig)}`
})
} catch (e) { } catch (e) {
closeScanPageAndNavigate(`/pages/device/detail?deviceNo=${deviceNo}`);
}
} else {
closeScanPageAndNavigate(`/pages/device/detail?deviceNo=${deviceNo}`);
}
} else {
// 先关闭扫码页,再跳转
const pages = getCurrentPages();
if (pages.length > 1 && pages[pages.length - 1].route.includes('scan')) {
uni.navigateBack({
success: () => {
setTimeout(() => {
uni.navigateTo({ uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}` url: `/pages/device/detail?deviceNo=${deviceNo}`
}) });
}, 100);
} }
});
} else { } else {
uni.navigateTo({ uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}` url: `/pages/device/detail?deviceNo=${deviceNo}`
}) });
} }
} else {
// uni.showToast({
// title: t('device.getDeviceInfoFailed'),
// icon: 'none'
// })
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
} }
} catch (error) { } catch (error) {
console.error('获取设备信息异常:', error) console.error('获取设备信息异常:', error)
// 先关闭扫码页,再跳转
const pages = getCurrentPages();
if (pages.length > 1 && pages[pages.length - 1].route.includes('scan')) {
uni.navigateBack({
success: () => {
setTimeout(() => {
uni.navigateTo({ uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}` url: `/pages/device/detail?deviceNo=${deviceNo}`
}) });
}, 100);
}
});
} else {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
});
}
} }
} }
} catch (error) { } catch (error) {
console.error('处理扫码结果失败:', error) console.error('处理扫码结果失败:', error)
// 关闭扫码页面
const pages = getCurrentPages();
if (pages.length > 1 && pages[pages.length - 1].route.includes('scan')) {
uni.navigateBack();
}
} }
} }
+135
View File
@@ -0,0 +1,135 @@
<template>
<view class="terms-page">
<view class="content">
<view class="title">{{ $t('legal.termsAndConditions') }}</view>
<view class="section">
<view class="section-title">{{ $t('legal.applicableLaw') }}</view>
<view class="section-content">
<text>{{ $t('legal.applicableLawContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.paymentMethods') }}</view>
<view class="section-content">
<text>{{ $t('legal.paymentMethodsContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.refundPolicy') }}</view>
<view class="section-content">
<text>{{ $t('legal.refundPolicyContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.serviceTerms') }}</view>
<view class="section-content">
<text>{{ $t('legal.serviceTermsContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.liabilityLimitation') }}</view>
<view class="section-content">
<text>{{ $t('legal.liabilityLimitationContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.disputeResolution') }}</view>
<view class="section-content">
<text>{{ $t('legal.disputeResolutionContent') }}</text>
</view>
</view>
<view class="footer">
<text class="update-time">{{ $t('legal.lastUpdate') }}: 2025-02-05</text>
<text class="notice">{{ $t('legal.footerNotice') }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { onMounted } from 'vue'
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
onMounted(() => {
uni.setNavigationBarTitle({
title: t('legal.termsAndConditions')
})
})
</script>
<style lang="scss" scoped>
.terms-page {
min-height: 100vh;
background-color: #f6f6f6;
padding-bottom: env(safe-area-inset-bottom);
}
.content {
background-color: #ffffff;
padding: 40rpx 30rpx;
margin: 20rpx;
border-radius: 16rpx;
}
.title {
font-size: 40rpx;
font-weight: bold;
color: #333333;
text-align: center;
margin-bottom: 40rpx;
}
.section {
margin-bottom: 40rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 20rpx;
padding-left: 20rpx;
border-left: 6rpx solid #07c160;
}
.section-content {
font-size: 28rpx;
color: #666666;
line-height: 1.8;
text-align: justify;
padding: 20rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
}
.footer {
margin-top: 60rpx;
padding-top: 30rpx;
border-top: 1rpx solid #e5e5e5;
text-align: center;
}
.update-time {
display: block;
font-size: 24rpx;
color: #999999;
margin-bottom: 20rpx;
}
.notice {
display: block;
font-size: 26rpx;
color: #666666;
line-height: 1.6;
}
</style>
+195 -225
View File
@@ -1,19 +1,23 @@
<template> <template>
<view class="payment-container"> <view class="payment-container">
<!-- 订单状态 --> <!-- 地点信息卡片 -->
<view class="status-card"> <view class="location-card">
<view class="status-icon-wrapper"> <view class="location-header">
<view class="status-icon" :class="orderStatus.class"> <!-- <view class="location-icon">📍</view> -->
<text class="icon-text">💳</text> <image src="@/static/device_location.png" mode="aspectFit" class="location-icon"></image>
<text class="location-name">{{ locationName }}</text>
<view class="status-badge">{{ $t('device.available') }}</view>
</view> </view>
<view class="device-info">
<text class="device-label">{{ $t('order.deviceNo') }}</text>
<text class="device-value">{{ orderInfo.deviceNo || '-' }}</text>
</view> </view>
<view class="status-text">{{ orderStatus.text }}</view>
<view class="status-desc">{{ orderStatus.desc }}</view>
</view> </view>
<!-- 订单信息 --> <!-- 订单信息和费用信息 -->
<view class="order-card"> <view class="order-card">
<view class="card-header"> <view class="card-header">
<view class="card-title-bar"></view>
<view class="card-title">{{ $t('payment.orderInfo') }}</view> <view class="card-title">{{ $t('payment.orderInfo') }}</view>
</view> </view>
<view class="info-item"> <view class="info-item">
@@ -28,52 +32,35 @@
<text class="label">{{ $t('payment.createTime') }}</text> <text class="label">{{ $t('payment.createTime') }}</text>
<text class="value">{{ orderInfo.createTime || '-' }}</text> <text class="value">{{ orderInfo.createTime || '-' }}</text>
</view> </view>
</view>
<!-- 费用信息 --> <!-- 费用信息部分 -->
<view class="price-card"> <view class="card-header" style="margin-top: 32rpx;">
<view class="card-header"> <view class="card-title-bar"></view>
<view class="card-title">{{ $t('payment.feeInfo') }}</view> <view class="card-title">{{ $t('payment.feeInfo') }}</view>
</view> </view>
<view class="price-item"> <view class="price-item">
<text class="label">{{ $t('payment.deposit') }}</text> <text class="label">{{ $t('payment.deposit') }}</text>
<text class="value">{{ orderInfo.deposit || '99.00' }}</text> <text class="value">¥ {{ orderInfo.deposit || '90' }}</text>
</view> </view>
<!-- <view class="price-divider"></view> --> <!-- <view class="price-item">
<text class="label">{{ $t('payment.package') }}</text>
<text class="value">{{ packageText }}</text>
</view> -->
<view class="price-item total"> <view class="price-item total">
<text class="label">{{ $t('payment.total') }}</text> <text class="label">{{ $t('payment.total') }}</text>
<text class="value">{{ totalAmount }}</text> <view class="total-value">
</view> <text class="currency">¥</text>
</view> <text class="amount">{{ totalAmount }}</text>
<!-- 支付方式选择 -->
<view class="payment-methods" v-if="paymentMethods.length > 0">
<view class="card-header">
<view class="card-title">支付方式</view>
</view>
<view class="method-item" v-for="method in paymentMethods" :key="method?.paymentMethodType"
:class="{ active: selectedPaymentMethod === method?.paymentMethodType }"
@click="selectPaymentMethod(method?.paymentMethodType)">
<view class="method-info">
<view class="method-icon">
<image :src="method?.logo?.logoUrl" mode="aspectFit" style="width: 48rpx; height: 48rpx;">
</image>
</view>
<text class="method-name">{{ method?.logo?.logoName }}</text>
</view>
<view class="method-radio" :class="{ checked: selectedPaymentMethod === method?.paymentMethodType }">
</view> </view>
</view> </view>
</view> </view>
<!-- 底部操作栏 --> <!-- 底部操作栏 -->
<view class="bottom-bar"> <view class="bottom-bar">
<view class="total-amount">
<text class="label-text">{{ $t('payment.total') }}</text>
<text class="amount">{{ totalAmount }}</text>
</view>
<view class="pay-btn" @click="handlePayment"> <view class="pay-btn" @click="handlePayment">
<text>{{ $t('payment.payNow') }}</text> <text class="currency-small">¥</text>
<text class="amount-large">{{ totalAmount }}</text>
<text class="pay-text">{{ $t('payment.payNow') }}</text>
</view> </view>
</view> </view>
</view> </view>
@@ -116,10 +103,23 @@
const passedTotalAmount = ref(null) const passedTotalAmount = ref(null)
const passedDepositAmount = ref(null) const passedDepositAmount = ref(null)
// 倒计时相关
const countdown = ref(15 * 60) // 15分钟 = 900秒
let countdownTimer = null
// 支付方式相关 // 支付方式相关
const paymentMethods = ref([]) const paymentMethods = ref([])
const selectedPaymentMethod = ref('ALIPAY') // 默认选择支付宝 const selectedPaymentMethod = ref('ALIPAY') // 默认选择支付宝
// 地点名称(可以从设备信息中获取,这里先用默认值)
const locationName = ref('澎创办公室')
// 套餐文本(可以从设备信息中获取,这里先用默认值)
const packageText = computed(() => {
// 这里可以根据实际的设备信息动态生成
return '2元/小时'
})
const orderStatus = reactive({ const orderStatus = reactive({
get text() { get text() {
return t('payment.waitingForPayment') return t('payment.waitingForPayment')
@@ -191,11 +191,17 @@
deviceNo: orderData.deviceNo, deviceNo: orderData.deviceNo,
createTime: formattedTime, createTime: formattedTime,
deposit: passedDepositAmount.value || orderData.depositAmount || '99.00', deposit: passedDepositAmount.value || orderData.depositAmount || '99.00',
orderStatus:orderData.orderStatus
} }
deviceNo.value = orderData.deviceNo; deviceNo.value = orderData.deviceNo;
await loadDeviceInfo(); await loadDeviceInfo();
await loadPaymentMethods(); await loadPaymentMethods();
// #ifdef H5
if(orderInfo.value.orderStatus=='waiting_for_payment'){
startPaymentStatusPolling();
}
// #endif
} else { } else {
throw new Error(t('order.getOrderFailed')) throw new Error(t('order.getOrderFailed'))
} }
@@ -322,12 +328,12 @@
uni.hideLoading(); uni.hideLoading();
// #ifdef H5 // #ifdef H5
uni.setStorageSync('pendingPaymentNo', orderId.value); uni.setStorageSync('pendingPaymentNo', orderId.value);
// #endif
// 跳转到支付页面 // 跳转到支付页面
uni.navigateTo({ // uni.navigateTo({
url: `/pages/webview/index?url=${encodeURIComponent(paymentUrl)}&title=支付` // url: `/pages/webview/index?url=${encodeURIComponent(paymentUrl)}&title=支付`
}); // });
window.open(paymentUrl);
// #endif
// 开始轮询支付状态 // 开始轮询支付状态
startPaymentStatusPolling(); startPaymentStatusPolling();
@@ -409,12 +415,56 @@
}, 5000); // 每5秒查询一次 }, 5000); // 每5秒查询一次
} }
// 更新导航栏倒计时
const updateNavBarCountdown = () => {
const minutes = Math.floor(countdown.value / 60).toString().padStart(2, '0')
const seconds = (countdown.value % 60).toString().padStart(2, '0')
uni.setNavigationBarTitle({
title: `待支付 ${minutes}:${seconds}`
})
}
// 开始倒计时
const startCountdown = () => {
// 清除之前的定时器
if (countdownTimer) {
clearInterval(countdownTimer)
}
// 立即更新一次
updateNavBarCountdown()
// 每秒更新
countdownTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(countdownTimer)
uni.showToast({
title: t('order.paymentFailedRetry'),
icon: 'none'
})
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
})
}, 1500)
return
}
updateNavBarCountdown()
}, 1000)
}
// 页面卸载时清除定时器 // 页面卸载时清除定时器
onMounted(() => { onMounted(() => {
return () => { return () => {
if (pollingTimer) { if (pollingTimer) {
clearInterval(pollingTimer); clearInterval(pollingTimer);
} }
if (countdownTimer) {
clearInterval(countdownTimer);
}
}; };
}); });
@@ -430,9 +480,8 @@
} }
onLoad((options) => { onLoad((options) => {
uni.setNavigationBarTitle({ // 启动倒计时
title: t('payment.orderPayment') startCountdown()
})
// 优先从 options 中获取 orderId // 优先从 options 中获取 orderId
if (options && options.orderId) { if (options && options.orderId) {
@@ -470,7 +519,7 @@
} }
} }
// #endif // #endif
loadOrderInfo()
// 调用 loadOrderInfo,内部会再次检查 orderId 是否存在 // 调用 loadOrderInfo,内部会再次检查 orderId 是否存在
}) })
@@ -479,78 +528,81 @@
<style lang="scss" scoped> <style lang="scss" scoped>
.payment-container { .payment-container {
min-height: 100vh; min-height: 100vh;
background: #f7f8fa; background: #F5F5F5;
padding: 30rpx; padding: 24rpx;
padding-bottom: 180rpx; padding-bottom: 200rpx;
box-sizing: border-box; box-sizing: border-box;
.status-card { .location-card {
background: #fff; background: #fff;
border-radius: 20rpx; border-radius: 24rpx;
padding: 40rpx 30rpx; padding: 32rpx;
margin-bottom: 20rpx; margin-bottom: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
.status-icon-wrapper { .location-header {
margin-bottom: 20rpx;
.status-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; margin-bottom: 24rpx;
.icon-text { .location-icon {
font-size: 60rpx; font-size: 40rpx;
width: 40rpx;
height: 40rpx;
margin-right: 12rpx;
} }
&.waiting { .location-name {
background: #FFF9C4;
}
&.success {
background: #E8F5E9;
}
&.failed {
background: #FFEBEE;
}
}
}
.status-text {
font-size: 36rpx; font-size: 36rpx;
font-weight: bold; font-weight: 600;
color: #333; color: #333;
margin-bottom: 10rpx; flex: 1;
} }
.status-desc { .status-badge {
background: #D4F4DD;
color: #52C41A;
font-size: 24rpx;
padding: 8rpx 20rpx;
border-radius: 8rpx;
}
}
.device-info {
font-size: 28rpx; font-size: 28rpx;
color: #666;
.device-label {
color: #999; color: #999;
} }
.device-value {
color: #333;
}
}
} }
.order-card, .order-card {
.price-card,
.payment-methods {
background: #fff; background: #fff;
border-radius: 20rpx; border-radius: 24rpx;
padding: 30rpx; padding: 32rpx;
margin-bottom: 20rpx; margin-bottom: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
.card-header { .card-header {
margin-bottom: 24rpx; display: flex;
align-items: center;
margin-bottom: 32rpx;
.card-title-bar {
width: 8rpx;
height: 32rpx;
background: #52C41A;
border-radius: 4rpx;
margin-right: 12rpx;
}
.card-title { .card-title {
font-size: 32rpx; font-size: 32rpx;
font-weight: bold; font-weight: 600;
color: #333; color: #333;
} }
} }
@@ -560,16 +612,11 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 16rpx 0; padding: 24rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.label { .label {
font-size: 28rpx; font-size: 28rpx;
color: #999; color: #666;
} }
.value { .value {
@@ -579,107 +626,33 @@
} }
} }
.price-divider {
height: 1rpx;
background: #f0f0f0;
margin: 10rpx 0;
}
.price-item.total { .price-item.total {
margin-top: 10rpx; padding-top: 32rpx;
padding-top: 20rpx; justify-content: flex-end !important;
border-top: none; // border-top: 1rpx solid #F0F0F0;
margin-top: 16rpx;
.label { .label {
font-size: 28rpx; font-size: 28rpx;
color: #999; color: #666;
margin-right: 10rpx;
} }
.value { .total-value {
font-size: 48rpx;
font-weight: bold;
color: #ff6b6b;
}
}
}
.payment-methods {
.method-item {
display: flex; display: flex;
justify-content: space-between; align-items: baseline;
align-items: center;
padding: 24rpx 20rpx;
margin-bottom: 16rpx;
background: #f7f8fa;
border-radius: 12rpx;
border: 2rpx solid transparent;
&:last-child { .currency {
margin-bottom: 0; font-size: 22rpx;
color: #52C41A;
margin-right: 4rpx;
} }
&.active { .amount {
background: #E8F5E9; font-size: 32rpx;
border-color: #07c160; font-weight: 700;
color: #52C41A;
} }
.method-info {
display: flex;
align-items: center;
.method-icon {
width: 48rpx;
height: 48rpx;
margin-right: 16rpx;
border-radius: 8rpx;
&.alipay {
background: #1677FF;
}
&.wechat {
background: #07C160;
}
&.default {
background: #999;
}
}
.method-name {
font-size: 30rpx;
color: #333;
font-weight: 500;
}
}
.method-radio {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #ddd;
border-radius: 50%;
position: relative;
&.checked {
border-color: #07c160;
background: #07c160;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 16rpx;
height: 16rpx;
background: #fff;
border-radius: 50%;
}
}
}
&:active {
opacity: 0.8;
} }
} }
} }
@@ -690,47 +663,44 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
background: #fff; background: #fff;
padding: 20rpx 30rpx; padding: 24rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
display: flex; box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
align-items: center; z-index: 100;
justify-content: space-between;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
z-index: 10;
gap: 20rpx;
.total-amount {
display: flex;
align-items: baseline;
.label-text {
font-size: 28rpx;
color: #666;
}
.amount {
font-size: 36rpx;
font-weight: bold;
color: #ff6b6b;
margin-left: 8rpx;
}
}
.pay-btn { .pay-btn {
height: 88rpx; width: 100%;
padding: 20rpx 0;
// height: 96rpx;
background: linear-gradient(135deg, #52C41A 0%, #73D13D 100%);
border-radius: 48rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: #07c160;
color: #fff; color: #fff;
font-size: 30rpx; box-shadow: 0 8rpx 24rpx rgba(82, 196, 26, 0.3);
.currency-small {
font-size: 32rpx;
font-weight: 600; font-weight: 600;
padding: 0 60rpx; margin-right: 4rpx;
border-radius: 44rpx; // text-align: ;
border: none; }
.amount-large {
font-size: 52rpx;
font-weight: 700;
margin-right: 16rpx;
}
.pay-text {
font-size: 32rpx;
font-weight: 600;
}
&:active { &:active {
opacity: 0.8; opacity: 0.9;
transform: scale(0.98);
} }
} }
} }
+69 -20
View File
@@ -1,14 +1,19 @@
<template> <template>
<view class="success-container"> <view class="success-container">
<!-- 支付成功状态和订单信息 -->
<view class="status-order-card">
<!-- 支付成功状态 --> <!-- 支付成功状态 -->
<view class="status-card"> <view class="status-section">
<view class="status-icon success"></view> <view class="status-icon success"></view>
<view class="status-text">{{ $t('success.paymentSuccess') }}</view> <view class="status-text">{{ $t('success.paymentSuccess') }}</view>
<view class="status-desc">{{ $t('success.paymentSuccessDesc') }}</view> <view class="status-desc">{{ $t('success.paymentSuccessDesc') }}</view>
</view> </view>
<!-- 分割线 -->
<view class="section-divider"></view>
<!-- 订单信息 --> <!-- 订单信息 -->
<view class="order-card"> <view class="order-section">
<view class="card-title">{{ $t('success.orderInfo') }}</view> <view class="card-title">{{ $t('success.orderInfo') }}</view>
<view class="info-item"> <view class="info-item">
<text class="label">{{ $t('order.orderNo') }}</text> <text class="label">{{ $t('order.orderNo') }}</text>
@@ -27,6 +32,7 @@
<text class="value">{{ orderInfo.payTime || '-' }}</text> <text class="value">{{ orderInfo.payTime || '-' }}</text>
</view> </view>
</view> </view>
</view>
<!-- 设备状态 --> <!-- 设备状态 -->
<view class="device-status"> <view class="device-status">
@@ -38,8 +44,8 @@
<!-- 操作按钮 --> <!-- 操作按钮 -->
<view class="button-group"> <view class="button-group">
<button class="primary-btn" @click="goToHome">{{ $t('success.backToHome') }}</button> <view class="secondary-btn" @click="goToHome">{{ $t('success.backToHome') }}</view>
<button class="secondary-btn" @click="goToOrderList">{{ $t('success.viewOrder') }}</button> <view class="primary-btn" @click="goToOrderList">{{ $t('success.viewOrder') }}</view>
</view> </view>
</view> </view>
</template> </template>
@@ -196,16 +202,21 @@
<style lang="scss" scoped> <style lang="scss" scoped>
.success-container { .success-container {
padding: 20px; padding: 20px;
padding-bottom: 180rpx;
background-color: #f5f5f5; background-color: #f5f5f5;
min-height: 100vh; min-height: 100vh;
box-sizing: border-box;
} }
.status-card { .status-order-card {
background-color: #fff; background-color: #fff;
border-radius: 12px; border-radius: 12px;
margin-bottom: 20px;
overflow: hidden;
.status-section {
padding: 30px; padding: 30px;
text-align: center; text-align: center;
margin-bottom: 20px;
.status-icon { .status-icon {
width: 60px; width: 60px;
@@ -244,17 +255,34 @@
} }
} }
.order-card { .section-divider {
background-color: #fff; height: 1px;
border-radius: 12px; background-color: #f0f0f0;
margin: 0 20px;
}
.order-section {
padding: 20px; padding: 20px;
margin-bottom: 20px;
.card-title { .card-title {
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
margin-bottom: 16px; margin-bottom: 16px;
color: #333; color: #333;
padding-left: 12px;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background-color: #07c160;
border-radius: 2px;
}
} }
.info-item { .info-item {
@@ -263,6 +291,10 @@
align-items: center; align-items: center;
margin-bottom: 12px; margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.label { .label {
color: #666; color: #666;
font-size: 14px; font-size: 14px;
@@ -271,6 +303,8 @@
.value { .value {
color: #333; color: #333;
font-size: 14px; font-size: 14px;
font-weight: 500;
}
} }
} }
} }
@@ -316,18 +350,30 @@
} }
.button-group { .button-group {
margin-top: 30px; position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
z-index: 10;
display: flex; display: flex;
// flex-direction: column; justify-content: flex-end;
gap: 16px; align-items: center;
gap: 20rpx;
.primary-btn { .primary-btn {
background-color: #07c160; background-color: #07c160;
color: #fff; color: #fff;
border: none; border: none;
border-radius: 24px; border-radius: 32rpx;
padding: 12px; padding: 0 32rpx;
font-size: 16px; font-size: 24rpx;
height: 64rpx;
line-height: 64rpx;
white-space: nowrap;
&:active { &:active {
opacity: 0.8; opacity: 0.8;
@@ -337,10 +383,13 @@
.secondary-btn { .secondary-btn {
background-color: #fff; background-color: #fff;
color: #07c160; color: #07c160;
border: 1px solid #07c160; border: 2rpx solid #07c160;
border-radius: 24px; border-radius: 32rpx;
padding: 12px; padding: 0 32rpx;
font-size: 16px; font-size: 24rpx;
height: 64rpx;
line-height: 64rpx;
white-space: nowrap;
&:active { &:active {
background-color: #f5f5f5; background-color: #f5f5f5;
+466 -170
View File
@@ -1,14 +1,14 @@
<template> <template>
<view class="scan-page"> <view class="scan-page">
<!-- 扫码区域 --> <!-- 扫码区域容器 -->
<view class="scan-window"> <view class="scan-window">
<video id="scanVideo" ref="videoRef" class="scan-video" autoplay playsinline muted></video> <!-- html5-qrcode 扫描器容器 -->
<canvas id="scanCanvas" ref="canvasRef" class="hidden-canvas" style="display: none;"></canvas> <view id="qr-reader" class="qr-reader"></view>
<!-- 扫描装饰 --> <!-- 扫描装饰 -->
<view class="scan-mask"> <view class="scan-mask">
<view class="scan-frame"> <view class="scan-frame">
<view class="scan-line"></view> <view class="scan-line" v-if="scanning"></view>
<view class="corner top-left"></view> <view class="corner top-left"></view>
<view class="corner top-right"></view> <view class="corner top-right"></view>
<view class="corner bottom-left"></view> <view class="corner bottom-left"></view>
@@ -21,18 +21,22 @@
<!-- 底部操作栏 --> <!-- 底部操作栏 -->
<view class="bottom-actions"> <view class="bottom-actions">
<view class="action-item" @click="chooseImage"> <view class="action-item" @click.stop="chooseImage">
<uv-icon name="photo" size="28" color="#fff"></uv-icon> <!-- <view class="action-icon">📷</view> -->
<uv-icon name="photo" size="24" color="#fff"></uv-icon>
<text>相册</text> <text>相册</text>
</view> </view>
<view class="action-item" @click="toggleInput"> <view class="action-item" @click.stop="toggleInput">
<uv-icon name="edit-pen" size="28" color="#fff"></uv-icon> <!-- <view class="action-icon"></view> -->
<uv-icon name="edit-pen" size="24" color="#fff"></uv-icon>
<text>手动输入</text> <text>手动输入</text>
</view> </view>
<view class="action-item" @click="goBack">
<uv-icon name="arrow-left" size="28" color="#fff"></uv-icon> <view class="action-item" @click.stop="goBack">
<!-- <view class="action-icon"></view> -->
<uv-icon name="arrow-left" size="24" color="#fff"></uv-icon>
<text>返回</text> <text>返回</text>
</view> </view>
</view> </view>
@@ -46,7 +50,6 @@
placeholder="请输入设备上的编号" placeholder="请输入设备上的编号"
class="device-input" class="device-input"
type="text" type="text"
focus
/> />
<view class="dialog-btns"> <view class="dialog-btns">
<button class="cancel-btn" @click="closeInput">取消</button> <button class="cancel-btn" @click="closeInput">取消</button>
@@ -59,234 +62,464 @@
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from 'vue'; import { ref, onMounted, onUnmounted } from 'vue';
import { getQueryString } from '@/util/index.js'; import { getQueryString } from '../../util/index.js';
import { Html5Qrcode } from 'html5-qrcode';
const videoRef = ref(null);
const canvasRef = ref(null);
const inputPopup = ref(null); const inputPopup = ref(null);
const manualDeviceNo = ref(''); const manualDeviceNo = ref('');
const tipText = ref('正在启动扫描...'); const tipText = ref('正在初始化...');
const scanning = ref(false); const scanning = ref(false);
const hasFlash = ref(false);
const flashOn = ref(false);
let stream = null; let html5QrCode = null;
let animationId = null; let isProcessing = false; // 防止重复处理
// 动态加载解码库 // 初始化扫码
const loadJsQR = () => { const initScan = async () => {
return new Promise((resolve, reject) => { try {
if (window.jsQR) { tipText.value = '正在初始化...';
resolve(window.jsQR); console.log('=== 开始初始化扫码 ===');
// 检查浏览器支持
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('您的浏览器不支持摄像头访问');
}
// 等待 DOM 渲染
await new Promise(resolve => setTimeout(resolve, 500));
// 检查容器元素
const readerElement = document.getElementById('qr-reader');
if (!readerElement) {
throw new Error('扫码容器元素未找到');
}
console.log('✓ 扫码容器元素已找到');
// 创建 Html5Qrcode 实例
html5QrCode = new Html5Qrcode('qr-reader', {
verbose: false // 关闭详细日志
});
console.log('✓ Html5Qrcode 实例创建成功');
// 启动扫描
await startScanning();
} catch (err) {
console.error('❌ 初始化失败:', err);
handleInitError(err);
}
};
// 开始扫描
const startScanning = async () => {
try {
if (!html5QrCode) {
throw new Error('Html5Qrcode 实例不存在');
}
tipText.value = '正在启动摄像头...';
console.log('=== 开始启动扫描 ===');
console.log('html5QrCode 实例:', html5QrCode);
console.log('html5QrCode.start 方法:', typeof html5QrCode.start);
// 配置扫描参数
const config = {
fps: 10, // 每秒10帧
qrbox: 250, // 扫描框大小(正方形)
disableFlip: false
};
console.log('扫描配置:', config);
console.log('准备调用 html5QrCode.start()...');
// 启动扫描 - 使用 { facingMode: "environment" } 优先后置摄像头
const startResult = await html5QrCode.start(
{ facingMode: "environment" },
config,
onScanSuccess,
onScanFailure
);
console.log('start() 调用结果:', startResult);
scanning.value = true;
tipText.value = '将二维码放入框内扫描';
console.log('✅ 扫描已成功启动');
// 延迟隐藏默认UI
setTimeout(() => {
hideDefaultUI();
}, 200);
} catch (err) {
console.error('❌ 启动扫描失败:', err);
console.error('错误详情:', {
name: err.name,
message: err.message,
stack: err.stack
});
// 尝试使用前置摄像头
console.log('尝试使用前置摄像头...');
try {
const config = {
fps: 10,
qrbox: 250,
disableFlip: false
};
await html5QrCode.start(
{ facingMode: "user" },
config,
onScanSuccess,
onScanFailure
);
scanning.value = true;
tipText.value = '将二维码放入框内扫描';
console.log('✅ 使用前置摄像头启动成功');
setTimeout(() => {
hideDefaultUI();
}, 200);
} catch (err2) {
console.error('❌ 前置摄像头也失败:', err2);
// 最后尝试:不指定摄像头
console.log('最后尝试:使用默认摄像头...');
try {
const config = {
fps: 10,
qrbox: 250,
disableFlip: false
};
// 获取摄像头列表
const cameras = await Html5Qrcode.getCameras();
console.log('可用摄像头:', cameras);
if (cameras && cameras.length > 0) {
const cameraId = cameras[0].id;
console.log('使用摄像头ID:', cameraId);
await html5QrCode.start(
cameraId,
config,
onScanSuccess,
onScanFailure
);
scanning.value = true;
tipText.value = '将二维码放入框内扫描';
console.log('✅ 使用默认摄像头启动成功');
setTimeout(() => {
hideDefaultUI();
}, 200);
} else {
throw new Error('未找到可用的摄像头');
}
} catch (err3) {
console.error('❌ 所有方式都失败:', err3);
throw err3;
}
}
}
};
// 隐藏默认UI
const hideDefaultUI = () => {
try {
const shaded = document.getElementById('qr-shaded-region');
if (shaded) {
shaded.style.border = 'none';
console.log('✓ 已隐藏默认边框');
}
} catch (err) {
console.warn('隐藏默认UI失败:', err);
}
};
// 扫描成功回调
const onScanSuccess = (decodedText, decodedResult) => {
// 防止重复处理
if (isProcessing) {
console.log('正在处理中,忽略重复扫码');
return; return;
} }
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js'; if (!scanning.value) {
script.onload = () => resolve(window.jsQR); console.log('扫描已停止,忽略结果');
script.onerror = (e) => { return;
console.error('jsQR 加载失败:', e); }
reject(new Error('解码组件加载失败,请检查网络'));
}; isProcessing = true;
document.head.appendChild(script);
console.log('========== 扫码成功 ==========');
console.log('解码文本:', decodedText);
console.log('解码结果:', decodedResult);
console.log('==============================');
// 验证结果
if (!decodedText || decodedText.trim() === '') {
console.error('扫码结果为空');
isProcessing = false;
return;
}
const finalResult = decodedText.trim();
// 停止扫描
stopScan().then(() => {
// 震动反馈
if (navigator.vibrate) {
navigator.vibrate(200);
}
// 通过全局事件通知首页
uni.$emit('h5ScanSuccess', {
result: finalResult,
scanType: 'QR_CODE',
path: finalResult
});
// 不要立即返回,等待首页处理完成
// 首页会在处理完成后自动关闭扫码页
console.log('扫码结果已发送,等待首页处理...');
}); });
}; };
const initScan = async () => { // 扫描失败回调
try { const onScanFailure = (error) => {
// 1. 检查环境 // 正常情况,不需要处理
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { };
throw new Error('您的浏览器不支持摄像头访问,请使用微信扫描或手动输入');
// 停止扫描
const stopScan = async () => {
if (!html5QrCode) {
return;
} }
// 2. 加载解码库 console.log('=== 停止扫描 ===');
const jsQR = await loadJsQR(); scanning.value = false;
flashOn.value = false;
// 3. 启动摄像头 - 尝试逐步降低约束
let constraints = {
video: {
facingMode: 'environment',
width: { ideal: 1280 },
height: { ideal: 720 }
}
};
try { try {
stream = await navigator.mediaDevices.getUserMedia(constraints); const isScanning = html5QrCode.isScanning;
} catch (e) { if (isScanning) {
console.warn('尝试理想约束失败,降级请求:', e); await html5QrCode.stop();
// 降级:仅请求视频,不限制分辨率和模式 console.log('✓ Html5Qrcode 已停止');
stream = await navigator.mediaDevices.getUserMedia({ video: true });
}
if (videoRef.value) {
const video = videoRef.value;
// 关键:在赋值 srcObject 之前先重置
video.pause();
video.srcObject = stream;
// 使用更稳健的事件监听
const startPlay = () => {
video.play().then(() => {
console.log('视频开始播放');
scanning.value = true;
tipText.value = '将二维码放入框内即可自动扫描';
tick(jsQR);
}).catch(e => {
console.error('视频播放 Promise 失败:', e);
// 如果是由于用户交互限制,提示用户点击
tipText.value = '请点击屏幕启动扫描';
});
};
if (video.readyState >= 2) {
startPlay();
} else {
video.onloadedmetadata = startPlay;
}
} }
} catch (err) { } catch (err) {
console.error('摄像头初始化失败:', err); console.warn('停止扫描失败:', err);
let errMsg = '摄像头开启失败'; }
};
// 处理初始化错误
const handleInitError = (err) => {
console.error('处理初始化错误:', err);
let errMsg = '初始化失败';
let errDetail = '';
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') { if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
errMsg = '请授予摄像头访问权限后重试'; errMsg = '摄像头权限被拒绝';
errDetail = '请在浏览器设置中允许访问摄像头';
} else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') { } else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
errMsg = '未找到可用的摄像头'; errMsg = '未找到可用的摄像头';
} else if (location.protocol !== 'https:' && location.hostname !== 'localhost') { errDetail = '请确保设备有摄像头';
errMsg = '非加密连接(HTTPS)无法开启摄像头'; } else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') {
errMsg = '摄像头被占用';
errDetail = '请关闭其他使用摄像头的应用';
} else if (err.name === 'NotSupportedError') {
errMsg = '浏览器不支持';
errDetail = '请使用现代浏览器访问';
} else if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
errMsg = '需要 HTTPS 环境';
errDetail = '摄像头功能需要在安全环境下使用';
} else {
errMsg = err.message || '摄像头启动失败';
errDetail = '请尝试刷新页面或使用其他方式';
} }
tipText.value = errMsg; tipText.value = errMsg;
// 显示错误提示,提供备选方案
uni.showModal({ uni.showModal({
title: '提示', title: errMsg,
content: errMsg, content: errDetail + '\n\n您可以:\n1. 从相册选择二维码图片\n2. 手动输入设备号',
showCancel: false, showCancel: true,
success: () => { cancelText: '返回',
// 如果失败且无法恢复,引导手动输入 confirmText: '手动输入',
success: (res) => {
if (res.confirm) {
toggleInput(); toggleInput();
} else if (res.cancel) {
goBack();
}
} }
}); });
}
}; };
const tick = (jsQR) => { // 从相册选择图片识别
if (!scanning.value) return; const chooseImage = async () => {
// #ifdef H5
const video = videoRef.value; const input = document.createElement('input');
const canvas = canvasRef.value; input.type = 'file';
input.accept = 'image/*';
if (video && video.readyState === video.HAVE_ENOUGH_DATA && canvas) { input.onchange = async (e) => {
const ctx = canvas.getContext('2d'); const file = e.target.files[0];
canvas.height = video.videoHeight; if (!file) {
canvas.width = video.videoWidth; return;
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'dontInvert',
});
if (code && code.data) {
console.log('扫码结果:', code.data);
handleSuccess(code.data);
return; // 停止循环
} }
}
animationId = requestAnimationFrame(() => tick(jsQR));
};
const handleSuccess = (result) => { uni.showLoading({ title: '正在识别...' });
stopScan();
try {
// 先停止摄像头扫描
const wasScanning = scanning.value;
if (wasScanning) {
await stopScan();
console.log('已停止摄像头扫描,准备识别图片');
// 等待停止完成
await new Promise(resolve => setTimeout(resolve, 300));
}
if (!html5QrCode) {
html5QrCode = new Html5Qrcode('qr-reader', { verbose: false });
}
// 使用 html5-qrcode 扫描文件
const result = await html5QrCode.scanFile(file, true);
console.log(result);
uni.hideLoading();
if (result) {
console.log('图片识别成功:', result);
// 震动反馈
if (navigator.vibrate) {
navigator.vibrate(200);
}
// 通过全局事件通知首页 // 通过全局事件通知首页
uni.$emit('h5ScanSuccess', { uni.$emit('h5ScanSuccess', {
result: result, result: result,
scanType: 'QR_CODE' scanType: 'QR_CODE',
path: result
}); });
uni.navigateBack(); // 不要立即返回,等待首页处理完成
}; console.log('图片识别结果已发送,等待首页处理...');
} else {
const stopScan = () => { uni.showToast({ title: '未识别到二维码', icon: 'none' });
scanning.value = false; // 识别失败,重新启动摄像头扫描
if (stream) { if (wasScanning) {
stream.getTracks().forEach(track => track.stop()); setTimeout(async () => {
stream = null; await startScanning();
}, 500);
} }
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
} }
}; } catch (err) {
console.error('图片识别失败:', err);
uni.hideLoading();
uni.showToast({ title: '识别失败', icon: 'none' });
// 识别失败,重新启动摄像头扫描
setTimeout(async () => {
try {
await startScanning();
} catch (e) {
console.error('重新启动扫描失败:', e);
}
}, 500);
}
};
input.click();
// #endif
const chooseImage = () => { // #ifndef H5
uni.chooseImage({ uni.chooseImage({
count: 1, count: 1,
sourceType: ['album'], sourceType: ['album'],
success: async (res) => { success: (res) => {
uni.showLoading({ title: '正在识别...' }); uni.showToast({ title: '该功能仅在H5环境可用', icon: 'none' });
try {
const jsQR = await loadJsQR();
const img = new Image();
img.src = res.tempFilePaths[0];
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height);
uni.hideLoading();
if (code && code.data) {
handleSuccess(code.data);
} else {
uni.showToast({ title: '未识别到二维码', icon: 'none' });
}
};
} catch (e) {
uni.hideLoading();
uni.showToast({ title: '识别出错', icon: 'none' });
}
} }
}); });
// #endif
}; };
// 打开手动输入弹窗
const toggleInput = () => { const toggleInput = () => {
if (inputPopup.value) {
inputPopup.value.open(); inputPopup.value.open();
}
}; };
// 关闭手动输入弹窗
const closeInput = () => { const closeInput = () => {
if (inputPopup.value) {
inputPopup.value.close(); inputPopup.value.close();
}
}; };
// 确认手动输入
const confirmManualInput = () => { const confirmManualInput = () => {
if (!manualDeviceNo.value) return; const deviceNo = manualDeviceNo.value.trim();
let deviceNo = manualDeviceNo.value.trim(); if (!deviceNo) {
if (deviceNo.includes('deviceNo=')) { uni.showToast({ title: '请输入设备号', icon: 'none' });
deviceNo = getQueryString(deviceNo, 'deviceNo') || deviceNo; return;
} }
closeInput(); closeInput();
stopScan(); stopScan();
// 处理可能包含 URL 的情况
let finalDeviceNo = deviceNo;
if (deviceNo.includes('deviceNo=')) {
finalDeviceNo = getQueryString(deviceNo, 'deviceNo') || deviceNo;
}
// 通过全局事件通知首页
uni.$emit('h5ScanSuccess', { uni.$emit('h5ScanSuccess', {
result: deviceNo, result: finalDeviceNo,
scanType: 'MANUAL' scanType: 'MANUAL'
}); });
uni.navigateBack(); // 不要立即返回,等待首页处理完成
console.log('手动输入结果已发送,等待首页处理...');
}; };
// 返回
const goBack = () => { const goBack = () => {
stopScan();
uni.navigateBack(); uni.navigateBack();
}; };
onMounted(() => { onMounted(() => {
console.log('扫码页面已挂载');
// 延迟初始化,确保 DOM 已渲染
setTimeout(() => {
console.log('开始初始化扫码');
initScan(); initScan();
}, 500);
}); });
onUnmounted(() => { onUnmounted(() => {
stopScan(); console.log('扫码页面卸载,清理资源');
isProcessing = false;
if (html5QrCode) {
stopScan().then(() => {
html5QrCode.clear().catch(err => {
console.warn('清理 Html5Qrcode 实例失败:', err);
});
html5QrCode = null;
});
}
}); });
</script> </script>
@@ -304,10 +537,31 @@ onUnmounted(() => {
height: 100%; height: 100%;
position: relative; position: relative;
.scan-video { .qr-reader {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover;
// 覆盖 html5-qrcode 默认样式
:deep(#qr-shaded-region) {
border: none !important; // 隐藏默认边框
background: transparent !important; // 透明背景
}
:deep(video) {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
display: block !important;
}
:deep(canvas) {
display: none !important;
}
// 隐藏 html5-qrcode 的所有内置 UI 元素
:deep(#qr-shaded-region > div) {
display: none !important;
}
} }
} }
@@ -317,17 +571,19 @@ onUnmounted(() => {
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: rgba(0, 0, 0, 0.4);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
pointer-events: none; pointer-events: none;
z-index: 10;
background: rgba(0, 0, 0, 0.5); // 添加半透明遮罩
.scan-frame { .scan-frame {
width: 500rpx; width: 500rpx;
height: 500rpx; height: 500rpx;
position: relative; position: relative;
box-shadow: 0 0 0 1000px rgba(0, 0, 0, 0.4); background: transparent; // 扫描区域透明
box-shadow: 0 0 0 2000px rgba(0, 0, 0, 0.5); // 使用 box-shadow 创建外部遮罩
.scan-line { .scan-line {
width: 100%; width: 100%;
@@ -336,6 +592,7 @@ onUnmounted(() => {
position: absolute; position: absolute;
top: 0; top: 0;
animation: scanMove 3s linear infinite; animation: scanMove 3s linear infinite;
box-shadow: 0 0 10rpx #3EAB64;
} }
.corner { .corner {
@@ -343,6 +600,7 @@ onUnmounted(() => {
width: 40rpx; width: 40rpx;
height: 40rpx; height: 40rpx;
border: 6rpx solid #3EAB64; border: 6rpx solid #3EAB64;
box-shadow: 0 0 10rpx rgba(62, 171, 100, 0.5);
} }
.top-left { top: -2rpx; left: -2rpx; border-right: none; border-bottom: none; } .top-left { top: -2rpx; left: -2rpx; border-right: none; border-bottom: none; }
@@ -353,8 +611,10 @@ onUnmounted(() => {
} }
@keyframes scanMove { @keyframes scanMove {
0% { top: 0; } 0% { top: 0; opacity: 0; }
100% { top: 100%; } 10% { opacity: 1; }
90% { opacity: 1; }
100% { top: 100%; opacity: 0; }
} }
.scan-tip { .scan-tip {
@@ -366,6 +626,10 @@ onUnmounted(() => {
color: #fff; color: #fff;
font-size: 28rpx; font-size: 28rpx;
padding: 0 40rpx; padding: 0 40rpx;
line-height: 1.6;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.8);
z-index: 11;
font-weight: 500;
} }
.bottom-actions { .bottom-actions {
@@ -376,16 +640,36 @@ onUnmounted(() => {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
padding: 0 60rpx; padding: 0 60rpx;
z-index: 20;
.action-item { .action-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 12rpx; gap: 8rpx;
margin-bottom: 20rpx;
padding: 12rpx 16rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 16rpx;
backdrop-filter: blur(10rpx);
transition: all 0.3s ease;
min-width: 100rpx;
border: 1rpx solid rgba(255, 255, 255, 0.1);
.action-icon {
font-size: 40rpx;
line-height: 1;
}
text { text {
color: #fff; color: #fff;
font-size: 24rpx; font-size: 22rpx;
white-space: nowrap;
}
&:active {
transform: scale(0.95);
background: rgba(62, 171, 100, 0.3);
} }
} }
} }
@@ -401,6 +685,7 @@ onUnmounted(() => {
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
margin-bottom: 40rpx; margin-bottom: 40rpx;
color: #333;
} }
.device-input { .device-input {
@@ -411,6 +696,12 @@ onUnmounted(() => {
font-size: 28rpx; font-size: 28rpx;
border: 1rpx solid #eee; border: 1rpx solid #eee;
margin-bottom: 40rpx; margin-bottom: 40rpx;
box-sizing: border-box;
&:focus {
border-color: #3EAB64;
background: #fff;
}
} }
.dialog-btns { .dialog-btns {
@@ -426,6 +717,11 @@ onUnmounted(() => {
font-size: 28rpx; font-size: 28rpx;
border-radius: 40rpx; border-radius: 40rpx;
border: none; border: none;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
} }
.cancel-btn { .cancel-btn {
+23 -7
View File
@@ -18,6 +18,10 @@
<text class="label">{{ $t('user.privacyPolicy') }}</text> <text class="label">{{ $t('user.privacyPolicy') }}</text>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon> <uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view> </view>
<view class="item" @click="navigateTo('/pages/legal/terms')">
<text class="label">{{ $t('legal.termsAndConditions') }}</text>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
</view> </view>
<view class="group"> <view class="group">
<view class="item" @click="handleLogout"> <view class="item" @click="handleLogout">
@@ -51,7 +55,13 @@ const currentLanguage = ref(uni.getStorageSync('language') || 'zh-CN')
// 当前语言文本显示 // 当前语言文本显示
const currentLanguageText = computed(() => { const currentLanguageText = computed(() => {
return currentLanguage.value === 'zh-CN' ? t('settings.chinese') : t('settings.english') if (currentLanguage.value === 'zh-CN') {
return t('settings.chinese')
} else if (currentLanguage.value === 'id-ID') {
return t('settings.indonesian')
} else {
return t('settings.english')
}
}) })
const navigateTo = (url) => { const navigateTo = (url) => {
@@ -60,21 +70,27 @@ const navigateTo = (url) => {
// 显示语言选择器 // 显示语言选择器
const showLanguageSelector = () => { const showLanguageSelector = () => {
const languages = [
{ code: 'zh-CN', label: t('settings.chinese') },
{ code: 'en-US', label: t('settings.english') },
{ code: 'id-ID', label: t('settings.indonesian') }
]
uni.showActionSheet({ uni.showActionSheet({
itemList: [t('settings.chinese'), t('settings.english')], itemList: languages.map(lang => lang.label),
success: (res) => { success: (res) => {
const lang = res.tapIndex === 0 ? 'zh-CN' : 'en-US' const selectedLang = languages[res.tapIndex].code
if (lang !== currentLanguage.value) { if (selectedLang !== currentLanguage.value) {
// 1. 保存到缓存 // 1. 保存到缓存
uni.setStorageSync('language', lang) uni.setStorageSync('language', selectedLang)
// 2. 立即更新 i18n 实例(重要!) // 2. 立即更新 i18n 实例(重要!)
if (globalI18n) { if (globalI18n) {
globalI18n.locale = lang globalI18n.locale = selectedLang
} }
// 3. 更新当前语言状态 // 3. 更新当前语言状态
currentLanguage.value = lang currentLanguage.value = selectedLang
// 4. 提示用户 // 4. 提示用户
uni.showToast({ uni.showToast({
+10 -7
View File
@@ -17,6 +17,9 @@ importers:
axios-miniprogram-adapter: axios-miniprogram-adapter:
specifier: 0.3.4 specifier: 0.3.4
version: 0.3.4 version: 0.3.4
html5-qrcode:
specifier: ^2.3.8
version: 2.3.8
uniapp-axios-adapter: uniapp-axios-adapter:
specifier: ^0.3.2 specifier: ^0.3.2
version: 0.3.2(axios@1.10.0) version: 0.3.2(axios@1.10.0)
@@ -83,9 +86,6 @@ packages:
'@jridgewell/source-map@0.3.6': '@jridgewell/source-map@0.3.6':
resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/sourcemap-codec@1.5.5': '@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
@@ -491,6 +491,9 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
html5-qrcode@2.3.8:
resolution: {integrity: sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==}
immutable@5.1.3: immutable@5.1.3:
resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==} resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==}
@@ -743,7 +746,7 @@ snapshots:
'@jridgewell/gen-mapping@0.3.8': '@jridgewell/gen-mapping@0.3.8':
dependencies: dependencies:
'@jridgewell/set-array': 1.2.1 '@jridgewell/set-array': 1.2.1
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
'@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/resolve-uri@3.1.2': {}
@@ -755,14 +758,12 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.8 '@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
'@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.25': '@jridgewell/trace-mapping@0.3.25':
dependencies: dependencies:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.5
'@parcel/watcher-android-arm64@2.5.1': '@parcel/watcher-android-arm64@2.5.1':
optional: true optional: true
@@ -1177,6 +1178,8 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
html5-qrcode@2.3.8: {}
immutable@5.1.3: {} immutable@5.1.3: {}
is-extglob@2.1.1: is-extglob@2.1.1: