28 Commits

Author SHA1 Message Date
pcwl_yancheng 836cdaf2dc 修改国际版本 2026-06-12 16:08:00 +08:00
pcwl_yancheng af758a0ccc 修改 2026-06-10 09:32:58 +08:00
pcwl_yancheng ec05584bb7 移除系统配置获取金额 2026-05-12 15:35:50 +08:00
pcwl_yancheng 2f0479ea05 修复bug 2026-05-04 17:49:00 +08:00
pcwl_yancheng e1c9068ab0 feat: 完善国际版支付与多语言展示
统一支付页与文案多语言配置,补充 ALIPAYDANA 语言项,并同步当前分支的相关配置与页面调整。

Made-with: Cursor
2026-04-29 14:30:20 +08:00
pcwl_yancheng 6913d9266b 国际版独立 2026-04-29 11:10:29 +08:00
pcwl_yancheng 120eba6c6a fix: payment page uses USD
Keep payment page currency display fixed to USD instead of changing with selected payment method.

Made-with: Cursor
2026-04-27 15:23:59 +08:00
pcwl_yancheng 555035388a fix: 修复登录跳转与支付展示异常
修复 H5 未登录扫码跳转登录页路径错误,补充手机号登录印尼/中国区号选择与校验,并修正支付方式单选及币种符号展示,避免支付页显示和选择异常。

Made-with: Cursor
2026-04-25 16:08:43 +08:00
pcwl_yancheng 2f618ef6ec 修复bug 2026-04-22 14:22:04 +08:00
pcwl_yancheng 09be26e2aa 新增暂停时长 2026-04-08 18:01:56 +08:00
pcwl_yancheng 9ca377907b 新增三码适配 2026-04-01 14:12:14 +08:00
pcwl_yancheng 337a92d8c1 支付宝上架 2026-03-26 09:47:22 +08:00
pcwl_yancheng 802aee59cb fix:兼容普通二维码跳转 2026-03-18 16:41:40 +08:00
pcwl_yancheng a79cf10bd4 fix:多平台兼容 2026-03-16 11:52:27 +08:00
pcwl_yancheng b3836b8bf2 支付宝兼容 2026-03-09 09:07:58 +08:00
pcwl_yancheng 069677957e fix:修复bug 2026-02-28 16:38:53 +08:00
pcwl_yancheng 3462a24d1e fix:修复bug 2026-02-26 09:16:35 +08:00
pcwl_yancheng 99872dd6df style:新增懒加载机制 2026-02-19 21:45:39 +08:00
pcwl_yancheng 7fd9c25ea8 fix:修复bug 2026-02-07 10:33:39 +08:00
pcwl_yancheng bb5a6dd100 fix:修复bug 2026-02-06 18:09:23 +08:00
pcwl_yancheng f476cee76d 新增h5qrcode依赖 2026-02-05 17:38:19 +08:00
pcwl_yancheng 5a13803743 fix:修复bug 2026-02-03 17:47:55 +08:00
pcwl_yancheng 9f66ee9658 fix:修复bug 2026-02-02 14:08:17 +08:00
pcwl_yancheng 6a1dff4b94 fix:修复bug 2026-01-22 10:52:58 +08:00
pcwl_yancheng b0daa7b59b add:新增会员、优惠券 2026-01-19 09:16:09 +08:00
pcwl_yancheng dbf7fa0c95 add:新增优化功能 2026-01-12 10:30:40 +08:00
pcwl_yancheng be01fb211e fix:修复bug;新增订单列表页面订单监控 2025-12-30 17:26:16 +08:00
pcwl_yancheng 9d4e229312 fix:修正优化首页、个人中心页面广告图url跳转 2025-12-15 17:37:52 +08:00
115 changed files with 27022 additions and 5851 deletions
+13
View File
@@ -0,0 +1,13 @@
{
"version" : "1.0",
"configurations" : [
{
"playground" : "standard",
"type" : "uni-app:app-android"
},
{
"playground" : "standard",
"type" : "uni-app:app-ios"
}
]
}
+130
View File
@@ -0,0 +1,130 @@
# AGENTS
<skills_system priority="1">
## Available Skills
<!-- SKILLS_TABLE_START -->
<usage>
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
How to use skills:
- Invoke: `npx openskills read <skill-name>` (run in your shell)
- For multiple: `npx openskills read skill-one,skill-two`
- The skill content will load with detailed instructions on how to complete the task
- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/)
Usage notes:
- Only use skills listed in <available_skills> below
- Do not invoke a skill that is already loaded in your context
- Each skill invocation is stateless
</usage>
<available_skills>
<skill>
<name>algorithmic-art</name>
<description>Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.</description>
<location>global</location>
</skill>
<skill>
<name>brand-guidelines</name>
<description>Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.</description>
<location>global</location>
</skill>
<skill>
<name>canvas-design</name>
<description>Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.</description>
<location>global</location>
</skill>
<skill>
<name>doc-coauthoring</name>
<description>Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision docs, or similar structured content. This workflow helps users efficiently transfer context, refine content through iteration, and verify the doc works for readers. Trigger when user mentions writing docs, creating proposals, drafting specs, or similar documentation tasks.</description>
<location>global</location>
</skill>
<skill>
<name>docx</name>
<description>"Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). Triggers include: any mention of \"Word doc\", \"word document\", \".docx\", or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a \"report\", \"memo\", \"letter\", \"template\", or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation."</description>
<location>global</location>
</skill>
<skill>
<name>frontend-design</name>
<description>Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.</description>
<location>global</location>
</skill>
<skill>
<name>internal-comms</name>
<description>A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.).</description>
<location>global</location>
</skill>
<skill>
<name>mcp-builder</name>
<description>Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).</description>
<location>global</location>
</skill>
<skill>
<name>pdf</name>
<description>Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill.</description>
<location>global</location>
</skill>
<skill>
<name>pptx</name>
<description>"Use this skill any time a .pptx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx filename, regardless of what they plan to do with the content afterward. If a .pptx file needs to be opened, created, or touched, use this skill."</description>
<location>global</location>
</skill>
<skill>
<name>skill-creator</name>
<description>Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.</description>
<location>global</location>
</skill>
<skill>
<name>slack-gif-creator</name>
<description>Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack."</description>
<location>global</location>
</skill>
<skill>
<name>template</name>
<description>Replace with description of the skill and when Claude should use it.</description>
<location>global</location>
</skill>
<skill>
<name>theme-factory</name>
<description>Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.</description>
<location>global</location>
</skill>
<skill>
<name>web-artifacts-builder</name>
<description>Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.</description>
<location>global</location>
</skill>
<skill>
<name>webapp-testing</name>
<description>Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.</description>
<location>global</location>
</skill>
<skill>
<name>xlsx</name>
<description>"Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like \"the xlsx in my downloads\") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved."</description>
<location>global</location>
</skill>
</available_skills>
<!-- SKILLS_TABLE_END -->
</skills_system>
+39 -5
View File
@@ -11,9 +11,45 @@
// 注意:语言初始化已移至 main.js,确保每次 reLaunch 都能正确加载新语言
},
onShow: async function() {
console.log('========================================')
console.log('=== App onShow 被调用 ===')
console.log('时间戳:', new Date().toLocaleTimeString())
// 检查启动路径,如果设置了启动路径则跳转
try {
const launchPath = uni.getStorageSync('launchPath')
if (launchPath) {
console.log('检测到启动路径:', launchPath)
// 获取当前页面栈
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentRoute = currentPage ? ('/' + currentPage.route) : ''
// 规范化路径格式进行比较
const normalizedLaunchPath = launchPath.replace(/\.html$/, '')
// 只要当前不在目标页面,就进行一次跳转
// 下单流程不会受影响,因为在下单离开设备详情页时不会写入 launchPath
if (currentRoute !== normalizedLaunchPath) {
console.log('当前页面:', currentRoute, '目标页面:', normalizedLaunchPath)
// 清除启动路径标记(在跳转前清除,避免重复触发)
uni.removeStorageSync('launchPath')
// 使用 reLaunch 跳转到启动路径(首页)
uni.reLaunch({
url: normalizedLaunchPath,
success: () => {
console.log('成功跳转到启动路径(reLaunch')
},
fail: (reLaunchErr) => {
console.error('跳转到启动路径失败:', reLaunchErr)
}
})
} else {
// 如果已经在目标页面,清除启动路径标记
console.log('当前已在目标页面,清除启动路径标记')
uni.removeStorageSync('launchPath')
}
}
} catch (e) {
console.error('App onShow - 启动路径检查失败:', e)
}
// 检查并更新语言(uni.reLaunch 会触发 onShow
try {
@@ -39,8 +75,6 @@
} catch (e) {
console.error('App onShow - 语言检查失败:', e)
}
console.log('========================================')
},
onHide: function() {
console.log('App Hide')
+187
View File
@@ -0,0 +1,187 @@
<template>
<view class="skeleton-container">
<!-- 设备信息卡片骨架 -->
<view class="card skeleton-card">
<view class="device-location-skeleton">
<view class="location-left-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="32rpx" rowsHeight="32rpx"
borderRadius="50%"></uv-skeleton>
<view style="margin-left: 12rpx;">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="200rpx" rowsHeight="28rpx">
</uv-skeleton>
</view>
</view>
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="100rpx" rowsHeight="40rpx"
borderRadius="30rpx"></uv-skeleton>
</view>
<view class="device-info-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="300rpx" rowsHeight="26rpx">
</uv-skeleton>
</view>
<view class="device-info-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="350rpx" rowsHeight="26rpx">
</uv-skeleton>
</view>
</view>
<!-- 计费规则卡片骨架 -->
<view class="card skeleton-card">
<view class="card-header-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="150rpx" rowsHeight="32rpx">
</uv-skeleton>
</view>
<view class="pricing-banner-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="200rpx" rowsHeight="64rpx">
</uv-skeleton>
<view style="margin-top: 16rpx;">
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="120rpx" rowsHeight="40rpx"
borderRadius="30rpx"></uv-skeleton>
</view>
</view>
<view class="pricing-info-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="100%" rowsHeight="26rpx">
</uv-skeleton>
</view>
<view class="pricing-info-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="100%" rowsHeight="26rpx">
</uv-skeleton>
</view>
</view>
<!-- 使用说明卡片骨架 -->
<view class="card skeleton-card">
<view class="card-header-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="150rpx" rowsHeight="32rpx">
</uv-skeleton>
</view>
<view class="notice-item-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="12rpx" rowsHeight="12rpx"
borderRadius="50%"></uv-skeleton>
<view style="flex: 1; margin-left: 16rpx;">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="100%" rowsHeight="28rpx">
</uv-skeleton>
</view>
</view>
<view class="notice-item-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="12rpx" rowsHeight="12rpx"
borderRadius="50%"></uv-skeleton>
<view style="flex: 1; margin-left: 16rpx;">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="100%" rowsHeight="28rpx">
</uv-skeleton>
</view>
</view>
<view class="notice-item-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="12rpx" rowsHeight="12rpx"
borderRadius="50%"></uv-skeleton>
<view style="flex: 1; margin-left: 16rpx;">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="100%" rowsHeight="28rpx">
</uv-skeleton>
</view>
</view>
</view>
<!-- 促销提示框骨架 -->
<view class="promotion-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="100%" rowsHeight="60rpx"
borderRadius="22rpx"></uv-skeleton>
</view>
<!-- 底部按钮骨架 -->
<view class="footer-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="100%" rowsHeight="96rpx"
borderRadius="48rpx"></uv-skeleton>
<view style="margin-top: 16rpx;">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="200rpx" rowsHeight="24rpx">
</uv-skeleton>
</view>
</view>
</view>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.skeleton-container {
min-height: 100vh;
background-color: #f5f7fa;
padding: 30rpx 30rpx 300rpx;
box-sizing: border-box;
}
.card {
background-color: #fff;
border-radius: 24rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
padding: 30rpx;
margin-bottom: 30rpx;
}
.skeleton-card {
.device-location-skeleton {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
.location-left-skeleton {
display: flex;
align-items: center;
}
}
.device-info-skeleton {
margin-bottom: 12rpx;
}
.card-header-skeleton {
margin-bottom: 24rpx;
}
.pricing-banner-skeleton {
background: #E6F7EC;
border-radius: 20rpx;
padding: 40rpx 30rpx;
margin-bottom: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.pricing-info-skeleton {
margin-bottom: 16rpx;
}
.notice-item-skeleton {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
}
}
.promotion-skeleton {
margin-bottom: 30rpx;
}
.footer-skeleton {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 24rpx 30rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
}
</style>
+263
View File
@@ -0,0 +1,263 @@
<template>
<view class="device-order-card" @click="onCardClick">
<!-- 头部标题和状态 -->
<view class="card-header">
<view class="header-left">
<view class="tag-bar"></view>
<text class="title">定制化订单</text>
</view>
<view class="status-badge">{{ statusText }}</view>
</view>
<!-- 产品信息区域 -->
<view class="product-section">
<image
:src="order.pictureUrl || order.productImage || '/static/default-product.png'"
mode="aspectFill"
class="product-image"
lazy-load="true"
></image>
<view class="product-info">
<view class="product-name">{{ order.productName || order.deviceName || $t('goods.defaultProductNameShort') }}</view>
<view style="display: flex;justify-content: space-between;">
<view class="product-style">款式{{ order.optionName || order.style || order.deviceStyle || '标准' }}</view>
<view class="product-price">¥ {{ totalAmount }}</view>
</view>
</view>
</view>
<!-- 分割线 -->
<view class="divider-wrapper">
<uv-divider :hairline="false" lineColor="#f5f5f5"></uv-divider>
</view>
<!-- 底部时间和删除按钮 -->
<view class="card-footer">
<text class="order-time">{{ order.startTime || order.createTime }}</text>
<view class="footer-actions">
<!-- 待付款状态显示立即支付按钮 -->
<view v-if="isWaitingPayment" class="pay-btn" @click.stop="onPay">
<text>立即支付</text>
</view>
<view class="delete-btn" @click.stop="onDelete">
<uv-icon name="trash" size="20" color="#999"></uv-icon>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed } from 'vue';
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
const props = defineProps({
order: {
type: Object,
required: true
}
});
const emit = defineEmits(['click', 'delete', 'pay']);
// 订单状态文本
const statusText = computed(() => {
const status = props.order.orderStatus || props.order.status;
if (status === 0 || status === '0') {
return '待付款';
}
if (status === 1 || status === '1') {
return '待发货';
}
if (status === 2 || status === '2') {
return '待收货';
}
if (status === 3 || status === '3') {
return '已完成';
}
if (status === 4 || status === '4') {
return '已取消';
}
if (status === 5 || status === '5') {
return '退款中';
}
return '待付款';
});
// 判断是否为待付款状态
const isWaitingPayment = computed(() => {
const status = props.order.orderStatus || props.order.status;
return status === 0 || status === '0';
});
// 总金额
const totalAmount = computed(() => {
return props.order.totalAmount || props.order.amount || props.order.payAmount || '99.0';
});
// 卡片点击事件
const onCardClick = () => {
emit('click', props.order);
};
// 删除订单
const onDelete = () => {
emit('delete', props.order);
};
// 立即支付
const onPay = () => {
emit('pay', props.order);
};
</script>
<style lang="scss" scoped>
.device-order-card {
background: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
// 头部区域
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 24rpx 0 24rpx;
// border-bottom: 1rpx solid #f5f5f5;
.header-left {
display: flex;
align-items: center;
.tag-bar {
width: 6rpx;
height: 32rpx;
background: #07c160;
border-radius: 12rpx;
margin-right: 12rpx;
}
.title {
font-size: 30rpx;
color: #3EAB64;
font-weight: 600;
}
}
.status-badge {
padding: 8rpx 20rpx;
background: rgba(7, 193, 96, 0.1);
color: #07c160;
font-size: 24rpx;
border-radius: 24rpx;
font-weight: 500;
}
}
// 产品信息区域
.product-section {
display: flex;
padding: 24rpx 24rpx 0 24rpx;
.product-image {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
background: #f5f5f5;
flex-shrink: 0;
}
.product-info {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
.product-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
line-height: 1.4;
margin-bottom: 8rpx;
}
.product-style {
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
}
.product-price {
font-size: 36rpx;
color: #07c160;
font-weight: 600;
}
}
}
// 分割线区域
.divider-wrapper {
padding: 0 24rpx;
}
// 底部区域
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0rpx 24rpx 24rpx;
background: #fff;
.order-time {
font-size: 24rpx;
color: #999;
}
.footer-actions {
display: flex;
align-items: center;
gap: 16rpx;
.pay-btn {
padding: 12rpx 32rpx;
background: linear-gradient(135deg, #07c160, #10d673);
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 24rpx;
color: #fff;
font-weight: 500;
}
&:active {
opacity: 0.8;
}
}
.delete-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
&:active {
opacity: 0.7;
}
}
}
}
}
</style>
+2 -2
View File
@@ -28,7 +28,7 @@
</view>
<view class="empty-state" v-if="!isLoading && (!positions || positions.length === 0)">
<image class="empty-icon" src="/static/scan-icon.png" mode="aspectFit" />
<image class="empty-icon" src="/static/scan-icon.png" mode="aspectFit" lazy-load="true" />
<text class="empty-text">{{ $t('home.noNearbyDevice') }}</text>
</view>
</view>
@@ -40,7 +40,7 @@
import { computed } from 'vue'
import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n()
const { t } = useI18n()
const props = defineProps({
show: { type: Boolean, default: false },
+103 -38
View File
@@ -1,41 +1,91 @@
<template>
<view class="map-container" :class="{ 'full-width': props.fullWidth }" :style="{ '--map-height': props.customHeight || '78vh' }">
<view class="map-container" :class="{ 'full-width': props.fullWidth }">
<!-- 地图容器 -->
<view class="map-wrapper">
<!-- 使用小程序原生地图组件 -->
<map id="map" class="native-map" :longitude="mapCenter.longitude" :latitude="mapCenter.latitude"
:markers="mapMarkers" :scale="mapZoom" :show-location="false" @regionchange="onMapRegionChange"
@markertap="onMapMarkerTap" @callouttap="onCalloutTap" @updated="onMapUpdated" @error="onMapError">
<map
id="map"
class="native-map"
:longitude="mapCenter.longitude"
:latitude="mapCenter.latitude"
:markers="mapMarkers"
:scale="mapZoom"
:show-location="false"
@regionchange="onMapRegionChange"
@markertap="onMapMarkerTap"
@callouttap="onCalloutTap"
@updated="onMapUpdated"
@error="onMapError"
>
<!-- 覆盖在地图上的广告轮播使用 cover-view 以兼容小程序原生组件层级 -->
<cover-view class="index-swiper" v-if="!props.hideControls && !props.hideMapOverlays && currentBannerImage">
<cover-image :src="currentBannerImage" class="index-swiper-img" mode="aspectFill" @tap="handleBannerTap"></cover-image>
<cover-view
class="index-swiper"
v-if="!props.hideControls && !props.hideMapOverlays && currentBannerImage"
>
<cover-image
:src="currentBannerImage"
class="index-swiper-img"
mode="aspectFill"
@tap="handleBannerTap"
></cover-image>
<!-- 轮播指示器 -->
<cover-view class="banner-indicators" v-if="props.bannerImages.length > 1">
<cover-view
class="banner-indicators"
v-if="props.bannerImages.length > 1"
>
<cover-view
v-for="(img, idx) in props.bannerImages"
:key="idx"
class="indicator-dot"
:class="{ active: idx === currentBannerIndex }">
:class="{ active: idx === currentBannerIndex }"
>
</cover-view>
</cover-view>
</cover-view>
<!-- 地图中心固定定位图标 -->
<cover-view class="center-location-marker" v-if="!props.hideMapOverlays">
<cover-image src="/static/location-icon.png" class="center-marker-icon"></cover-image>
<cover-view
class="center-location-marker"
v-if="!props.hideMapOverlays"
>
<cover-image
src="/static/location-icon.png"
class="center-marker-icon"
></cover-image>
</cover-view>
<cover-view class="map-side-controls" v-if="!props.hideControls && !props.hideMapOverlays">
<cover-view class="side-btn locate" @tap="handleRelocate">
<cover-image class="side-icon" src="/static/location.png"></cover-image>
<!-- 侧边控制按钮 -->
<cover-view
class="map-side-controls"
v-if="!props.hideControls && !props.hideMapOverlays"
>
<cover-view class="side-btn guide" @tap="handleGuide">
<cover-image
class="side-icon"
src="/static/use_help.png"
style="border-radius: 50%;"
></cover-image>
</cover-view>
<cover-view class="side-btn service" @tap="handleService">
<cover-image class="side-icon" src="/static/customer-service.png"></cover-image>
<cover-view class="side-btn locate" @tap="handleRelocate">
<cover-image
class="side-icon"
src="/static/location.png"
></cover-image>
</cover-view>
<cover-view class="side-btn search" @tap="handleSearch">
<cover-image class="side-icon" src="/static/other_device.png"></cover-image>
<cover-image
class="side-icon"
src="/static/other_device.png"
></cover-image>
</cover-view>
<cover-view class="side-btn service" @tap="handleService">
<cover-image
class="side-icon"
src="/static/customer-service.png"
></cover-image>
</cover-view>
</cover-view>
</map>
@@ -68,7 +118,7 @@
import { useI18n } from '../utils/i18n.js'
// 获取 i18n 实例
const { t: $t } = useI18n()
const { t } = useI18n()
// 引用折叠面板组件的ref
const collapseRef = ref(null)
@@ -128,7 +178,8 @@
'showList',
'markerTap',
'mapCenterChange',
'bannerClick'
'bannerClick',
'guide'
])
// 响应式数据
@@ -247,14 +298,12 @@
// 监听广告图片变化,启动或停止轮播
watch(() => props.bannerImages, (newImages, oldImages) => {
console.log('广告图片变化:', newImages?.length, '张')
// 先停止旧的轮播
stopBannerRotation()
currentBannerIndex.value = 0
// 如果有多张图片,启动新的轮播
if (newImages && newImages.length > 1) {
console.log('启动广告轮播,共', newImages.length, '张图片')
// 使用 nextTick 确保 DOM 已更新
nextTick(() => {
startBannerRotation()
@@ -274,13 +323,19 @@
const onMapRegionChange = (e) => {
// 只处理结束事件
if (!e || e.type !== 'end') {
if (!e || (e.type !== 'end' && e.type !== 'regionchange')) {
return
}
const causedBy = e.causedBy || e.detail?.causedBy
if (causedBy === 'gesture' || causedBy === 'scale' || causedBy === 'drag'||causedBy==='update') {
// H5 环境下可能没有 causedBy,只要是 end 事件就处理
const isH5 = false;
// #ifdef H5
const h5Status = true;
// #endif
if (causedBy === 'gesture' || causedBy === 'scale' || causedBy === 'drag' || causedBy === 'update' || (typeof h5Status !== 'undefined' && e.type === 'end')) {
// 清除之前的定时器
if (regionChangeTimer) {
clearTimeout(regionChangeTimer)
@@ -372,41 +427,39 @@ const handleSearch = () => {
const handleService = () => {
uni.navigateTo({
url: '/pages/help/index'
url: '/subPackages/service/help/index'
})
}
const handleGuide = () => {
emit('guide')
}
const handleJoinTap = () => {
uni.navigateTo({
url: '/pages/join/index'
url: '/subPackages/business/join/index'
})
}
// 处理广告点击
const handleBannerTap = () => {
console.log('点击地图广告:', currentBannerIndex.value, currentBannerImage.value)
// 触发父组件处理点击事件
emit('bannerClick', currentBannerIndex.value)
// 默认跳转到合作加盟页面
handleJoinTap()
}
// 启动广告轮播
const startBannerRotation = () => {
// 如果只有一张或没有图片,不需要轮播
if (!props.bannerImages || props.bannerImages.length <= 1) {
console.log('图片数量不足,不启动轮播')
return
}
// 清除旧的定时器
stopBannerRotation()
console.log('开始广告轮播定时器')
// 每3秒切换一次
bannerTimer = setInterval(() => {
const nextIndex = (currentBannerIndex.value + 1) % props.bannerImages.length
console.log('轮播切换:', currentBannerIndex.value, '->', nextIndex)
currentBannerIndex.value = nextIndex
}, 3000)
}
@@ -414,7 +467,6 @@ const handleSearch = () => {
// 停止广告轮播
const stopBannerRotation = () => {
if (bannerTimer) {
console.log('停止广告轮播')
clearInterval(bannerTimer)
bannerTimer = null
}
@@ -450,7 +502,6 @@ const handleSearch = () => {
// 初始化广告轮播
if (props.bannerImages && props.bannerImages.length > 1) {
console.log('onMounted: 初始化广告轮播')
startBannerRotation()
}
})
@@ -491,22 +542,35 @@ const handleSearch = () => {
/* 地图容器 */
.map-container {
flex: 1;
position: relative;
// position: fixed;
// top: 0;
left: 0;
right: 0;
bottom: 0;
width: 94vw;
height: calc(100% - 20rpx); /* 减少高度,避免覆盖底部按钮 */
// height: var(--map-height, calc(100% - 20rpx)); /* 使用变量或默认高度 */
margin: 20rpx;
margin-bottom: 0; /* 底部不需要边距 */
border-radius: 20rpx;
overflow: hidden;
display: flex;
flex-direction: column;
// #ifdef H5
height: 78vh;
// #endif
// #ifdef MP-WEIXIN
height: 72vh;
// #endif
&.full-width {
width: 100%;
margin: 0;
border-radius: 0;
height: 100%;
}
.map-wrapper {
@@ -596,13 +660,13 @@ const handleSearch = () => {
margin: auto;
// height: 72rpx;
background: rgba(255, 255, 255, 0.96);
border-radius: 36rpx;
border-radius: 24rpx;
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
// box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.12);
padding: 20rpx;
padding: 13rpx;
border: 2rpx solid #e0e0e0;
&:active {
@@ -634,8 +698,8 @@ const handleSearch = () => {
}
.side-icon {
width: 44rpx;
height: 44rpx;
width: 40rpx;
height: 40rpx;
}
.index-swiper {
@@ -667,6 +731,7 @@ const handleSearch = () => {
justify-content: center;
gap: 8rpx;
z-index: 2;
pointer-events: none;
.indicator-dot {
width: 12rpx;
+687
View File
@@ -0,0 +1,687 @@
<template>
<view class="map-container" :class="{ 'full-width': props.fullWidth }">
<!-- 地图容器 -->
<view class="map-wrapper">
<!-- 使用小程序原生地图组件 -->
<map
id="map"
class="native-map"
:longitude="mapCenter.longitude"
:latitude="mapCenter.latitude"
:markers="mapMarkers"
:scale="mapZoom"
:show-location="false"
@regionchange="onMapRegionChange"
@markertap="onMapMarkerTap"
@callouttap="onCalloutTap"
@updated="onMapUpdated"
@error="onMapError"
></map>
<!-- 支付宝小程序所有 cover-image cover-view 必须放在 map 标签外部 -->
<!-- 广告轮播直接使用 cover-image不能嵌套在 cover-view -->
<cover-image
v-if="!props.hideControls && !props.hideMapOverlays && currentBannerImage"
:src="currentBannerImage"
class="index-swiper-img-alipay"
mode="aspectFill"
@tap="handleBannerTap"
></cover-image>
<!-- 轮播指示器每个点都是独立的 cover-view避免嵌套 -->
<template v-if="!props.hideControls && !props.hideMapOverlays && props.bannerImages.length > 1">
<cover-view
v-for="(img, idx) in props.bannerImages"
:key="idx"
class="indicator-dot-alipay"
:class="{ active: idx === currentBannerIndex }"
:style="{
left: `calc(50% + ${(idx - (props.bannerImages.length - 1) / 2) * 20}rpx)`,
transform: 'translateX(-50%)'
}"
>
</cover-view>
</template>
<!-- 地图中心固定定位图标使用 cover-view 嵌套 cover-image -->
<cover-view
class="center-location-marker-alipay"
v-if="!props.hideMapOverlays"
>
<cover-image
src="/static/location-icon.png"
class="center-marker-icon-alipay"
></cover-image>
</cover-view>
<!-- 侧边控制按钮使用 cover-view 嵌套 cover-image -->
<cover-view
class="map-side-controls-alipay"
v-if="!props.hideControls && !props.hideMapOverlays"
>
<cover-view class="side-btn-alipay guide-alipay" @tap="handleGuide">
<cover-image
class="side-icon-alipay"
src="/static/use_help.png"
style="border-radius: 50%;"
></cover-image>
</cover-view>
<cover-view class="side-btn-alipay locate-alipay" @tap="handleRelocate">
<cover-image
class="side-icon-alipay"
src="/static/location.png"
></cover-image>
</cover-view>
<cover-view class="side-btn-alipay search-alipay" @tap="handleSearch">
<cover-image
class="side-icon-alipay"
src="/static/other_device.png"
></cover-image>
</cover-view>
<cover-view class="side-btn-alipay service-alipay" @tap="handleService">
<cover-image
class="side-icon-alipay"
src="/static/customer-service.png"
></cover-image>
</cover-view>
</cover-view>
<!-- 地图加载状态 -->
<view class="map-loading" v-if="isLoading">
<view class="loading-content">
<view class="loading-spinner"></view>
<text>{{ $t('common.loadingMap') }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
getCurrentInstance
} from 'vue'
// 导入地图工具函数
import {
calculateDistanceSync
} from '../utils/mapUtils.js'
// 导入国际化
import { useI18n } from '../utils/i18n.js'
// 获取 i18n 实例
const { t } = useI18n()
// 引用折叠面板组件的ref
const collapseRef = ref(null)
// Props
const props = defineProps({
userLocation: {
type: Object,
default: null
},
positionList: {
type: Array,
default: () => []
},
filteredPositions: {
type: Array,
default: () => []
},
searchKeyword: {
type: String,
default: ''
},
noticeText: {
type: String,
default: ''
},
enableMarkers: {
type: Boolean,
default: false
},
customHeight: {
type: String,
default: '' // 自定义高度,如 '48vh', '400rpx' 等
},
hideControls: {
type: Boolean,
default: false // 是否隐藏侧边控制按钮
},
fullWidth: {
type: Boolean,
default: false // 是否全宽显示(去掉 margin 和固定宽度)
},
hideMapOverlays: {
type: Boolean,
default: false // 是否隐藏地图上的覆盖层元素(如中心定位图标、轮播图等)
},
bannerImages: {
type: Array,
default: () => [] // 广告图片列表
}
})
// Emits
const emit = defineEmits([
'relocate',
'scan',
'showList',
'markerTap',
'mapCenterChange',
'bannerClick',
'guide'
])
// 响应式数据
const isLoading = ref(true)
const mapCenter = ref({
longitude: 116.397128,
latitude: 39.916527
})
const mapZoom = ref(17)
const mapMarkers = ref([]) // 用于地图组件的markers
const mapContext = ref(null) // 地图上下文
const currentBannerIndex = ref(0) // 当前显示的广告索引
let bannerTimer = null // 广告轮播定时器
// 计算当前显示的广告图片
const currentBannerImage = computed(() => {
if (props.bannerImages && props.bannerImages.length > 0) {
return props.bannerImages[currentBannerIndex.value]
}
// 降级:如果没有广告,显示默认图片
return ''
})
// 验证坐标有效性
const isValidCoordinate = (lat, lng) => {
const latitude = parseFloat(lat)
const longitude = parseFloat(lng)
return !isNaN(latitude) && !isNaN(longitude) &&
latitude >= -90 && latitude <= 90 &&
longitude >= -180 && longitude <= 180 &&
!(latitude === 0 && longitude === 0) // 排除 0,0 这种无效坐标
}
// 防抖定时器
let regionChangeTimer = null
// 方法
const updateMapMarkers = () => {
const markers = []
// 只添加周边场地位置点,中心定位图标改用固定的cover-view显示
if (props.enableMarkers && props.filteredPositions && props.filteredPositions.length > 0) {
props.filteredPositions.forEach((pos, index) => {
if (pos.longitude && pos.latitude && isValidCoordinate(pos.latitude, pos.longitude)) {
const lat = parseFloat(pos.latitude)
const lng = parseFloat(pos.longitude)
markers.push({
id: index + 1,
latitude: lat,
longitude: lng,
iconPath: '/static/markes_fdz.png',
width: 40,
height: 40,
callout: {
content: pos.name,
fontSize: 12,
borderRadius: 8,
bgColor: '#ffffff',
padding: 8,
display: 'BYCLICK'
}
})
}
})
}
mapMarkers.value = markers
isLoading.value = false
}
// 移动地图到指定位置(直接更新地图中心坐标)
const moveToLocation = (location) => {
if (!location || !location.longitude || !location.latitude) {
return
}
const newCenter = {
longitude: Number(location.longitude),
latitude: Number(location.latitude)
}
// 直接更新地图中心,触发地图组件的视图更新
mapCenter.value = newCenter
// 更新标记点
updateMapMarkers()
}
// 监听用户位置变化
watch(() => props.userLocation, (newLocation, oldLocation) => {
if (newLocation && newLocation.longitude && newLocation.latitude) {
// 检查位置是否真的变化了(避免重复更新)
const isChanged = !oldLocation ||
oldLocation.longitude !== newLocation.longitude ||
oldLocation.latitude !== newLocation.latitude
if (isChanged) {
mapCenter.value = {
longitude: newLocation.longitude,
latitude: newLocation.latitude
}
updateMapMarkers()
}
}
}, {
immediate: true,
deep: true
})
// 监听位置列表变化
watch(() => props.filteredPositions, (newPositions) => {
updateMapMarkers()
}, {
deep: true
})
// 监听广告图片变化,启动或停止轮播
watch(() => props.bannerImages, (newImages, oldImages) => {
// 先停止旧的轮播
stopBannerRotation()
currentBannerIndex.value = 0
// 如果有多张图片,启动新的轮播
if (newImages && newImages.length > 1) {
// 使用 nextTick 确保 DOM 已更新
nextTick(() => {
startBannerRotation()
})
}
}, {
immediate: true,
deep: true
})
// 地图加载完成事件
const onMapUpdated = () => {
isLoading.value = false
}
// 地图区域变化事件(带防抖优化)
const onMapRegionChange = (e) => {
// 只处理结束事件
if (!e || (e.type !== 'end' && e.type !== 'regionchange')) {
return
}
const causedBy = e.causedBy || e.detail?.causedBy
if (causedBy === 'gesture' || causedBy === 'scale' || causedBy === 'drag' || causedBy === 'update') {
// 清除之前的定时器
if (regionChangeTimer) {
clearTimeout(regionChangeTimer)
}
// 直接从事件对象中获取最新的中心点位置
const centerLocation = e.detail?.centerLocation || e.centerLocation
if (centerLocation && centerLocation.longitude && centerLocation.latitude) {
// 防抖:500ms后执行查询
regionChangeTimer = setTimeout(() => {
const newCenter = {
longitude: Number(centerLocation.longitude),
latitude: Number(centerLocation.latitude)
}
mapCenter.value = newCenter;
// 触发父组件查询新位置的场地
emit('mapCenterChange', newCenter)
}, 500)
} else {
// 兜底方案:如果事件中没有centerLocation,才使用API获取
regionChangeTimer = setTimeout(() => {
if (mapContext.value) {
mapContext.value.getCenterLocation({
success: (res) => {
if (res && res.longitude && res.latitude) {
const newCenter = {
longitude: res.longitude,
latitude: res.latitude
}
mapCenter.value = newCenter
emit('mapCenterChange', newCenter)
}
},
fail: (err) => {
console.error('获取地图中心失败:', err)
}
})
}
}, 500)
}
}
}
// 标记点点击事件
const onMapMarkerTap = (e) => {
const markerId = e.detail?.markerId || e.markerId
// 查找对应的场地位置信息
if (props.filteredPositions && props.filteredPositions.length > 0) {
const position = props.filteredPositions[markerId - 1]
if (position) {
emit('markerTap', position)
}
}
}
// 标记点气泡点击事件
const onCalloutTap = (e) => {
const markerId = e.markerId
const marker = mapMarkers.value.find(item => item.id === markerId)
if (marker && marker.position) {
emit('markerTap', marker.position)
}
}
// 地图错误事件
const onMapError = (error) => {
console.error('地图加载失败:', error)
isLoading.value = false
}
const handleRelocate = () => {
// 直接委托父级处理定位并移动地图,避免内部重复弹 loading
try {
emit('relocate')
} catch (e) {}
}
const handleSearch = () => {
try {
uni.navigateTo({ url: '/pages/search/index' })
} catch (e) {}
}
const handleService = () => {
uni.navigateTo({
url: '/subPackages/service/help/index'
})
}
const handleGuide = () => {
emit('guide')
}
// 处理广告点击
const handleBannerTap = () => {
// 触发父组件处理点击事件
emit('bannerClick', currentBannerIndex.value)
}
// 启动广告轮播
const startBannerRotation = () => {
// 如果只有一张或没有图片,不需要轮播
if (!props.bannerImages || props.bannerImages.length <= 1) {
return
}
// 清除旧的定时器
stopBannerRotation()
// 每3秒切换一次
bannerTimer = setInterval(() => {
const nextIndex = (currentBannerIndex.value + 1) % props.bannerImages.length
currentBannerIndex.value = nextIndex
}, 3000)
}
// 停止广告轮播
const stopBannerRotation = () => {
if (bannerTimer) {
clearInterval(bannerTimer)
bannerTimer = null
}
}
// 生命周期钩子
onMounted(() => {
// 初始化地图上下文
nextTick(() => {
// 需要使用nextTick确保地图组件已经渲染
const inst = getCurrentInstance()
const vm = (inst && (inst.proxy || inst)) || undefined
try {
mapContext.value = uni.createMapContext('map', vm)
} catch (e) {
// 兼容:如果第二参不被支持,退回单参
mapContext.value = uni.createMapContext('map')
}
updateMapMarkers()
// 初始化折叠面板
if (collapseRef.value) {
collapseRef.value.init()
}
// 初始化广告轮播
if (props.bannerImages && props.bannerImages.length > 1) {
startBannerRotation()
}
})
})
onUnmounted(() => {
// 清理工作
if (regionChangeTimer) {
clearTimeout(regionChangeTimer)
regionChangeTimer = null
}
// 停止广告轮播
stopBannerRotation()
mapContext.value = null
})
// 暴露给父组件的方法
defineExpose({
mapCenter: computed(() => mapCenter.value),
moveToLocation,
updateMapMarkers,
initCollapse: () => {
if (collapseRef.value) {
collapseRef.value.init()
}
}
})
</script>
<style lang="scss" scoped>
/* 地图容器 */
.map-container {
flex: 1;
position: relative;
left: 0;
right: 0;
bottom: 0;
width: 94vw;
margin: 20rpx;
margin-bottom: 0; /* 底部不需要边距 */
border-radius: 20rpx;
overflow: hidden;
display: flex;
flex-direction: column;
height: 72vh;
&.full-width {
width: 100%;
margin: 0;
border-radius: 0;
height: 100%;
}
.map-wrapper {
width: 100%;
height: 100%;
position: relative;
overflow: visible; /* 支付宝小程序:允许覆盖层显示在地图外部 */
border-radius: 0;
.native-map {
width: 100%;
height: 100%;
display: block;
border-radius: 0;
}
}
.map-loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 8rpx solid #f3f3f3;
border-top: 8rpx solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
text {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
}
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 支付宝小程序专用样式 - 所有覆盖层元素相对于 map-wrapper 定位 */
/* 广告图片样式 */
.index-swiper-img-alipay {
position: absolute;
top: 20rpx;
left: 50%;
transform: translateX(-50%);
width: 90vw;
height: 120rpx;
border-radius: 20rpx;
z-index: 100; /* 确保在地图之上 */
}
/* 轮播指示器独立定位 */
.indicator-dot-alipay {
position: absolute;
top: 130rpx; /* 广告图片 top(20rpx) + height(120rpx) - 10rpx = 130rpx */
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.5);
transition: all 0.3s ease;
pointer-events: none;
z-index: 101; /* 确保在广告图片之上 */
&.active {
width: 24rpx;
border-radius: 6rpx;
background-color: rgba(255, 255, 255, 0.9);
}
}
/* 地图中心定位图标样式 */
.center-location-marker-alipay {
position: absolute;
left: 50%;
top: 50%;
z-index: 99; /* 确保在地图之上 */
width: 60rpx;
height: 80rpx;
margin-left: -30rpx;
margin-top: -80rpx;
pointer-events: none;
background: transparent;
.center-marker-icon-alipay {
width: 60rpx;
height: 60rpx;
display: block;
}
}
/* 侧边控制按钮容器 */
.map-side-controls-alipay {
position: absolute;
right: 20rpx;
bottom: 160rpx; /* 向上移动,避免被底部按钮遮挡 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
height: 300rpx;
margin: auto;
gap: 12rpx;
z-index: 102; /* 确保在最上层 */
background: transparent;
.side-btn-alipay {
margin: auto;
background: rgba(255, 255, 255, 0.96);
border-radius: 24rpx;
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 13rpx;
border: 2rpx solid #e0e0e0;
&:active {
transform: scale(0.95);
}
.side-icon-alipay {
width: 40rpx;
height: 40rpx;
z-index:1000;
}
}
}
</style>
+28 -9
View File
@@ -9,7 +9,7 @@
<view class="header-right">
<!-- 支付方式标识移到头部右侧 -->
<view class="payment-badge wx-score" v-if="order.payWay == 'wx_score_pay'">
<image src="/static/images/wxpayflag.png" mode="aspectFit" class="badge-icon"></image>
<image src="/static/images/wxpayflag.png" mode="aspectFit" class="badge-icon" lazy-load="true"></image>
<view class="badge-text">
<text>{{ $t('order.wxPayScore') }}</text>
<text class="divider">|</text>
@@ -22,6 +22,12 @@
<view class="payment-badge member" v-else-if="order.payWay == 'wx_member_pay'">
<text class="badge-text">{{ $t('order.memberOrder') }}</text>
</view>
<view class="payment-badge member" v-else-if="order.payWay == 'ali_pay'">
<text class="badge-text">{{ $t('order.aliPay') }}</text>
</view>
<view class="payment-badge member" v-else-if="order.payWay == 'antom_pay'">
<text class="badge-text">{{ $t('order.antomPay') }}</text>
</view>
<view class="payment-badge deposit" v-else>
<text class="badge-text">{{ $t('order.wxPay') }}</text>
<text class="divider">|</text>
@@ -63,16 +69,16 @@
<view class="order-footer">
<view class="footer-left">
<view v-if="isInUse" class="renting">
<image src="/static/order_time.png" mode="aspectFit" class="icon-time"></image>
<image src="/static/order_time.png" mode="aspectFit" class="icon-time" lazy-load="true"></image>
{{ $t('order.renting') }}
</view>
<view v-else-if="isFinished" class="meta">
<view class="meta-item">
<image src="/static/order_time.png" mode="aspectFit" class="icon-time"></image>
<image src="/static/order_time.png" mode="aspectFit" class="icon-time" lazy-load="true"></image>
{{ usedDurationText }}
</view>
<view class="meta-item">
<image src="/static/order_price.png" mode="aspectFit" class="icon-price"></image>
<image src="/static/order_price.png" mode="aspectFit" class="icon-price" lazy-load="true"></image>
{{ displayAmount }}
</view>
</view>
@@ -98,7 +104,7 @@
import { computed } from 'vue';
import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n()
const { t } = useI18n()
const props = defineProps({
order: { type: Object, required: true },
@@ -128,9 +134,19 @@
}
});
const hasPauseTime = computed(() => {
const pt = props.order.pauseTime
return pt !== undefined && pt !== null && String(pt).trim() !== ''
})
const isBillingPaused = computed(() => normalizedStatus.value === 'in_used' && hasPauseTime.value)
const statusDef = computed(() => props.orderStatusMap?.[rawStatus.value] || props.orderStatusMap?.[normalizedStatus.value] || {});
const statusText = computed(() => statusDef.value.text || '');
const statusText = computed(() => {
if (isBillingPaused.value) return t('order.orderStatusBillingPaused')
return statusDef.value.text || ''
});
const statusChipClass = computed(() => {
if (isBillingPaused.value) return 'chip-paused'
const cls = statusDef.value.class || '';
if (cls.includes('status-using')) return 'chip-using';
if (cls.includes('status-waiting')) return 'chip-waiting';
@@ -144,7 +160,7 @@
const isFinished = computed(() => normalizedStatus.value === 'used_done');
const isCancelled = computed(() => normalizedStatus.value === 'order_cancelled');
const titleText = computed(() => $t('order.rentFan'));
const titleText = computed(() => t('order.rentFan'));
// 显示金额(优先后端给定字段)
const displayAmount = computed(() => props.order.amount || props.order.payAmount || props.order.actualDeviceAmount || props.order.currentFee || '0.00');
@@ -158,8 +174,10 @@
const minutes = Math.floor(diffMs / 60000);
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (hours > 0) return `${hours}${$t('time.hour')}${mins}${$t('time.minute')}`;
return `${mins}${$t('time.minute')}`;
if (hours > 0) return `${hours}${t('time.hour')}${mins}${t('time.minute')}`;
// 如果小于1分钟,显示"小于1分钟"
if (minutes < 1) return `${t('time.lessThan')}1${t('time.minute')}`;
return `${mins}${t('time.minute')}`;
});
function parseDate(str) {
@@ -204,6 +222,7 @@
overflow: hidden;
.chip-text { display: inline-block; transform: skewX(15deg); }
&.chip-using { background: rgba(7,193,96,0.12); color: #07c160; }
&.chip-paused { background: rgba(255,152,0,0.16); color: #e65100; }
&.chip-waiting { background: rgba(255,152,0,0.12); color: #FF9800; }
&.chip-finished { background: rgba(76,175,80,0.12); color: #4CAF50; }
&.chip-cancelled { background: rgba(158,158,158,0.12); color: #9E9E9E; }
+293
View File
@@ -0,0 +1,293 @@
<template>
<view class="h5-home-wrap">
<view class="main-content" :style="mainContentStyle">
<view class="h5-banner-section" v-if="bannerImages && bannerImages.length > 0">
<swiper class="h5-banner-swiper" :indicator-dots="bannerImages.length > 1" :autoplay="true" :interval="4000"
:duration="500" circular>
<swiper-item v-for="(img, idx) in bannerImages" :key="idx">
<view class="h5-banner-item" @click="$emit('bannerClick', idx)">
<image :src="img" mode="aspectFill" class="h5-banner-image"></image>
</view>
</swiper-item>
</swiper>
</view>
<view class="h5-banner-empty" v-else>
<text>{{ loadingText }}</text>
</view>
<view class="h5-cta-card">
<view class="h5-cta-header">
<text class="h5-cta-title">{{ scanText }}</text>
<text class="h5-cta-desc">{{ t('home.h5ScanDesc') }}</text>
</view>
<view class="h5-cta-btn" @click="$emit('scan')">
<image class="h5-cta-icon" src="/static/scan-icon.png" mode="aspectFit" />
<text class="h5-cta-btn-text">{{ scanText }}</text>
</view>
</view>
<view class="h5-section">
<view class="h5-section-title">{{ t('home.h5HowItWorks') }}</view>
<view class="h5-steps">
<view class="h5-step-item">
<text class="h5-step-index">1</text>
<text class="h5-step-text">{{ t('home.h5StepScan') }}</text>
</view>
<view class="h5-step-item">
<text class="h5-step-index">2</text>
<text class="h5-step-text">{{ t('home.h5StepUnlock') }}</text>
</view>
<view class="h5-step-item">
<text class="h5-step-index">3</text>
<text class="h5-step-text">{{ t('home.h5StepReturn') }}</text>
</view>
</view>
</view>
<view class="h5-section">
<view class="h5-section-title">{{ t('home.h5ServiceTitle') }}</view>
<view class="h5-tags">
<text class="h5-tag">{{ t('home.h5ServiceSupport') }}</text>
<text class="h5-tag">{{ t('home.h5ServiceSafePayment') }}</text>
<text class="h5-tag">{{ t('home.h5ServiceEasyReturn') }}</text>
</view>
</view>
</view>
<view class="h5-bottom-actions">
<view class="action-btn primary" @click="$emit('scan')">
<image class="action-icon" src="/static/scan-icon.png" mode="aspectFit" />
<text class="action-label primary-label">{{ scanText }}</text>
</view>
<view class="action-btn secondary" @click="$emit('my')">
<image class="action-icon" src="/static/user.png" mode="aspectFit" />
<text class="action-label">{{ personalCenterText }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { useI18n } from '../../utils/i18n.js'
const { t } = useI18n()
defineEmits(['bannerClick', 'scan', 'buy', 'my'])
defineProps({
mainContentStyle: { type: Object, default: () => ({}) },
bannerImages: { type: Array, default: () => [] },
loadingText: { type: String, default: '' },
buyDeviceText: { type: String, default: '' },
scanText: { type: String, default: '' },
personalCenterText: { type: String, default: '' }
})
</script>
<style lang="scss" scoped>
.h5-home-wrap {
min-height: 100%;
}
.main-content {
padding-bottom: 220rpx;
}
.h5-banner-section {
padding: 24rpx 20rpx 0;
box-sizing: border-box;
}
.h5-banner-swiper {
width: 100%;
height: 320rpx;
border-radius: 20rpx;
overflow: hidden;
background: #f5f5f5;
}
.h5-banner-item,
.h5-banner-image {
width: 100%;
height: 100%;
}
.h5-banner-empty {
padding: 80rpx 20rpx 0;
text-align: center;
color: #999;
font-size: 26rpx;
}
.h5-cta-card {
margin: 20rpx;
padding: 24rpx;
border-radius: 20rpx;
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
box-shadow: 0 6rpx 18rpx rgba(0, 0, 0, 0.05);
}
.h5-cta-header {
flex: 1;
}
.h5-cta-title {
font-size: 32rpx;
font-weight: 600;
color: #1f7d43;
display: block;
}
.h5-cta-desc {
font-size: 24rpx;
color: #4f7b61;
margin-top: 8rpx;
display: block;
}
.h5-cta-btn {
height: 76rpx;
padding: 0 24rpx;
background: #3EAB64;
border-radius: 38rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
}
.h5-cta-icon {
width: 30rpx;
height: 30rpx;
filter: brightness(0) invert(1);
}
.h5-cta-btn-text {
color: #fff;
font-size: 24rpx;
font-weight: 600;
}
.h5-section {
margin: 0 20rpx 20rpx;
background: #fff;
border-radius: 20rpx;
padding: 20rpx;
box-shadow: 0 6rpx 18rpx rgba(0, 0, 0, 0.04);
}
.h5-section-title {
font-size: 28rpx;
font-weight: 600;
color: #2d2d2d;
margin-bottom: 16rpx;
}
.h5-steps {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12rpx;
}
.h5-step-item {
background: #f4f8f6;
border-radius: 14rpx;
padding: 16rpx 10rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.h5-step-index {
width: 34rpx;
height: 34rpx;
background: #3EAB64;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
}
.h5-step-text {
font-size: 22rpx;
color: #315c46;
}
.h5-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.h5-tag {
padding: 10rpx 16rpx;
background: #eef8f1;
color: #2e7d4f;
border-radius: 999rpx;
font-size: 22rpx;
}
.h5-bottom-actions {
position: fixed;
left: 20rpx;
right: 20rpx;
bottom: 30rpx;
z-index: 1200;
padding: 12rpx;
background: rgba(255, 255, 255, 0.96);
border-radius: 28rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
display: flex;
align-items: stretch;
gap: 12rpx;
}
.action-btn {
min-height: 84rpx;
border-radius: 20rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
box-sizing: border-box;
}
.action-btn.primary {
flex: 1;
background: #3EAB64;
min-width: 0;
}
.action-btn.secondary {
width: 180rpx;
background: #f4f6f8;
border: 2rpx solid #e7eaee;
}
.action-icon {
width: 32rpx;
height: 32rpx;
}
.action-btn.primary .action-icon {
filter: brightness(0) invert(1);
}
.action-label {
font-size: 24rpx;
color: #333;
text-align: center;
}
.primary-label {
color: #fff;
font-weight: 600;
}
</style>
+276
View File
@@ -0,0 +1,276 @@
<template>
<view>
<view class="main-content" :style="mainContentStyle">
<!-- 支付宝小程序使用专用组件 -->
<!-- #ifdef MP-ALIPAY -->
<MapComponentAlipay v-if="!isLoading && userLocation && !locationPermissionDenied" ref="innerMapRef"
:userLocation="userLocation" :positionList="positionList" :filteredPositions="filteredPositions"
:searchKeyword="searchKeyword" :enableMarkers="true" :bannerImages="bannerImages"
:hideMapOverlays="hideMapOverlays" @relocate="$emit('relocate')" @scan="$emit('scan')"
@showList="$emit('showList')" @markerTap="$emit('markerTap', $event)"
@mapCenterChange="$emit('mapCenterChange', $event)" @bannerClick="$emit('bannerClick', $event)"
@guide="$emit('guide')" />
<!-- #endif -->
<!-- 非支付宝小程序使用通用组件 -->
<!-- #ifndef MP-ALIPAY -->
<MapComponent v-if="!isLoading && userLocation && !locationPermissionDenied" ref="innerMapRef"
:userLocation="userLocation" :positionList="positionList" :filteredPositions="filteredPositions"
:searchKeyword="searchKeyword" :enableMarkers="true" :bannerImages="bannerImages"
:hideMapOverlays="hideMapOverlays" @relocate="$emit('relocate')" @scan="$emit('scan')"
@showList="$emit('showList')" @markerTap="$emit('markerTap', $event)"
@mapCenterChange="$emit('mapCenterChange', $event)" @bannerClick="$emit('bannerClick', $event)"
@guide="$emit('guide')" />
<!-- #endif -->
<!-- 地图加载状态 -->
<!-- #ifdef MP-ALIPAY -->
<view v-if="!userLocation" class="location-denied-placeholder">
<view class="denied-content">
<text class="denied-text">{{ locationPermissionText }}</text>
<view class="denied-enable-btn" @click="$emit('enableLocation')">
<text class="denied-enable-btn-text">{{ enableLocationText }}</text>
</view>
</view>
</view>
<!-- #endif -->
<!-- #ifndef MP-ALIPAY -->
<view v-if="isLoading || !userLocation" class="map-loading-placeholder">
<view class="loading-content">
<view class="loading-spinner"></view>
<text>{{ loadingLocationText }}</text>
</view>
</view>
<!-- #endif -->
</view>
<view class="bottom-actions">
<view class="action-btn secondary small btn-nearby" @click="$emit('buy')">
<view class="icon-wrap">
<image src="/static/shop_icon.png" class="action-icon" mode="scaleToFill" lazy-load="true"></image>
</view>
<text class="action-label">{{ buyDeviceText }}</text>
</view>
<view class="action-btn primary btn-scan" @click="$emit('scan')">
<view class="icon-wrap">
<image class="action-icon" src="/static/scan-icon.png" mode="aspectFill" lazy-load="true" />
</view>
<text class="primary-label">{{ scanText }}</text>
</view>
<view class="action-btn secondary small btn-my" @click="$emit('my')">
<view class="icon-wrap">
<image class="action-icon" src="/static/user.png" mode="aspectFit" lazy-load="true" />
</view>
<text class="action-label">{{ personalCenterText }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import MapComponent from '../MapComponent.vue'
// #ifdef MP-ALIPAY
import MapComponentAlipay from '../MapComponentAlipay.vue'
// #endif
defineEmits(['relocate', 'scan', 'showList', 'markerTap', 'mapCenterChange', 'bannerClick', 'guide', 'enableLocation', 'buy', 'my'])
const props = defineProps({
mainContentStyle: { type: Object, default: () => ({}) },
isLoading: { type: Boolean, default: false },
userLocation: { type: Object, default: null },
locationPermissionDenied: { type: Boolean, default: false },
positionList: { type: Array, default: () => [] },
filteredPositions: { type: Array, default: () => [] },
searchKeyword: { type: String, default: '' },
bannerImages: { type: Array, default: () => [] },
hideMapOverlays: { type: Boolean, default: false },
locationPermissionText: { type: String, default: '' },
enableLocationText: { type: String, default: '' },
loadingLocationText: { type: String, default: '' },
buyDeviceText: { type: String, default: '' },
scanText: { type: String, default: '' },
personalCenterText: { type: String, default: '' }
})
const innerMapRef = ref(null)
defineExpose({
mapCenter: computed(() => innerMapRef.value?.mapCenter || null),
moveToLocation: (location) => innerMapRef.value && innerMapRef.value.moveToLocation && innerMapRef.value.moveToLocation(location)
})
</script>
<style lang="scss" scoped>
.main-content {
flex: 1;
position: relative;
width: 100%;
height: 100%;
padding-bottom: 180rpx;
box-sizing: border-box;
}
.map-loading-placeholder {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 180rpx;
background: #f6f7fb;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
.loading-content {
background: #ffffff;
border-radius: 16rpx;
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f0f0f0;
border-top: 4rpx solid #2196F3;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 24rpx;
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.location-denied-placeholder {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 180rpx;
background: #f6f7fb;
display: flex;
align-items: center;
justify-content: center;
padding: 0 40rpx;
box-sizing: border-box;
}
.denied-content {
width: 100%;
background: #ffffff;
border-radius: 16rpx;
padding: 40rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.denied-text {
display: block;
font-size: 28rpx;
color: #333;
line-height: 1.6;
}
.denied-enable-btn {
margin-top: 28rpx;
width: 100%;
height: 88rpx;
background: #3EAB64;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.denied-enable-btn-text {
font-size: 32rpx;
font-weight: 700;
color: #ffffff;
}
.bottom-actions {
position: fixed;
left: 20rpx;
right: 20rpx;
bottom: 40rpx;
z-index: 1200;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.action-btn.primary {
background: #3EAB64;
color: #fff;
border-radius: 56rpx;
height: 112rpx;
flex: 1;
max-width: 400rpx;
padding: 0 24rpx;
flex-direction: row;
gap: 8rpx;
}
.action-btn.secondary {
color: #333;
border-radius: 24rpx;
height: 100rpx;
width: 140rpx;
flex-shrink: 0;
padding: 8rpx 12rpx;
}
.icon-wrap {
width: 36rpx;
height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
}
.btn-scan .icon-wrap {
width: 48rpx;
height: 48rpx;
}
.action-icon {
width: 36rpx;
height: 36rpx;
}
.btn-scan .action-icon {
width: 32rpx;
height: 32rpx;
filter: brightness(0) invert(1);
}
.primary-label {
color: #ffffff;
font-size: 28rpx;
font-weight: 600;
}
.action-label {
line-height: 1.2;
text-align: center;
}
</style>
+5021
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
import request from '../http'
// 按场地查询可用优惠券
export const getCouponsByPosition = (positionId) => {
return request({
url: `/device/coupon/app/position/${positionId}`,
method: 'get'
})
}
// 创建优惠券支付订单
export const createCouponPayment = (couponId, paymentPlatform) => {
return request({
url: '/app/coupon/pay',
method: 'post',
data: {
couponId,
// 支付平台类型:WECHAT / ALIPAY / ANTOM(不传则后端默认 WECHAT
...(paymentPlatform ? { paymentPlatform } : {})
}
})
}
// 查询用户优惠券
export const getUserCoupons = (status) => {
return request({
url: '/device/userPurchase/app/my',
method: 'get',
data: {
status
}
})
}
export const cancelCouponPayment = (orderNo) => {
return request({
url: `/device/userPurchase/app/cancel/${orderNo}`,
method: 'post'
})
}
+3 -21
View File
@@ -50,32 +50,14 @@ export const transformDeviceData = (device) => {
}
// 立即租借
export const rentPowerBank = (deviceNo, phone) => {
export const rentPowerBank = (deviceNo, phone,payway) => {
return request({
url: `/app/device/rentPowerBank?deviceNo=${deviceNo}`,
method: 'post',
data: {
// deviceNo,
phone
phone,
payway
}
})
}
// 确认支付并弹出风扇
export const confirmPaymentAndRent = (orderId) => {
console.log(`确认支付并弹出风扇, orderId: ${orderId}`)
return request({
url: `/app/device/confirmPaymentAndRent?orderId=${orderId}`,
method: 'GET'
})
}
// 强制打开空格子
export const forcefOpenEmptyGrid = (deviceNo) => {
console.log(`强制打开空格子, deviceNo: ${deviceNo}`)
return request({
url: `/app/device/forcef/${deviceNo}`,
method: 'post'
})
}
+41
View File
@@ -0,0 +1,41 @@
import request from '../http'
// 创建会员卡支付订单
export const createMemberCardPayment = (memberCardId, paymentPlatform) => {
return request({
url: '/app/member/pay',
method: 'post',
data: {
memberCardId,
// 支付平台类型:WECHAT / ALIPAY / ANTOM(不传则后端默认 WECHAT
...(paymentPlatform ? { paymentPlatform } : {})
}
})
}
// 根据场地ID查询可用会员卡列表
export const getMemberCardsByPosition = (positionId) => {
return request({
url: `/device/memberCard/app/position/${positionId}`,
method: 'get'
})
}
//根据状态查询个人会员卡
export const getMemberCardsByStatus = () => {
return request({
url: `/device/userMemberCard/app/my`,
method: 'get'
})
}
// 取消会员卡支付订单
export const cancelMemberCardPayment = (orderNo) => {
return request({
url: `/device/userMemberCard/app/cancel/${orderNo}`,
method: 'post'
})
}
+160 -13
View File
@@ -10,6 +10,16 @@ export const getOrderList = (data) => {
})
}
// 用户端查询商品订单列表
export const getProductOrderList = (data) => {
return request({
url: '/app/product/order/list',
method: 'get',
data,
hideLoading: true
})
}
// 查询是否有订单
export const queryHasOrder = (deviceNo) => {
return request({
@@ -41,7 +51,6 @@ export const createOrder = (data) => {
// 查询订单
export const queryById = (id) => {
console.log(`查询订单详情, orderId: ${id}`)
return request({
url: `/app/order/${id}`,
method: 'get',
@@ -49,6 +58,15 @@ export const queryById = (id) => {
})
}
// 用户查询商品订单详情
export const getProductOrderDetail = (id) => {
return request({
url: `/app/product/order/${id}`,
method: 'get',
hideLoading: true
})
}
// 取消订单
export const cancelOrder = (data) => {
return request({
@@ -60,7 +78,6 @@ export const cancelOrder = (data) => {
// 结束订单
export const overOrderById = (orderId) => {
console.log(`调用结束订单API, orderId: ${orderId}`)
return request({
url: `/app/order/close/${orderId}`,
method: 'get',
@@ -76,9 +93,130 @@ export const getOrderByOrderNo = (orderNo) => {
})
}
// 充电宝未弹出反馈(快捷反馈)
export const reportDeviceNoEject = (data) => {
return request({
url: '/app/order/report-no-eject',
method: 'post',
data
})
}
// 充电宝转为自用
export const convertToOwned = (orderId) => {
return request({
url: `/app/order/convert-to-owned/${orderId}`,
method: 'post'
})
}
// 不想还了转为自用(按最高费用)
export const closeWithMaxFee = (orderNo) => {
return request({
url: `/app/order/closeWithMaxFee/${orderNo}`,
method: 'post'
})
}
// 创建微信支付订单
export const createWxPayment = (orderNo) => {
return request({
url: `/app/wx-payment/create/${orderNo}`,
method: 'get'
})
}
// 创建支付宝支付订单(租借押金 H5 支付)
// 对应文档《支付宝接口文档》:GET /app/ali-payment/create/{orderNo}
export const createAliPayment = (orderNo) => {
return request({
url: `/app/ali-payment/create/${orderNo}`,
method: 'get'
})
}
// 获取正在使用中的订单(可传 hideLoading: true 由业务自行控制 loading
export const getInUseOrder = (opts = {}) => {
return request({
url: '/app/order/inUse',
method: 'get',
...opts
})
}
// 查询订单是否可申请暂停计费
export const getPauseBillingEligible = (orderId) => {
return request({
url: `/app/order/pauseBilling/eligible/${orderId}`,
method: 'get',
hideLoading: true
})
}
// 对订单执行暂停计费
export const requestPauseBilling = (orderId) => {
return request({
url: `/app/order/pauseBilling/${orderId}`,
method: 'post',
hideLoading: true
})
}
// 获取待支付订单
export const getUnpaidOrder = () => {
return request({
url: '/app/order/unpaid',
method: 'get'
})
}
// 查询微信支付状态
export const getWxPaymentStatus = (orderNo) => {
return request({
url: `/app/wx-payment/status/${orderNo}`,
method: 'get'
})
}
// 查询支付宝支付状态
// 对应文档:GET /app/ali-payment/status/{orderNo}
export const getAliPaymentStatus = (orderNo) => {
return request({
url: `/app/ali-payment/status/${orderNo}`,
method: 'get'
})
}
// ==================== Antom 支付相关接口 ====================
// 创建 Antom H5 支付订单
export const createAntomPayment = (orderNo, paymentType, osType) => {
return request({
url: `/app/antom-payment/create/${orderNo}?paymentType=${paymentType}&osType=${osType}`,
method: 'get'
})
}
// 获取 Antom 可用支付方式列表
export const getAntomPaymentMethods = (orderNo, osType) => {
return request({
url: `/app/antom-payment/consult/${orderNo}?osType=${osType}`,
method: 'get',
hideLoading: true
})
}
// Antom 支付结果查询
export const getAntomPaymentStatus = (orderNo, osType) => {
return request({
url: `/app/antom-payment/inquiry/${orderNo}?osType=${osType}`,
method: 'get',
hideLoading: true
})
}
// 通过订单号获取支付分订单信息
export const getOrderByOrderNoScore = (orderNo) => {
console.log('通过订单号获取支付分订单信息', orderNo);
return request({
url: `/app/wx-payment/score/create/${orderNo}`,
method: 'get',
@@ -88,7 +226,6 @@ export const getOrderByOrderNoScore = (orderNo) => {
// 通过订单号获取支付分订单状态
export const getOrderByOrderNoScorePayStatus = (orderNo) => {
console.log('通过订单号获取支付分订单状态', orderNo);
return request({
url: `/app/wx-payment/score/status/${orderNo}`,
method: 'get',
@@ -98,7 +235,6 @@ export const getOrderByOrderNoScorePayStatus = (orderNo) => {
// 更新订单套餐信息
export const updateOrderPackage = (data) => {
console.log('更新订单套餐信息:', data)
return request({
url: '/app/device/updateOrderPackage',
method: 'post',
@@ -106,15 +242,26 @@ export const updateOrderPackage = (data) => {
})
}
/*
* 弃用
*/
export const getPotionsDetail = (data) => {
console.log(data);
// 用户端删除商品订单(逻辑删除)
export const deleteProductOrder = (id) => {
return request({
url: '/device/position/positionDetails',
method: 'get',
data
url: `/app/product/order/${id}`,
method: 'delete'
})
}
// 用户端取消商品订单支付
export const cancelProductOrder = (OutOrderNo) => {
return request({
url: `/app/product/order/${OutOrderNo}/cancel`,
method: 'put'
})
}
// 解决丢包风扇未弹出,尝试重新弹出风扇
export const deviceRentByOrderNo = (orderNo)=>{
return request({
url:`/app/order/tryRent/${orderNo}`,
method:'post'
})
}
+75
View File
@@ -0,0 +1,75 @@
import request from '../http'
/**
* 商品列表查询接口
* @param {Object} params - 查询参数
* @param {string} params.productName - 商品名称(可选,模糊查询)
* @param {number} params.pageNum - 页码(默认1
* @param {number} params.pageSize - 每页数量(默认10
* @returns {Promise} 分页的商品列表
*/
export const getProductList = ({ productName = '', pageNum = 1, pageSize = 10 }) => {
return request({
url: '/app/product/list',
method: 'get',
params: {
productName,
pageNum,
pageSize
}
})
}
/**
* 商品详情查询接口
* @param {string|number} id - 商品ID
* @returns {Promise} 商品详细信息,包含规格列表(skuList)
*/
export const getProductDetail = (id) => {
return request({
url: `/app/product/${id}`,
method: 'get'
})
}
/**
* 创建商品支付订单(多支付平台)
* 对应《商品购买多支付平台方案》:
* paymentPlatform: WECHAT / ALIPAY / ANTOM
* 其他字段见文档
*/
export const createProductOrder = (data) => {
return request({
url: '/app/product/pay',
method: 'post',
data
})
}
/**
* 商品订单退款
* @param {Object} data - 退款数据
* @param {number} data.productOrderId - 商品订单ID
* @param {number} data.refundAmount - 退款金额(可选,不传则全额退款)
* @param {string} data.refundReason - 退款原因(可选)
* @returns {Promise} 退款结果
*/
export const refundProductOrder = (data) => {
return request({
url: '/app/product/refund',
method: 'post',
data
})
}
/**
* 获取用户收货地址
* @returns {Promise} 用户收货地址信息
*/
export const getUserAddress = () => {
return request({
url: '/app/product/address',
method: 'get'
})
}
+35 -8
View File
@@ -1,11 +1,12 @@
import request from '../http'
// 获取系统配置(预留接口)
// 期望后端返回形如:{ code: 200, data: { expressReturnCountdownSeconds: number } }
export const getSystemConfig = () => {
// 获取系统配置
// 可传参示例:{ configKey: 'overseas_payment_dana_total' }
export const getSystemConfig = (data = {}) => {
return request({
url: '/app/system/config',
url: '/system/config/list',
method: 'get',
data,
hideLoading: true
})
}
@@ -27,12 +28,38 @@ export const getCommonByBrand = (brandName) => {
})
}
// 查询激活的且临近关闭时间最近的活动
export const getActiveActivity = () => {
// 获取当前协议内容
export const getCurrentAgreement = (data) => {
return request({
url: '/device/activity/agent/list',
url: '/device/agreementConfig/current',
method: 'get',
hideLoading: true
data
})
}
// 获取当前广告内容
export const getCurrentAdvertisement = (data) => {
return request({
url: '/device/advertisementConfig/current',
method: 'get',
data
})
}
// 获取当前公告内容
export const getCurrentAnnouncement = (data) => {
return request({
url: '/device/announcementConfig/current',
method: 'get',
data
})
}
export const getActiveActivity = (data) => {
return request({
url: '/device/activeActivity/current',
method: 'get',
data
})
}
+60 -3
View File
@@ -1,7 +1,7 @@
import request from '../http'
import { URL, appid } from '../url'
// 用户登录
// 旧登录接口(兼容保留,后端将逐步废弃)
export const login = (data) => {
return request({
url: '/app/user/login',
@@ -10,6 +10,42 @@ export const login = (data) => {
})
}
// 统一快捷登录接口 /app/user/quickLogin
// 对应文档《快捷登录最终方案》中的 QuickLoginDto
// loginType: WECHAT / ALIPAY / SMS
// appid: 平台应用ID
// openId: 第三方 openId(微信必传)
// code: 授权码(微信手机号授权码 / 支付宝 authCode
// phonenumber: 短信登录手机号
// smsCode: 短信验证码
export const quickLogin = (data) => {
return request({
url: '/app/user/quickLogin',
method: 'post',
data
})
}
// 发送验证码
export const sendVerifyCode = (phonenumber) => {
return request({
url: '/app/user/sms/code',
method: 'get',
data: { phonenumber }
})
}
// 手机号+验证码登录
export const loginWithCode = (phonenumber, smsCode) => {
// 兼容保留:统一走 quickLoginSMS
return quickLogin({
loginType: 'SMS',
appid,
phonenumber,
smsCode
})
}
// 用户退出登录
export const userLogout = (data) => {
return request({
@@ -56,7 +92,8 @@ export const uploadUserAvatar = (filePath) => {
header: {
'appid': appid,
'Authorization': 'Bearer ' + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
'Clientid': uni.getStorageSync('client_id'),
'Content-Language': (uni.getStorageSync('language') || 'zh-CN').replace(/-/g, '_')
},
success: (res) => {
try {
@@ -83,7 +120,8 @@ export const uploadOssResource = (filePath) => {
header: {
'appid': appid,
'Authorization': 'Bearer ' + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
'Clientid': uni.getStorageSync('client_id'),
'Content-Language': (uni.getStorageSync('language') || 'zh-CN').replace(/-/g, '_')
},
success: (res) => {
try {
@@ -115,3 +153,22 @@ export const withdrawDeposit = (orderNo) => {
})
}
// 获取微信用户手机号
export const getWxUserPhoneNumber = (data) => {
return request({
url: '/app/user/getPhoneNumber',
method: 'post',
data
})
}
// 获取支付宝用户手机号(复用同一后端接口,由后端按 appid / 参数结构区分平台)
// 期望后端返回:{ code:200, data:{ phoneNumber: 'xxx' } }
export const getAliUserPhoneNumber = (data) => {
return request({
url: '/app/user/alipay/getPhone',
method: 'post',
data
})
}
+77
View File
@@ -0,0 +1,77 @@
/**
* Console 日志配置
* 用于控制是否在控制台打印日志
*/
// 配置项:true 表示打印日志,false 表示不打印日志
export const CONSOLE_CONFIG = {
// 是否启用 console.log
enableLog: true,
// 是否启用 console.warn
enableWarn: false,
// 是否启用 console.error
enableError: false,
// 是否启用 console.info
enableInfo: false
}
// 保存原始的 console 方法
const originalConsole = {
log: console.log,
warn: console.warn,
error: console.error,
info: console.info
}
/**
* 初始化 console 控制
* 根据配置决定是否打印日志
*/
export function initConsoleControl() {
// 重写 console.log
console.log = function(...args) {
if (CONSOLE_CONFIG.enableLog) {
originalConsole.log.apply(console, args)
}
}
// 重写 console.warn
console.warn = function(...args) {
if (CONSOLE_CONFIG.enableWarn) {
originalConsole.warn.apply(console, args)
}
}
// 重写 console.error
console.error = function(...args) {
if (CONSOLE_CONFIG.enableError) {
originalConsole.error.apply(console, args)
}
}
// 重写 console.info
console.info = function(...args) {
if (CONSOLE_CONFIG.enableInfo) {
originalConsole.info.apply(console, args)
}
}
}
/**
* 恢复原始的 console 方法
*/
export function restoreConsole() {
console.log = originalConsole.log
console.warn = originalConsole.warn
console.error = originalConsole.error
console.info = originalConsole.info
}
/**
* 动态设置 console 配置
* @param {Object} config - 配置对象
*/
export function setConsoleConfig(config) {
Object.assign(CONSOLE_CONFIG, config)
}
+31 -9
View File
@@ -1,8 +1,15 @@
import {
URL,
appid
appid,
ZFBappid
} from './url'
// 根据运行平台选择正确的小程序 appid(后端通常依赖该 header 做平台识别)
let platformAppid = appid
// #ifdef MP-ALIPAY
platformAppid = ZFBappid
// #endif
// 获取多语言翻译文本
const getLoadingText = () => {
try {
@@ -29,11 +36,14 @@ const request = (option) => {
method: option.method,
data: option.data,
header: {
"Content-Type": option.headers && option.headers["Content-Type"] ? option.headers["Content-Type"] : (option.method && option.method.toUpperCase() === 'POST' ? 'application/json' : 'application/x-www-form-urlencoded'),
"Content-Type": option.headers && option.headers["Content-Type"] ? option.headers[
"Content-Type"] : (option.method && option.method.toUpperCase() === 'POST' ?
'application/json' : 'application/x-www-form-urlencoded'),
...option.headers,
'appid': appid,
'appid': platformAppid,
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
'Clientid': uni.getStorageSync('client_id'),
'Content-Language': (uni.getStorageSync('language') || 'zh-CN').replace(/-/g, '_')
},
success(res) {
@@ -48,7 +58,9 @@ const request = (option) => {
return
}
reject({msg: `请求失败,状态码:${res.statusCode}`})
reject({
msg: `请求失败,状态码:${res.statusCode}`
})
return
}
@@ -60,12 +72,22 @@ const request = (option) => {
// 计算重定向地址
const pages = getCurrentPages()
const current = pages && pages.length ? pages[pages.length - 1] : null
const route = current && current.route ? ('/' + current.route) : '/pages/index/index'
const query = current && current.options ? Object.keys(current.options).map(k => `${k}=${encodeURIComponent(current.options[k])}`).join('&') : ''
const route = current && current.route ? ('/' + current.route) :
'/pages/index/index'
const query = current && current.options ? Object.keys(current.options).map(
k => `${k}=${encodeURIComponent(current.options[k])}`).join('&') :
''
const redirect = encodeURIComponent(query ? `${route}?${query}` : route)
// console.log(redirect, "===========");
// 跳转到登录页
uni.reLaunch({ url: `/pages/login/index?redirect=${redirect}` })
} catch (e) {}
uni.reLaunch({
url: "/subPackages/user/login/index"
})
} catch (e) {
uni.reLaunch({
url: "/subPackages/user/login/index"
})
}
}
// 检查业务状态码
+4 -3
View File
@@ -1,7 +1,8 @@
// export const URL = "https://my.gxfs123.com/api" //正式服务器-弃用
// export const URL = "https://manager.fdzpower.com/api" //正式服务器
export const URL = "https://fansdev.gxfs123.com/api" //测试服务器
// export const URL = "http://192.168.5.30:8080" //本地调试
// export const URL = "https://manager.fdzpower.com/api" //正式国内服务器
export const URL = "https://ina.fdzpower.com/api" //正式国外服务器
// export const URL = "https://fansdev.gxfs123.com/api" //测试服务器
// export const URL = "http://192.168.0.58:8080" //本地调试
// export const URL = "http://127.0.0.1:8080" //本地调试
export const appid = "wx2165f0be356ae7a9" //微信小程序appid
+278 -20
View File
@@ -11,6 +11,7 @@ export default {
filling: 'Filling...',
save: 'Save',
loadFailed: 'Load failed',
invalidUrl: 'Invalid URL',
statusCode: 'Status Code',
message: 'Message',
none: 'None',
@@ -48,10 +49,20 @@ export default {
loginRequired: 'Please login first',
operationSuccess: 'Operation successful',
operationFailed: 'Operation failed',
sending: 'Sending...',
loggingIn: 'Logging in...',
refresh: 'Refresh',
pull: 'Pull to refresh',
release: 'Release to refresh',
noMore: 'No more'
noMore: 'No more',
functionDeveloping: 'Function under development',
saveImage: 'Save to Phone',
saveSuccess: 'Saved successfully',
saving: 'Saving...',
saveFailed: 'Save failed',
downloadFailed: 'Download failed',
reset: 'Reset',
preview: 'Preview'
},
nav: {
@@ -60,26 +71,36 @@ export default {
orders: 'Orders',
settings: 'Settings',
back: 'Back',
title: 'FengDianZhe'
title: 'Isidaya'
},
app: {
name: 'FengDianZhe',
name: 'Isidaya',
slogan: 'Fan & Power Bank Rental',
fullName: 'FengDianZhe',
welcome: 'Welcome'
fullName: 'Isidaya',
welcome: 'Welcome to Isidaya'
},
home: {
title: 'FengDianZhe',
title: 'Isidaya',
nearbyDevices: 'Nearby',
scanToUse: 'Scan',
personalCenter: 'Profile',
useGuide: 'Guide',
buyDevice: 'Custom Powewr',
navigate: 'Navigate',
relocate: 'Relocate',
search: 'Search',
service: 'Service',
h5ScanDesc: 'Scan the device QR code to start',
h5HowItWorks: 'How It Works',
h5StepScan: 'Scan',
h5StepUnlock: 'Device Ejects',
h5StepReturn: 'Return',
h5ServiceTitle: 'Service',
h5ServiceSupport: '24/7 Support',
h5ServiceSafePayment: 'Safe Payment',
h5ServiceEasyReturn: 'Easy Return',
searchPlaceholder: 'Search locations',
nearbyDeviceLocation: 'Nearby',
noNearbyDevice: 'No devices nearby',
@@ -89,7 +110,41 @@ export default {
invalidQRCode: 'Invalid QR code',
scanFailed: 'Scan failed',
noticeTitle: 'Notice',
getLocationFailed: 'Location unavailable'
getLocationFailed: 'Location unavailable',
locationPermissionOffTip: 'Location permission is not enabled. Nearby devices cannot be found yet. Tap the button below to enable location services.',
enableLocation: 'Enable Location'
},
scan: {
title: 'Scan to Use',
album: 'Album',
manualInput: 'Enter Manually',
manualInputTitle: 'Enter Device No.',
deviceNoPlaceholder: 'Enter the number on the device',
initializing: 'Initializing...',
startingCamera: 'Starting camera...',
alignQRCode: 'Align the QR code within the frame',
initFailed: 'Initialization failed',
browserNotSupportCamera: 'Your browser does not support camera access',
containerNotFound: 'Scanner container not found',
noCameraFound: 'No camera found',
ensureCameraExists: 'Please make sure your device has a camera',
cameraPermissionDenied: 'Camera permission denied',
cameraPermissionHint: 'Please allow camera access in browser settings',
cameraInUse: 'Camera is in use',
closeOtherCameraApps: 'Please close other apps using the camera',
browserNotSupported: 'Browser not supported',
useModernBrowser: 'Please use a modern browser',
cameraStartFailed: 'Failed to start camera',
tryRefreshOrAlternative: 'Try refreshing the page or use another method',
errorFallbackHint: 'You can:',
errorFallbackAlbum: 'Select a QR code image from album',
errorFallbackManual: 'Enter device number manually',
recognizing: 'Recognizing...',
qrNotFound: 'QR code not detected',
recognizeFailed: 'Recognition failed',
h5Only: 'This feature is only available on H5',
deviceNoRequired: 'Please enter device number'
},
guide: {
@@ -112,7 +167,8 @@ export default {
businessHours: 'Business Hours: ',
navigateHere: 'Navigate Here',
coordinateError: 'Invalid location coordinates',
notExist: 'Location does not exist'
notExist: 'Location does not exist',
supportCouponOrMember: 'Coupons & Cards Available'
},
device: {
@@ -129,11 +185,14 @@ export default {
offline: 'Offline',
pricingRules: 'Pricing Rules',
capLimit: ' Cap',
detailBillingByUnit: 'Less than {unit} {minute} is billed as {unit} {minute}. Capped at ¥{cap}. Billing up to ¥{cap} counts as purchase.',
detailBillingIdr: 'Less than 1 {hour} is billed as 1 {hour}. Capped at {cap}. Billing up to {cap} counts as purchase.',
usageInstructions: 'Usage Instructions',
checkBeforeUse: 'Please check if the device is in good condition before use',
autoChargeOvertime: 'Overtime will be charged automatically by hour',
useInDesignatedArea: 'Please use the device in designated area',
rentDepositFree: 'Rent Deposit-free',
rentNow: 'Rent Now',
wxPayScoreDesc: 'WeChat Pay Score | 550+ points enjoy',
checking: 'Checking',
deviceNoNotRecognized: 'Device number not recognized',
@@ -144,18 +203,23 @@ export default {
rentSuccess: 'Rent successful',
rentFailedRetry: 'Rent failed, please retry',
getPayParamsFailed: 'Failed to get payment parameters',
payScoreFailedCancelled: 'Pay score call failed, order cancelled'
payScoreFailedCancelled: 'Pay score call failed, order cancelled',
canUsePromotion: 'Tips: Coupons and membership cards available',
goToBuy: 'Buy Now'
},
order: {
myOrders: 'My Orders',
myDeviceOrders: 'Customize Orders',
noOrderRecord: 'No order records',
getOrderListFailed: 'Failed to get order list',
confirmCancelContent: 'Are you sure to cancel this order?',
confirmDeleteContent: 'Are you sure you want to delete this order?',
orderDetail: 'Order Detail',
orderNo: 'Order No.',
orderStatus: 'Order Status',
deviceNo: 'Device No.',
deviceName: 'Device Name',
rentLocation: 'Rent Location',
rentTime: 'Rent Time',
returnTime: 'Return Time',
@@ -168,6 +232,8 @@ export default {
deposit: 'Deposit',
rentFee: 'Rent Fee',
payNow: 'Pay Now',
myCoupons: 'Coupons',
myCards: 'Member Cards',
cancelOrder: 'Cancel Order',
quickReturn: 'Quick Return',
returnDevice: 'Return Device',
@@ -194,6 +260,8 @@ export default {
depositFree: 'Deposit-free',
whitelistOrder: 'Whitelist Order',
memberOrder: 'Member Order',
aliPay: 'AliPay',
antomPay: 'Antom Pay',
wxPay: 'WeChat Pay',
depositPay: 'Deposit Pay',
paymentInProgress: 'Payment in Progress',
@@ -203,12 +271,21 @@ export default {
returnedThankYou: 'Your fan has been returned, thank you for using',
used: 'Used',
rentInfo: 'Rent Information',
rentInfoExpand: 'Expand',
rentInfoCollapse: 'Collapse',
fanNo: 'Fan No.',
rentMethod: 'Rent Method',
returnLocation: 'Return Location',
paid: 'Paid',
canExpressReturn: ' later for express return',
pauseBilling: 'Pause Billing',
pauseBillingSuccess: 'Billing paused',
pauseBillingFailed: 'Could not pause billing. Please try again.',
pauseBillingNotEligible: 'This order cannot pause billing right now.',
billingPausedBadge: 'Billing paused',
orderStatusBillingPaused: 'Billing paused',
billingPausedDurationLabel: 'Paused for',
pausedExpressAvailable: 'Express return is available',
rentAgain: 'Rent Again',
backToHome: 'Back to Home',
feeAppeal: 'Fee Appeal',
@@ -239,7 +316,37 @@ export default {
paymentMethod: 'Payment Method',
perHour: 'per hour',
perMinute: 'per minute',
perHalfHour: 'per half hour'
perHalfHour: 'per half hour',
deviceNoEject: 'Not Ejected',
returnReminder: 'Return Reminder',
canUsePromotion: 'Coupons & Cards Available',
usedPromotion: 'Promotion Applied',
convertToOwn: 'Don\'t want to return? Convert to own',
convertToOwnTitle: 'Convert to Own',
convertToOwnConfirm: 'Only ¥99 to convert to own. The power bank will be yours. Confirm?',
convertToOwnSuccess: 'Successfully converted to own',
convertToOwnFailed: 'Operation failed, please try again',
convertToOwnConfirmBtn: 'Own It',
convertToOwnCancelBtn: 'Keep Rent',
convertToOwnWithMaxFee: 'Don\'t want to return? Convert to own',
convertToOwnWithMaxFeeTitle: 'Take It Home!',
convertToOwnWithMaxFeeConfirm: 'Since you love using it, buy it out and take it home! Only ¥99, the device is yours forever no return needed~\n✅Type-C charging supported, perfect for home use~\n✅No usage limits after purchase, use it freely!',
convertToOwnWithMaxFeeSuccess: 'Purchase successful',
convertToOwnWithMaxFeeFailed: 'Purchase failed, please try again',
deviceNoEjectTitle: 'Device Not Ejected',
deviceNoEjectConfirm: 'Your power bank didn\'t eject? We will handle it immediately, expected to resolve within 5 minutes.',
deviceNoEjectSuccess: 'Feedback received, will be handled within 5 minutes',
deviceNoEjectFailed: 'Feedback submission failed, please try again',
returnProblemTip: 'After returning, if the order is still active, please go to ',
contactStaff: ' to contact staff.',
returnLocationMap: 'Return Location Map',
deviceEjectWait: 'Please wait ',
deviceEjectRetry: ' seconds before trying again',
deviceEjectProcessing: 'Processing...',
deviceEjectSuccess: 'Device ejection retry initiated',
deviceEjectFailed: 'Operation failed',
deviceEjectError: 'Operation failed, please try again later',
Pause:'Pause'
},
user: {
@@ -253,12 +360,16 @@ export default {
quickReturnDesc: '(View active orders)',
expressReturn: 'Express Return',
myOrders: 'Orders',
myCards: 'My Cards',
myCoupons: 'My Coupons',
customerService: 'Support',
feedback: 'Feedback',
businessLicense: 'License',
cooperation: 'Partner',
settings: 'Settings',
userAgreement: 'Terms',
settinguserAgreement:'Terms',
settinguserprivacyPolicy:'Privacy',
privacyPolicy: 'Privacy',
version: 'v',
logout: 'Logout',
@@ -303,7 +414,22 @@ export default {
phoneSuccess: 'Success',
phoneError: 'Error',
phoneGetFailed: 'Failed',
authCodeFailed: 'Auth failed'
authCodeFailed: 'Auth failed',
phoneLogin: 'Phone Login',
phonePlaceholder: 'Enter phone number',
codePlaceholder: 'Enter verification code',
getCode: 'Get Code',
resend: 'Resend',
loginBtn: 'Login',
phoneRequired: 'Phone required',
phoneInvalid: 'Invalid phone number',
codeRequired: 'Verification code required',
codeSent: 'Code sent',
sendCodeFailed: 'Send code failed',
regionNotSupported: 'Non-mainland China, Hong Kong, Macau users please login via platform phone authorization',
onlyMainlandSupported: 'Currently only Mainland China is supported',
getServicePhoneFailed: 'Failed to get service phone',
noAuthToken: 'Login successful but no credentials obtained'
},
permission: {
@@ -320,6 +446,9 @@ export default {
paymentMethod: 'Method',
wechatPay: 'WeChat',
alipay: 'Alipay',
alipayHk: 'Alipay (Hong Kong)',
alipayId: 'Alipay (Indonesia)',
ALIPAYDANA: 'Alipay DANA',
balance: 'Balance',
payNow: 'Pay',
paying: 'Processing...',
@@ -337,7 +466,9 @@ export default {
package: 'Package',
total: 'Total',
paymentFailedRetry: 'Payment failed, retry?',
createPayOrderFailed: 'Failed'
createPayOrderFailed: 'Failed',
subscriptionSuccess: 'Subscription successful',
subscriptionFailed: 'Subscription failed, please try again'
},
feedback: {
@@ -397,6 +528,7 @@ export default {
phone: 'Phone',
email: 'Email',
workingHours: 'Working Hours',
workingHoursValue: 'Mon-Sun 09:00-22:00',
functionDeveloping: 'Feature in development',
faq1Question: 'How to rent a fan?',
faq1Answer: 'Click "Scan to Rent" on the homepage, scan the QR code on the device with WeChat, and complete payment as prompted.',
@@ -407,7 +539,9 @@ export default {
faq4Question: 'When will deposit be refunded?',
faq4Answer: 'Deposit refund is automatically initiated after returning. It takes 0-7 business days.',
faq5Question: 'What if device doesn\'t work?',
faq5Answer: 'Submit feedback via "My - Feedback", or call customer service directly.'
faq5Answer: 'Submit feedback via "My - Feedback", or call customer service directly.',
pauseBillingButton: 'Pause billing',
pauseBillingHint: 'You may request to pause billing during an active rental (see your order for details).'
},
settings: {
@@ -416,6 +550,8 @@ export default {
languageSetting: 'Language Setting',
chinese: '简体中文',
english: 'English',
indonesian: 'Bahasa Indonesia',
languageSwitched: 'Language switched, refreshing...',
notification: 'Notification',
privacy: 'Privacy',
about: 'About',
@@ -440,7 +576,7 @@ export default {
received: 'Received',
detail: 'Detail',
recipientInfo: 'Ship To',
recipientName: 'FengDianZhe 18163601305',
recipientName: 'Isidaya 18163601305',
recipientAddress: 'Rm 623, Bldg A2, Xinchanghai Park, Luogu St, Yuelu, Changsha, Hunan',
copyAllInfo: 'Copy All',
recipient: 'To',
@@ -518,10 +654,30 @@ export default {
agreement: 'User Agreement',
privacy: 'Privacy Policy',
termsOfService: 'Terms of Service',
termsAndConditions: 'Terms & Conditions',
lastUpdate: 'Last Update',
applicableToService: 'Applicable to "FengDianZhe" shared fan rental service',
applicableToService: 'Applicable to "Isidaya" shared fan rental 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: {
@@ -536,7 +692,7 @@ export default {
},
share: {
title: 'FengDianZhe - Shared Fan & Power Bank',
title: 'Isidaya - Shared Fan & Power Bank',
path: '/pages/index/index'
},
@@ -565,7 +721,8 @@ export default {
tomorrow: 'Tomorrow',
hours: 'hour(s)',
minutes: 'minute(s)',
halfHours: 'half hour(s)'
halfHours: 'half hour(s)',
lessThan: 'less than'
},
unit: {
@@ -640,6 +797,8 @@ export default {
withdrawNotice2: 'Withdrawal expected to arrive within 0-7 business days',
withdrawNotice3: 'If delayed, please contact customer service',
depositRecord: 'Deposit Record',
payRecord: 'Payment Record',
refundRecord: 'Refund Record',
orderNotReturned: 'Current order not returned, please return before withdraw',
alreadyRefunded: 'Deposit already refunded',
refundProcessing: 'Refund processing, please wait'
@@ -663,7 +822,106 @@ export default {
nicknameUpdated: 'Nickname updated successfully',
updateFailed: 'Update failed',
uploading: 'Uploading...'
},
purchase: {
title: 'Offers',
memberCard: 'Member Card',
coupon: 'Coupon',
buyNow: 'Buy Now',
myCards: 'My Cards',
myCoupons: 'My Coupons',
cardDescription: 'Card Description',
couponDescription: 'Coupon Description',
pleaseSelect: 'Please select a product to purchase',
noCards: 'No cards available',
noCoupons: 'No coupons available',
cardUseInstruction: 'Usage Instructions',
cardValidityPeriod: 'Validity Period',
cardRefundPolicy: 'Refund Policy',
cardUseDescription: 'Membership card takes effect immediately after purchase and can be used at designated locations. Per-use cards are charged by number of uses, time-based cards are charged by duration. Please choose the appropriate card type according to your actual needs.',
cardValidityDescription: 'Membership card takes effect from the date of purchase, with validity periods varying by card type. Per-use cards expire when all uses are consumed within the validity period, and time-based cards expire when the accumulated usage duration reaches the limit within the validity period.',
cardRefundDescription: 'Refunds are not supported after purchasing membership cards. Unused portions can continue to be used within the validity period. In case of special circumstances requiring a refund, please contact customer service for processing.',
couponUseInstruction: 'Usage Instructions',
couponValidityPeriod: 'Validity Period',
couponUsageScope: 'Usage Scope',
couponUseDescription: 'Coupons take effect immediately after purchase and can be used at checkout. Only one coupon can be used per order, and coupons cannot be stacked with other promotional offers.',
couponValidityDescription: 'Coupons take effect from the date of purchase, please use within the validity period. After expiration, coupons will automatically become invalid and cannot be extended.',
couponUsageDescription: 'Coupons can be used at designated locations. Please check the coupon details for specific available locations. Some coupons have minimum spending requirements, please pay attention to the usage conditions.'
},
myCard: {
type: 'Type',
timesCard: 'Times Card',
durationCard: 'Duration Card',
remainingTimes: 'Remaining: ',
remainingDuration: 'Remaining Duration',
hours: 'Hours',
validPeriod: 'Valid Period',
active: 'Active',
expired: 'Expired',
used: 'Used Up',
position: 'Usage Location',
price: 'Purchase Price',
noCards: 'No cards',
buyNow: 'Buy Now',
getListFailed: 'Failed to get card list',
dailyLimit: 'Daily Limit',
singleTimeLimit: 'Single Use Limit',
unlimited: 'Unlimited',
times: 'Times',
minutes: 'Minutes',
validWithinDays: 'days valid',
validFromPurchase: 'Valid from purchase',
daysValid: 'days',
currentCycleUsed: 'Current Cycle Used',
totalCount: 'Total Count',
expire: 'Expire',
expiredOn: 'Expired on ',
renew: 'Renew',
toUse: 'Use Now',
onlyForRegionBefore: 'Only for ',
onlyForRegionAfter: ''
},
myCoupon: {
available: 'Available',
used: 'Used',
expired: 'Expired',
useNow: 'Use Now',
usedStatus: 'Used',
expiredStatus: 'Expired',
refundedStatus: 'Refunded',
noAvailableCoupons: 'No available coupons',
noUsedCoupons: 'No used coupons',
noExpiredCoupons: 'No expired coupons',
buyNow: 'Buy Now',
getListFailed: 'Failed to get coupon list',
onlyForRegionBefore: 'Only for ',
onlyForRegionAfter: ''
},
goods: {
title: 'Product Details',
goodsTitle: 'Customize Details',
defaultProductNameShort: 'Isidaya 2026',
defaultProductNameFull: 'Isidaya 2026 Fan, Power Bank & Hand Warmer All-in-One',
productName: 'Isidaya Shared Fan + Power Bank + Hand Warmer Series - Cherry Blossom Pink',
perUnit: '/pc',
buyNow: 'Buy Now',
productDetail: 'Customize Details',
features: {
battery: '8000Ahm',
batteryDesc: 'Large Capacity Battery',
wind: 'Efficient Fan',
temp: 'Smart Temperature',
charge: 'Fast Charging'
},
description: 'Isidaya shared fan, integrating fan, power bank, and hand warmer functions. Equipped with 8000mAh large capacity battery for long-lasting use. Efficient fan design with 3-speed adjustment. Smart temperature control hand warmer, warm in winter and cool in summer. Fast charging technology supports multiple device charging. Cherry blossom pink color, fashionable and beautiful, your best travel companion.',
confirmPurchase: 'Confirm Purchase',
confirmPurchaseContent: 'Confirm to purchase this product for ¥{price}?',
purchaseSuccess: 'Purchase Successful',
purchaseFailed: 'Purchase Failed',
processing: 'Processing...'
}
}
+926
View File
@@ -0,0 +1,926 @@
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',
saveImage: 'Simpan ke Ponsel',
saveSuccess: 'Berhasil disimpan',
saving: 'Menyimpan...',
saveFailed: 'Gagal menyimpan',
downloadFailed: 'Gagal mengunduh',
reset: 'Atur ulang',
preview: 'Pratinjau'
},
nav: {
home: 'Beranda',
my: 'Saya',
orders: 'Pesanan',
settings: 'Pengaturan',
back: 'Kembali',
title: 'Kipas Angin & Power Bank Berbagi Isidaya'
},
app: {
name: 'Isidaya',
slogan: 'Kipas Angin & Power Bank Berbagi',
fullName: 'Isidaya - Kipas Angin & Power Bank Berbagi',
welcome: 'Selamat datang menggunakan Isidaya'
},
home: {
title: 'Kipas Angin & Power Bank Berbagi Isidaya',
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',
h5ScanDesc: 'Pindai kode QR perangkat untuk mulai menggunakan',
h5HowItWorks: 'Cara Penggunaan',
h5StepScan: 'Pindai',
h5StepUnlock: 'Perangkat Keluar',
h5StepReturn: 'Kembalikan',
h5ServiceTitle: 'Layanan',
h5ServiceSupport: 'Dukungan 24/7',
h5ServiceSafePayment: 'Pembayaran Aman',
h5ServiceEasyReturn: 'Pengembalian Mudah',
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',
locationPermissionOffTip: 'Izin lokasi belum diaktifkan. Sementara tidak dapat mencari perangkat terdekat. Jika ingin mencari perangkat terdekat, klik tombol di bawah untuk mengaktifkan lokasi.',
enableLocation: 'Aktifkan Lokasi'
},
scan: {
title: 'Pindai untuk Menggunakan',
album: 'Album',
manualInput: 'Input Manual',
manualInputTitle: 'Masukkan Nomor Perangkat',
deviceNoPlaceholder: 'Masukkan nomor pada perangkat',
initializing: 'Menginisialisasi...',
startingCamera: 'Menghidupkan kamera...',
alignQRCode: 'Letakkan kode QR dalam bingkai',
initFailed: 'Inisialisasi gagal',
browserNotSupportCamera: 'Browser Anda tidak mendukung akses kamera',
containerNotFound: 'Kontainer pemindai tidak ditemukan',
noCameraFound: 'Kamera tidak ditemukan',
ensureCameraExists: 'Pastikan perangkat Anda memiliki kamera',
cameraPermissionDenied: 'Izin kamera ditolak',
cameraPermissionHint: 'Harap izinkan akses kamera di pengaturan browser',
cameraInUse: 'Kamera sedang digunakan',
closeOtherCameraApps: 'Harap tutup aplikasi lain yang menggunakan kamera',
browserNotSupported: 'Browser tidak didukung',
useModernBrowser: 'Harap gunakan browser modern',
cameraStartFailed: 'Gagal menghidupkan kamera',
tryRefreshOrAlternative: 'Coba segarkan halaman atau gunakan cara lain',
errorFallbackHint: 'Anda dapat:',
errorFallbackAlbum: 'Pilih gambar kode QR dari album',
errorFallbackManual: 'Masukkan nomor perangkat secara manual',
recognizing: 'Mengenali...',
qrNotFound: 'Kode QR tidak terdeteksi',
recognizeFailed: 'Pengenalan gagal',
h5Only: 'Fitur ini hanya tersedia di H5',
deviceNoRequired: 'Harap masukkan nomor perangkat'
},
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',
deviceName: 'Nama 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',
detailBillingByUnit: 'Kurang dari {unit} {minute} ditagih sebagai {unit} {minute}. Maksimum ¥{cap}. Penagihan hingga ¥{cap} dianggap sebagai pembelian.',
detailBillingIdr: 'Kurang dari 1 {hour} ditagih sebagai 1 {hour}. Maksimum {cap}. Penagihan hingga {cap} dianggap sebagai pembelian.',
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',
rentNow: 'Sewa Sekarang',
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?',
confirmDeleteContent: 'Apakah Anda yakin ingin menghapus 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',
aliPay: 'Alipay',
antomPay: 'Antom Pay',
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',
rentInfoExpand: 'Buka',
rentInfoCollapse: 'Tutup',
fanNo: 'Nomor Kipas Angin',
rentMethod: 'Metode Penyewaan',
returnLocation: 'Lokasi Pengembalian',
paid: 'Dibayar',
canExpressReturn: 'Dapat dikembalikan melalui ekspres',
pauseBilling: 'Jeda Penagihan',
pauseBillingSuccess: 'Penagihan dijeda',
pauseBillingFailed: 'Gagal menjeda penagihan, coba lagi.',
pauseBillingNotEligible: 'Pesanan ini belum memenuhi syarat jeda penagihan.',
billingPausedBadge: 'Penagihan dijeda',
orderStatusBillingPaused: 'Penagihan dijeda',
billingPausedDurationLabel: 'Dijeda selama',
pausedExpressAvailable: 'Pengembalian ekspres dapat diajukan',
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.',
returnLocationMap: 'Peta Lokasi Pengembalian',
deviceEjectWait: 'Harap tunggu ',
deviceEjectRetry: ' detik sebelum mencoba lagi',
deviceEjectProcessing: 'Memproses...',
deviceEjectSuccess: 'Percobaan pengeluaran perangkat dimulai',
deviceEjectFailed: 'Operasi gagal',
deviceEjectError: 'Operasi gagal, harap coba lagi nanti',
},
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》',
settinguserAgreement:'Perjanjian Pengguna',
settinguserprivacyPolicy:'Kebijakan Privasi',
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',
alipayHk: 'Alipay Hong Kong',
alipayId: 'Alipay Indonesia',
ALIPAYDANA: 'Alipay DANA',
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.',
pauseBillingButton: 'Jeda penagihan',
pauseBillingHint: 'Anda dapat mengajukan jeda penagihan saat sewa aktif (rincian di halaman pesanan).'
},
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: 'Isidaya 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 "Isidaya"',
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: 'Isidaya - 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',
lessThan: 'kurang dari'
},
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',
defaultProductNameShort: 'Isidaya 2026',
defaultProductNameFull: 'Isidaya 2026 Kipas Angin, Power Bank & Hand Warmer 3-in-1',
productName: 'Isidaya 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: 'Isidaya 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 enUS from './en-US.js'
import idID from './id-ID.js'
export default {
'zh-CN': zhCN,
'en-US': enUS
'en-US': enUS,
'id-ID': idID
}
+280 -23
View File
@@ -11,6 +11,7 @@ export default {
filling: '补填中',
save: '保存',
loadFailed: '加载失败',
invalidUrl: '无效的链接',
statusCode: '状态码',
message: '消息',
none: '无',
@@ -48,10 +49,19 @@ export default {
loginRequired: '请先登录',
operationSuccess: '操作成功',
operationFailed: '操作失败',
sending: '发送中...',
loggingIn: '登录中...',
refresh: '刷新',
pull: '下拉刷新',
release: '释放刷新',
noMore: '没有更多了'
noMore: '没有更多了',
functionDeveloping: '功能开发中',
saveImage: '保存到手机',
saveSuccess: '保存成功',
saving: '保存中...',
saveFailed: '保存失败',
reset: '重置',
preview: '预览'
},
nav: {
@@ -60,26 +70,36 @@ export default {
orders: '订单',
settings: '设置',
back: '返回',
title: '风电者共享风扇&暖手充电宝'
title: 'Isidaya共享风扇&暖手充电宝'
},
app: {
name: '风电者',
name: 'Isidaya',
slogan: '共享风扇暖手充电宝',
fullName: '风电者 - 共享风扇暖手充电宝',
welcome: '欢迎使用风电者'
fullName: 'Isidaya - 共享风扇暖手充电宝',
welcome: '欢迎使用Isidaya'
},
home: {
title: '风电者共享风扇&暖手充电宝',
title: 'Isidaya共享风扇&暖手充电宝',
nearbyDevices: '附近设备',
scanToUse: '扫码使用',
personalCenter: '个人中心',
useGuide: '使用指南',
buyDevice: '产品定制',
navigate: '导航',
relocate: '重新定位',
search: '搜索',
service: '客服',
h5ScanDesc: '扫描设备二维码,立即开始使用',
h5HowItWorks: '使用流程',
h5StepScan: '扫码',
h5StepUnlock: '设备弹出',
h5StepReturn: '归还',
h5ServiceTitle: '服务保障',
h5ServiceSupport: '24小时在线客服',
h5ServiceSafePayment: '安全支付',
h5ServiceEasyReturn: '便捷归还',
searchPlaceholder: '搜索附近场地',
nearbyDeviceLocation: '附近设备场地',
noNearbyDevice: '附近暂无设备',
@@ -89,7 +109,41 @@ export default {
invalidQRCode: '无效的设备二维码',
scanFailed: '扫码失败',
noticeTitle: '通知公告',
getLocationFailed: '获取位置失败,显示默认地图'
getLocationFailed: '获取位置失败,显示默认地图',
locationPermissionOffTip: '定位权限未打开,暂无法寻找附近设备。如需寻找附近设备,点击下方按钮开启定位。',
enableLocation: '开启定位'
},
scan: {
title: '扫码使用',
album: '相册',
manualInput: '手动输入',
manualInputTitle: '手动输入设备号',
deviceNoPlaceholder: '请输入设备上的编号',
initializing: '正在初始化...',
startingCamera: '正在启动摄像头...',
alignQRCode: '将二维码放入框内扫描',
initFailed: '初始化失败',
browserNotSupportCamera: '您的浏览器不支持摄像头访问',
containerNotFound: '扫码容器未找到',
noCameraFound: '未找到可用的摄像头',
ensureCameraExists: '请确保设备有摄像头',
cameraPermissionDenied: '摄像头权限被拒绝',
cameraPermissionHint: '请在浏览器设置中允许访问摄像头',
cameraInUse: '摄像头被占用',
closeOtherCameraApps: '请关闭其他使用摄像头的应用',
browserNotSupported: '浏览器不支持',
useModernBrowser: '请使用现代浏览器访问',
cameraStartFailed: '摄像头启动失败',
tryRefreshOrAlternative: '请尝试刷新页面或使用其他方式',
errorFallbackHint: '您可以:',
errorFallbackAlbum: '从相册选择二维码图片',
errorFallbackManual: '手动输入设备号',
recognizing: '正在识别...',
qrNotFound: '未识别到二维码',
recognizeFailed: '识别失败',
h5Only: '该功能仅在H5环境可用',
deviceNoRequired: '请输入设备号'
},
guide: {
@@ -112,7 +166,8 @@ export default {
businessHours: '营业时间:',
navigateHere: '导航去这',
coordinateError: '该场地坐标信息异常',
notExist: '场地不存在'
notExist: '场地不存在',
supportCouponOrMember: '可使用优惠券、会员卡'
},
device: {
@@ -129,11 +184,14 @@ export default {
offline: '离线',
pricingRules: '计费规则',
capLimit: '元封顶',
detailBillingByUnit: '不足{unit}{minute}按{unit}{minute}计费,封顶{cap}元,持续计费至{cap}元视为买断',
detailBillingIdr: '不足1{hour}按1{hour}计费,封顶{cap},持续计费至{cap}视为买断',
usageInstructions: '使用说明',
checkBeforeUse: '请在使用前检查设备是否完好',
autoChargeOvertime: '超出使用时间将自动按小时计费',
useInDesignatedArea: '请在指定区域内使用设备',
rentDepositFree: '免押金租借',
rentNow: '立即租借',
wxPayScoreDesc: '微信支付分 | 550分以上优享',
checking: '检查中',
deviceNoNotRecognized: '未识别到设备编号',
@@ -144,18 +202,23 @@ export default {
rentSuccess: '租借成功',
rentFailedRetry: '租借失败,请重试',
getPayParamsFailed: '获取支付参数失败',
payScoreFailedCancelled: '支付分调用失败,订单已取消'
payScoreFailedCancelled: '支付分调用失败,订单已取消',
canUsePromotion: '提示:可使用优惠券、会员卡',
goToBuy: '去购买'
},
order: {
myOrders: '我的订单',
myDeviceOrders: '我的定制',
noOrderRecord: '暂无订单记录',
getOrderListFailed: '获取订单列表失败',
confirmCancelContent: '确定要取消此订单吗?',
confirmDeleteContent: '确定要删除这个订单吗?',
orderDetail: '订单详情',
orderNo: '订单号',
orderStatus: '订单状态',
deviceNo: '设备号',
deviceName: '设备名称',
rentLocation: '租借地点',
rentTime: '租借时间',
returnTime: '归还时间',
@@ -167,9 +230,11 @@ export default {
payAmount: '支付金额',
deposit: '押金',
rentFee: '租金',
myCards: '会员卡优惠',
myCoupons: '优惠券优惠',
payNow: '立即支付',
cancelOrder: '取消订单',
quickReturn: '快速归还',
quickReturn: '附近可归还',
returnDevice: '归还设备',
viewDetails: '查看详情',
orderCompleted: '订单已完成',
@@ -194,6 +259,8 @@ export default {
depositFree: '免押租借',
whitelistOrder: '白名单订单',
memberOrder: '会员订单',
aliPay: '支付宝支付',
antomPay: '海外支付',
wxPay: '微信支付',
depositPay: '押金租借',
paymentInProgress: '支付中',
@@ -203,12 +270,21 @@ export default {
returnedThankYou: '您的风扇已归还,感谢使用',
used: '已使用',
rentInfo: '租借信息',
rentInfoExpand: '展开',
rentInfoCollapse: '收起',
fanNo: '风扇编号',
rentMethod: '租借方式',
returnLocation: '归还地点',
paid: '已支付',
canExpressReturn: '后可快递归还',
pauseBilling: '暂停计费',
pauseBillingSuccess: '暂停计费已生效',
pauseBillingFailed: '暂停计费失败,请稍后重试',
pauseBillingNotEligible: '当前订单暂不符合暂停计费条件',
billingPausedBadge: '计费已暂停',
orderStatusBillingPaused: '已暂停计费',
billingPausedDurationLabel: '暂停时长',
pausedExpressAvailable: '已可申请快递归还',
rentAgain: '再次租借',
backToHome: '返回首页',
feeAppeal: '费用申诉',
@@ -239,7 +315,37 @@ export default {
paymentMethod: '支付方式',
perHour: '每小时',
perMinute: '每分钟',
perHalfHour: '每半小时'
perHalfHour: '每半小时',
deviceNoEject: '宝未弹出',
returnReminder: '归还提醒',
canUsePromotion: '可使用优惠券、会员卡',
usedPromotion: '优惠类型',
convertToOwn: '不想还了?点击转为自用',
convertToOwnTitle: '转为自用',
convertToOwnConfirm: '仅需花费99元,即可转为自用,充电宝将归您所有,确认操作吗?',
convertToOwnSuccess: '已成功转为自用',
convertToOwnFailed: '操作失败,请稍后重试',
convertToOwnConfirmBtn: '买断自用',
convertToOwnCancelBtn: '继续租借',
convertToOwnWithMaxFee: '不想还了?转为自用',
convertToOwnWithMaxFeeTitle: '买断带回家!',
convertToOwnWithMaxFeeConfirm: '既然用得顺手,直接买断带回家!仅需99元,设备永久归你,无需归还~\n✅支持Type-C充电,居家使用超方便~\n✅买断后无任何使用限制,随心用!',
convertToOwnWithMaxFeeSuccess: '买断成功',
convertToOwnWithMaxFeeFailed: '买断失败,请稍后重试',
deviceNoEjectTitle: '充电宝未弹出',
deviceNoEjectConfirm: '您的充电宝未弹出吗?我们将立即为您处理,预计5分钟内解决问题。',
deviceNoEjectSuccess: '反馈已受理,将在5分钟内处理',
deviceNoEjectFailed: '反馈提交失败,请稍后重试',
returnProblemTip: '产品归还入仓后,订单仍未结束,请前往',
contactStaff: '联系工作人员。',
returnLocationMap: '归还点地图',
deviceEjectWait: '请等待',
deviceEjectRetry: '秒后再试',
deviceEjectProcessing: '处理中...',
deviceEjectSuccess: '已重新尝试弹出设备',
deviceEjectFailed: '操作失败',
deviceEjectError: '操作失败,请稍后重试',
Pause:'暂停计费'
},
user: {
@@ -253,13 +359,17 @@ export default {
quickReturnDesc: '(直接查看使用中的订单)',
expressReturn: '快递归还记录',
myOrders: '我的订单',
myCards: '我的会员卡',
myCoupons: '我的优惠券',
customerService: '客服中心',
feedback: '投诉与建议',
businessLicense: '营业资质',
cooperation: '合作加盟',
cooperation: '信息咨询',
settings: '设置',
userAgreement: '《用户协议》',
settinguserAgreement: '用户协议',
privacyPolicy: '《隐私政策》',
settinguserprivacyPolicy: '隐私政策',
version: 'v',
logout: '退出登录',
confirmLogout: '确认退出登录?',
@@ -303,7 +413,22 @@ export default {
phoneSuccess: '手机号获取成功',
phoneError: '获取手机号异常',
phoneGetFailed: '获取手机号失败',
authCodeFailed: '获取授权码失败'
authCodeFailed: '获取授权码失败',
phoneLogin: '手机号登录',
phonePlaceholder: '请输入手机号码',
codePlaceholder: '请输入验证码',
getCode: '获取验证码',
resend: '重新发送',
loginBtn: '立即登录',
phoneRequired: '请输入手机号',
phoneInvalid: '请输入正确的手机号',
codeRequired: '请输入验证码',
codeSent: '验证码已发送',
sendCodeFailed: '发送验证码失败',
regionNotSupported: '非大陆、香港、澳门用户请通过平台手机号授权登录',
onlyMainlandSupported: '当前仅支持中国大陆地区',
getServicePhoneFailed: '获取客服电话失败',
noAuthToken: '登录成功但未获取到授权凭证'
},
permission: {
@@ -320,6 +445,9 @@ export default {
paymentMethod: '支付方式',
wechatPay: '微信支付',
alipay: '支付宝',
alipayHk: 'Alipay 香港',
alipayId: 'Alipay 印尼',
ALIPAYDANA:'Alipay DANA',
balance: '余额支付',
payNow: '立即支付',
paying: '支付中...',
@@ -337,7 +465,9 @@ export default {
package: '套餐',
total: '合计',
paymentFailedRetry: '支付失败,请重试',
createPayOrderFailed: '创建支付订单失败'
createPayOrderFailed: '创建支付订单失败',
subscriptionSuccess: '订阅成功',
subscriptionFailed: '订阅失败,请稍后重试'
},
feedback: {
@@ -397,6 +527,7 @@ export default {
phone: '电话',
email: '邮箱',
workingHours: '工作时间',
workingHoursValue: '周一至周日 09:00-22:00',
functionDeveloping: '功能开发中',
faq1Question: '如何租借风扇?',
faq1Answer: '点击首页"扫码租借"按钮,使用微信扫描设备上的二维码,按提示完成支付即可使用。',
@@ -407,7 +538,9 @@ export default {
faq4Question: '押金多久能退还?',
faq4Answer: '归还设备后押金将自动发起退款,预计0-7个工作日到账。',
faq5Question: '设备无法正常使用怎么办?',
faq5Answer: '您可以通过"我的-投诉与建议"提交故障反馈,或直接拨打客服电话处理。'
faq5Answer: '您可以通过"我的-投诉与建议"提交故障反馈,或直接拨打客服电话处理。',
pauseBillingButton: '暂停计费',
pauseBillingHint: '使用中有疑问可申请暂停计费(具体以订单页为准)'
},
settings: {
@@ -416,6 +549,8 @@ export default {
languageSetting: '语言设置',
chinese: '简体中文',
english: 'English',
indonesian: 'Bahasa Indonesia',
languageSwitched: '语言已切换,正在刷新...',
notification: '通知',
privacy: '隐私',
about: '关于',
@@ -440,7 +575,7 @@ export default {
received: '已签收',
detail: '详情',
recipientInfo: '收件信息',
recipientName: '风电者 18163601305',
recipientName: 'Isidaya 18163601305',
recipientAddress: '湖南省长沙市岳麓区麓谷街道新长海尖科技园A2栋623',
copyAllInfo: '一键复制全部信息',
recipient: '收件人',
@@ -498,7 +633,7 @@ export default {
},
join: {
title: '合作加盟',
title: '信息咨询',
cooperationTitle: '合作方式',
contactUs: '联系我们',
phone: '联系电话',
@@ -518,10 +653,30 @@ export default {
agreement: '用户协议',
privacy: '隐私政策',
termsOfService: '服务条款',
termsAndConditions: '条款与细则',
lastUpdate: '最后更新',
applicableToService: '适用于"风电者"共享风扇租借服务',
applicableToService: '适用于"Isidaya"共享风扇租借服务',
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: {
@@ -536,7 +691,7 @@ export default {
},
share: {
title: '风电者 - 共享风扇暖手充电宝',
title: 'Isidaya - 共享风扇暖手充电宝',
path: '/pages/index/index'
},
@@ -565,7 +720,8 @@ export default {
tomorrow: '明天',
hours: '小时',
minutes: '分钟',
halfHours: '半小时'
halfHours: '半小时',
lessThan: '小于'
},
unit: {
@@ -640,6 +796,8 @@ export default {
withdrawNotice2: '提现申请提交后预计0-7个工作日到账',
withdrawNotice3: '如超时未收到,请联系客服处理',
depositRecord: '押金记录',
payRecord: '支付记录',
refundRecord: '退还记录',
orderNotReturned: '当前订单尚未归还,请归还后再提现',
alreadyRefunded: '押金已退还,无需重复提现',
refundProcessing: '押金退还处理中,请耐心等待'
@@ -663,7 +821,106 @@ export default {
nicknameUpdated: '昵称修改成功',
updateFailed: '修改失败',
uploading: '上传中...'
},
purchase: {
title: '优惠专区',
memberCard: '会员卡',
coupon: '优惠券',
buyNow: '立即购买',
myCards: '我的会员卡',
myCoupons: '我的优惠券',
cardDescription: '会员卡说明',
couponDescription: '优惠券说明',
pleaseSelect: '请选择要购买的商品',
noCards: '暂无可用会员卡',
noCoupons: '暂无可用优惠券',
cardUseInstruction: '使用说明',
cardValidityPeriod: '有效期限',
cardRefundPolicy: '退款说明',
cardUseDescription: '会员卡购买后即时生效,可在指定场地使用。次卡按使用次数计费,时长卡按使用时长计费,请根据您的实际需求选择合适的卡种。',
cardValidityDescription: '会员卡自购买之日起生效,有效期根据卡种不同而有所区别。次卡在有效期内使用完毕即失效,时长卡在有效期内累计使用时长达到后失效。',
cardRefundDescription: '会员卡购买后不支持退款,未使用部分可以在有效期内继续使用。如遇特殊情况需要退款,请联系客服进行处理。',
couponUseInstruction: '使用说明',
couponValidityPeriod: '有效期限',
couponUsageScope: '使用范围',
couponUseDescription: '优惠券购买后即时生效,可在订单结算时使用。每张订单仅可使用一张优惠券,优惠券不可与其他优惠活动叠加使用。',
couponValidityDescription: '优惠券自购买之日起生效,请在有效期内使用。过期后优惠券将自动失效,不可延期使用。',
couponUsageDescription: '优惠券可在指定场地使用,具体可用场地请查看优惠券详情。部分优惠券有最低消费门槛要求,请注意查看使用条件。'
},
myCard: {
type: '类型',
timesCard: '次卡',
durationCard: '时长卡',
remainingTimes: '剩余次数:',
remainingDuration: '剩余时长',
hours: '小时',
validPeriod: '有效期',
active: '使用中',
expired: '已失效',
used: '已用完',
position: '使用地点',
price: '购买价格',
noCards: '暂无会员卡',
buyNow: '立即购买',
getListFailed: '获取会员卡列表失败',
dailyLimit: '每日限用',
singleTimeLimit: '单次限时',
unlimited: '不限',
times: '次',
minutes: '分钟',
validWithinDays: '天内有效',
validFromPurchase: '从购买时间起',
daysValid: '天有效',
currentCycleUsed: '本周期已使用',
totalCount: '总次数',
expire: '到期',
expiredOn: '失效于',
renew: '续卡',
toUse: '去使用',
onlyForRegionBefore: '仅限',
onlyForRegionAfter: '使用'
},
myCoupon: {
available: '可使用',
used: '已使用',
expired: '已过期',
useNow: '去使用',
usedStatus: '已使用',
expiredStatus: '已过期',
refundedStatus: '已退款',
noAvailableCoupons: '暂无可用优惠券',
noUsedCoupons: '暂无已使用优惠券',
noExpiredCoupons: '暂无已过期优惠券',
buyNow: '立即购买',
getListFailed: '获取优惠券列表失败',
onlyForRegionBefore: '仅限',
onlyForRegionAfter: '使用'
},
goods: {
title: '商品详情',
goodsTitle: '定制详情',
defaultProductNameShort: 'Isidaya 2026新款',
defaultProductNameFull: 'Isidaya 2026新款风扇、充电宝、暖手宝三合一',
productName: 'Isidaya共享风扇 + 充电宝 + 暖手宝系列-樱花粉',
perUnit: '/个',
buyNow: '立即购买',
productDetail: '定制详情',
features: {
battery: '8000Ahm',
batteryDesc: '大容量电池',
wind: '高效风扇',
temp: '智能控温',
charge: '快速充电'
},
description: 'Isidaya共享风扇,集风扇、充电宝、暖手宝三合一功能。采用8000mAh大容量电池,续航持久。高效风扇设计,三档风力可调。智能控温暖手宝,冬暖夏凉。快速充电技术,支持多设备充电。樱花粉配色,时尚美观,是您出行的最佳伴侣。',
confirmPurchase: '确认购买',
confirmPurchaseContent: '确认购买该商品,需支付 ¥{price}?',
purchaseSuccess: '购买成功',
purchaseFailed: '购买失败',
processing: '正在处理...'
}
}
+125 -34
View File
@@ -4,33 +4,125 @@ import { createSSRApp } from 'vue'
import { createI18n } from 'vue-i18n'
import zhCN from './locale/zh-CN.js'
import enUS from './locale/en-US.js'
import idID from './locale/id-ID.js'
import uView from '@climblee/uv-ui'
import { initConsoleControl } from './config/console.js'
import { getSystemConfig } from './config/api/system.js'
// #ifdef H5
// 兼容部分依赖/构建产物在浏览器环境访问 process.env 的场景
if (typeof globalThis !== 'undefined' && typeof globalThis.process === 'undefined') {
globalThis.process = { env: {} }
}
if (typeof globalThis !== 'undefined' && globalThis.process && !globalThis.process.env) {
globalThis.process.env = {}
}
// #endif
// 初始化 console 控制
initConsoleControl()
const LANGUAGE_STORAGE_KEY = 'language'
const SUPPORTED_LANGUAGES = ['zh-CN', 'en-US', 'id-ID']
const LANGUAGE_ALIASES = {
zh: 'zh-CN',
'zh-cn': 'zh-CN',
'zh_cn': 'zh-CN',
en: 'en-US',
'en-us': 'en-US',
'en_us': 'en-US',
id: 'id-ID',
'id-id': 'id-ID',
'id_id': 'id-ID',
in: 'id-ID',
'in-id': 'id-ID',
'in_id': 'id-ID'
}
const normalizeLanguage = (lang) => {
if (!lang || typeof lang !== 'string') return ''
const cleaned = lang.trim()
if (!cleaned) return ''
const lower = cleaned.toLowerCase()
if (LANGUAGE_ALIASES[lower]) return LANGUAGE_ALIASES[lower]
if (SUPPORTED_LANGUAGES.includes(cleaned)) return cleaned
return ''
}
// 检测是否为 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 = () => {
let language = 'zh-CN'
try {
const systemInfo = uni.getSystemInfoSync()
if (systemInfo && systemInfo.language) {
language = systemInfo.language === 'zh' || systemInfo.language.indexOf('zh') === 0
? 'zh-CN'
: 'en-US'
const systemInfo = uni.getSystemInfoSync() || {}
const systemLanguage = normalizeLanguage(systemInfo.language)
if (systemLanguage) {
language = systemLanguage
} else if (isH5Platform() && typeof navigator !== 'undefined') {
const browserLanguage = normalizeLanguage(navigator.language || '')
if (browserLanguage) language = browserLanguage
}
} catch (e) {
console.error('获取系统语言失败:', e)
language = 'zh-CN'
}
return language
}
const extractLanguageFromConfig = (data) => {
if (!data) return ''
if (typeof data === 'string') {
return normalizeLanguage(data)
}
if (Array.isArray(data)) {
for (const item of data) {
const fromItem = extractLanguageFromConfig(item)
if (fromItem) return fromItem
}
return ''
}
if (typeof data === 'object') {
const direct = normalizeLanguage(
data.language || data.lang || data.locale || data.defaultLanguage || data.defaultLang
)
if (direct) return direct
for (const [key, value] of Object.entries(data)) {
const keyLower = String(key).toLowerCase()
if (keyLower.includes('lang') || keyLower.includes('locale')) {
const parsed = extractLanguageFromConfig(value)
if (parsed) return parsed
}
}
}
return ''
}
// 获取用户选择的语言
const getSavedLanguage = () => {
try {
const savedLang = uni.getStorageSync('language')
const savedLang = normalizeLanguage(uni.getStorageSync(LANGUAGE_STORAGE_KEY))
if (savedLang) {
return savedLang
}
const systemLang = getSystemLanguage()
uni.setStorageSync('language', systemLang)
uni.setStorageSync(LANGUAGE_STORAGE_KEY, systemLang)
return systemLang
} catch (e) {
console.error('语言设置出错:', e)
@@ -45,10 +137,6 @@ function getI18nInstance() {
// 每次都重新读取当前语言
const currentLang = getSavedLanguage()
console.log('=== getI18nInstance 被调用 ===')
console.log('缓存中的语言:', currentLang)
console.log('当前 i18n 实例存在?', !!i18nInstance)
console.log('当前 i18n.global.locale:', i18nInstance?.global?.locale)
// 检查是否需要更新语言
if (i18nInstance && i18nInstance.global.locale !== currentLang) {
@@ -59,44 +147,51 @@ function getI18nInstance() {
// 直接更新 locale(这应该会触发所有组件重新渲染)
i18nInstance.global.locale = currentLang
console.log('i18n.global.locale 已更新为:', i18nInstance.global.locale)
console.log('测试翻译 (common.loading):', i18nInstance.global.t('common.loading'))
console.log('测试翻译 (home.title):', i18nInstance.global.t('home.title'))
console.log('===================================')
return i18nInstance
}
// 首次创建实例
if (!i18nInstance) {
console.log('=== 首次创建 i18n 实例 ===')
i18nInstance = createI18n({
legacy: true, // 使用 Legacy API 模式,支持全局 $t
locale: currentLang,
fallbackLocale: 'zh-CN',
messages: {
'zh-CN': zhCN,
'en-US': enUS
'en-US': enUS,
'id-ID': idID
},
silentTranslationWarn: true,
silentFallbackWarn: true
})
console.log('i18n 实例已创建,语言:', currentLang)
console.log('测试翻译 (common.loading):', i18nInstance.global.t('common.loading'))
console.log('测试翻译 (home.title):', i18nInstance.global.t('home.title'))
console.log('============================')
}
return i18nInstance
}
const syncLanguageFromRemoteConfig = async (i18n) => {
if (!isH5Platform()) return
try {
const res = await getSystemConfig()
if (!res || res.code !== 200) return
const remoteLang = extractLanguageFromConfig(res.data)
if (!remoteLang) return
const current = normalizeLanguage(i18n?.global?.locale || '')
if (current !== remoteLang) {
uni.setStorageSync(LANGUAGE_STORAGE_KEY, remoteLang)
i18n.global.locale = remoteLang
console.log('H5 语言已按系统配置更新为:', remoteLang)
}
} catch (e) {
console.warn('读取系统配置语言失败,使用本地语言设置:', e)
}
}
export function createApp() {
console.log('========================================')
console.log('=== createApp 被调用 ===')
console.log('时间戳:', new Date().toLocaleTimeString())
console.log('========================================')
const app = createSSRApp(App)
@@ -109,6 +204,9 @@ export function createApp() {
// 使用 i18n
app.use(i18n)
// H5 端通过系统配置同步语言(异步,不阻塞应用启动)
syncLanguageFromRemoteConfig(i18n)
// 手动注入 $i18n 到全局属性(确保组件可以访问)
app.config.globalProperties.$i18n = i18n.global
@@ -126,13 +224,6 @@ export function createApp() {
}
}
console.log('=== Vue 3 应用创建完成 ===')
console.log('最终 locale:', i18n.global.locale)
console.log('app.config.globalProperties.$t 存在?', !!app.config.globalProperties.$t)
console.log('app.config.globalProperties.$i18n 存在?', !!app.config.globalProperties.$i18n)
console.log('测试 $t 调用:', i18n.global.t('common.loading'))
console.log('===========================')
return {
app
}
+17
View File
@@ -69,6 +69,9 @@
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
},
"mp-alipay" : {
"component2" : true,
"transpile" : [ "uview-ui", "vue-i18n" ],
"skia" : true,
"usingComponents" : true,
"appid" : "2021006117693332",
"unipush" : {
@@ -81,6 +84,20 @@
"mp-toutiao" : {
"usingComponents" : true
},
"h5" : {
"sdkConfigs" : {
"maps" : {
"qqmap" : {
"key" : "DJQBZ-WB53Q-WPS5B-4S6J7-53RMS-X4FJ2"
}
}
},
"router" : {
"mode" : "history",
"base" : "/"
},
"title" : "Isidaya"
},
"uniStatistics" : {
"enable" : false
},
+1
View File
@@ -3,6 +3,7 @@
"@climblee/uv-ui": "^1.1.20",
"axios": "^1.7.9",
"axios-miniprogram-adapter": "0.3.4",
"html5-qrcode": "^2.3.8",
"uniapp-axios-adapter": "^0.3.2",
"uview-ui": "1.8.8",
"vue-i18n": "9"
+319 -185
View File
@@ -5,7 +5,8 @@
"^uv-(.*)": "@climblee/uv-ui/components/uv-$1/uv-$1.vue"
}
},
"pages": [{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "",
@@ -15,147 +16,6 @@
"enableShareTimeline": true
}
},
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/legal/agreement",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/legal/agreement-zh",
"style": {
"navigationBarTitleText": "用户协议",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/legal/agreement-en",
"style": {
"navigationBarTitleText": "User Agreement",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/legal/privacy",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/legal/privacy-zh",
"style": {
"navigationBarTitleText": "隐私政策",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/legal/privacy-en",
"style": {
"navigationBarTitleText": "Privacy Policy",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/my/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#D1FFE1",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/userProfile/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#D1FFE1",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/setting/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/deposit/index",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/order/index",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/order/payment",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/expressReturn/addExpressReturn",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/feedback/index",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/feedback/list",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/feedback/detail",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/help/index",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/device/detail",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/serve/bagCheck/index",
"style": {
@@ -163,23 +23,15 @@
}
},
{
"path": "pages/return/index",
"path": "pages/scan/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/order/success",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/order/return-success",
"path": "pages/device/detail",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
@@ -194,21 +46,6 @@
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/expressReturn/index",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "default"
}
},
{
"path": "pages/expressReturn/detail",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/search/index",
"style": {
@@ -217,22 +54,6 @@
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/position/detail",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#D1FFE1",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/join/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/waiting/index",
"style": {
@@ -240,6 +61,319 @@
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "toProgram",
"style": {
"navigationStyle": "custom"
}
}
],
"subPackages": [
{
"root": "subPackages/user",
"pages": [
{
"path": "login/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#C8F4D9",
"navigationBarTextStyle": "black"
}
},
{
"path": "login/phone",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#C8F4D9",
"navigationBarTextStyle": "black"
}
},
{
"path": "my/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#D1FFE1",
"navigationBarTextStyle": "black"
}
},
{
"path": "userProfile/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#D1FFE1",
"navigationBarTextStyle": "black"
}
},
{
"path": "setting/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "user/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
}
]
},
{
"root": "subPackages/order",
"pages": [
{
"path": "index",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "payment",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "success",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "return-success",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "return-map",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "white"
}
}
]
},
{
"root": "subPackages/service",
"pages": [
{
"path": "feedback/index",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "feedback/list",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "feedback/detail",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "help/index",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "expressReturn/index",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "default"
}
},
{
"path": "expressReturn/addExpressReturn",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "expressReturn/detail",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "return/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
}
]
},
{
"root": "subPackages/business",
"pages": [
{
"path": "purchase/index",
"style": {
"navigationBarTitleText": "优惠专区",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "my-card",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "my-coupon",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "position/detail",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#D1FFE1",
"navigationBarTextStyle": "black"
}
},
{
"path": "device-goods",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "device-orderList",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "device-orderDetail",
"style": {
"navigationBarTitleText": "定制详情",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
}
]
},
{
"root": "subPackages/other",
"pages": [
{
"path": "legal/agreement",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "legal/agreement-zh",
"style": {
"navigationBarTitleText": "用户协议",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "legal/agreement-en",
"style": {
"navigationBarTitleText": "User Agreement",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "legal/privacy",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "legal/privacy-zh",
"style": {
"navigationBarTitleText": "隐私政策",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "legal/privacy-en",
"style": {
"navigationBarTitleText": "Privacy Policy",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "legal/terms",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "join/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "webview/index",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
}
},
{
"path": "serve/bagCheck/index",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "deposit/index",
"style": {
"navigationBarTitleText": ""
}
}
]
}
],
"globalStyle": {
-341
View File
@@ -1,341 +0,0 @@
<template>
<view class="deposit-container">
<!-- 押金金额卡片 -->
<view class="deposit-card">
<view class="title">{{ $t('deposit.depositBalance') }}</view>
<view class="amount">¥{{ depositAmount }}</view>
<button class="withdraw-btn" @click="handleWithdraw" :disabled="depositAmount <= 0">{{ $t('deposit.withdraw') }}</button>
</view>
<!-- 提现说明 -->
<view class="notice-card">
<view class="notice-title">
<view class="dot"></view>
<text>{{ $t('deposit.withdrawNotice') }}</text>
</view>
<view class="notice-content">
<view class="notice-item">1. {{ $t('deposit.withdrawNotice1') }}</view>
<view class="notice-item">2. {{ $t('deposit.withdrawNotice2') }}</view>
<view class="notice-item">3. {{ $t('deposit.withdrawNotice3') }}</view>
</view>
</view>
<!-- 押金记录 -->
<view class="record-card" v-if="records.length > 0">
<view class="record-title">{{ $t('deposit.depositRecord') }}</view>
<view class="record-list">
<view class="record-item" v-for="(item, index) in records" :key="index">
<view class="record-info">
<text class="record-type">{{ item.type }}</text>
<text class="record-time">{{ item.time }}</text>
</view>
<text class="record-amount" :class="item.type === '退还' ? 'refund' : ''">
{{ item.type === '退还' ? '+' : '-' }}¥{{ item.amount }}
</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { getUserInfo } from '../../util/index.js'
import { withdrawDeposit } from '../../config/api/user.js'
import { queryById } from '../../config/api/order.js'
export default {
data() {
return {
depositAmount: '0.00',
orderNo: '',
records: [],
orderId:''
}
},
onLoad() {
// 设置页面标题
uni.setNavigationBarTitle({
title: this.$t('deposit.title')
})
// this.loadUserInfo()
},
onShow() {
this.loadUserInfo()
},
methods: {
async loadUserInfo() {
try {
const res = await getUserInfo()
console.log('loadUserInfo',res);
if (res.code === 200) {
this.depositAmount = res.data.balanceAmount || '0.00'
this.orderNo = res.data.latestOrderNo || ''
this.orderId = res.data.latestOrderId||''
// 如果存在余额,获取押金记录
if (parseFloat(this.depositAmount) > 0 && this.orderNo) {
this.records = [
{
type: '支付',
time: this.formatDate(new Date()),
amount: this.depositAmount
}
]
} else {
this.records = []
}
}
} catch (error) {
console.error('获取用户信息失败:', error)
uni.showToast({
title: this.$t('user.getUserInfoFailed'),
icon: 'none'
})
}
},
async handleWithdraw() {
if (parseFloat(this.depositAmount) <= 0) {
uni.showToast({
title: this.$t('deposit.noBalance'),
icon: 'none'
})
return
}
if(this.orderId.length!=0||this.orderNo.length!=0){
const res = await queryById(Number(this.orderId))
console.log(res);
}
// if(this.orderNo.length!=0){
// uni.showToast({
// title:'当前存在进行中的订单',
// icon:'none'
// })
// return
// }
uni.showModal({
title: this.$t('deposit.confirmWithdraw'),
content: this.$t('deposit.withdrawDesc'),
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: this.$t('deposit.withdrawing')
})
try {
console.log('发起提现请求,订单号:', this.orderNo)
const result = await withdrawDeposit(this.orderNo)
console.log('提现响应:', result)
if (result.code === 200) {
uni.hideLoading()
uni.showToast({
title: this.$t('deposit.withdrawSubmitted'),
icon: 'success'
})
// 更新余额为0
this.depositAmount = '0.00'
this.records.push({
type: '退还',
time: this.formatDate(new Date()),
amount: this.depositAmount
})
// 重新加载用户信息
setTimeout(() => {
this.loadUserInfo()
}, 1500)
} else {
throw new Error(result.msg || this.$t('deposit.withdrawFailed'))
}
} catch (error) {
console.error('提现失败:', error)
uni.hideLoading()
// 更详细的错误处理
let errorMessage = this.$t('deposit.withdrawFailed');
// 如果有具体错误信息,使用它
if (error.message) {
// 常见错误消息处理
if (error.message.includes('尚未归还')) {
errorMessage = this.$t('deposit.orderNotReturned');
} else if (error.message.includes('已退还')) {
errorMessage = this.$t('deposit.alreadyRefunded');
} else if (error.message.includes('处理中')) {
errorMessage = this.$t('deposit.refundProcessing');
} else if (error.message.includes('余额为0')) {
errorMessage = this.$t('deposit.noBalance');
} else {
// 使用后端返回的具体错误消息
errorMessage = error.message;
}
}
// 显示错误提示
uni.showModal({
title: this.$t('deposit.withdrawFailed'),
content: errorMessage,
showCancel: false
})
}
}
}
})
},
formatDate(date) {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
}
}
</script>
<style lang="scss" scoped>
.deposit-container {
min-height: 100vh;
background: #f8f8f8;
padding: 30rpx;
.deposit-card {
background: linear-gradient(135deg, #1976D2, #64B5F6);
border-radius: 20rpx;
padding: 40rpx;
color: #fff;
text-align: center;
box-shadow: 0 4rpx 20rpx rgba(25,118,210,0.2);
.title {
font-size: 28rpx;
opacity: 0.9;
margin-bottom: 20rpx;
}
.amount {
font-size: 72rpx;
font-weight: bold;
margin-bottom: 40rpx;
}
.withdraw-btn {
background: #fff;
color: #1976D2;
width: 80%;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 32rpx;
font-weight: 500;
margin: 0 auto;
&:active {
transform: scale(0.98);
}
&[disabled] {
background: rgba(255,255,255,0.6);
color: rgba(25,118,210,0.5);
}
}
}
.notice-card {
margin-top: 30rpx;
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
.notice-title {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.dot {
width: 12rpx;
height: 12rpx;
background: #1976D2;
border-radius: 50%;
margin-right: 10rpx;
}
text {
font-size: 30rpx;
font-weight: 500;
color: #333;
}
}
.notice-content {
.notice-item {
font-size: 26rpx;
color: #666;
line-height: 1.8;
padding-left: 22rpx;
}
}
}
.record-card {
margin-top: 30rpx;
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
.record-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 20rpx;
border-left: 8rpx solid #1976D2;
padding-left: 20rpx;
}
.record-list {
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.record-info {
.record-type {
font-size: 28rpx;
color: #333;
margin-bottom: 6rpx;
display: block;
}
.record-time {
font-size: 24rpx;
color: #999;
}
}
.record-amount {
font-size: 32rpx;
color: #333;
font-weight: 500;
&.refund {
color: #4CAF50;
}
}
}
}
}
}
</style>
+291 -90
View File
@@ -1,16 +1,25 @@
<template>
<view class="container">
<!-- 骨架屏 -->
<DeviceDetailSkeleton v-if="loading&&!deviceInfo" />
<!-- 实际内容 -->
<view v-else>
<!-- 设备信息卡片 -->
<view class="card device-info-card">
<view class="device-location">
<view class="location-left">
<image src="/static/images/location-map.svg" mode="aspectFit" class="location-icon"></image>
<image src="/static/device_location.png" mode="aspectFit" class="location-icon" lazy-load="true"></image>
<text class="location-name">{{ deviceLocation }}</text>
</view>
<view class="device-status" :class="deviceStatus.class">
<text class="status-text">{{ deviceStatus.text }}</text>
</view>
</view>
<view class="device-id">
<text class="id-label">{{ $t('order.deviceName') }}</text>
<text class="id-value">{{ deviceInfo.name }}</text>
</view>
<view class="device-id">
<text class="id-label">{{ $t('device.deviceNo') }}</text>
<text class="id-value">{{ deviceId }}</text>
@@ -25,12 +34,12 @@
<view class="pricing-banner">
<view class="pricing-main">
<text class="price-symbol">¥</text>
<text class="price">{{ deviceFeeConfig.maxHourPrice || '5.00' }}</text>
<text class="price-symbol">{{ displayCurrencySymbol }}</text>
<text class="price">{{ displayHourlyPrice }}</text>
<text class="unit">/{{ getPriceUnit() }}</text>
</view>
<view class="cap-badge">
<text class="cap-text">{{ deviceInfo.depositAmount || '99' }}{{ $t('device.capLimit') }}</text>
<text class="cap-text">{{ displayDepositCap }}{{ $t('device.capLimit') }}</text>
</view>
</view>
@@ -69,14 +78,25 @@
</view>
</view>
<view class="promotion-tip" @click="goToPurchase">
<view class="tip-left">
<text class="tip-text">{{ $t('device.canUsePromotion') }}</text>
</view>
<view class="tip-right">
<text class="buy-text">{{ $t('device.goToBuy') }}</text>
<image src="/static/gotoBuy.png" mode="aspectFit" class="arrow-icon"></image>
</view>
</view>
<!-- 底部操作区 -->
<view class="footer">
<button class="rent-button" :class="{ 'return-button': hasActiveOrder }"
@click="handleRent('wx-score-pay')">
<text>{{ hasActiveOrder ? $t('order.returnDevice') : $t('device.rentDepositFree') }}</text>
</button>
<view class="wechat-credit">
<image src="/static/images/wxpayflag.png" mode="aspectFit" class="wx-icon"></image>
<view class="rent-button" :class="{ 'return-button': hasActiveOrder }"
@click="handleRent">
<text>{{ hasActiveOrder ? $t('order.returnDevice') : getRentButtonText() }}</text>
</view>
<!-- 微信支付分标识仅在微信小程序环境显示 -->
<view class="wechat-credit" v-if="isWechatMiniProgram">
<image src="/static/images/wxpayflag.png" mode="aspectFit" class="wx-icon" lazy-load="true"></image>
<text class="credit-text">{{ $t('device.wxPayScoreDesc') }}</text>
</view>
</view>
@@ -102,16 +122,19 @@
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
reactive,
computed,
onMounted
} from 'vue'
import {
onLoad
onLoad,
onUnload
} from '@dcloudio/uni-app'
import {
getDeviceInfo,
@@ -120,7 +143,9 @@
import {
getOrderByOrderNoScore,
getOrderByOrderNo,
cancelOrder
cancelOrder,
getInUseOrder,
getUnpaidOrder
} from '@/config/api/order.js'
import {
initiateWeChatScorePayment,
@@ -128,14 +153,17 @@
getUserPhoneNumber
} from '@/util/index.js'
import {
useI18n
useI18n,
showModalI18n
} from '@/utils/i18n.js'
import DeviceDetailSkeleton from '@/components/DeviceDetailSkeleton.vue'
const {
t: $t
t
} = useI18n()
// 响应式状态
const loading = ref(true)
const deviceInfo = ref({})
const deviceId = ref('')
const deviceFeeConfig = ref({})
@@ -143,15 +171,37 @@
const deviceLocation = ref('一号教学楼大厅')
const hasActiveOrder = ref(false)
const deviceStatus = reactive({
text: $t('device.available'),
text: t('device.available'),
class: 'available'
})
const isLoggedIn = ref(true)
const phoneNumber = ref('')
const showPhoneAuthPopup = ref(false)
const isWechatMiniProgram = ref(false)
const isAlipayMiniProgram = ref(false)
const isH5 = ref(false)
// 生命周期 onLoad 钩子
onLoad(async (options) => {
// 普通链接二维码进入时,参数通常在 options.q(且为编码后的完整 URL
if (!options.deviceNo && options.q) {
const fullUrl = decodeURIComponent(options.q)
const queryStr = fullUrl.includes('?') ? fullUrl.split('?')[1] : ''
if (queryStr) {
const params = queryStr.split('&').reduce((acc, pair) => {
if (!pair) return acc
const idx = pair.indexOf('=')
const rawKey = idx >= 0 ? pair.slice(0, idx) : pair
const rawVal = idx >= 0 ? pair.slice(idx + 1) : ''
const key = decodeURIComponent(rawKey || '').trim()
const val = decodeURIComponent(rawVal || '').trim()
if (key) acc[key] = val
return acc
}, {})
if (params.deviceNo) options.deviceNo = params.deviceNo
}
}
if (options.deviceNo != uni.getStorageSync('deviceId') || !uni.getStorageSync('deviceId')) {
deviceId.value = options.deviceNo
uni.setStorageSync('deviceId', options.deviceNo)
@@ -164,12 +214,43 @@
onMounted(async () => {
uni.setNavigationBarTitle({
title: $t('device.deviceInfo')
title: t('device.deviceInfo')
})
// 检测当前运行环境:微信小程序 / 支付宝小程序 / H5
// #ifdef MP-WEIXIN
isWechatMiniProgram.value = true
isAlipayMiniProgram.value = false
isH5.value = false
// #endif
// #ifdef MP-ALIPAY
isWechatMiniProgram.value = false
isAlipayMiniProgram.value = true
isH5.value = false
// #endif
// #ifdef H5
isWechatMiniProgram.value = false
isAlipayMiniProgram.value = false
isH5.value = true
// #endif
await checkUserPhone()
await fetchDeviceInfo()
})
// 页面卸载时设置默认启动路径为首页(仅在非下单流程时生效)
onUnload(() => {
// 如果是下单流程跳转(在提交订单时设置了标记),则本次不设置启动路径
const skipSetLaunchPathOnce = uni.getStorageSync('skipSetLaunchPathOnce')
if (skipSetLaunchPathOnce) {
console.log('下单流程离开设备详情页,本次不设置启动路径')
uni.removeStorageSync('skipSetLaunchPathOnce')
return
}
// 正常离开设备详情页(比如返回、关闭小程序)时,记录启动路径为首页
uni.setStorageSync('launchPath', '/pages/index/index')
console.log('设备详情页卸载,已设置启动路径为首页')
})
const checkUserPhone = async () => {
try {
const userInfoRes = await getUserInfo()
@@ -193,7 +274,7 @@
// 用户拒绝授权的情况
if (e.detail.errMsg && e.detail.errMsg.includes('deny')) {
uni.showToast({
title: $t('auth.phoneRequired'),
title: t('auth.phoneRequired'),
icon: 'none'
})
return
@@ -201,9 +282,9 @@
// 获取到授权code
if (e.detail.code) {
uni.showLoading({
title: $t('auth.getting')
})
// uni.showLoading({
// title: t('auth.getting')
// })
console.log('获取到的授权code:', e.detail.code)
@@ -212,12 +293,12 @@
getUserPhoneNumber(e.detail.code)
.then(res => {
console.log('获取手机号API响应原始数据:', JSON.stringify(res))
uni.hideLoading()
// uni.hideLoading()
// 不立即抛出错误,而是记录问题并继续处理
if (!res) {
console.error('API返回数据为空')
uni.showModal({
showModalI18n({
title: '数据异常',
content: 'API返回为空',
showCancel: false
@@ -234,43 +315,43 @@
showPhoneAuthPopup.value = false
uni.showToast({
title: $t('auth.phoneSuccess'),
title: t('auth.phoneSuccess'),
icon: 'success'
})
} else {
// 记录详细信息,不抛出错误
console.warn('获取手机号响应异常:', res.msg || '未知错误')
uni.showModal({
title: $t('auth.phoneError'),
content: `${$t('common.statusCode')}: ${res.code}, ${$t('common.message')}: ${res.msg || $t('common.none')}`,
showModalI18n({
title: t('auth.phoneError'),
content: `${t('common.statusCode')}: ${res.code}, ${t('common.message')}: ${res.msg || t('common.none')}`,
showCancel: false
})
}
})
.catch(err => {
uni.hideLoading()
// uni.hideLoading()
console.error('获取手机号码失败(catch):', err)
// 显示更详细的错误信息
let errMsg = err.message || err.toString()
uni.showModal({
title: $t('auth.phoneGetFailed'),
content: $t('common.errorInfo') + ': ' + errMsg,
showModalI18n({
title: t('auth.phoneGetFailed'),
content: t('common.errorInfo') + ': ' + errMsg,
showCancel: false
})
})
} catch (outerError) {
uni.hideLoading()
// uni.hideLoading()
console.error('获取手机号外部错误:', outerError)
uni.showModal({
title: $t('common.unexpectedError'),
content: $t('common.processException') + ': ' + (outerError.message || outerError),
showModalI18n({
title: t('common.unexpectedError'),
content: t('common.processException') + ': ' + (outerError.message || outerError),
showCancel: false
})
}
} else {
uni.showToast({
title: $t('auth.authCodeFailed'),
title: t('auth.authCodeFailed'),
icon: 'none'
})
}
@@ -278,6 +359,9 @@
// 检查登录状态和订单
const fetchDeviceInfo = async () => {
try {
loading.value = true
// console.log(deviceId.value);
const res = await getDeviceInfo(deviceId.value)
if (res.code == 200) {
deviceInfo.value = res.data.device || {}
@@ -298,10 +382,10 @@
// 更新设备状态
if (deviceInfo.value.status) {
if (deviceInfo.value.status === 'online') {
deviceStatus.text = $t('device.available')
deviceStatus.text = t('device.available')
deviceStatus.class = 'available'
} else if (deviceInfo.value.status === 'offline') {
deviceStatus.text = $t('device.offline')
deviceStatus.text = t('device.offline')
deviceStatus.class = 'offline'
}
}
@@ -314,59 +398,78 @@
maxHourPrice: '5.00',
}
}
}else{
// uni.reLaunch({
// url:'/pages/index/index'
// })
}
}catch(error){
console.error('获取设备信息失败:', error)
}
finally {
}
}
// 显示登录提示
const showLoginTip = () => {
uni.showModal({
title: $t('common.tips'),
content: $t('common.loginRequired'),
confirmText: $t('auth.goToLogin'),
showModalI18n({
title: t('common.tips'),
content: t('common.loginRequired'),
confirmText: t('auth.goToLogin'),
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/pages/login/index'
url: '/subPackages/user/login/index'
})
}
}
})
}
// 跳转到优惠专区
const goToPurchase = () => {
const positionId = positionInfo.value?.positionId || positionInfo.value?.id
uni.navigateTo({
url: `/subPackages/business/purchase/index?positionId=${positionId}`
})
}
// 检查订单状态
const checkOrderStatus = async () => {
try {
// 调用接口检查是否有进行中的订单
const result = await uni.$api.checkActiveOrder()
const inUseRes = await getInUseOrder()
if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
const order = inUseRes.data
// 如果有正在进行的订单,跳转到归还页面,带上设备ID
uni.redirectTo({
url: `/pages/order/detail?orderId=${order.orderId}`
})
return
}
if (result.hasOrder) {
const order = result.order // 假设后端返回 order 对象
// 检查订单状态
if (order.status === 'waiting_for_payment') {
// 检查是否有待支付的订单
const unpaidRes = await getUnpaidOrder()
if (unpaidRes && unpaidRes.code === 200 && unpaidRes.data) {
const order = unpaidRes.data
// 跳转支付页面,带上订单ID
uni.redirectTo({
url: `/pages/order/payment?orderId=${order.orderId}&deviceId=${deviceId.value}`
})
} else if (order.status === 'in_used') {
// 如果有正在进行的订单,跳转到归还页面,带上设备ID
uni.redirectTo({
url: `/pages/device/return?deviceId=${deviceId.value}`
})
}
}
} catch (error) {
console.error('检查订单状态失败:', error)
uni.showToast({
title: $t('order.getOrderStatusFailed'),
title: t('order.getOrderStatusFailed'),
icon: 'none'
})
}
}
// 处理租借操作
const handleRent = (payWay) => {
const handleRent = () => {
if (!isLoggedIn.value) {
showLoginTip()
return
@@ -378,12 +481,26 @@
return
}
// 提交订单
submitRentOrder(payWay)
// 根据运行环境选择不同的租借/支付流程
// 微信小程序:走微信支付分免押租借
if (isWechatMiniProgram.value) {
submitRentOrder('wx-score-pay')
return
}
// 支付宝小程序:走押金租借,后续在支付页内调起支付宝支付
if (isAlipayMiniProgram.value) {
submitRentOrder('wx-pay')
return
}
// H5 等其他环境:统一走押金租借,支付页内根据平台选择支付方式(Antom 等)
submitRentOrder('wx-pay')
}
// 获取价格单位文本
const getPriceUnit = () => {
if (isIdrCurrency.value) return t('time.hour')
console.log(deviceInfo.value);
// 按分钟计费
if (deviceInfo.value && deviceInfo.value.feeType === 'minute') {
@@ -392,7 +509,7 @@
return '30分钟'
}
// 按小时计费(默认)
return $t('time.hour')
return t('time.hour')
}
// 计算计费单位时间(分钟)
@@ -432,6 +549,9 @@
// 生成计费说明文本
const getPricingInfoText = () => {
if (isIdrCurrency.value) {
return `${displayCurrencySymbol.value}${displayHourlyPrice.value}/${t('time.hour')}`
}
const unitPrice = getBillingUnitPrice()
const maxHourPrice = deviceFeeConfig.value.maxHourPrice || '5'
@@ -447,24 +567,52 @@
// 生成详细说明文本
const getDetailInfoText = () => {
const freeMinutes = getFreeMinutes()
if (isIdrCurrency.value) {
const cap = `${displayCurrencySymbol.value}${displayDepositCap.value}`
return t('device.detailBillingIdr', {
hour: t('time.hour'),
cap
})
}
const unitMinutes = getBillingUnitMinutes()
const depositAmount = deviceInfo.value.depositAmount || '99'
// 按分钟计费
if (deviceInfo.value && deviceInfo.value.feeType === 'minute') {
return `不足${unitMinutes}分钟按${unitMinutes}分钟计费,封顶${depositAmount}元,持续计费至${depositAmount}元视为买断`
return t('device.detailBillingByUnit', {
unit: unitMinutes,
minute: t('time.minute'),
cap: depositAmount
})
}
// 按小时计费
return `不足${unitMinutes}分钟按${unitMinutes}分钟计费,封顶${depositAmount}元,持续计费至${depositAmount}元视为买断`
// 获取租借按钮文本
const getRentButtonText = () => {
if (isWechatMiniProgram.value) {
return t('device.rentDepositFree')
} else {
return t('device.rentNow')
}
}
const currencyCode = computed(() => {
return (positionInfo.value?.currency || '').toUpperCase()
})
const isIdrCurrency = computed(() => currencyCode.value === 'IDR')
const displayCurrencySymbol = computed(() => (isIdrCurrency.value ? 'Rp ' : '¥'))
const displayHourlyPrice = computed(() => {
return deviceFeeConfig.value.maxHourPrice || '5.00'
})
const displayDepositCap = computed(() => {
return deviceInfo.value.depositAmount || '99'
})
// 提交租借订单
const submitRentOrder = async (payWay) => {
try {
uni.showLoading({
title: $t('common.processing')
title: t('common.processing')
})
// --- 第一步:先请求订阅消息(必须在用户点击的同步上下文中)---
if (payWay === 'wx-score-pay') {
@@ -495,15 +643,22 @@
console.log(deviceId.value);
// 调用设备租借接口
const rentResult = await rentPowerBank(deviceId.value, phoneNumber.value)
const rentResult = await rentPowerBank(deviceId.value, phoneNumber.value,payWay)
if (rentResult.code !== 200) {
throw new Error(rentResult.msg || $t('device.rentFailed'))
throw new Error(rentResult.msg || t('device.rentFailed'))
}
// 获取后端返回的订单信息
const order = rentResult.data
console.log('订单信息', order);
// 标记:本次是从设备详情页发起的下单流程,离开页面时不设置启动路径
try {
uni.setStorageSync('skipSetLaunchPathOnce', true)
} catch (e) {
console.warn('设置 skipSetLaunchPathOnce 失败:', e)
}
if (payWay == 'wx-pay') {
// 当支付方式为押金支付时
uni.hideLoading()
@@ -516,7 +671,7 @@
// 跳转到订单支付页面
uni.redirectTo({
url: `/pages/order/payment?orderId=${order.orderId}&packagePrice=${packagePrice}&totalAmount=${totalAmount}&depositAmount=${deposit}${deviceInfo.value && deviceInfo.value.feeConfig ? '&feeConfig=' + encodeURIComponent(deviceInfo.value.feeConfig) : ''}`
url: `/subPackages/order/payment?orderId=${order.orderId}&packagePrice=${packagePrice}&totalAmount=${totalAmount}&depositAmount=${deposit}${deviceInfo.value && deviceInfo.value.feeConfig ? '&feeConfig=' + encodeURIComponent(deviceInfo.value.feeConfig) : ''}`
})
} else if (payWay == 'wx-score-pay') {
@@ -545,7 +700,7 @@
// 用户取消授权,需要取消订单
try {
uni.showLoading({
title: $t('order.cancelling')
title: t('order.cancelling')
});
const cancelRes = await cancelOrder({
orderId: order.orderNo
@@ -554,7 +709,7 @@
uni.hideLoading();
uni.showToast({
title: $t('order.orderCancelled'),
title: t('order.orderCancelled'),
icon: 'none',
duration: 2000
});
@@ -569,7 +724,7 @@
console.error('取消订单失败:', cancelError);
uni.hideLoading();
uni.showToast({
title: $t('order.cancelFailedContactService'),
title: t('order.cancelFailedContactService'),
icon: 'none'
});
}
@@ -580,7 +735,7 @@
// 支付分调用异常,也需要取消订单
try {
uni.showLoading({
title: $t('order.cancelling')
title: t('order.cancelling')
});
const cancelRes = await cancelOrder({
orderId: order.orderNo
@@ -593,7 +748,7 @@
}
uni.showToast({
title: $t('device.payScoreFailedCancelled'),
title: t('device.payScoreFailedCancelled'),
icon: 'none'
});
@@ -605,7 +760,7 @@
}
} else {
uni.showToast({
title: res?.msg || $t('device.getPayParamsFailed'),
title: res?.msg || t('device.getPayParamsFailed'),
icon: 'none'
});
}
@@ -613,7 +768,7 @@
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || $t('device.rentFailedRetry'),
title: error.message || t('device.rentFailedRetry'),
icon: 'none'
})
}
@@ -664,15 +819,15 @@
align-items: center;
.location-icon {
width: 40rpx;
height: 40rpx;
width: 32rpx;
height: 32rpx;
margin-right: 12rpx;
background-color: #10d673;
// background-color: #10d673;
border-radius: 50%;
}
.location-name {
font-size: 32rpx;
font-size: 28rpx;
color: #333;
font-weight: 500;
}
@@ -681,7 +836,7 @@
.device-status {
padding: 8rpx 24rpx;
border-radius: 30rpx;
font-size: 24rpx;
font-size: 22rpx;
&.available {
background-color: #d4f4dd;
@@ -708,9 +863,10 @@
.device-id {
display: flex;
align-items: center;
margin-bottom: 5rpx;
.id-label {
font-size: 28rpx;
font-size: 26rpx;
color: #999;
}
@@ -724,7 +880,7 @@
// 计费规则卡片
.pricing-card {
.pricing-banner {
background: linear-gradient(135deg, #e8f5e9, #c8e6c9);
background: #E6F7EC;
border-radius: 20rpx;
padding: 40rpx 30rpx;
margin-bottom: 24rpx;
@@ -738,21 +894,21 @@
margin-bottom: 16rpx;
.price-symbol {
font-size: 48rpx;
font-size: 36rpx;
font-weight: bold;
color: #07c160;
margin-right: 4rpx;
}
.price {
font-size: 80rpx;
font-size: 64rpx;
font-weight: bold;
color: #07c160;
line-height: 1;
}
.unit {
font-size: 32rpx;
font-size: 28rpx;
color: #07c160;
margin-left: 8rpx;
}
@@ -760,11 +916,11 @@
.cap-badge {
background-color: #07c160;
padding: 10rpx 32rpx;
padding: 10rpx 28rpx;
border-radius: 30rpx;
line-height: 1;
.cap-text {
font-size: 26rpx;
font-size: 24rpx;
color: #fff;
font-weight: 500;
}
@@ -836,6 +992,51 @@
}
}
// 促销提示框
.promotion-tip {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background: rgba(255, 244, 227, 1);
border-radius: 22rpx;
margin-bottom: 30rpx;
transition: all 0.3s;
&:active {
opacity: 0.8;
transform: scale(0.98);
}
.tip-left {
display: flex;
align-items: center;
.tip-text {
font-size: 26rpx;
color: #A16300;
font-weight: 400;
}
}
.tip-right {
display: flex;
align-items: center;
gap: 12rpx;
.buy-text {
font-size: 26rpx;
color: #A16300;
font-weight: 500;
}
.arrow-icon {
width: 20rpx;
height: 20rpx;
}
}
}
// 底部操作区
.footer {
position: fixed;
-147
View File
@@ -1,147 +0,0 @@
<template>
<view class="help-container">
<!-- 常见问题 -->
<view class="faq-section">
<uv-collapse :border="false">
<uv-collapse-item
v-for="(item, index) in faqList"
:key="index"
:title="$t(item.question)"
:name="index"
>
<view class="answer-content">
<text class="answer-text">{{ $t(item.answer) }}</text>
</view>
</uv-collapse-item>
</uv-collapse>
</view>
<!-- 联系客服 -->
<view class="contact-card">
<view class="contact-title">{{ $t('help.contactUs') }}</view>
<view class="contact-content">
<view class="contact-item">
<text class="label">{{ $t('help.phone') }}</text>
<text class="value" @click="makePhoneCall">{{ customerPhone }}</text>
</view>
<view class="contact-item">
<text class="label">{{ $t('help.workingHours') }}</text>
<text class="value">{{ HELP_CONTENT.CONTACT.SERVICE_TIME.VALUE }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { HELP_CONTENT } from '@/constants/help'
import { getCustomerPhone } from '@/util/index.js'
export default {
data() {
return {
HELP_CONTENT,
faqList: HELP_CONTENT.FAQ_LIST,
customerPhone: HELP_CONTENT.CONTACT.PHONE.VALUE // 默认客服电话
}
},
onLoad() {
// 设置页面标题
uni.setNavigationBarTitle({
title: this.$t('help.title')
})
// 从缓存读取客服电话
this.customerPhone = getCustomerPhone()
},
methods: {
makePhoneCall() {
uni.makePhoneCall({
phoneNumber: this.customerPhone
})
}
}
}
</script>
<style lang="scss" scoped>
.help-container {
min-height: 100vh;
background: #f8f8f8;
padding: 30rpx;
.faq-section {
background: #fff;
border-radius: 20rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
overflow: hidden;
.answer-content {
padding: 20rpx 30rpx 30rpx;
background: #f9f9f9;
.answer-text {
font-size: 28rpx;
color: #666;
line-height: 1.8;
display: block;
}
}
}
.contact-card {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
.contact-title {
position: relative;
z-index: 4;
display: inline-block;
font-size: 32rpx;
color: #333;
font-weight: 500;
margin-bottom: 20rpx;
padding-bottom: 8rpx;
width: fit-content;
&::after {
z-index: -1;
content: '';
position: absolute;
left: 0;
bottom: 8rpx;
width: 88%;
height: 16rpx;
border-radius: 20rpx;
background: #07C160;
}
}
.contact-content {
.contact-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
.label {
font-size: 28rpx;
color: #666;
}
.value {
font-size: 28rpx;
color: #333;
font-weight: 500;
&:active {
opacity: 0.7;
}
}
}
}
}
}
</style>
+746 -208
View File
File diff suppressed because it is too large Load Diff
+1156 -188
View File
File diff suppressed because it is too large Load Diff
-587
View File
@@ -1,587 +0,0 @@
<template>
<view class="payment-container">
<!-- 订单状态 -->
<view class="status-card">
<view class="status-icon" :class="orderStatus.class"></view>
<view class="status-text">{{ orderStatus.text }}</view>
<view class="status-desc">{{ orderStatus.desc }}</view>
</view>
<!-- 订单信息 -->
<view class="order-card">
<view class="card-title">{{ $t('payment.orderInfo') }}</view>
<view class="info-item">
<text class="label">{{ $t('order.orderNo') }}</text>
<text class="value">{{ orderInfo.orderNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('order.deviceNo') }}</text>
<text class="value">{{ orderInfo.deviceNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('payment.createTime') }}</text>
<text class="value">{{ orderInfo.createTime || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('payment.contactPhone') }}</text>
<text class="value">{{ orderInfo.phone || '-' }}</text>
</view>
</view>
<!-- 费用信息 -->
<view class="price-card">
<view class="card-title">{{ $t('payment.feeInfo') }}</view>
<view class="price-item">
<text class="label">{{ $t('payment.deposit') }}</text>
<text class="value">{{ orderInfo.deposit || '99.00' }}</text>
</view>
<view class="price-item">
<text class="label">{{ $t('payment.package') }}</text>
<text class="value">{{ packageInfo.price }}{{ $t('unit.yuan') }}/{{ packageInfo.time }}{{ $t('time.hour') }}</text>
</view>
<view class="price-item total">
<text class="label">{{ $t('payment.total') }}</text>
<text class="value">{{ totalAmount }}</text>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="total-amount">
<text>{{ $t('payment.total') }}</text>
<text class="amount">{{ totalAmount }}</text>
</view>
<view class="pay-btn" @click="handlePayment">{{ $t('payment.payNow') }}</view>
</view>
</view>
</template>
<script>
import { queryById, updateOrderPackage } from '@/config/api/order.js'
import { getDeviceInfo } from '@/config/api/device.js'
import { updateUserBalance } from '@/config/api/user.js'
import {
URL
}from"@/config/url.js"
export default {
data() {
return {
orderId: null,
deviceNo: null,
orderInfo: {},
packageInfo: {
time: '',
price: '0.00'
},
deviceInfo: null,
passedTotalAmount: null,
passedDepositAmount: null,
orderStatus: {
get text() { return this.$t('payment.waitingForPayment') },
get desc() { return this.$t('payment.pleasePayIn15Min') },
class: 'waiting'
}
}
},
computed: {
totalAmount() {
if (this.passedTotalAmount !== null) {
return parseFloat(this.passedTotalAmount).toFixed(2);
}
const deposit = parseFloat(this.orderInfo.deposit || this.passedDepositAmount || 99)
const packagePrice = parseFloat(this.packageInfo.price || 0)
return (deposit + packagePrice).toFixed(2)
},
// 计算每小时的价格
hourlyRate() {
const price = parseFloat(this.packageInfo.price || 0);
let time = parseFloat(this.packageInfo.time || 1);
// 如果时间单位不是小时(例如分钟),需要转换
if (this.packageInfo.time && this.packageInfo.time.includes('分钟')) {
time = time / 60; // 将分钟转换为小时
} else if (this.packageInfo.time && this.packageInfo.time.includes('次')) {
// 按次计费时,暂时显示单次价格
return price.toFixed(2);
}
// 避免除以零
if (time <= 0) time = 1;
// 计算每小时价格
return (price / time).toFixed(2);
}
},
onLoad(options) {
// 设置页面标题
uni.setNavigationBarTitle({
title: this.$t('payment.orderPayment')
})
if (options && options.orderId) {
this.orderId = options.orderId
if (options.totalAmount) {
this.passedTotalAmount = options.depositAmount;
}
if (options.depositAmount) {
this.passedDepositAmount = options.depositAmount;
}
// 如果URL中包含了feeConfig参数,保存它
if (options.feeConfig) {
try {
console.log('从URL获取到feeConfig:', options.feeConfig)
const feeConfigStr = decodeURIComponent(options.feeConfig)
// 创建一个临时的deviceInfo对象保存feeConfig
this.deviceInfo = { feeConfig: feeConfigStr }
} catch (e) {
console.error('解析URL中的feeConfig失败:', e)
}
}
this.loadOrderInfo()
} else {
uni.showToast({
title: this.$t('order.orderNotExist'),
icon: 'none'
})
setTimeout(() => {
uni.redirectTo({
url: '/pages/index/index'
})
}, 1500)
}
},
methods: {
// 加载订单信息
async loadOrderInfo() {
try {
uni.showLoading({
title: this.$t('common.loading')
})
const res = await queryById(this.orderId)
if (res.code === 200 && res.data) {
const orderData = res.data
// 处理创建时间,确保显示的是格式化后的时间
let formattedTime;
try {
// 如果orderData.createTime存在并且是有效的日期字符串/时间戳,则格式化它
if (orderData.createTime) {
formattedTime = this.formatTime(new Date(orderData.createTime));
} else {
// 如果createTime不存在,使用当前时间作为创建时间
formattedTime = this.formatTime(new Date());
}
} catch (e) {
console.error('时间格式化错误:', e);
formattedTime = this.formatTime(new Date());
}
this.orderInfo = {
orderNo: orderData.orderNo || orderData.orderId,
deviceNo: orderData.deviceNo,
createTime: formattedTime,
phone: orderData.phone,
deposit: this.passedDepositAmount || orderData.depositAmount || '99.00',
}
// 直接从订单数据中获取套餐信息
if (orderData.packageTime && orderData.packagePrice) {
// 将分钟转换为小时
const timeInHours = (parseFloat(orderData.packageTime) / 60).toFixed(1);
this.packageInfo = {
time: timeInHours.toString(),
price: orderData.packagePrice.toString()
}
}
// 获取设备信息(但不再用于设置套餐信息)
this.deviceNo = orderData.deviceNo;
await this.loadDeviceInfo();
} else {
throw new Error('获取订单信息失败')
}
uni.hideLoading()
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || '获取订单信息失败',
icon: 'none'
})
}
},
// 加载设备信息
async loadDeviceInfo() {
if (!this.deviceNo) return;
try {
const res = await getDeviceInfo(this.deviceNo);
if (res.code === 200 && res.data) {
this.deviceInfo = res.data.device;
// 设置存款金额
if (this.deviceInfo && this.deviceInfo.depositAmount) {
this.orderInfo.deposit = this.deviceInfo.depositAmount;
}
}
} catch (error) {
console.error('获取设备信息失败:', error);
}
},
// 处理支付
async handlePayment() {
try {
uni.showLoading({
title: this.$t('common.processing')
})
// 调用后端创建微信支付订单接口
const res = await uni.request({
url: `${URL || 'http://127.0.0.1:8080'}/app/wx-payment/create/${this.orderInfo.orderNo}`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
})
if (res.statusCode === 200 && res.data.code === 200) {
const payParams = res.data.data
// 调用微信支付
await uni.requestPayment({
...payParams,
success: async () => {
uni.showToast({
title: this.$t('payment.paymentSuccess'),
icon: 'success'
});
// 更新用户余额
try {
await updateUserBalance(this.orderId);
} catch (error) {
console.warn('更新用户余额失败:', error);
}
// 支付成功后直接跳转到订单页面,不再轮询
setTimeout(() => {
uni.redirectTo({
url: `/pages/order/index?orderId=${this.orderId}`
});
}, 1500);
},
fail: (err) => {
console.error('支付失败:', err)
throw new Error(this.$t('payment.paymentFailedRetry'))
}
})
} else {
throw new Error(res.data.msg || '创建支付订单失败')
}
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || '支付失败',
icon: 'none'
})
}
},
// 发送租借指令
async sendRentCommand() {
try {
uni.showLoading({
title: this.$t('common.processing')
})
// 调用发送租借指令的接口
const res = await this.sendRentRequest()
if (res.code === 200) {
uni.hideLoading()
uni.showToast({
title: this.$t('device.rentSuccess'),
icon: 'success'
})
// 跳转到订单列表页面
setTimeout(() => {
uni.redirectTo({
url: `/pages/order/index?orderId=${this.orderId}`
})
}, 1500)
} else {
throw new Error(res.msg || this.$t('device.rentFailed'))
}
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || this.$t('device.rentFailed'),
icon: 'none'
})
}
},
// 发送租借请求
sendRentRequest() {
return new Promise((resolve, reject) => {
uni.request({
url: `${URL || 'http://127.0.0.1:8080'}/app/device/sendRentCommand`,
method: 'POST',
data: {
orderId: this.orderId
},
header: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
},
success(res) {
if (res.statusCode === 200) {
resolve(res.data)
} else {
reject(new Error('请求失败'))
}
},
fail(err) {
reject(err)
}
})
})
},
// 格式化时间
formatTime(date) {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
},
// 检查订单状态(单次查询,不轮询)
async checkOrderStatus() {
try {
const res = await uni.request({
url: `${URL || 'http://127.0.0.1:8080'}/app/wx-payment/status/${this.orderInfo.orderNo}`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
});
if (res.statusCode === 200 && res.data.code === 200) {
return res.data.data;
} else {
throw new Error('查询订单状态失败');
}
} catch (error) {
console.error('查询订单状态错误:', error);
return null;
}
},
}
}
</script>
<style lang="scss" scoped>
.payment-container {
min-height: 100vh;
background: #f8f8f8;
padding: 30rpx;
padding-bottom: 180rpx;
box-sizing: border-box;
.status-card {
background: #fff;
border-radius: 24rpx;
padding: 40rpx 30rpx;
margin-bottom: 30rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
.status-icon {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: #f5f5f5;
margin-bottom: 20rpx;
&.waiting {
background: #FFF9C4;
}
&.success {
background: #E8F5E9;
}
&.failed {
background: #FFEBEE;
}
}
.status-text {
font-size: 36rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
}
.status-desc {
font-size: 28rpx;
color: #999;
}
}
.order-card, .price-card {
background: #fff;
border-radius: 24rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
.card-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
position: relative;
padding-left: 20rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8rpx;
height: 32rpx;
background: #1976D2;
border-radius: 4rpx;
}
}
.info-item, .price-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1px solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.label {
font-size: 28rpx;
color: #666;
}
.value {
font-size: 28rpx;
color: #333;
}
&.total {
margin-top: 10rpx;
padding-top: 30rpx;
border-top: 1px solid #f5f5f5;
.label, .value {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.value {
color: #FF5722;
}
}
}
}
.wechat-tip {
background: #fff;
border-radius: 24rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
display: flex;
align-items: center;
.method-icon {
width: 48rpx;
height: 48rpx;
margin-right: 20rpx;
&.wechat {
background: url('../../static/images/wechat.svg') no-repeat center/contain;
}
}
.method-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
}
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
.total-amount {
font-size: 28rpx;
color: #666;
.amount {
font-size: 36rpx;
font-weight: 600;
color: #FF5722;
margin-left: 10rpx;
}
}
.pay-btn {
background: #1976D2;
color: #fff;
font-size: 32rpx;
font-weight: 600;
padding: 20rpx 60rpx;
border-radius: 100rpx;
border: none;
&:active {
opacity: 0.9;
}
}
}
}
</style>
+163 -110
View File
@@ -1,14 +1,19 @@
<template>
<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-text">{{ $t('success.paymentSuccess') }}</view>
<view class="status-desc">{{ $t('success.paymentSuccessDesc') }}</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="info-item">
<text class="label">{{ $t('order.orderNo') }}</text>
@@ -27,6 +32,7 @@
<text class="value">{{ orderInfo.payTime || '-' }}</text>
</view>
</view>
</view>
<!-- 设备状态 -->
<view class="device-status">
@@ -38,84 +44,120 @@
<!-- 操作按钮 -->
<view class="button-group">
<button class="primary-btn" @click="goToHome">{{ $t('success.backToHome') }}</button>
<button class="secondary-btn" @click="goToOrderList">{{ $t('success.viewOrder') }}</button>
<view class="secondary-btn" @click="goToHome">{{ $t('success.backToHome') }}</view>
<view class="primary-btn" @click="goToOrderList">{{ $t('success.viewOrder') }}</view>
</view>
</view>
</template>
<script>
import { queryById } from '@/config/api/order.js'
import { confirmPaymentAndRent } from '@/config/api/device.js'
<script setup>
import {
ref,
getCurrentInstance
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
queryById,
getOrderByOrderNo
} from '@/config/api/order.js'
export default {
data() {
return {
orderId: '',
orderInfo: {},
isLoading: true,
deviceMessage: '',
hasTriggeredDevice: false
}
},
onLoad(options) {
// 获取当前实例以访问 $t 方法
const {
proxy
} = getCurrentInstance()
// 响应式数据
const orderId = ref('')
const orderInfo = ref({})
const isLoading = ref(true)
const deviceMessage = ref('')
const hasTriggeredDevice = ref(false)
// 页面加载
onLoad((options) => {
// 设置页面标题
uni.setNavigationBarTitle({
title: this.$t('success.paymentSuccess')
title: proxy.$t('success.paymentSuccess')
})
this.deviceMessage = this.$t('success.preparingDevice')
deviceMessage.value = proxy.$t('success.preparingDevice')
if (options && options.orderId) {
this.orderId = options.orderId
this.loadOrderInfo()
// #ifdef H5
if (uni.getStorageSync('pendingPaymentNo')) {
orderId.value = options.orderId
loadOrderInfo()
// 添加页面显示监听,防止页面切换后重复触发弹出
uni.$once('orderSuccess:' + this.orderId, () => {
uni.$once('orderSuccess:' + orderId.value, () => {
console.log('已经触发过弹出逻辑,不再重复触发')
this.hasTriggeredDevice = true
hasTriggeredDevice.value = true
})
} else {
uni.showToast({
title: this.$t('order.orderNotExist'),
title: proxy.$t('order.orderNotExist'),
icon: 'none'
})
setTimeout(() => {
this.goToHome()
goToHome()
}, 1500)
}
},
methods: {
async loadOrderInfo() {
try {
uni.showLoading({
title: this.$t('common.loading')
// #endif
// #ifndef H5
if (options && options.orderId) {
orderId.value = options.orderId
loadOrderInfo()
// 添加页面显示监听,防止页面切换后重复触发弹出
uni.$once('orderSuccess:' + orderId.value, () => {
console.log('已经触发过弹出逻辑,不再重复触发')
hasTriggeredDevice.value = true
})
} else {
uni.showToast({
title: proxy.$t('order.orderNotExist'),
icon: 'none'
})
setTimeout(() => {
goToHome()
}, 1500)
}
// #endif
})
const res = await queryById(this.orderId)
// 加载订单信息
const loadOrderInfo = async () => {
try {
uni.showLoading({
title: proxy.$t('common.loading')
})
const res = await queryById(orderId.value)
if (res.code === 200 && res.data) {
const orderData = res.data
this.orderInfo = {
orderInfo.value = {
orderNo: orderData.orderNo || orderData.orderId,
deviceNo: orderData.deviceNo,
amount: orderData.payAmount || orderData.amount,
payTime: orderData.payTime || this.formatTime(new Date())
payTime: orderData.payTime || formatTime(new Date())
}
// 检查订单状态
if (orderData.orderStatus === 'IN_USED') {
// 如果已经是使用中状态,可能说明开锁已经完成
this.deviceMessage = '设备已弹出,请取走您的风扇'
this.isLoading = false
deviceMessage.value = '设备已弹出,请取走您的风扇'
isLoading.value = false
// 如果是第一次加载页面且设备已弹出,记录状态,避免重复弹出
if (!this.hasTriggeredDevice) {
uni.$emit('orderSuccess:' + this.orderId)
this.hasTriggeredDevice = true
if (!hasTriggeredDevice.value) {
uni.$emit('orderSuccess:' + orderId.value)
hasTriggeredDevice.value = true
}
} else {
// 正常触发弹出逻辑
this.triggerDeviceEject()
// 在此页面不再触发设备弹出操作,仅更新展示文案和加载状态
deviceMessage.value = proxy.$t('success.paymentSuccessDesc')
isLoading.value = false
}
} else {
throw new Error('获取订单信息失败')
@@ -125,53 +167,14 @@ export default {
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || this.$t('order.getOrderFailed'),
title: error.message || proxy.$t('order.getOrderFailed'),
icon: 'none'
})
}
},
// 触发弹出风扇
async triggerDeviceEject() {
if (this.hasTriggeredDevice) {
console.log('已经触发过弹出风扇,不重复触发')
return
}
this.hasTriggeredDevice = true
uni.$emit('orderSuccess:' + this.orderId)
this.isLoading = true
this.deviceMessage = this.$t('success.preparingDevice')
try {
console.log(`准备触发弹出风扇,orderId: ${this.orderId}`)
// 调用确认支付并弹出的方法
const result = await confirmPaymentAndRent(this.orderId)
console.log('确认支付并弹出风扇结果:', JSON.stringify(result))
if (result && result.code === 200) {
this.deviceMessage = this.$t('success.deviceReady')
uni.showToast({
title: this.$t('success.deviceReady'),
icon: 'success'
})
} else {
throw new Error((result && result.msg) || this.$t('success.deviceFailed'))
}
} catch (error) {
console.error('弹出风扇错误:', error)
this.deviceMessage = this.$t('success.deviceFailed')
uni.showToast({
title: error.message || this.$t('success.deviceFailed'),
icon: 'none'
})
} finally {
this.isLoading = false
}
},
formatTime(date) {
// 格式化时间
const formatTime = (date) => {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
@@ -179,34 +182,41 @@ export default {
const minute = date.getMinutes().toString().padStart(2, '0')
const second = date.getSeconds().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
},
goToHome() {
uni.switchTab({
}
// 返回首页
const goToHome = () => {
uni.reLaunch({
url: '/pages/index/index'
})
},
goToOrderList() {
}
// 查看订单列表
const goToOrderList = () => {
uni.redirectTo({
url: '/pages/order/index'
})
}
}
}
</script>
<style lang="scss" scoped>
.success-container {
padding: 20px;
padding-bottom: 180rpx;
background-color: #f5f5f5;
min-height: 100vh;
box-sizing: border-box;
}
.status-card {
.status-order-card {
background-color: #fff;
border-radius: 12px;
margin-bottom: 20px;
overflow: hidden;
.status-section {
padding: 30px;
text-align: center;
margin-bottom: 20px;
.status-icon {
width: 60px;
@@ -245,17 +255,34 @@ export default {
}
}
.order-card {
background-color: #fff;
border-radius: 12px;
.section-divider {
height: 1px;
background-color: #f0f0f0;
margin: 0 20px;
}
.order-section {
padding: 20px;
margin-bottom: 20px;
.card-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 16px;
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 {
@@ -264,6 +291,10 @@ export default {
align-items: center;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.label {
color: #666;
font-size: 14px;
@@ -272,6 +303,8 @@ export default {
.value {
color: #333;
font-size: 14px;
font-weight: 500;
}
}
}
}
@@ -305,25 +338,42 @@ export default {
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
}
.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;
// flex-direction: column;
gap: 16px;
justify-content: flex-end;
align-items: center;
gap: 20rpx;
.primary-btn {
background-color: #07c160;
color: #fff;
border: none;
border-radius: 24px;
padding: 12px;
font-size: 16px;
border-radius: 32rpx;
padding: 0 32rpx;
font-size: 24rpx;
height: 64rpx;
line-height: 64rpx;
white-space: nowrap;
&:active {
opacity: 0.8;
@@ -333,10 +383,13 @@ export default {
.secondary-btn {
background-color: #fff;
color: #07c160;
border: 1px solid #07c160;
border-radius: 24px;
padding: 12px;
font-size: 16px;
border: 2rpx solid #07c160;
border-radius: 32rpx;
padding: 0 32rpx;
font-size: 24rpx;
height: 64rpx;
line-height: 64rpx;
white-space: nowrap;
&:active {
background-color: #f5f5f5;
+754
View File
@@ -0,0 +1,754 @@
<template>
<view class="scan-page">
<!-- 扫码区域容器 -->
<view class="scan-window">
<!-- html5-qrcode 扫描器容器 -->
<view id="qr-reader" class="qr-reader"></view>
<!-- 扫描装饰 -->
<view class="scan-mask">
<view class="scan-frame">
<view class="scan-line" v-if="scanning"></view>
<view class="corner top-left"></view>
<view class="corner top-right"></view>
<view class="corner bottom-left"></view>
<view class="corner bottom-right"></view>
</view>
</view>
<view class="scan-tip">{{ tipText }}</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-actions">
<view class="action-item" @click.stop="chooseImage">
<!-- <view class="action-icon">📷</view> -->
<uv-icon name="photo" size="24" color="#fff"></uv-icon>
<text>{{ $t('scan.album') }}</text>
</view>
<view class="action-item" @click.stop="toggleInput">
<!-- <view class="action-icon"></view> -->
<uv-icon name="edit-pen" size="24" color="#fff"></uv-icon>
<text>{{ $t('scan.manualInput') }}</text>
</view>
<view class="action-item" @click.stop="goBack">
<!-- <view class="action-icon"></view> -->
<uv-icon name="arrow-left" size="24" color="#fff"></uv-icon>
<text>{{ $t('common.back') }}</text>
</view>
</view>
<!-- 手动输入弹窗 -->
<uv-popup ref="inputPopup" mode="center" round="16" :closeOnClickOverlay="true">
<view class="input-dialog">
<view class="dialog-title">{{ $t('scan.manualInputTitle') }}</view>
<input
v-model="manualDeviceNo"
:placeholder="$t('scan.deviceNoPlaceholder')"
class="device-input"
type="text"
/>
<view class="dialog-btns">
<button class="cancel-btn" @click="closeInput">{{ $t('common.cancel') }}</button>
<button class="confirm-btn" @click="confirmManualInput">{{ $t('common.confirm') }}</button>
</view>
</view>
</uv-popup>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { getQueryString } from '../../util/index.js';
import { Html5Qrcode, Html5QrcodeSupportedFormats } from 'html5-qrcode';
import { useI18n, showModalI18n } from '@/utils/i18n.js';
const { t } = useI18n();
const inputPopup = ref(null);
const manualDeviceNo = ref('');
const tipText = ref(t('scan.initializing'));
const scanning = ref(false);
const hasFlash = ref(false);
const flashOn = ref(false);
let html5QrCode = null;
let isProcessing = false; // 防止重复处理
// 统一扫描配置:增大识别区域,提升低清晰度二维码识别成功率
const getScanConfig = () => ({
fps: 12,
qrbox: (viewfinderWidth, viewfinderHeight) => {
const minEdge = Math.min(viewfinderWidth, viewfinderHeight);
const size = Math.max(220, Math.floor(minEdge * 0.72));
return { width: size, height: size };
},
disableFlip: false,
formatsToSupport: [Html5QrcodeSupportedFormats.QR_CODE],
experimentalFeatures: {
useBarCodeDetectorIfSupported: true
}
});
// 初始化扫码
const initScan = async () => {
try {
tipText.value = t('scan.initializing');
console.log('=== 开始初始化扫码 ===');
// 检查浏览器支持
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error(t('scan.browserNotSupportCamera'));
}
// 等待 DOM 渲染
await new Promise(resolve => setTimeout(resolve, 500));
// 检查容器元素
const readerElement = document.getElementById('qr-reader');
if (!readerElement) {
throw new Error(t('scan.containerNotFound'));
}
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(t('scan.initFailed'));
}
tipText.value = t('scan.startingCamera');
console.log('=== 开始启动扫描 ===');
console.log('html5QrCode 实例:', html5QrCode);
console.log('html5QrCode.start 方法:', typeof html5QrCode.start);
// 配置扫描参数
const config = getScanConfig();
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 = t('scan.alignQRCode');
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 = getScanConfig();
await html5QrCode.start(
{ facingMode: "user" },
config,
onScanSuccess,
onScanFailure
);
scanning.value = true;
tipText.value = t('scan.alignQRCode');
console.log('✅ 使用前置摄像头启动成功');
setTimeout(() => {
hideDefaultUI();
}, 200);
} catch (err2) {
console.error('❌ 前置摄像头也失败:', err2);
// 最后尝试:不指定摄像头
console.log('最后尝试:使用默认摄像头...');
try {
const config = getScanConfig();
// 获取摄像头列表
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 = t('scan.alignQRCode');
console.log('✅ 使用默认摄像头启动成功');
setTimeout(() => {
hideDefaultUI();
}, 200);
} else {
throw new Error(t('scan.noCameraFound'));
}
} 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;
}
if (!scanning.value) {
console.log('扫描已停止,忽略结果');
return;
}
isProcessing = true;
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 onScanFailure = (error) => {
// 正常情况,不需要处理
};
// 停止扫描
const stopScan = async () => {
if (!html5QrCode) {
return;
}
console.log('=== 停止扫描 ===');
scanning.value = false;
flashOn.value = false;
try {
const isScanning = html5QrCode.isScanning;
if (isScanning) {
await html5QrCode.stop();
console.log('✓ Html5Qrcode 已停止');
}
} catch (err) {
console.warn('停止扫描失败:', err);
}
};
// 处理初始化错误
const handleInitError = (err) => {
console.error('处理初始化错误:', err);
let errMsg = t('scan.initFailed');
let errDetail = '';
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
errMsg = t('scan.cameraPermissionDenied');
errDetail = t('scan.cameraPermissionHint');
}
else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
errMsg = t('scan.noCameraFound');
errDetail = t('scan.ensureCameraExists');
}
else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') {
errMsg = t('scan.cameraInUse');
errDetail = t('scan.closeOtherCameraApps');
}
else if (err.name === 'NotSupportedError') {
errMsg = t('scan.browserNotSupported');
errDetail = t('scan.useModernBrowser');
}
// else if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
// errMsg = '需要 HTTPS 环境';
// errDetail = '摄像头功能需要在安全环境下使用';
// }
else {
errMsg = err.message || t('scan.cameraStartFailed');
errDetail = t('scan.tryRefreshOrAlternative');
}
tipText.value = errMsg;
const fallbackContent = `${errDetail}\n\n${t('scan.errorFallbackHint')}\n1. ${t('scan.errorFallbackAlbum')}\n2. ${t('scan.errorFallbackManual')}`;
// 显示错误提示,提供备选方案
showModalI18n({
title: errMsg,
content: fallbackContent,
showCancel: true,
cancelText: t('common.back'),
confirmText: t('scan.manualInput'),
success: (res) => {
if (res.confirm) {
toggleInput();
} else if (res.cancel) {
goBack();
}
}
});
};
// 从相册选择图片识别
const chooseImage = async () => {
// #ifdef H5
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) {
return;
}
uni.showLoading({ title: t('scan.recognizing') });
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', {
result: result,
scanType: 'QR_CODE',
path: result
});
// 不要立即返回,等待首页处理完成
console.log('图片识别结果已发送,等待首页处理...');
} else {
uni.showToast({ title: t('scan.qrNotFound'), icon: 'none' });
// 识别失败,重新启动摄像头扫描
if (wasScanning) {
setTimeout(async () => {
await startScanning();
}, 500);
}
}
} catch (err) {
console.error('图片识别失败:', err);
uni.hideLoading();
uni.showToast({ title: t('scan.recognizeFailed'), icon: 'none' });
// 识别失败,重新启动摄像头扫描
setTimeout(async () => {
try {
await startScanning();
} catch (e) {
console.error('重新启动扫描失败:', e);
}
}, 500);
}
};
input.click();
// #endif
// #ifndef H5
uni.chooseImage({
count: 1,
sourceType: ['album'],
success: (res) => {
uni.showToast({ title: t('scan.h5Only'), icon: 'none' });
}
});
// #endif
};
// 打开手动输入弹窗
const toggleInput = () => {
if (inputPopup.value) {
inputPopup.value.open();
}
};
// 关闭手动输入弹窗
const closeInput = () => {
if (inputPopup.value) {
inputPopup.value.close();
}
};
// 确认手动输入
const confirmManualInput = () => {
const deviceNo = manualDeviceNo.value.trim();
if (!deviceNo) {
uni.showToast({ title: t('scan.deviceNoRequired'), icon: 'none' });
return;
}
closeInput();
stopScan();
// 处理可能包含 URL 的情况
let finalDeviceNo = deviceNo;
if (deviceNo.includes('deviceNo=')) {
finalDeviceNo = getQueryString(deviceNo, 'deviceNo') || deviceNo;
}
// 通过全局事件通知首页
uni.$emit('h5ScanSuccess', {
result: finalDeviceNo,
scanType: 'MANUAL'
});
// 不要立即返回,等待首页处理完成
console.log('手动输入结果已发送,等待首页处理...');
};
// 返回
const goBack = () => {
stopScan();
uni.navigateBack();
};
onMounted(() => {
console.log('扫码页面已挂载');
uni.setNavigationBarTitle({
title: t('scan.title')
});
// 延迟初始化,确保 DOM 已渲染
setTimeout(() => {
console.log('开始初始化扫码');
initScan();
}, 500);
});
onUnmounted(() => {
console.log('扫码页面卸载,清理资源');
isProcessing = false;
if (html5QrCode) {
stopScan().then(() => {
html5QrCode.clear().catch(err => {
console.warn('清理 Html5Qrcode 实例失败:', err);
});
html5QrCode = null;
});
}
});
</script>
<style lang="scss" scoped>
.scan-page {
width: 100vw;
height: 100vh;
background: #000;
position: relative;
overflow: hidden;
}
.scan-window {
width: 100%;
height: 100%;
position: relative;
.qr-reader {
width: 100%;
height: 100%;
// 覆盖 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;
}
}
}
.scan-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 10;
background: rgba(0, 0, 0, 0.5); // 添加半透明遮罩
.scan-frame {
width: 500rpx;
height: 500rpx;
position: relative;
background: transparent; // 扫描区域透明
box-shadow: 0 0 0 2000px rgba(0, 0, 0, 0.5); // 使用 box-shadow 创建外部遮罩
.scan-line {
width: 100%;
height: 4rpx;
background: linear-gradient(to right, transparent, #3EAB64, transparent);
position: absolute;
top: 0;
animation: scanMove 3s linear infinite;
box-shadow: 0 0 10rpx #3EAB64;
}
.corner {
position: absolute;
width: 40rpx;
height: 40rpx;
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-right { top: -2rpx; right: -2rpx; border-left: none; border-bottom: none; }
.bottom-left { bottom: -2rpx; left: -2rpx; border-right: none; border-top: none; }
.bottom-right { bottom: -2rpx; right: -2rpx; border-left: none; border-top: none; }
}
}
@keyframes scanMove {
0% { top: 0; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { top: 100%; opacity: 0; }
}
.scan-tip {
position: absolute;
top: 15%;
left: 0;
right: 0;
text-align: center;
color: #fff;
font-size: 28rpx;
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 {
position: absolute;
bottom: 80rpx;
left: 0;
right: 0;
display: flex;
justify-content: space-around;
padding: 0 60rpx;
z-index: 20;
.action-item {
display: flex;
flex-direction: column;
align-items: center;
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 {
color: #fff;
font-size: 22rpx;
white-space: nowrap;
}
&:active {
transform: scale(0.95);
background: rgba(62, 171, 100, 0.3);
}
}
}
.input-dialog {
width: 600rpx;
background: #fff;
padding: 40rpx;
border-radius: 24rpx;
.dialog-title {
font-size: 32rpx;
font-weight: 600;
text-align: center;
margin-bottom: 40rpx;
color: #333;
}
.device-input {
height: 88rpx;
background: #F8F9FA;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
border: 1rpx solid #eee;
margin-bottom: 40rpx;
box-sizing: border-box;
&:focus {
border-color: #3EAB64;
background: #fff;
}
}
.dialog-btns {
display: flex;
gap: 20rpx;
button {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
border-radius: 40rpx;
border: none;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
}
.cancel-btn {
background: #f5f5f5;
color: #666;
}
.confirm-btn {
background: #3EAB64;
color: #fff;
}
}
}
</style>
+60 -33
View File
@@ -6,7 +6,7 @@
@relocate="init" @markerTap="goToPositionDetail" />
<!-- 定位按钮 -->
<view class="relocate-btn" @click="init">
<image src="/static/location.png" class="relocate-icon" mode="aspectFit"></image>
<image src="/static/location.png" class="relocate-icon" mode="aspectFit" lazy-load="true"></image>
</view>
</view>
<view class="list-wrap">
@@ -22,15 +22,19 @@
:class="{ available: isRentable(item), invalid: !isValidCoordinate(item.latitude, item.longitude) }"
v-for="(item, index) in filteredPositions" :key="item.positionId || index"
@click="goToPositionDetail(item)">
<!-- 第一行三列布局 -->
<view class="card-row-first">
<!-- 第一列缩略图 -->
<view class="thumb">
<image v-if="item.deviceImg" :src="item.deviceImg" class="thumb-img" mode="aspectFill">
</image>
<image v-else src="/static/device-info.png" class="thumb-img" mode="aspectFit"></image>
</view>
<!-- 第二列信息 -->
<view class="info">
<view class="row top">
<view class="name">{{ item.name }}</view>
</view>
<view class="row sub" v-if="item.location">
<text class="addr">{{ item.location }}</text>
@@ -41,31 +45,28 @@
<view class="row meta" v-if="!isValidCoordinate(item.latitude, item.longitude)">
<text class="time" style="color: #ff6b6b;">{{ $t('location.coordinateError') }}</text>
</view>
<view class="row meta"
v-if="item.availablePowerBankCount !== undefined && item.availablePowerBankCount !== null">
<text class="time">可租借{{ item.availablePowerBankCount }} </text>
</view>
<view class="row meta"
v-if="item.availableEmptyGridCount !== undefined && item.availableEmptyGridCount !== null">
<text class="time">可归还{{ item.availableEmptyGridCount }} 个空位</text>
</view>
<view class="row meta remark-info" v-if="item.remark">
<text class="time">💰 {{ item.remark }}</text>
</view>
<view class="tags">
<view class="tag rent" v-if="isRentable(item)">{{ $t('location.rent') }}</view>
<view class="tag return" v-if="isReturnable(item)">{{ $t('location.return') }}</view>
</view>
</view>
<view class="actions">
<!-- 第三列操作 -->
<view class="actions">
<view class="nav" :class="{ disabled: !isValidCoordinate(item.latitude, item.longitude) }"
@click.stop="navigateToPosition(item)">
<image src="/static/luxian.png" class="action-icon" mode="aspectFit"></image>
</view>
<view class="distance"
v-if="item.distance && isValidCoordinate(item.latitude, item.longitude)">
{{ item.distance }}</view>
{{ item.distance }}
</view>
</view>
</view>
<!-- 第二行标签 -->
<view class="card-row-second">
<view class="tags">
<view class="tag rent" v-if="isRentable(item)">{{ $t('location.rent') }}</view>
<view class="tag return" v-if="isReturnable(item)">{{ $t('location.return') }}</view>
<view class="tag coupon" v-if="item.supportCouponOrMember">{{ $t('location.supportCouponOrMember') }}</view>
</view>
</view>
</view>
@@ -100,13 +101,13 @@
} from '@/utils/i18n.js'
const {
t: $t
t
} = useI18n()
onMounted(() => {
uni.setNavigationBarTitle({
title: $t('search.title')
title: t('search.title')
})
// uni.showLoading({
// title:'11111',
@@ -133,8 +134,13 @@
}
const formatDistance = (meters) => {
if (meters < 1000) return `${Math.round(meters)}m`
return `${(meters / 1000).toFixed(1)}km`
// 兼容支付宝小程序等环境:保证始终对 Number 调用 toFixed
let m = meters
if (typeof m === 'bigint') m = Number(m)
m = Number(m)
if (!Number.isFinite(m) || m < 0) return ''
if (m < 1000) return `${Math.round(m)}m`
return `${(m / 1000).toFixed(1)}km`
}
const setTab = (name) => {
@@ -216,8 +222,8 @@
return {
...transformed,
canRent: activeTab.value === 'rent' ? true : (device.availablePowerBankCount > 0),
canReturn: activeTab.value === 'return' ? true : (device.availableEmptyGridCount >
0)
canReturn: activeTab.value === 'return' ? true : (device.availableEmptyGridCount > 0),
supportCouponOrMember: device.supportCouponOrMember || false
}
})
@@ -256,7 +262,7 @@ const init = async () => {
positionList.value = []
filteredPositions.value = []
uni.showToast({
title: $t('home.getLocationFailed'),
title: t('home.getLocationFailed'),
icon: 'none'
})
} finally {
@@ -278,7 +284,7 @@ const init = async () => {
const navigateToPosition = (position) => {
if (!isValidCoordinate(position.latitude, position.longitude)) {
uni.showToast({
title: $t('search.invalidCoordinate'),
title: t('search.invalidCoordinate'),
icon: 'none'
})
return
@@ -296,13 +302,13 @@ const init = async () => {
const goToPositionDetail = (position) => {
if (!position.positionId) {
uni.showToast({
title: $t('search.positionInfoError'),
title: t('search.positionInfoError'),
icon: 'none'
})
return
}
uni.navigateTo({
url: `/pages/position/detail?positionId=${position.positionId}`
url: `/subPackages/business/position/detail?positionId=${position.positionId}`
})
}
</script>
@@ -392,9 +398,8 @@ const init = async () => {
}
.card {
display: grid;
grid-template-columns: 120rpx 1fr 72rpx;
align-items: center;
display: flex;
flex-direction: column;
gap: 16rpx;
padding: 20rpx;
border-radius: 20rpx;
@@ -413,6 +418,21 @@ const init = async () => {
background: #FFF9F9;
}
// 第一行:三列布局
.card-row-first {
display: grid;
grid-template-columns: 120rpx 1fr 72rpx;
align-items: center;
gap: 16rpx;
}
// 第二行:标签
.card-row-second {
width: 100%;
padding-top: 8rpx;
// border-top: 1rpx solid #F0F0F0;
}
.thumb {
width: 120rpx;
height: 120rpx;
@@ -442,7 +462,7 @@ const init = async () => {
font-size: 30rpx;
font-weight: 700;
color: #2A2A2A;
max-width: 70%;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -474,9 +494,11 @@ const init = async () => {
}
}
}
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
@@ -496,6 +518,10 @@ const init = async () => {
background: #E8F2FF;
color: #3578e5;
}
.tag.coupon {
background: #FFF9F0;
color: #D4A574;
}
.actions {
@@ -503,6 +529,7 @@ const init = async () => {
align-items: center;
justify-content: center;
flex-direction: column;
gap: 8rpx;
}
.nav {
+10 -7
View File
@@ -7,15 +7,15 @@
<script>
import {
wxLogin,
} from '../../../util/index'
} from '@/util/index'
import {
getMyIndexInfo
} from "../../../config/api/user.js";
} from "@/config/api/user.js";
import {
queryHasOrder,
checkOrdersByStatus
} from "../../../config/api/order.js";
} from "@/config/api/order.js";
export default {
data() {
@@ -56,7 +56,7 @@
// 如果是使用中的订单,跳转到归还页面
console.log('检测到使用中订单,跳转归还页:', latestOrder.orderId);
uni.redirectTo({
url: `/pages/device/return?orderId=${latestOrder.orderId}`
url: `/subPackages/service/return/index?orderId=${latestOrder.orderId}`
});
} else if (latestOrder.orderStatus === 'waiting_for_payment') {
// 如果是待支付订单,跳转到支付页面,并传递必要信息
@@ -81,13 +81,13 @@
const totalAmount = (parseFloat(depositAmount) + parseFloat(packagePrice)).toFixed(2);
uni.redirectTo({
url: `/pages/order/payment?orderId=${latestOrder.orderId}&packageTimeHours=${packageTimeHours}&packagePrice=${packagePrice}&hourlyRate=${hourlyRate}&totalAmount=${totalAmount}&depositAmount=${depositAmount}`
url: `/subPackages/order/payment?orderId=${latestOrder.orderId}&packageTimeHours=${packageTimeHours}&packagePrice=${packagePrice}&hourlyRate=${hourlyRate}&totalAmount=${totalAmount}&depositAmount=${depositAmount}`
});
} else {
// 其他状态(理论上不应该到这里,除非statusesToCheck配置错误),默认到详情页
console.log('检测到其他状态订单,跳转详情页:', latestOrder.orderId);
uni.redirectTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
url: `/pages/order/detail?deviceNo=${deviceNo}`
});
}
} else {
@@ -122,8 +122,11 @@
url: `/pages/device/detail?deviceNo=${option.deviceNo}`
});
} else {
// uni.switchTab({
// url:'/pages/index/index'
// })
// 如果连deviceNo都没有,则返回首页
uni.switchTab({ url: '/pages/index/index' });
uni.reLaunch({ url: '/pages/index/index' });
}
}, 2000);
} finally {
-240
View File
@@ -1,240 +0,0 @@
<template>
<view class="container">
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="avatar">
<image :src="userInfo.avatar || '/static/images/default-avatar.png'" mode="aspectFill"></image>
</view>
<view class="user-info">
<text class="nickname">{{ userInfo.nickName || $t('user.notLoggedIn') }}</text>
<text class="phone">{{ userInfo.phone || $t('user.phoneNotBound') }}</text>
</view>
</view>
<!-- 余额卡片 -->
<view class="balance-card">
<view class="balance-title">{{ $t('userProfile.balance') }}</view>
<view class="balance-amount">{{ userInfo.balanceAmount || '0.00' }}</view>
<view class="balance-desc">{{ $t('user.balanceDesc') }}</view>
</view>
<!-- 功能菜单 -->
<view class="menu-list">
<view class="menu-item" @click="navigateTo('/pages/order/index')">
<text class="menu-icon">📋</text>
<text class="menu-text">{{ $t('user.myOrders') }}</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="navigateTo('/pages/feedback/index')">
<text class="menu-icon">💬</text>
<text class="menu-text">{{ $t('user.feedback') }}</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="navigateTo('/pages/help/index')">
<text class="menu-icon"></text>
<text class="menu-text">{{ $t('help.title') }}</text>
<text class="menu-arrow">></text>
</view>
</view>
<!-- 退出登录按钮 -->
<view class="logout-btn" @click="handleLogout" v-if="isLogin">
<text>{{ $t('user.logout') }}</text>
</view>
</view>
</template>
<script>
import { getUserInfo } from '@/api/user'
import { URL } from '@/config/url'
export default {
data() {
return {
userInfo: {},
isLogin: false
}
},
onLoad() {
// 设置页面标题
uni.setNavigationBarTitle({
title: this.$t('user.personalCenter')
})
},
onShow() {
this.loadUserInfo()
},
methods: {
async loadUserInfo() {
try {
const res = await getUserInfo()
if (res.code === 401 || res.code === 40101) {
// 无提示跳转至登录
try {
const pages = getCurrentPages()
const current = pages && pages.length ? pages[pages.length - 1] : null
const route = current && current.route ? ('/' + current.route) : '/pages/index/index'
const query = current && current.options ? Object.keys(current.options).map(k => `${k}=${encodeURIComponent(current.options[k])}`).join('&') : ''
const redirect = encodeURIComponent(query ? `${route}?${query}` : route)
uni.reLaunch({ url: `/pages/login/index?redirect=${redirect}` })
} catch (e) {
uni.reLaunch({ url: '/pages/login/index' })
}
} else if (res.code === 200) {
this.userInfo = res.data
this.isLogin = true
} else {
this.isLogin = false
}
} catch (error) {
console.error('加载用户信息失败:', error)
this.isLogin = false
}
},
navigateTo(url) {
uni.navigateTo({ url })
},
handleLogout() {
uni.showModal({
title: this.$t('common.tips'),
content: this.$t('user.confirmLogout'),
success: (res) => {
if (res.confirm) {
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
this.isLogin = false
uni.showToast({
title: this.$t('user.logoutSuccess'),
icon: 'success'
})
setTimeout(() => {
uni.redirectTo({
url: '/pages/login/index'
})
}, 500)
}
}
})
}
}
}
</script>
<style>
.container {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.user-card {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
display: flex;
align-items: center;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
overflow: hidden;
margin-right: 30rpx;
}
.avatar image {
width: 100%;
height: 100%;
}
.user-info {
flex: 1;
}
.nickname {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
display: block;
}
.phone {
font-size: 28rpx;
color: #666;
}
.balance-card {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.balance-title {
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
}
.balance-amount {
font-size: 48rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.balance-desc {
font-size: 24rpx;
color: #999;
}
.menu-list {
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.menu-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-icon {
font-size: 36rpx;
margin-right: 20rpx;
}
.menu-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
.menu-arrow {
font-size: 28rpx;
color: #999;
}
.logout-btn {
margin-top: 40rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
text-align: center;
color: #ff4d4f;
font-size: 28rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
</style>
-413
View File
@@ -1,413 +0,0 @@
<template>
<view class="profile-page">
<view class="avatar-section">
<view class="avatar-container">
<image class="avatar" v-if="userInfo.avatar" :src="userInfo.avatar" mode="aspectFill"></image>
<image v-else class="avatar" src="@/static/head.png" mode="aspectFill"></image>
<!-- 覆盖在头像上的微信选择头像授权按钮仅小程序生效 -->
<!-- #ifdef MP-WEIXIN -->
<button class="avatar-choose-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar"></button>
<!-- #endif -->
</view>
<view class="avatar-tip">{{ $t('userProfile.clickToChange') }}</view>
</view>
<view class="form-section">
<!-- 昵称编辑区域 -->
<view class="form-item nickname-item" :class="{ editing: isEditingNickname }">
<view class="label">{{ $t('userProfile.nickname') }}</view>
<view class="value" v-if="!isEditingNickname" @click="startEditNickname">
<text class="value-text">{{ userInfo.nickName || $t('userProfile.notSet') }}</text>
<uv-icon name="edit-pen" size="16" color="#999999"></uv-icon>
</view>
</view>
<!-- 昵称编辑输入框展开状态 -->
<view class="nickname-edit-area" v-if="isEditingNickname">
<input
class="nickname-input"
v-model="newNickname"
:placeholder="$t('userProfile.enterNickname')"
maxlength="20"
:focus="true"
/>
<view class="edit-buttons">
<button class="cancel-btn" @click="cancelEditNickname">{{ $t('common.cancel') }}</button>
<button class="save-btn" @click="saveNickname">{{ $t('common.save') }}</button>
</view>
</view>
<view class="form-item">
<view class="label">{{ $t('userProfile.phone') }}</view>
<view class="value">
<text class="value-text">{{ userInfo.phone ? maskPhone(userInfo.phone) : $t('userProfile.notBound') }}</text>
</view>
</view>
<!-- <view class="form-item" v-if="userInfo.balanceAmount !== undefined">
<view class="label">{{ $t('userProfile.balance') }}</view>
<view class="value">
<text class="value-text amount">¥{{ userInfo.balanceAmount || '0.00' }}</text>
</view>
</view> -->
</view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { getMyIndexInfo, uploadUserAvatar, updateUserInfo } from '../../config/api/user.js';
import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n()
// 响应式状态
const userInfo = ref({
nickName: '',
phone: '',
avatar: '',
balanceAmount: '0.00'
});
const newNickname = ref('');
const isEditingNickname = ref(false);
// 页面加载时初始化
onMounted(() => {
uni.setNavigationBarTitle({
title: $t('userProfile.title')
})
loadUserInfo();
});
// 获取用户信息
const loadUserInfo = async () => {
try {
const res = await getMyIndexInfo();
console.log('User info response:', res);
if (res.code == 401 || res.code == 40101) {
redirectToLogin();
return;
} else if (res.code == 200) {
userInfo.value = {
nickName: res.data.nickname,
phone: res.data.phone,
avatar: res.data.iconUrl,
balanceAmount: res.data.balanceAmount || '0.00',
isAdmin: res.data.isAdmin
};
uni.setStorageSync('userInfo', userInfo.value);
}
} catch (error) {
console.error('获取用户信息失败:', error);
uni.showToast({
title: $t('user.getUserInfoFailed'),
icon: 'none'
});
}
};
// 跳转登录
const redirectToLogin = () => {
try {
const pages = getCurrentPages();
const current = pages && pages.length ? pages[pages.length - 1] : null;
const route = current && current.route ? ('/' + current.route) : '/pages/index/index';
const query = current && current.options ? Object.keys(current.options).map(k =>
`${k}=${encodeURIComponent(current.options[k])}`).join('&') : '';
const redirect = encodeURIComponent(query ? `${route}?${query}` : route);
uni.reLaunch({
url: `/pages/login/index?redirect=${redirect}`
});
} catch (e) {
uni.reLaunch({
url: '/pages/login/index'
});
}
};
// 小程序原生选择头像回调
const onChooseAvatar = async (e) => {
try {
const token = uni.getStorageSync('token');
if (!token) {
redirectToLogin();
return;
}
const avatarLocalPath = e?.detail?.avatarUrl;
if (!avatarLocalPath) {
uni.showToast({
title: $t('user.noAvatar'),
icon: 'none'
});
return;
}
uni.showLoading({
title: $t('userProfile.uploading'),
mask: true
});
const uploadRes = await uploadUserAvatar(avatarLocalPath);
const serverAvatar = uploadRes?.data?.url || uploadRes?.url || uploadRes?.data || '';
if (serverAvatar) {
userInfo.value = {
...userInfo.value,
avatar: serverAvatar
};
uni.setStorageSync('userInfo', userInfo.value);
}
uni.showToast({
title: $t('user.avatarUpdated'),
icon: 'success'
});
await loadUserInfo();
} catch (err) {
console.error('选择/上传头像失败:', err);
uni.showToast({
title: $t('user.avatarUploadFailed'),
icon: 'none'
});
} finally {
uni.hideLoading();
}
};
// 开始编辑昵称
const startEditNickname = () => {
newNickname.value = userInfo.value.nickName || '';
isEditingNickname.value = true;
};
// 取消编辑昵称
const cancelEditNickname = () => {
isEditingNickname.value = false;
newNickname.value = '';
};
// 保存昵称
const saveNickname = async () => {
if (!newNickname.value || !newNickname.value.trim()) {
uni.showToast({
title: $t('userProfile.nicknameRequired'),
icon: 'none'
});
return;
}
try {
uni.showLoading({
title: $t('userProfile.saving'),
mask: true
});
// 先获取最新的用户信息,确保数据是最新的
const latestUserInfo = await getMyIndexInfo();
if (latestUserInfo.code !== 200) {
throw new Error('获取用户信息失败');
}
// 使用最新的服务器数据,只修改昵称字段
const updateData = {
nickname: newNickname.value.trim(),
phone: latestUserInfo.data.phone,
iconUrl: latestUserInfo.data.iconUrl,
// 保留其他可能的字段
...latestUserInfo.data
};
// 确保昵称使用新值
updateData.nickname = newNickname.value.trim();
// 调用后端接口更新用户信息
const res = await updateUserInfo(updateData);
if (res.code === 200) {
// 更新成功后重新获取用户信息,确保数据同步
await loadUserInfo();
uni.hideLoading();
uni.showToast({
title: $t('userProfile.nicknameUpdated'),
icon: 'success'
});
isEditingNickname.value = false;
} else {
throw new Error(res.message || $t('userProfile.updateFailed'));
}
} catch (error) {
console.error('修改昵称失败:', error);
uni.hideLoading();
uni.showToast({
title: error.message || $t('userProfile.updateFailed'),
icon: 'none'
});
}
};
// 手机号掩码函数
function maskPhone(phone) {
if (!phone) return '';
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
</script>
<style lang="scss" scoped>
.profile-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: env(safe-area-inset-bottom);
}
.avatar-section {
background: linear-gradient(180deg, #D1FFE1 0%, #ffffff 100%);
padding: 60rpx 0 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.avatar-container {
position: relative;
margin-bottom: 20rpx;
}
.avatar {
width: 160rpx;
height: 160rpx;
border-radius: 80rpx;
background-color: #f0f0f0;
display: block;
}
/* 仅小程序端存在,此按钮覆盖在头像上捕获点击以触发选择头像 */
/* #ifdef MP-WEIXIN */
.avatar-choose-btn {
position: absolute;
left: 0;
top: 0;
width: 160rpx;
height: 160rpx;
border: none;
background: transparent;
padding: 0;
margin: 0;
opacity: 0;
border-radius: 80rpx;
}
/* #endif */
.avatar-tip {
font-size: 24rpx;
color: #999999;
}
.form-section {
margin: 20rpx 30rpx;
background-color: #ffffff;
border-radius: 20rpx;
overflow: hidden;
}
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
transition: all 0.3s ease;
}
.form-item:last-child {
border-bottom: none;
}
.form-item.nickname-item.editing {
border-bottom: none;
padding-bottom: 20rpx;
}
.label {
font-size: 30rpx;
color: #333333;
font-weight: 500;
}
.value {
display: flex;
align-items: center;
}
.value-text {
font-size: 28rpx;
color: #666666;
margin-right: 10rpx;
}
.value-text.amount {
color: #e2231a;
font-weight: 600;
font-size: 32rpx;
}
/* 昵称编辑区域样式 */
.nickname-edit-area {
padding: 0 30rpx 30rpx 30rpx;
border-bottom: 1rpx solid #f5f5f5;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.nickname-input {
width: 100%;
height: 80rpx;
border: 1rpx solid #e0e0e0;
border-radius: 10rpx;
padding: 0 20rpx;
font-size: 28rpx;
color: #333333;
box-sizing: border-box;
margin-bottom: 20rpx;
background-color: #fafafa;
}
.edit-buttons {
display: flex;
justify-content: flex-end;
gap: 20rpx;
}
.cancel-btn,
.save-btn {
padding: 0 40rpx;
height: 64rpx;
line-height: 64rpx;
text-align: center;
border-radius: 32rpx;
font-size: 28rpx;
border: none;
min-width: 120rpx;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666666;
}
.save-btn {
background: linear-gradient(135deg, #42d392 0%, #28c76f 100%);
color: #ffffff;
}
</style>
+6 -6
View File
@@ -26,7 +26,7 @@
import { getOrderByOrderNoScorePayStatus, cancelOrder } from '@/config/api/order.js'
import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n()
const { t } = useI18n()
const progress = ref(0)
const leftRotateDeg = ref(0)
@@ -91,9 +91,9 @@
await cancelOrder({ orderId: orderNo.value })
}
} catch (e) {}
uni.showToast({ title: $t('waiting.rentFailed'), icon: 'none' })
uni.showToast({ title: t('waiting.rentFailed'), icon: 'none' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
uni.reLaunch({ url: '/pages/index/index' })
}, 800)
}
@@ -129,16 +129,16 @@
// 60
timeoutTimer = setTimeout(() => {
stopAllTimers()
uni.showToast({ title: $t('waiting.timeout'), icon: 'none' })
uni.showToast({ title: t('waiting.timeout'), icon: 'none' })
setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' })
uni.reLaunch({ url: '/pages/index/index' })
}, 800)
}, 60000)
}
onLoad((query) => {
uni.setNavigationBarTitle({
title: $t('waiting.title')
title: t('waiting.title')
})
if (query) {
if (query.orderNo) {
+10 -7
View File
@@ -17,6 +17,9 @@ importers:
axios-miniprogram-adapter:
specifier: 0.3.4
version: 0.3.4
html5-qrcode:
specifier: ^2.3.8
version: 2.3.8
uniapp-axios-adapter:
specifier: ^0.3.2
version: 0.3.2(axios@1.10.0)
@@ -83,9 +86,6 @@ packages:
'@jridgewell/source-map@0.3.6':
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':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
@@ -491,6 +491,9 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
html5-qrcode@2.3.8:
resolution: {integrity: sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==}
immutable@5.1.3:
resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==}
@@ -743,7 +746,7 @@ snapshots:
'@jridgewell/gen-mapping@0.3.8':
dependencies:
'@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/resolve-uri@3.1.2': {}
@@ -755,14 +758,12 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.25':
dependencies:
'@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':
optional: true
@@ -1177,6 +1178,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
html5-qrcode@2.3.8: {}
immutable@5.1.3: {}
is-extglob@2.1.1:
Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 KiB

After

Width:  |  Height:  |  Size: 91 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because it is too large Load Diff
+783
View File
@@ -0,0 +1,783 @@
<template>
<view class="order-detail-container">
<!-- 状态标题 -->
<!-- <view class="status-header">
<view class="status-icon">
<uv-icon name="checkbox-mark" size="40" color="#07c160"></uv-icon>
</view>
<text class="status-text">{{ statusText }}</text>
</view> -->
<!-- 产品信息卡片 -->
<view class="product-card">
<image
:src="orderDetail.pictureUrl || orderDetail.productImage || '/static/default-product.png'"
mode="aspectFill"
class="product-image"
></image>
<view class="product-info">
<view class="product-name">{{ orderDetail.productName || orderDetail.deviceName || $t('goods.defaultProductNameFull') }}</view>
<view class="product-style">款式{{ orderDetail.optionName || orderDetail.style || '标准' }}</view>
<view class="product-price">¥{{ orderDetail.price || orderDetail.totalAmount }}</view>
</view>
</view>
<!-- 订单信息 -->
<view class="info-section">
<view class="section-title">订单信息</view>
<view class="info-item">
<text class="label">订单号</text>
<text class="value">{{ orderDetail.orderNo || '-' }}</text>
</view>
<view class="info-item" v-if="orderDetail.outTradeNo">
<text class="label">交易单号</text>
<text class="value">{{ orderDetail.outTradeNo }}</text>
</view>
<view class="info-item">
<text class="label">订单状态</text>
<text class="value" :style="{color: statusColor}">{{ statusText }}</text>
</view>
<view class="info-item">
<text class="label">创建时间</text>
<text class="value">{{ orderDetail.createTime || '-' }}</text>
</view>
<view class="info-item">
<text class="label">支付方式</text>
<text class="value">{{ paymentMethodText }}</text>
</view>
<view class="info-item">
<text class="label">支付时间</text>
<text class="value">{{ orderDetail.payTime || '未支付' }}</text>
</view>
<view class="info-item">
<text class="label">收货人</text>
<text class="value">{{ receiverInfo }}</text>
</view>
<view class="info-item">
<text class="label">收货地址</text>
<text class="value address">{{ orderDetail.receiverAddress || '-' }}</text>
</view>
<view class="info-item" v-if="orderDetail.expressageNo">
<text class="label">快递单号</text>
<text class="value">{{ orderDetail.expressageNo }}</text>
</view>
<view class="info-item" v-if="orderDetail.remark">
<text class="label">备注</text>
<text class="value address">{{ orderDetail.remark }}</text>
</view>
</view>
<!-- 费用信息 -->
<view class="info-section">
<view class="section-title">费用信息</view>
<view class="info-item" v-if="orderDetail.quantity">
<text class="label">数量</text>
<text class="value">x{{ orderDetail.quantity }}</text>
</view>
<view class="info-item">
<text class="label">单价</text>
<text class="value">¥ {{ orderDetail.price || orderDetail.totalAmount }}</text>
</view>
<view class="info-item total">
<text class="label">合计</text>
<text class="value highlight">¥ {{ totalAmount }}</text>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-actions">
<!-- 待付款状态显示取消订单和立即支付 -->
<template v-if="orderDetail.status === 0 || orderDetail.status === '0'">
<view class="action-btn secondary" @click="onCancelOrder">
取消订单
</view>
<view class="action-btn primary" @click="onPayNow">
立即支付
</view>
</template>
<!-- 已完成/已取消状态显示删除订单和再次定制 -->
<template v-else-if="orderDetail.status === 3 || orderDetail.status === '3' || orderDetail.status === 4 || orderDetail.status === '4'">
<view class="action-btn secondary" @click="onDeleteOrder">
删除订单
</view>
<view class="action-btn primary" @click="onReorder(orderDetail.productId)">
再次定制
</view>
</template>
<!-- 其他状态显示联系客服和再次定制 -->
<template v-else>
<view class="action-btn secondary" @click="onContactService">
联系客服
</view>
<view class="action-btn primary" @click="onReorder(orderDetail.productId)">
再次定制
</view>
</template>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import {
queryById,
getProductOrderDetail,
deleteProductOrder,
cancelProductOrder,
createWxPayment
} from '../../config/api/order.js';
// import { getSystemParamByKey } from '../../config/api/system.js';
import { useI18n, showModalI18n } from '@/utils/i18n.js'
const { t } = useI18n()
const orderDetail = ref({});
const orderId = ref('');
const countdownText = ref('');
let countdownTimer = null;
//
const statusText = computed(() => {
const status = orderDetail.value.status;
if (status === 0 || status === '0') {
return '待付款';
}
if (status === 1 || status === '1') {
return '待发货';
}
if (status === 2 || status === '2') {
return '待收货';
}
if (status === 3 || status === '3') {
return '已完成';
}
if (status === 4 || status === '4') {
return '已取消';
}
if (status === 5 || status === '5') {
return '退款中';
}
return '待付款';
});
//
const statusColor = computed(() => {
const status = orderDetail.value.status;
if (status === 0 || status === '0') {
return '#ff976a'; // -
}
if (status === 1 || status === '1') {
return '#1989fa'; // -
}
if (status === 2 || status === '2') {
return '#1989fa'; // -
}
if (status === 3 || status === '3') {
return '#07c160'; // - 绿
}
if (status === 4 || status === '4') {
return '#999'; // -
}
if (status === 5 || status === '5') {
return '#ff6b6b'; // 退 -
}
return '#ff976a';
});
//
const paymentMethodText = computed(() => {
const payWay = orderDetail.value.payWay;
if (payWay === 'wx_score_pay') return '微信支付';
if (payWay === 'wx_global_pay') return '微信支付';
if (payWay === 'wx_member_pay') return '微信支付';
return '微信支付';
});
//
const receiverInfo = computed(() => {
const name = orderDetail.value.receiverName || '-';
const phone = orderDetail.value.receiverPhone || '-';
return `${name} ${phone}`;
});
//
const totalAmount = computed(() => {
return orderDetail.value.totalAmount || orderDetail.value.amount || orderDetail.value.payAmount || '99.0';
});
//
onLoad(async (options) => {
if (options && options.orderId) {
orderId.value = options.orderId;
await loadOrderDetail();
}
});
//
const updatePageTitle = () => {
const status = orderDetail.value.status;
let title = '订单详情';
if (status === 0 || status === '0') {
title = '待付款';
//
if (countdownText.value) {
title = `${title} ${countdownText.value}`;
}
} else if (status === 1 || status === '1') {
title = '待发货';
} else if (status === 2 || status === '2') {
title = '待收货';
} else if (status === 3 || status === '3') {
title = '已完成';
} else if (status === 4 || status === '4') {
title = '已取消';
} else if (status === 5 || status === '5') {
title = '退款中';
}
uni.setNavigationBarTitle({
title: title
});
};
//
const startCountdown = () => {
//
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
const status = orderDetail.value.status;
const expireTime = orderDetail.value.expireTime;
//
if ((status === 0 || status === '0') && expireTime) {
//
const updateCountdown = () => {
const now = new Date().getTime();
const expireTimestamp = new Date(expireTime).getTime();
const diff = expireTimestamp - now;
if (diff > 0) {
//
const totalMinutes = Math.floor(diff / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
// MM:SS
const minutesStr = String(totalMinutes).padStart(2, '0');
const secondsStr = String(seconds).padStart(2, '0');
countdownText.value = `${minutesStr}:${secondsStr}`;
updatePageTitle();
} else {
//
countdownText.value = '';
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
//
loadOrderDetail();
}
};
//
updateCountdown();
//
countdownTimer = setInterval(updateCountdown, 1000);
} else {
countdownText.value = '';
updatePageTitle();
}
};
//
onUnmounted(() => {
if (countdownTimer) {
clearInterval(countdownTimer);
countdownTimer = null;
}
});
//
const loadOrderDetail = async () => {
try {
uni.showLoading({
title: '加载中...'
});
const res = await getProductOrderDetail(orderId.value);
if (res.code === 200 && res.data) {
const data = res.data;
orderDetail.value = {
//
id: data.id,
orderNo: data.orderNo,
outTradeNo: data.outTradeNo,
userId: data.userId,
//
status: data.status,
payStatus: data.payStatus,
//
totalAmount: data.totalAmount,
payAmount: data.payAmount,
price: data.price,
//
createTime: data.createTime,
updateTime: data.updateTime,
payTime: data.payTime,
expireTime: data.expireTime, //
//
receiverName: data.receiverName,
receiverPhone: data.receiverPhone,
receiverAddress: data.receiverAddress,
//
expressageNo: data.expressageNo,
//
remark: data.remark,
//
productName: data.productName,
optionName: data.optionName,
pictureUrl: data.pictureUrl,
color: data.color,
quantity: data.quantity,
productId:data.productId,
//
orderId: data.id,
deviceId: data.deviceNo || '',
deviceName: data.deviceName || '',
style: data.optionName || data.style || data.deviceStyle || '',
payWay: data.payWay || 'wx_global_pay',
phone: data.receiverPhone,
address: data.receiverAddress,
deposit: data.deposit || '0',
package: data.package || '',
amount: data.payAmount,
productImage: data.pictureUrl || data.productImage || ''
};
//
startCountdown();
}
uni.hideLoading();
} catch (error) {
uni.hideLoading();
console.error('加载订单详情失败:', error);
uni.showToast({
title: '加载失败',
icon: 'none'
});
}
};
// 退/
const onRefund = () => {
uni.showToast({
title: '退款/售后功能开发中',
icon: 'none'
});
};
//
const onCancelOrder = () => {
showModalI18n({
title: t('common.tips'),
content: t('order.confirmCancelContent'),
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({
title: '取消中...',
mask: true
});
const result = await cancelProductOrder(orderDetail.value.id);
uni.hideLoading();
if (result && result.code === 200) {
uni.showToast({
title: '订单已取消',
icon: 'success'
});
//
await loadOrderDetail();
} else {
throw new Error(result?.msg || '取消失败');
}
} catch (error) {
uni.hideLoading();
console.error('取消订单失败:', error);
uni.showToast({
title: error.message || '取消失败',
icon: 'none'
});
}
}
}
});
};
//
const onDeleteOrder = () => {
showModalI18n({
title: t('common.tips'),
content: t('order.confirmDeleteContent'),
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({
title: '删除中...',
mask: true
});
const result = await deleteProductOrder(orderDetail.value.id);
uni.hideLoading();
if (result && result.code === 200) {
uni.showToast({
title: '删除成功',
icon: 'success'
});
//
setTimeout(() => {
uni.navigateBack();
}, 1500);
} else {
throw new Error(result?.msg || '删除失败');
}
} catch (error) {
uni.hideLoading();
console.error('删除订单失败:', error);
uni.showToast({
title: error.message || '删除失败',
icon: 'none'
});
}
}
}
});
};
//
const onPayNow = async () => {
try {
uni.showLoading({
title: '正在创建支付...',
mask: true
});
const res = await createWxPayment(orderDetail.value.orderNo);
if (res && res.code === 200 && res.data) {
uni.hideLoading();
const payParams = res.data;
//
uni.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType,
paySign: payParams.paySign,
success: async (payRes) => {
console.log('支付成功:', payRes);
uni.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
});
//
await loadOrderDetail();
},
fail: async (err) => {
console.error('支付失败:', err);
//
if (err.errMsg && err.errMsg.includes('cancel')) {
//
try {
await cancelProductOrder(orderDetail.value.id);
uni.showToast({
title: '支付已取消',
icon: 'none'
});
//
await loadOrderDetail();
} catch (cancelError) {
console.error('取消订单失败:', cancelError);
uni.showToast({
title: '支付已取消',
icon: 'none'
});
}
} else {
//
uni.showToast({
title: '支付失败,请重试',
icon: 'none'
});
}
}
});
} else {
uni.hideLoading();
uni.showToast({
title: res?.msg || '创建支付订单失败',
icon: 'none'
});
}
} catch (error) {
console.error('支付异常:', error);
uni.hideLoading();
uni.showToast({
title: error.message || '支付失败,请重试',
icon: 'none'
});
}
};
//
const onContactService = async () => {
const phoneNumber = uni.getStorageSync('customerPhone');
//
uni.makePhoneCall({
phoneNumber: phoneNumber,
success: () => {
console.log('拨打电话成功');
},
fail: (err) => {
console.error('拨打电话失败:', err);
uni.showToast({
title: '拨打电话失败',
icon: 'none'
});
}
});
};
//
const onReorder = (order) => {
if(order){
uni.navigateTo({
url: `/subPackages/business/device-goods?productId=${order}`
});
}
// console.log(order);
};
</script>
<style lang="scss" scoped>
.order-detail-container {
min-height: 100vh;
background: #f7f8fa;
padding-bottom: 120rpx;
//
.status-header {
background: #fff;
padding: 60rpx 0 40rpx;
text-align: center;
.status-icon {
margin-bottom: 20rpx;
}
.status-text {
font-size: 36rpx;
color: #333;
font-weight: 600;
}
}
//
.product-card {
background: #fff;
margin: 20rpx;
padding: 24rpx;
border-radius: 16rpx;
display: flex;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
.product-image {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
background: #f5f5f5;
flex-shrink: 0;
}
.product-info {
flex: 1;
margin-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
.product-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
line-height: 1.4;
margin-bottom: 12rpx;
}
.product-style {
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
}
.product-price {
font-size: 32rpx;
color: #07c160;
font-weight: 600;
}
}
}
//
.info-section {
background: #fff;
margin: 20rpx;
padding: 24rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
.section-title {
font-size: 28rpx;
color: #333;
font-weight: 600;
margin-bottom: 24rpx;
}
.info-item {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
font-size: 28rpx;
height: 50rpx;
&:last-child {
margin-bottom: 0;
}
.label {
color: #999;
flex-shrink: 0;
width: 140rpx;
}
.value {
color: #333;
flex: 1;
text-align: right;
word-break: break-all;
&.address {
text-align: right;
}
}
&.total {
margin-top: 12rpx;
padding-top: 20rpx;
border-top: 1rpx dashed #e5e5e5;
.label {
color: #333;
font-weight: 500;
}
.value.highlight {
color: #07c160;
font-size: 36rpx;
font-weight: 600;
}
}
}
}
//
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 20rpx;
display: flex;
justify-content: flex-end;
gap: 20rpx;
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.04);
z-index: 100;
.action-btn {
width: 180rpx;
height: 64rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 500;
&.secondary {
background: #fff;
color: #07c160;
border: 2rpx solid #07c160;
}
&.primary {
background: #07c160;
color: #fff;
}
&:active {
opacity: 0.8;
}
}
}
}
</style>
+864
View File
@@ -0,0 +1,864 @@
<template>
<view class="order-container">
<!-- 状态切换 -->
<view class="status-tabs">
<view v-for="(tab, index) in orderStatusTabs" :key="index" class="tab-item"
:class="{ active: currentTab === index }" @click="switchTab(index)">
{{ tab.text }}
</view>
</view>
<!-- 订单列表 -->
<view class="order-list">
<view class="empty-state" v-if="orderList.length === 0">
<view class="empty-icon">
<image src="/static/orderList.png" mode="aspectFill" class="empty-icon" lazy-load="true"></image>
</view>
<text class="empty-text">{{ $t('order.noOrderRecord') }}</text>
</view>
<DeviceOrderItemCard
v-for="(order, index) in orderList"
:key="index"
:order="order"
@click="navigateToDeviceOrderDetail"
@delete="handleDeleteOrder"
@pay="handlePayment"
/>
</view>
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted
} from 'vue';
import DeviceOrderItemCard from '../../components/DeviceOrderItemCard.vue';
import {
onLoad,
onShow
} from '@dcloudio/uni-app';
import {
getOrderList,
getProductOrderList,
queryById,
getOrderByOrderNoScorePayStatus,
cancelOrder,
createWxPayment,
deleteProductOrder,
cancelProductOrder,
} from '../../config/api/order.js';
import{
createProductOrder
}from "@/config/api/product.js"
import {
updateUserBalance
} from '../../config/api/user.js';
import {
URL
} from '../../config/url.js';
import { useI18n, showModalI18n } from '@/utils/i18n.js'
const { t } = useI18n()
//
const currentTab = ref(0);
const orderList = ref([]);
//
const orderStatusMap = reactive({
'0': {
text: '待付款',
class: 'status-waiting'
},
'1': {
text: '待发货',
class: 'status-shipping'
},
'2': {
text: '待收货',
class: 'status-receiving'
},
'3': {
text: '已完成',
class: 'status-finished'
},
'4': {
text: '已取消',
class: 'status-cancelled'
},
'5': {
text: '退款中',
class: 'status-refunding'
}
});
//
const orderStatusTabs = reactive([
{
text: '全部',
status: []
},
{
text: '待付款',
status: [0]
},
{
text: '待发货',
status: [1]
},
{
text: '待收货',
status: [2]
},
{
text: '已完成',
status: [3]
},
{
text: '已取消',
status: [4]
},
// {
// text: '退',
// status: [5]
// }
]);
//
onLoad(async (options) => {
// orderId
if (options && options.orderId) {
try {
//
const res = await queryById(options.orderId);
if (res.code === 200 && res.data) {
//
const orderData = res.data;
// 使startTime使createTime
const orderStartTime = orderData.startTime || orderData.createTime || '';
//
const formattedOrder = {
orderNo: orderData.orderId,
status: orderData.orderStatus,
deviceId: orderData.deviceNo,
payWay: orderData.payWay,
startTime: orderStartTime,
endTime: orderData.endTime || '',
positionName: orderData.positionName || orderData.positionLocation || '',
deviceName: orderData.deviceName || '',
amount: orderData.payAmount || orderData.actualDeviceAmount || orderData.currentFee || orderData.residueAmount || '0.00'
};
//
orderList.value = [formattedOrder, ...orderList.value];
//
const tabIndex = orderStatusTabs.findIndex(tab =>
tab.status.includes(orderData.orderStatus)
);
if (tabIndex !== -1) {
switchTab(tabIndex);
}
}
} catch (error) {
console.error('获取订单详情失败:', error);
}
}
//
await loadOrderList();
});
//
onShow(async () => {
//
const statusArray = orderStatusTabs[currentTab.value].status;
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
await loadOrderList(statusValue);
});
//
const switchTab = async (index) => {
currentTab.value = index;
//
const statusArray = orderStatusTabs[index].status;
// undefined
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
await loadOrderList(statusValue);
};
//
const loadOrderList = async (statusList) => {
try {
let params = {};
if(statusList !== undefined){
params = {
status: statusList
}
}
const res = await getProductOrderList(params);
//
if (res.code === 200 && res.data) {
// res.data.rows res.data.records
const dataList = res.data.rows || res.data.records || [];
//
orderList.value = dataList.map(item => {
return {
//
id: item.id,
orderNo: item.orderNo,
orderId: item.id,
userId: item.userId,
//
status: item.status,
orderStatus: item.status,
payStatus: item.payStatus,
//
totalAmount: item.totalAmount,
payAmount: item.payAmount,
price: item.price,
//
createTime: item.createTime,
payTime: item.payTime,
//
receiverName: item.receiverName,
receiverPhone: item.receiverPhone,
receiverAddress: item.receiverAddress,
//
expressageNo: item.expressageNo,
//
remark: item.remark,
//
productName: item.productName,
optionName: item.optionName,
pictureUrl: item.pictureUrl,
color: item.color,
quantity: item.quantity,
//
deviceId: item.deviceNo || '',
deviceName: item.productName || item.deviceName || '',
productImage: item.pictureUrl || '',
style: item.optionName || item.style || item.deviceStyle || '',
payWay: item.payWay || '',
startTime: item.createTime || item.startTime || '',
endTime: item.endTime || '',
positionName: item.positionName || item.positionLocation || '',
amount: item.payAmount || item.totalAmount || '0.00'
};
});
}
} catch (error) {
console.error('获取订单列表失败:', error);
uni.showToast({
title: t('order.getOrderListFailed'),
icon: 'none'
});
}
};
//
const handleOrderCompleted = (orderData) => {
console.log('订单列表页收到订单完成事件:', orderData)
//
const statusArray = orderStatusTabs[currentTab.value].status
const statusValue = statusArray.length === 0 ? undefined : statusArray[0]
loadOrderList(statusValue)
}
//
onMounted(() => {
uni.setNavigationBarTitle({
title: t('order.myDeviceOrders')
})
//
uni.$on('orderCompleted', handleOrderCompleted)
})
//
onUnmounted(() => {
uni.$off('orderCompleted', handleOrderCompleted)
})
//
const getOrderStatus = async (order) => {
try {
const res = await getOrderByOrderNoScorePayStatus(order.orderNo);
if (res.code === 200) {
uni.showToast({
title: t('order.syncSuccess'),
icon: 'success'
});
const statusArray = orderStatusTabs[currentTab.value].status;
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
await loadOrderList(statusValue);
}
} catch (error) {
uni.showToast({
title: t('order.syncFailed'),
icon: 'none'
});
}
};
//
const navigateToOrderDetail = (order) => {
uni.navigateTo({
url: `/pages/order/detail?orderId=${order.orderId || order.orderNo}&deviceId=${order.deviceId}`
});
};
//
const onReturnDevice = (order) => {
navigateToOrderDetail(order);
};
//
const navigateToDetails = (order) => {
navigateToOrderDetail(order);
};
// device-goods.vue
const handlePayment = async (order) => {
try {
uni.showLoading({
title: '正在创建支付...',
mask: true
});
console.log('订单列表立即支付,订单信息:', order);
// device-goods.vue
let paymentPlatform = 'WECHAT'; //
// #ifdef MP-ALIPAY
paymentPlatform = 'ALIPAY';
// #endif
// #ifdef H5
// H5 Antom ANTOM paymentType / osType
paymentPlatform = 'WECHAT';
// #endif
// 使
const res = await createProductOrder({
orderNo: order.orderNo,
paymentPlatform
});
if (res && res.code === 200 && res.data) {
uni.hideLoading();
//
const outOrderNo = res.data.OutOrderNo || res.data.outOrderNo;
// ====================== ======================
// #ifdef MP-WEIXIN
const payParams = res.data;
uni.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType,
paySign: payParams.paySign,
success: async (payRes) => {
console.log('支付成功:', payRes);
uni.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
});
//
const statusArray = orderStatusTabs[currentTab.value].status;
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
await loadOrderList(statusValue);
},
fail: async (payErr) => {
console.error('支付失败:', payErr);
//
if (payErr.errMsg && payErr.errMsg.includes('cancel')) {
//
try {
// await cancelProductOrder(outOrderNo || order.orderNo);
uni.showToast({
title: '支付已取消',
icon: 'none'
});
//
const statusArray = orderStatusTabs[currentTab.value].status;
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
await loadOrderList(statusValue);
} catch (cancelError) {
console.error('取消订单失败:', cancelError);
uni.showToast({
title: '支付已取消',
icon: 'none'
});
}
} else {
//
uni.showToast({
title: '支付失败,请重试',
icon: 'none'
});
}
}
});
// #endif
// ====================== ======================
// #ifdef MP-ALIPAY
console.log(res.data, '支付宝支付参数');
const tradeNO = res.data.tradeNo || res.data.tradeNO;
if (!tradeNO) {
uni.showToast({
title: '未获取到支付宝支付参数',
icon: 'none'
});
return;
}
my.tradePay({
tradeNO,
success: async (payRes) => {
console.log('支付宝支付结果:', payRes);
if (payRes.resultCode === '9000') {
uni.showToast({
title: '支付成功',
icon: 'success',
duration: 2000
});
//
const statusArray = orderStatusTabs[currentTab.value].status;
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
await loadOrderList(statusValue);
} else {
uni.showToast({
title: '支付失败,请重试',
icon: 'none'
});
}
},
fail: () => {
uni.showToast({
title: '支付失败,请重试',
icon: 'none'
});
}
});
// #endif
// ====================== H5 Antom ======================
// #ifdef H5
uni.showToast({
title: '当前环境暂不支持购买,请使用微信或支付宝小程序',
icon: 'none'
});
// #endif
} else {
uni.hideLoading();
uni.showToast({
title: res?.msg || '创建支付订单失败',
icon: 'none'
});
}
} catch (error) {
console.error('支付异常:', error);
uni.hideLoading();
uni.showToast({
title: error.message || '支付失败,请重试',
icon: 'none'
});
}
};
//
const handleCancelOrder = async (order) => {
try {
showModalI18n({
title: t('order.confirmCancel'),
content: t('order.confirmCancelContent'),
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: t('common.processing')
});
const result = await cancelOrder({
orderId: order.orderNo
});
if (result) {
uni.hideLoading();
uni.showToast({
title: t('order.cancelSuccess'),
icon: 'success'
});
//
await loadOrderList();
} else {
throw new Error(result.msg || t('order.cancelFailed'));
}
}
}
});
} catch (error) {
uni.hideLoading();
uni.showToast({
title: error.message || t('order.cancelFailed'),
icon: 'none'
});
}
};
//
const navigateToDeviceOrderDetail = (order) => {
uni.navigateTo({
url: `/subPackages/business/device-orderDetail?orderId=${order.orderId || order.orderNo}`
});
};
//
const handleDeleteOrder = (order) => {
showModalI18n({
title: t('common.tips'),
content: t('order.confirmDeleteContent'),
success: async (res) => {
if (res.confirm) {
try {
uni.showLoading({
title: '删除中...',
mask: true
});
//
const result = await deleteProductOrder(order.id || order.orderId);
uni.hideLoading();
if (result && result.code === 200) {
uni.showToast({
title: '删除成功',
icon: 'success'
});
//
const statusArray = orderStatusTabs[currentTab.value].status;
const statusValue = statusArray.length === 0 ? undefined : statusArray[0];
await loadOrderList(statusValue);
} else {
throw new Error(result?.msg || '删除失败');
}
} catch (error) {
uni.hideLoading();
console.error('删除订单失败:', error);
uni.showToast({
title: error.message || '删除失败',
icon: 'none'
});
}
}
}
});
};
</script>
<style lang="scss" scoped>
.order-container {
min-height: 100vh;
background: #f7f8fa;
padding-bottom: 30rpx;
//
.status-tabs {
display: flex;
background: #fff;
padding: 0 20rpx;
position: sticky;
top: 0;
z-index: 10;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.tab-item {
flex: 1;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
color: #666;
position: relative;
&.active {
color: #07c160;
font-weight: 500;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background: #07c160;
border-radius: 2rpx;
}
}
}
}
//
.order-list {
padding: 20rpx;
//
.order-item {
background: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
//
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
.order-id {
font-size: 26rpx;
color: #666;
}
.order-status {
font-size: 26rpx;
font-weight: 500;
&.status-waiting {
color: #FF9800;
}
&.status-shipping {
color: #1989fa;
}
&.status-receiving {
color: #1989fa;
}
&.status-finished {
color: #07c160;
}
&.status-cancelled {
color: #9E9E9E;
}
&.status-refunding {
color: #ff6b6b;
}
&.status-express-return {
color: #FF9800;
}
}
}
//
.order-body {
padding: 24rpx;
.device-info {
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: flex-start;
.device-left {
flex: 1;
margin-right: 20rpx;
.device-name {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 6rpx;
}
.device-id {
font-size: 26rpx;
color: #999;
margin-bottom: 0;
}
}
.device-right {
//
.payment-badge {
display: inline-flex;
align-items: center;
padding: 6rpx 12rpx;
border-radius: 8rpx;
white-space: nowrap;
&.wx-score {
background: rgba(7, 193, 96, 0.08);
.badge-icon {
width: 32rpx;
height: 26rpx;
margin-right: 8rpx;
}
.badge-text {
font-size: 22rpx;
color: #07c160;
display: flex;
align-items: center;
.divider {
margin: 0 6rpx;
}
.highlight {
font-weight: 500;
}
}
}
&.member {
background: rgba(25, 118, 210, 0.08);
.badge-text {
font-size: 22rpx;
color: #1976D2;
font-weight: 500;
}
}
&.deposit {
background: #f5f5f5;
.badge-text {
font-size: 22rpx;
color: #666;
font-weight: 500;
}
}
}
}
}
.order-times {
.time-row {
display: flex;
font-size: 26rpx;
margin-bottom: 8rpx;
.time-label {
color: #999;
width: 140rpx;
}
.time-value {
color: #333;
flex: 1;
}
}
}
}
//
.order-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 24rpx;
background: #fafafa;
border-top: 1rpx solid #f0f0f0;
.price {
font-size: 34rpx;
font-weight: 500;
color: #ff6b6b;
}
.actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
.action-item {
font-size: 26rpx;
padding: 10rpx 30rpx;
border-radius: 30rpx;
margin-left: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
&.primary {
background: #07c160;
color: #fff;
}
&.secondary {
background: #f5f5f5;
color: #666;
border: 1rpx solid #e0e0e0;
}
&:active {
opacity: 0.8;
}
}
}
}
}
//
.empty-state {
padding: 100rpx 0;
text-align: center;
.empty-icon {
width: 180rpx;
height: 180rpx;
margin: 0 auto 30rpx;
background: #f5f5f5;
// border-radius: 50%;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
}
}
}
</style>
+479
View File
@@ -0,0 +1,479 @@
<template>
<view class="my-card-page">
<!-- 会员卡列表 -->
<view class="card-list" v-if="cardList.length > 0">
<view v-for="card in cardList" :key="card.id" style="position: relative;background-color: #f5f5f5;"
@click="viewCardDetail(card)" :style="card.cardType==='COUNT'?'height: 240rpx;':'height: 240rpx;'">
<view
style="height: 120rpx;background-color: #ffffff;z-index: 999;border-radius: 25rpx;padding: 32rpx;position: absolute;top: 0;left: 0;right: 0;">
<!-- 卡片头部标题和日期 -->
<view class="card-header">
<text class="card-name">{{ card.name }}</text>
<view class="card-date">
<text class="date-text" v-if="card.status !== 'expired'">{{ card.endDate }}{{
$t('myCard.expire') }}</text>
<text class="date-text expired" v-else>{{ $t('myCard.expiredOn') }}{{ card.endDate }}</text>
</view>
</view>
<!-- 地区信息 -->
<view class="card-region">
<text
class="region-text">{{ $t('myCard.onlyForRegionBefore') }}{{ card.positionName }}{{ $t('myCard.onlyForRegionAfter') }}</text>
<!-- 状态标签 / 去使用按钮 -->
<view v-if="card.status !== 'expired'" class="status-tag active"
style="display: flex; align-items: center; gap: 4rpx; background-color: #FFE2B8; border-radius: 20rpx; padding: 6rpx 20rpx;"
@click.stop="handleUseCard(card)">
<text class="status-text" style="color: #A16300;">{{ $t('myCard.toUse') }}</text>
<!-- <uv-icon name="scan" size="12" color="#D4A574"></uv-icon> -->
</view>
<view v-else class="status-tag" :class="getStatusClass(card.status)">
<text class="status-text">{{ getStatusText(card.status) }}</text>
</view>
</view>
</view>
<!-- 使用情况和操作按钮 -->
<view style="position: absolute; bottom: -20rpx; left: 0; right: 0; padding: 20rpx;z-index:1;">
<view class="card-footer">
<!-- 次卡信息 -->
<view v-if="card.cardType === 'COUNT'" class="card-usage-info">
<text class="usage-text">{{ $t('myCard.remainingTimes') }}{{ card.remainingCount }}{{
$t('myCard.times') }}</text>
</view>
<!-- 时长卡信息 -->
<view v-else class="card-usage-info">
<text class="usage-text">每日限用次数{{card.dailyLimitCount}}</text>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<!-- 续卡按钮仅次卡显示 -->
<view v-if="card.cardType === 'COUNT'" class="renew-btn" @click.stop="renewCard(card)">
<text class="renew-text">{{ $t('myCard.renew') }}</text>
<uv-icon name="arrow-right" size="14" color="#D4A574"></uv-icon>
</view>
<view v-else class="renew-btn">
<text class="renew-text">单次限时{{card.singleLimitMinutes}}分钟</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<image class="empty-icon" src="/static/empty-card.png" mode="aspectFit"></image>
<text class="empty-text">{{ $t('myCard.noCards') }}</text>
<view class="buy-btn" @click="goToBuy">
<text class="buy-text">{{ $t('myCard.buyNow') }}</text>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted
} from 'vue'
import {
useI18n
} from '@/utils/i18n.js'
import {
getMemberCardsByStatus
} from '@/config/api/member.js'
import {
getQueryString
} from '@/util/index.js'
import {
getDeviceInfo
} from '@/config/api/device.js'
import {
getInUseOrder,
getUnpaidOrder
} from '@/config/api/order.js'
const {
t
} = useI18n()
//
const cardList = ref([])
//
const getCardList = async () => {
try {
const response = await getMemberCardsByStatus()
// API
if (response.code === 200 && response.data) {
cardList.value = response.data.map(item => ({
id: item.id,
name: item.cardName,
cardType: item.cardType, // TIME COUNT
//
totalCount: item.totalCount,
remainingCount: item.remainingCount,
singleLimitMinutesForCount: item.singleLimitMinutesForCount,
//
cycleDays: item.cycleDays,
dailyLimitCount: item.dailyLimitCount,
singleLimitMinutes: item.singleLimitMinutes,
currentCycleStartTime: item.currentCycleStartTime,
currentCycleUsedCount: item.currentCycleUsedCount,
//
status: item.status,
startDate: item.startTime?.split(' ')[0] || item.startTime,
endDate: item.endTime?.split(' ')[0] || item.endTime,
positionId: item.positionId,
positionName: item.positionName,
purchasePrice: item.purchasePrice,
remark: item.remark
}))
} else {
cardList.value = []
}
} catch (error) {
console.error('获取会员卡列表失败:', error)
uni.showToast({
title: t('myCard.getListFailed'),
icon: 'none'
})
}
}
//
const getProgressWidth = (used, total) => {
if (!total || total === 0) return '0%'
const percentage = (used / total) * 100
return `${Math.min(percentage, 100)}%`
}
//
const getStatusClass = (status) => {
const statusMap = {
'unused': 'active',
'expired': 'expired',
'used': 'used',
'active': 'active' //
}
return statusMap[status] || 'active' // active
}
//
const getStatusText = (status) => {
const statusMap = {
'unused': t('myCard.active'), // unused使
'expired': t('myCard.expired'),
'used': t('myCard.used'),
'active': t('myCard.active') //
}
return statusMap[status] || t('myCard.active') // active
}
//
const viewCardDetail = (card) => {
// TODO:
// uni.showToast({
// title: t('common.functionDeveloping'),
// icon: 'none'
// })
}
//
const renewCard = (card) => {
uni.navigateTo({
url: `/pages/purchase/index?positionId=${card.positionId}`
})
}
// 使
const handleUseCard = async (card) => {
try {
const scanResult = await new Promise((resolve, reject) => {
uni.scanCode({
success: resolve,
fail: reject
})
})
console.log('扫码结果:', scanResult);
let deviceNo;
//
if (scanResult.scanType === 'QR_CODE' || scanResult.scanType === 'qrCode') {
deviceNo = getQueryString(scanResult.result, 'deviceNo')
} else if (scanResult.path) {
deviceNo = getQueryString(scanResult.path, 'deviceNo')
} else {
deviceNo = scanResult.result
}
if (!deviceNo) {
uni.showToast({
title: t('home.invalidQRCode'),
icon: 'none'
})
return
}
uni.showLoading({
title: t('common.getting')
})
// 使
const inUseRes = await getInUseOrder()
if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
uni.hideLoading()
const inUseOrder = inUseRes.data
uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
})
return
}
//
const orderRes = await getUnpaidOrder()
if (orderRes && orderRes.code === 200 && orderRes.data) {
uni.hideLoading()
const unpaidOrder = orderRes.data
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
})
return
}
//
const deviceInfoRes = await getDeviceInfo(deviceNo)
uni.hideLoading()
if (deviceInfoRes.code === 200 && deviceInfoRes.data && deviceInfoRes.data.device) {
const deviceInfo = deviceInfoRes.data.device
let url = `/pages/device/detail?deviceNo=${deviceNo}`
if (deviceInfo.feeConfig) {
url += `&feeConfig=${encodeURIComponent(deviceInfo.feeConfig)}`
}
uni.navigateTo({
url
})
} else {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} catch (error) {
console.error('扫码处理失败:', error)
if (error && error.errMsg !== 'scanCode:fail cancel') {
uni.showToast({
title: t('home.scanFailed'),
icon: 'none'
})
}
}
}
//
const goToBuy = () => {
uni.navigateTo({
url: '/pages/purchase/index'
})
}
onMounted(() => {
uni.setNavigationBarTitle({
title: t('user.myCards')
})
getCardList()
})
</script>
<style lang="scss" scoped>
.my-card-page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 24rpx;
}
.card-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.card-item {
// background-color: #ffffff;
border-radius: 25rpx;
padding: 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
position: relative;
z-index: 5;
}
//
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
}
.card-name {
font-size: 36rpx;
font-weight: 600;
color: #333333;
line-height: 50rpx;
}
.card-date {
.date-text {
font-size: 24rpx;
color: #999999;
line-height: 34rpx;
&.expired {
color: #999999;
}
}
}
//
.card-region {
margin-bottom: 24rpx;
align-items: center;
display: flex;
justify-content: space-between;
gap: 16rpx;
.region-text {
font-size: 26rpx;
color: #666666;
line-height: 36rpx;
}
}
//
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
padding-top: 50rpx;
position: absolute;
background: rgba(255, 244, 227, 1);
z-index: 1;
right: 0;
bottom: 0;
left: 0;
border-radius: 0 0 25rpx 25rpx;
/* text-align: 30rpx ; */
}
.card-usage-info {
flex: 1;
.usage-text {
font-size: 26rpx;
color: #D4A574;
font-weight: 500;
line-height: 36rpx;
}
}
.card-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
//
.renew-btn {
display: flex;
align-items: center;
gap: 4rpx;
padding: 8rpx 16rpx;
// background-color: #FFF9F0;
border-radius: 8rpx;
.renew-text {
font-size: 24rpx;
color: #D4A574;
line-height: 34rpx;
}
.arrow {
font-size: 24rpx;
color: #D4A574;
}
}
//
.status-tag {
padding: 8rpx 20rpx;
border-radius: 8rpx;
.status-text {
font-size: 24rpx;
line-height: 34rpx;
}
&.active {
// background-color: #FFF9F0;
.status-text {
color: #D4A574;
}
}
&.expired {
// background-color: #F5F5F5;
.status-text {
color: #999999;
}
}
&.used {
// background-color: #F5F5F5;
.status-text {
color: #999999;
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 40rpx;
}
.buy-btn {
padding: 20rpx 60rpx;
background-color: #B8741A;
border-radius: 48rpx;
.buy-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
}
}
</style>
+542
View File
@@ -0,0 +1,542 @@
<template>
<view class="my-coupon-page">
<!-- Tab 切换 -->
<!-- <view class="tab-container">
<view class="tab-item" :class="{ active: currentTab === 'available' }" @click="switchTab('available')">
<text class="tab-text">{{ $t('myCoupon.available') }}</text>
</view>
<view class="tab-item" :class="{ active: currentTab === 'used' }" @click="switchTab('used')">
<text class="tab-text">{{ $t('myCoupon.used') }}</text>
</view>
<view class="tab-item" :class="{ active: currentTab === 'expired' }" @click="switchTab('expired')">
<text class="tab-text">{{ $t('myCoupon.expired') }}</text>
</view>
</view> -->
<!-- 优惠券列表 -->
<view class="coupon-list" v-if="filteredCoupons.length > 0">
<view v-for="coupon in filteredCoupons" :key="coupon.id" class="coupon-item-wrapper">
<view class="coupon-item" :class="getCouponClass(coupon.status)">
<!-- 虚线上下圆形缺口 -->
<view class="coupon-circle-top"></view>
<view class="coupon-circle-bottom"></view>
<view class="coupon-left">
<view class="coupon-value">
<text v-if="coupon.type === 'cash'" class="coupon-unit">¥</text>
<text class="coupon-amount">{{ coupon.type === 'discount' ? coupon.discount : coupon.value }}</text>
<text v-if="coupon.type === 'discount'" class="coupon-unit"></text>
</view>
<view style="display: flex;flex-direction: column;">
<text class="coupon-condition">{{ coupon.condition }}</text>
<text class="coupon-validity-left">{{ coupon.validity }}</text>
</view>
</view>
<view class="coupon-divider"></view>
<view class="coupon-right">
<!-- <text class="coupon-name">{{ coupon.name }}</text> -->
<!-- <text class="coupon-region" v-if="coupon.positionName"
style="font-size: 22rpx; color: #999; margin-top: 4rpx;">
{{ $t('myCoupon.onlyForRegionBefore') }}{{ coupon.positionName }}{{ $t('myCoupon.onlyForRegionAfter') }}
</text> -->
<view class="use-btn" v-if="coupon.status == 'unused'" @click="useCoupon(coupon)">
<text class="use-text">{{ $t('myCoupon.useNow') }}</text>
</view>
<text class="coupon-status" v-else>{{ getStatusText(coupon.status) }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<image class="empty-icon" src="/static/empty-coupon.png" mode="aspectFit"></image>
<text class="empty-text">{{ getEmptyText() }}</text>
<view class="buy-btn" @click="goToBuy" v-if="currentTab === 'available'">
<text class="buy-text">{{ $t('myCoupon.buyNow') }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useI18n } from '@/utils/i18n.js'
import { getUserCoupons } from '@/config/api/coupon.js'
import { getQueryString } from '@/util/index.js'
import { getDeviceInfo } from '@/config/api/device.js'
import { getInUseOrder, getUnpaidOrder } from '@/config/api/order.js'
const { t } = useI18n()
// Tab
const currentTab = ref('available')
// // Tab API
// const tabToStatusMap = {
// available: 'unused',
// used: 'used',
// expired: 'expired'
// }
//
const couponList = ref([])
//
const filteredCoupons = computed(() => {
return couponList.value;
})
//
const getCouponList = async () => {
try {
// const apiStatus = tabToStatusMap[currentTab.value]
const res = await getUserCoupons('')
if (res.code === 200 && res.data) {
//
couponList.value = (res.data || []).map(item => {
// discount_coupon deduction_coupon
const isCashCoupon = item.couponType === 'deduction_coupon'
// 使
let condition = '无门槛'
if (item.usableCondition && item.usableCondition > 0) {
condition = `${item.usableCondition}可用`
}
//
let validity = ''
if (currentTab.value === 'used') {
// 使使
validity = item.couponStartTime ? `使用时间 ${item.couponStartTime.split(' ')[0]}` : ''
} else if (item.couponEndTime) {
validity = `${item.couponEndTime.split(' ')[0]} 过期`
}
console.log(item.status);
return {
id: item.id,
name: item.couponName || '优惠券',
type: isCashCoupon ? 'cash' : 'discount',
value: item.deductAmount ? parseFloat(item.deductAmount) : 0,
discount: item.discountRate ? parseFloat(item.discountRate) * 10 : null,
condition: condition,
validity: validity,
status: item.status,
positionName: item.positionName
}
})
} else {
couponList.value = []
}
} catch (error) {
console.error('获取优惠券列表失败:', error)
couponList.value = []
uni.showToast({
title: t('myCoupon.getListFailed'),
icon: 'none'
})
}
}
// Tab
const switchTab = (tab) => {
currentTab.value = tab
getCouponList()
}
//
const getCouponClass = (status) => {
return status === 'unused' ? '' : 'disabled'
}
//
const getStatusText = (status) => {
const statusMap = {
'used': t('myCoupon.usedStatus'),
'expired': t('myCoupon.expiredStatus'),
'refunded':t('myCoupon.refundedStatus')
}
console.log("获取状态文本:"+statusMap[status]);
return statusMap[status] || ''
}
//
const getEmptyText = () => {
const textMap = {
'available': t('myCoupon.noAvailableCoupons'),
'used': t('myCoupon.noUsedCoupons'),
'expired': t('myCoupon.noExpiredCoupons')
}
return textMap[currentTab.value] || ''
}
// 使
const useCoupon = async (coupon) => {
try {
const scanResult = await new Promise((resolve, reject) => {
uni.scanCode({
success: resolve,
fail: reject
})
})
console.log('扫码结果:', scanResult);
let deviceNo;
//
if (scanResult.scanType === 'QR_CODE' || scanResult.scanType === 'qrCode') {
deviceNo = getQueryString(scanResult.result, 'deviceNo')
} else if (scanResult.path) {
deviceNo = getQueryString(scanResult.path, 'deviceNo')
} else {
deviceNo = scanResult.result
}
if (!deviceNo) {
uni.showToast({
title: t('home.invalidQRCode'),
icon: 'none'
})
return
}
uni.showLoading({
title: t('common.getting')
})
// 使
const inUseRes = await getInUseOrder()
if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
uni.hideLoading()
const inUseOrder = inUseRes.data
uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
})
return
}
//
const orderRes = await getUnpaidOrder()
if (orderRes && orderRes.code === 200 && orderRes.data) {
uni.hideLoading()
const unpaidOrder = orderRes.data
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
})
return
}
//
const deviceInfoRes = await getDeviceInfo(deviceNo)
uni.hideLoading()
if (deviceInfoRes.code === 200 && deviceInfoRes.data && deviceInfoRes.data.device) {
const deviceInfo = deviceInfoRes.data.device
let url = `/pages/device/detail?deviceNo=${deviceNo}`
if (deviceInfo.feeConfig) {
url += `&feeConfig=${encodeURIComponent(deviceInfo.feeConfig)}`
}
uni.navigateTo({
url
})
} else {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} catch (error) {
console.error('扫码处理失败:', error)
if (error && error.errMsg !== 'scanCode:fail cancel') {
uni.showToast({
title: t('home.scanFailed'),
icon: 'none'
})
}
}
}
//
const goToBuy = () => {
uni.navigateTo({
url: '/pages/purchase/index?tab=coupon'
})
}
onMounted(() => {
uni.setNavigationBarTitle({
title: t('user.myCoupons')
})
getCouponList()
})
</script>
<style lang="scss" scoped>
//
$coupon-theme-color: #A16300;
$coupon-divider-color: #B8741A;
$coupon-bg-faded: #f5f5f5;
$coupon-active-bg-start: #FFF4E6;
$coupon-active-bg-end: #FFE8CC;
$coupon-divider-left: 65%;
$coupon-circle-radius: 16rpx;
.my-coupon-page {
min-height: 100vh;
background-color: #f5f5f5;
}
/* Tab 切换 */
.tab-container {
position: sticky;
top: 0;
display: flex;
background-color: #ffffff;
z-index: 999;
padding: 20rpx 0;
}
.tab-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
position: relative;
.tab-text {
font-size: 28rpx;
color: #666;
font-weight: 500;
}
&.active {
.tab-text {
color: #333;
font-weight: 600;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 6rpx;
background-color: #FFA928;
border-radius: 3rpx;
}
}
}
.coupon-list {
padding: 20rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.coupon-item-wrapper {
position: relative;
}
.coupon-item {
background: #FFF4E3;
border-radius: 20rpx;
padding: 40rpx 30rpx;
display: flex;
align-items: stretch;
position: relative;
overflow: visible;
min-height: 180rpx;
box-sizing: border-box;
border: 2rpx solid transparent;
transition: all 0.3s;
&.selected {
border-color: $coupon-divider-color;
box-shadow: 0 4rpx 20rpx rgba(184, 116, 26, 0.2);
.coupon-circle-top,
.coupon-circle-bottom {
background-color: $coupon-active-bg-start;
border: 2rpx solid $coupon-divider-color;
}
}
&.disabled {
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
opacity: 0.6;
.coupon-value,
.coupon-condition,
.coupon-name {
color: #999;
}
.coupon-circle-top,
.coupon-circle-bottom {
background-color: #f5f5f5;
}
}
}
/* 虚线顶部圆形缺口 */
.coupon-circle-top {
position: absolute;
left: $coupon-divider-left+4%;
top: -$coupon-circle-radius;
transform: translateX(-50%);
width: $coupon-circle-radius * 2;
height: $coupon-circle-radius * 2;
border-radius: 50%;
background-color: $coupon-bg-faded;
z-index: 10;
}
/* 虚线底部圆形缺口 */
.coupon-circle-bottom {
position: absolute;
left: $coupon-divider-left+4%;
bottom: -$coupon-circle-radius;
transform: translateX(-50%);
width: $coupon-circle-radius * 2;
height: $coupon-circle-radius * 2;
border-radius: 50%;
background-color: $coupon-bg-faded;
z-index: 10;
}
.coupon-left {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
gap: 8rpx;
}
.coupon-value {
display: flex;
align-items: flex-end; //
color: $coupon-theme-color;
line-height: 1;
width:120rpx;
}
.coupon-amount {
font-size: 56rpx; //
font-weight: 700;
}
.coupon-unit {
font-size: 24rpx; //
font-weight: 500;
margin-bottom: 6rpx; // 使
margin-left: 4rpx;
margin-right: 4rpx;
}
.coupon-condition {
font-size: 24rpx;
color: #000;
font-weight: 600;
}
.coupon-validity-left {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.coupon-divider {
width: 2rpx;
height: 100%;
position: absolute;
left: $coupon-divider-left;
transform: translateX(-50%);
top: 0;
bottom: 0;
align-self: stretch;
background: repeating-linear-gradient(to bottom,
$coupon-divider-color 0rpx,
$coupon-divider-color 8rpx,
transparent 8rpx,
transparent 16rpx);
margin: 0 30rpx;
}
.coupon-right {
// flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
align-items: center;
margin: auto 0;
width: 160rpx;
// align-content: center;
// transform: translateY(50%);
}
.coupon-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.coupon-validity {
font-size: 22rpx;
color: #999;
}
.use-btn {
// margin-top: 10rpx;
// padding: 12rpx 28rpx;
// background-color: #B8741A;
// border-radius: 40rpx;
.use-text {
font-size: 28rpx;
color: #A16300;
font-weight: 600;
}
}
.coupon-status {
margin-top: 10rpx;
font-size: 24rpx;
color: #999;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 40rpx;
}
.buy-btn {
padding: 20rpx 60rpx;
background-color: #B8741A;
border-radius: 48rpx;
.buy-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
}
}
</style>
@@ -71,10 +71,10 @@ import {
import {
getNearbyDevices,
transformDeviceData
} from '../../config/api/device.js'
import { useI18n } from '../../utils/i18n.js'
} from '@/config/api/device.js'
import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n()
const { t } = useI18n()
const positionInfo = ref({})
const positionId = ref('')
@@ -112,7 +112,7 @@ const { t: $t } = useI18n()
const loadPositionDetail = async () => {
try {
uni.showLoading({
title: $t('common.loading')
title: t('common.loading')
})
//
@@ -147,7 +147,7 @@ const { t: $t } = useI18n()
positionInfo.value = position
} else {
uni.showToast({
title: $t('location.notExist'),
title: t('location.notExist'),
icon: 'none'
})
}
@@ -155,7 +155,7 @@ const { t: $t } = useI18n()
} catch (e) {
console.error('加载设备详情失败:', e)
uni.showToast({
title: $t('common.loadFailed'),
title: t('common.loadFailed'),
icon: 'none'
})
} finally {
@@ -166,7 +166,7 @@ const { t: $t } = useI18n()
const navigateToPosition = () => {
if (!positionInfo.value.latitude || !positionInfo.value.longitude) {
uni.showToast({
title: $t('location.coordinateError'),
title: t('location.coordinateError'),
icon: 'none'
})
return
@@ -181,7 +181,7 @@ const { t: $t } = useI18n()
longitude < -180 || longitude > 180 ||
(latitude === 0 && longitude === 0)) {
uni.showToast({
title: $t('location.coordinateError'),
title: t('location.coordinateError'),
icon: 'none'
})
return
File diff suppressed because it is too large Load Diff
@@ -12,7 +12,7 @@
<view class="order-list">
<view class="empty-state" v-if="orderList.length === 0">
<view class="empty-icon">
<image src="/static/orderList.png" mode="aspectFill" class="empty-icon"></image>
<image src="/static/orderList.png" mode="aspectFill" class="empty-icon" lazy-load="true"></image>
</view>
<text class="empty-text">{{ $t('order.noOrderRecord') }}</text>
</view>
@@ -35,7 +35,8 @@
import {
ref,
reactive,
onMounted
onMounted,
onUnmounted
} from 'vue';
import OrderItemCard from '../../components/OrderItemCard.vue';
import {
@@ -44,28 +45,16 @@
import {
getOrderList,
queryById,
getOrderByOrderNo,
getOrderByOrderNoScorePayStatus,
cancelOrder
} from '../../config/api/order.js';
import {
confirmPaymentAndRent
getDeviceInfo
} from '../../config/api/device.js';
import {
updateUserBalance
} from '../../config/api/user.js';
import {
URL
} from '../../config/url.js';
import { useI18n } from '@/utils/i18n.js'
import { useI18n, showModalI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n()
//
onMounted(() => {
uni.setNavigationBarTitle({
title: $t('order.myOrders')
})
})
const { t } = useI18n()
//
const currentTab = ref(0);
@@ -74,62 +63,62 @@
//
const orderStatusMap = reactive({
'0': {
get text() { return $t('order.waitingForPayment') },
get text() { return t('order.waitingForPayment') },
class: 'status-waiting'
},
'1': {
get text() { return $t('order.inUse') },
get text() { return t('order.inUse') },
class: 'status-using'
},
'2': {
get text() { return $t('order.finished') },
get text() { return t('order.finished') },
class: 'status-finished'
},
'3': {
get text() { return $t('order.cancelled') },
get text() { return t('order.cancelled') },
class: 'status-cancelled'
},
'waiting_for_payment': {
get text() { return $t('order.waitingForPayment') },
get text() { return t('order.waitingForPayment') },
class: 'status-waiting'
},
'in_used': {
get text() { return $t('order.inUse') },
get text() { return t('order.inUse') },
class: 'status-using'
},
'used_done': {
get text() { return $t('order.finished') },
get text() { return t('order.finished') },
class: 'status-finished'
},
'order_cancelled': {
get text() { return $t('order.cancelled') },
get text() { return t('order.cancelled') },
class: 'status-cancelled'
},
'express_return': {
get text() { return $t('express.title') },
get text() { return t('express.title') },
class: 'status-express-return'
}
});
//
const orderStatusTabs = reactive([{
get text() { return $t('common.all') },
get text() { return t('common.all') },
status: []
},
{
get text() { return $t('order.waitingForPayment') },
get text() { return t('order.waitingForPayment') },
status: ['waiting_for_payment']
},
{
get text() { return $t('order.inUse') },
get text() { return t('order.inUse') },
status: ['in_used']
},
{
get text() { return $t('order.finished') },
get text() { return t('order.finished') },
status: ['used_done']
},
{
get text() { return $t('order.cancelled') },
get text() { return t('order.cancelled') },
status: ['order_cancelled']
}
]);
@@ -150,7 +139,9 @@
//
const formattedOrder = {
orderNo: orderData.orderId,
orderNo: orderData.orderNo || orderData.orderId,
orderId: orderData.orderId,
orderStatus: orderData.orderStatus,
status: orderData.orderStatus,
deviceId: orderData.deviceNo,
payWay: orderData.payWay,
@@ -158,7 +149,8 @@
endTime: orderData.endTime || '',
positionName: orderData.positionName || orderData.positionLocation || '',
deviceName: orderData.deviceName || '',
amount: orderData.payAmount || orderData.actualDeviceAmount || orderData.currentFee || orderData.residueAmount || '0.00'
amount: orderData.payAmount || orderData.actualDeviceAmount || orderData.currentFee || orderData.residueAmount || '0.00',
pauseTime: orderData.pauseTime != null ? orderData.pauseTime : orderData.pause_time
};
//
@@ -215,33 +207,57 @@
endTime: item.endTime || '',
positionName: item.positionName || item.positionLocation || '',
deviceName: item.deviceName || '',
amount: item.payAmount || item.actualDeviceAmount || item.currentFee || item.residueAmount || '0.00'
amount: item.payAmount || item.actualDeviceAmount || item.currentFee || item.residueAmount || '0.00',
pauseTime: item.pauseTime != null ? item.pauseTime : item.pause_time
};
});
}
} catch (error) {
console.error('获取订单列表失败:', error);
uni.showToast({
title: $t('order.getOrderListFailed'),
title: t('order.getOrderListFailed'),
icon: 'none'
});
}
};
//
const handleOrderCompleted = (orderData) => {
console.log('订单列表页收到订单完成事件:', orderData)
//
const statusList = orderStatusTabs[currentTab.value].status[0]
loadOrderList(statusList)
}
//
onMounted(() => {
uni.setNavigationBarTitle({
title: t('order.myOrders')
})
//
uni.$on('orderCompleted', handleOrderCompleted)
})
//
onUnmounted(() => {
uni.$off('orderCompleted', handleOrderCompleted)
})
//
const getOrderStatus = async (order) => {
try {
const res = await getOrderByOrderNoScorePayStatus(order.orderNo);
if (res.code === 200) {
uni.showToast({
title: $t('order.syncSuccess'),
title: t('order.syncSuccess'),
icon: 'success'
});
await loadOrderList(orderStatusTabs[currentTab.value].status);
}
} catch (error) {
uni.showToast({
title: $t('order.syncFailed'),
title: t('order.syncFailed'),
icon: 'none'
});
}
@@ -264,74 +280,92 @@
navigateToOrderDetail(order);
};
//
/**
* 待支付订单与设备租借押金流程一致进入支付详情页微信/支付宝/H5-Antom/DANA 等在支付页内选择并完成
*/
const handlePayment = async (order) => {
const orderNo = order.orderNo
const orderId = order.orderId || order.orderNo
if (!orderId && !orderNo) {
uni.showToast({
title: t('order.orderInfoMissing'),
icon: 'none'
})
return
}
try {
uni.showLoading({
title: $t('common.processing')
});
title: t('common.loading')
})
//
const res = await uni.request({
url: `${URL || 'http://127.0.0.1:8080'}/app/wx-payment/create/${order.orderNo}`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
let od = null
if (orderNo) {
const res = await getOrderByOrderNo(orderNo)
if (res && res.code === 200 && res.data) {
od = res.data
}
}
if (!od && orderId) {
const res = await queryById(orderId)
if (res && res.code === 200 && res.data) {
od = res.data
}
}
});
if (res.statusCode === 200 && res.data.code === 200) {
const payParams = res.data.data;
const idForPay = String(od?.orderId ?? orderId ?? orderNo ?? '')
const qsParts = [`orderId=${encodeURIComponent(idForPay)}`]
//
await uni.requestPayment({
...payParams,
success: async () => {
uni.showToast({
title: $t('payment.paymentSuccess'),
icon: 'success'
});
if (od) {
const deposit = parseFloat(od.depositAmount)
const packagePrice = parseFloat(od.unitPrice)
if (Number.isFinite(packagePrice)) {
qsParts.push(`packagePrice=${packagePrice}`)
}
if (Number.isFinite(deposit)) {
const totalAmount = deposit.toFixed(2)
qsParts.push(`totalAmount=${encodeURIComponent(totalAmount)}`)
qsParts.push(`depositAmount=${encodeURIComponent(String(deposit))}`)
}
//
const deviceNo = od.deviceNo || order.deviceId
if (deviceNo) {
try {
await updateUserBalance(order.orderId || order.orderNo);
} catch (error) {
console.warn('更新用户余额失败:', error);
const devRes = await getDeviceInfo(deviceNo)
const feeCfg = devRes?.data?.device?.feeConfig
if (feeCfg) {
qsParts.push(`feeConfig=${encodeURIComponent(feeCfg)}`)
}
} catch (e) {
console.warn('获取设备 feeConfig 失败,支付页将仅依赖订单信息:', e)
}
}
}
//
await loadOrderList(orderStatusTabs[currentTab.value].status);
},
fail: (err) => {
console.error('支付失败:', err);
throw new Error($t('payment.paymentFailedRetry'));
}
});
} else {
throw new Error(res.data.msg || '创建支付订单失败');
}
uni.hideLoading();
uni.hideLoading()
uni.navigateTo({
url: `/subPackages/order/payment?${qsParts.join('&')}`
})
} catch (error) {
uni.hideLoading();
uni.hideLoading()
console.error('跳转支付页失败:', error)
uni.showToast({
title: error.message || $t('payment.paymentFailed'),
title: error?.message || t('payment.paymentFailed'),
icon: 'none'
});
})
}
};
//
const handleCancelOrder = async (order) => {
try {
uni.showModal({
title: $t('order.confirmCancel'),
content: $t('order.confirmCancelContent'),
showModalI18n({
title: t('order.confirmCancel'),
content: t('order.confirmCancelContent'),
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: $t('common.processing')
title: t('common.processing')
});
const result = await cancelOrder({
@@ -341,14 +375,14 @@
if (result) {
uni.hideLoading();
uni.showToast({
title: $t('order.cancelSuccess'),
title: t('order.cancelSuccess'),
icon: 'success'
});
//
await loadOrderList();
} else {
throw new Error(result.msg || $t('order.cancelFailed'));
throw new Error(result.msg || t('order.cancelFailed'));
}
}
}
@@ -356,7 +390,7 @@
} catch (error) {
uni.hideLoading();
uni.showToast({
title: error.message || $t('order.cancelFailed'),
title: error.message || t('order.cancelFailed'),
icon: 'none'
});
}
+985
View File
@@ -0,0 +1,985 @@
<template>
<view class="payment-container">
<!-- 地点信息卡片 -->
<view class="location-card">
<view class="location-header">
<!-- <view class="location-icon">📍</view> -->
<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 class="device-info">
<text class="device-label">{{ $t('order.deviceNo') }}</text>
<text class="device-value">{{ orderInfo.deviceNo || '-' }}</text>
</view>
</view>
<!-- 订单信息和费用信息 -->
<view class="order-card">
<view class="card-header">
<view class="card-title-bar"></view>
<view class="card-title">{{ $t('payment.orderInfo') }}</view>
</view>
<view class="info-item">
<text class="label">{{ $t('order.orderNo') }}</text>
<text class="value">{{ orderInfo.orderNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('order.deviceNo') }}</text>
<text class="value">{{ orderInfo.deviceNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('payment.createTime') }}</text>
<text class="value">{{ orderInfo.createTime || '-' }}</text>
</view>
<!-- 费用信息部分 -->
<view class="card-header" style="margin-top: 32rpx;">
<view class="card-title-bar"></view>
<view class="card-title">{{ $t('payment.feeInfo') }}</view>
</view>
<view class="price-item">
<text class="label">{{ $t('payment.deposit') }}</text>
<text class="value">{{ currencySymbol }} {{ orderInfo.deposit || '90' }}</text>
</view>
<!-- <view class="price-item">
<text class="label">{{ $t('payment.package') }}</text>
<text class="value">{{ packageText }}</text>
</view> -->
<view class="price-item total">
<text class="label">{{ $t('payment.total') }}</text>
<view class="total-value">
<text class="currency">{{ currencySymbol }}</text>
<text class="amount">{{ totalAmount }}</text>
</view>
</view>
</view>
<!-- 支付方式选择 -->
<view class="order-card" v-if="paymentMethods.length">
<view class="card-header">
<view class="card-title-bar"></view>
<view class="card-title">{{ $t('payment.paymentMethod') }}</view>
</view>
<view class="payment-method-item" v-for="(item, idx) in paymentMethods" :key="`${item.paymentMethodType}-${idx}`"
@click="selectPaymentMethod(item.paymentMethodType)">
<view class="method-left">
<text class="method-name">{{ item.paymentMethodName }}</text>
</view>
<view class="method-right">
<view class="method-radio" :class="{ active: selectedPaymentMethod === item.paymentMethodType }">
</view>
</view>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="pay-btn" @click="handlePayment">
<text class="currency-small">{{ currencySymbol }}</text>
<text class="amount-large">{{ totalAmount }}</text>
<text class="pay-text">{{ $t('payment.payNow') }}</text>
</view>
<view class="cancel-btn" @click="handleCancelOrder">
{{ $t('order.cancelOrder') }}
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
reactive,
onMounted
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
queryById,
cancelOrder,
createWxPayment,
getWxPaymentStatus,
createAliPayment,
getAliPaymentStatus,
createAntomPayment,
getAntomPaymentMethods,
getAntomPaymentStatus
} from '@/config/api/order.js'
import {
getDeviceInfo
} from '@/config/api/device.js'
import {
updateUserBalance
} from '@/config/api/user.js'
import {
useI18n,
showModalI18n
} from '@/utils/i18n.js'
const {
t
} = useI18n()
const orderId = ref(null)
const deviceNo = ref(null)
const orderInfo = ref({})
const deviceInfo = ref(null)
const passedTotalAmount = ref(null)
const passedDepositAmount = ref(null)
const currencyCode = ref('USD')
// //H5-Antom
const paymentMethods = ref([])
const selectedPaymentMethod = ref('WECHAT') //
//
const locationName = ref('澎创办公室')
//
const packageText = computed(() => {
//
return '2元/小时'
})
const orderStatus = reactive({
get text() {
return t('payment.waitingForPayment')
},
get desc() {
return t('payment.pleasePayIn15Min')
},
class: 'waiting'
})
const totalAmount = computed(() => {
if (currencyCode.value === 'IDR') {
const raw =
passedTotalAmount.value != null && passedTotalAmount.value !== ''
? passedTotalAmount.value
: orderInfo.value.deposit
const num = Number(String(raw ?? '').replace(/[^\d.-]/g, ''))
return Number.isFinite(num) ? String(Math.trunc(num)) : String(raw || '0')
}
if (passedTotalAmount.value !== null) {
return parseFloat(passedTotalAmount.value).toFixed(2);
}
const deposit = parseFloat(orderInfo.value.deposit || passedDepositAmount.value || 99)
return deposit.toFixed(2)
})
const currencySymbol = computed(() => {
if (currencyCode.value === 'IDR') return 'Rp'
return 'USD'
})
//
const loadOrderInfo = async () => {
// orderId
if (!orderId.value) {
// #ifdef H5
const cachedOrderId = uni.getStorageSync('pendingPaymentNo');
if (cachedOrderId) {
orderId.value = cachedOrderId;
}
// #endif
}
// orderId
if (!orderId.value) {
uni.showToast({
title: t('order.orderNotExist'),
icon: 'none'
});
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
});
}, 1500);
return;
}
try {
uni.showLoading({
title: t('common.loading')
})
const res = await queryById(orderId.value)
if (res.code === 200 && res.data) {
const orderData = res.data
//
let formattedTime;
try {
if (orderData.createTime) {
formattedTime = formatTime(new Date(orderData.createTime));
} else {
formattedTime = formatTime(new Date());
}
} catch (e) {
console.error('时间格式化错误:', e);
formattedTime = formatTime(new Date());
}
orderInfo.value = {
orderNo: orderData.orderNo || orderData.orderId,
deviceNo: orderData.deviceNo,
createTime: formattedTime,
deposit: passedDepositAmount.value || orderData.depositAmount || '99.00',
orderStatus: orderData.orderStatus
}
deviceNo.value = orderData.deviceNo;
await loadDeviceInfo();
await loadPaymentMethods();
//
if (orderInfo.value.orderStatus == 'waiting_for_payment') {
// 使
startPaymentStatusPolling();
}
} else {
throw new Error(t('order.getOrderFailed'))
}
} catch (error) {
console.error('获取订单信息失败:', error)
uni.showToast({
title: error.message || t('order.getOrderFailed'),
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
//
const loadDeviceInfo = async () => {
if (!deviceNo.value) return;
try {
const res = await getDeviceInfo(deviceNo.value);
if (res.code === 200 && res.data) {
deviceInfo.value = res.data.device;
currencyCode.value = (res.data?.position?.currency || currencyCode.value || 'USD').toUpperCase()
if (deviceInfo.value && deviceInfo.value.depositAmount) {
orderInfo.value.deposit = deviceInfo.value.depositAmount
}
}
} catch (error) {
console.error('获取设备信息失败:', error);
}
}
//
const getOsType = () => {
const systemInfo = uni.getSystemInfoSync();
console.log(uni.getSystemInfoSync());
const platform = systemInfo.platform;
console.log('当前系统类型:', uni.getSystemInfoSync().platform);
// osType
if (platform === 'android') {
return 'ANDROID';
} else if (platform === 'ios') {
return 'IOS';
} else {
// ANDROID
return 'ANDROID';
}
}
//
const loadPaymentMethods = async () => {
const methods = []
// /
// #ifdef MP-WEIXIN
methods.push({
paymentMethodType: 'WECHAT',
paymentMethodName: '微信支付'
})
// #endif
// #ifdef MP-ALIPAY
methods.push({
paymentMethodType: 'ALIPAY',
paymentMethodName: '支付宝支付'
})
// #endif
// H5 使 Antom
// #ifdef H5
if (orderInfo.value.orderNo) {
try {
const osType = getOsType();
const res = await getAntomPaymentMethods(orderInfo.value.orderNo, osType);
console.log(res.data);
console.log(res.data.paymentOptions,'支付方式');
// if (res.code === 200 && res.data && res.data.paymentOptions) {
// res.data.paymentOptions.forEach(item => {
// methods.push({
// paymentMethodType: item.paymentMethodType,
// paymentMethodName: item.paymentMethodName || item.paymentMethodType
// });
// });
// }
// paymentMethodType Antom paymentType v-for key
methods.push({
paymentMethodType: 'ALIPAY_DANA',
paymentMethodName: t('payment.ALIPAYDANA')
})
// methods.push({
// paymentMethodType: 'ALIPAY_HK',
// paymentMethodName: t('payment.alipayHk')
// })
// methods.push({
// paymentMethodType: 'ALIPAY_ID',
// paymentMethodName: t('payment.alipayId')
// })
} catch (error) {
console.error('获取 Antom 支付方式失败:', error);
}
}
// #endif
//
if (!methods.length) {
methods.push({
paymentMethodType: 'WECHAT',
paymentMethodName: '微信支付'
})
}
paymentMethods.value = methods
if (paymentMethods.value.length > 0) {
selectedPaymentMethod.value = paymentMethods.value[0].paymentMethodType
}
}
//
const selectPaymentMethod = (methodType) => {
selectedPaymentMethod.value = methodType;
}
//
const getPaymentIcon = (methodType) => {
const iconMap = {
'ALIPAY': 'alipay',
'WECHATPAY': 'wechat',
'WECHAT': 'wechat'
};
return iconMap[methodType] || 'default';
}
//
const handlePayment = async () => {
if (!selectedPaymentMethod.value) {
uni.showToast({
title: '请选择支付方式',
icon: 'none'
});
return;
}
try {
uni.showLoading({
title: t('common.processing')
})
const method = selectedPaymentMethod.value
//
// #ifdef MP-WEIXIN
if (method === 'WECHAT') {
const wxRes = await createWxPayment(orderInfo.value.orderNo)
if (wxRes.code === 200 && wxRes.data) {
const payData = wxRes.data
await new Promise((resolve, reject) => {
wx.requestPayment({
...payData,
success: resolve,
fail: reject
})
})
//
startPaymentStatusPolling('WECHAT')
return
} else {
throw new Error(wxRes.msg || t('payment.createPayOrderFailed'))
}
}
// #endif
//
// #ifdef MP-ALIPAY
if (method === 'ALIPAY') {
const aliRes = await createAliPayment(orderInfo.value.orderNo)
if (aliRes.code === 200 && aliRes.data) {
//
// { code:200, msg:'', data:{ tradeNo:'xxx', outTradeNo:'yyy' } }
const tradeNO = aliRes.data.tradeNo || aliRes.data.outTradeNo
const payForm = aliRes.data.payForm || aliRes.data.orderStr
if (!tradeNO && !payForm) {
throw new Error('未获取到支付宝支付参数')
}
// 使 tradeNO orderStr
if (tradeNO) {
my.tradePay({
tradeNO,
success: (res) => {
if (res.resultCode === '9000') {
startPaymentStatusPolling('ALIPAY')
} else {
uni.showToast({
title: t('payment.paymentFailed'),
icon: 'none'
})
}
},
fail: () => {
uni.showToast({
title: t('payment.paymentFailed'),
icon: 'none'
})
}
})
} else {
my.tradePay({
orderStr: payForm,
success: (res) => {
if (res.resultCode === '9000') {
startPaymentStatusPolling('ALIPAY')
} else {
uni.showToast({
title: t('payment.paymentFailed'),
icon: 'none'
})
}
},
fail: () => {
uni.showToast({
title: t('payment.paymentFailed'),
icon: 'none'
})
}
})
}
return
} else {
throw new Error(aliRes.msg || t('payment.createPayOrderFailed'))
}
}
// #endif
// H5 + Antom
// #ifdef H5
const osType = getOsType();
const res = await createAntomPayment(orderInfo.value.orderNo, method, osType)
if (res && res.code === 200 && res.data) {
const paymentUrl = res.data.h5Url;
if (!paymentUrl) {
throw new Error('未获取到支付链接');
}
uni.hideLoading();
uni.setStorageSync('pendingPaymentNo', orderId.value);
window.location.href = paymentUrl;
// plus.runtime.openURL(paymentUrl, function(err) {
// console.log('');
// window.location.href = paymentUrl;
// })
// ==========================================================
//
startPaymentStatusPolling(method);
return
} else {
throw new Error(res?.msg || t('payment.createPayOrderFailed'))
}
// #endif
} catch (error) {
console.error('支付失败:', error)
uni.showToast({
title: error.message || t('payment.paymentFailed'),
icon: 'none'
})
} finally {
uni.hideLoading()
}
}
const handleCancelOrder = () => {
if (!orderId.value) {
uni.showToast({
title: t('order.orderNotExist'),
icon: 'none'
});
return;
}
showModalI18n({
title: t('order.confirmCancel'),
content: t('order.confirmCancelContent'),
success: async (res) => {
if (!res.confirm) return;
try {
uni.showLoading({
title: t('common.processing')
});
const result = await cancelOrder({
orderId: orderId.value
});
if (result && result.code === 200) {
uni.hideLoading();
uni.showToast({
title: t('order.cancelSuccess'),
icon: 'success'
});
setTimeout(() => {
uni.reLaunch({
url: '/pages/index/index'
});
}, 800);
} else {
throw new Error(result?.msg || t('order.cancelFailed'));
}
} catch (error) {
uni.hideLoading();
uni.showToast({
title: error.message || t('order.cancelFailed'),
icon: 'none'
});
}
}
});
}
//
let pollingTimer = null;
/**
* 统一的支付状态轮询方法
* @param {string} paymentMethodType - 支付方式类型 'WECHAT' | 'ALIPAY' | 'WECHATPAY' selectedPaymentMethod 保持一致
*/
const startPaymentStatusPolling = (paymentMethodType = null) => {
//
if (pollingTimer) {
clearInterval(pollingTimer);
}
// 使
const methodType = paymentMethodType || selectedPaymentMethod.value;
let pollCount = 0;
const maxPollCount = 60; // 605
pollingTimer = setInterval(async () => {
pollCount++;
if (pollCount > maxPollCount) {
clearInterval(pollingTimer);
uni.showToast({
title: '支付超时,请重新支付',
icon: 'none'
});
return;
}
try {
let res;
let status, successStatus, failStatuses;
// #ifdef H5
// H5 使 Antom API
const osType = getOsType();
res = await getAntomPaymentStatus(orderInfo.value.orderNo, osType);
if (res && res.code === 200 && res.data) {
status = res.data.paymentStatus;
successStatus = 'SUCCESS';
failStatuses = ['FAIL', 'CANCELLED'];
}
// #endif
// #ifdef MP-WEIXIN
//
if (methodType === 'WECHAT' || methodType === 'WECHATPAY') {
res = await getWxPaymentStatus(orderInfo.value.orderNo);
if (res && res.code === 200 && res.data) {
status = res.data.tradeStatus;
successStatus = 'SUCCESS';
failStatuses = ['FAIL', 'CANCELLED'];
}
}
// #endif
// #ifdef MP-ALIPAY
//
if (methodType === 'ALIPAY') {
res = await getAliPaymentStatus(orderInfo.value.orderNo);
if (res && res.code === 200 && res.data) {
status = res.data.tradeStatus;
successStatus = 'TRADE_SUCCESS';
failStatuses = ['TRADE_FAIL', 'TRADE_CANCELLED'];
}
}
// #endif
//
if (res && res.code === 200 && res.data && status) {
console.log(status === successStatus);
//
if (status === successStatus) {
clearInterval(pollingTimer);
try {
await updateUserBalance(orderId.value);
} catch (error) {
console.warn('更新用户余额失败:', error);
}
setTimeout(() => {
uni.redirectTo({
url: `/pages/order/success?orderId=${orderId.value}&deviceId=${orderInfo.value.deviceNo}`
});
}, 1500);
}
//
else if (failStatuses && failStatuses.includes(status)) {
clearInterval(pollingTimer);
uni.showToast({
title: '支付失败,请重新支付',
icon: 'none'
});
}
}
} catch (error) {
const errorMsg = methodType === 'ALIPAY' ? '支付宝' : (methodType === 'WECHAT' ||
methodType === 'WECHATPAY') ? '微信' : '支付';
console.error(`查询${errorMsg}支付状态失败:`, error);
}
}, 10000); // 10
}
//
const startWxPaymentStatusPolling = () => startPaymentStatusPolling('WECHAT');
const startAliPaymentStatusPolling = () => startPaymentStatusPolling('ALIPAY');
//
onMounted(() => {
return () => {
if (pollingTimer) {
clearInterval(pollingTimer);
}
};
});
//
const formatTime = (date) => {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
}
onLoad((options) => {
//
uni.setNavigationBarTitle({
title: t('payment.waitingForPayment')
})
// options orderId
if (options && options.orderId) {
orderId.value = options.orderId
if (options.totalAmount) {
passedTotalAmount.value = options.totalAmount;
}
if (options.depositAmount) {
passedDepositAmount.value = options.depositAmount;
}
if (options.feeConfig) {
try {
const feeConfigStr = decodeURIComponent(options.feeConfig)
deviceInfo.value = {
feeConfig: feeConfigStr
}
} catch (e) {
console.error('解析URL中的feeConfig失败:', e)
}
}
loadOrderInfo()
}
// #ifdef H5
// options orderIdH5
else {
const cachedOrderId = uni.getStorageSync('pendingPaymentNo');
console.log("订单编号:" + cachedOrderId);
if (cachedOrderId) {
orderId.value = cachedOrderId;
}
}
// #endif
loadOrderInfo()
// loadOrderInfo orderId
})
</script>
<style lang="scss" scoped>
.payment-container {
min-height: 100vh;
background: #F5F5F5;
padding: 24rpx;
padding-bottom: 200rpx;
box-sizing: border-box;
.location-card {
background: #fff;
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
.location-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
.location-icon {
font-size: 40rpx;
width: 40rpx;
height: 40rpx;
margin-right: 12rpx;
}
.location-name {
font-size: 36rpx;
font-weight: 600;
color: #333;
flex: 1;
}
.status-badge {
background: #D4F4DD;
color: #52C41A;
font-size: 24rpx;
padding: 8rpx 20rpx;
border-radius: 8rpx;
}
}
.device-info {
font-size: 28rpx;
color: #666;
.device-label {
color: #999;
}
.device-value {
color: #333;
}
}
}
.order-card {
background: #fff;
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
.card-header {
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 {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
.info-item,
.price-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 0;
.label {
font-size: 28rpx;
color: #666;
}
.value {
font-size: 28rpx;
color: #333;
text-align: right;
}
}
.price-item.total {
padding-top: 32rpx;
justify-content: flex-end !important;
// border-top: 1rpx solid #F0F0F0;
margin-top: 16rpx;
.label {
font-size: 28rpx;
color: #666;
margin-right: 10rpx;
}
.total-value {
display: flex;
align-items: baseline;
.currency {
font-size: 22rpx;
color: #52C41A;
margin-right: 4rpx;
}
.amount {
font-size: 32rpx;
font-weight: 700;
color: #52C41A;
}
}
}
.payment-method-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom-width: 0;
}
.method-left {
display: flex;
align-items: center;
flex: 1;
}
.method-name {
font-size: 28rpx;
color: #333;
}
.method-right {
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: 24rpx;
}
.method-radio {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #d9d9d9;
box-sizing: border-box;
}
.method-radio.active {
border-color: #52c41a;
background-color: #52c41a;
}
}
}
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 24rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 100;
.pay-btn {
width: 100%;
padding: 20rpx 0;
// height: 96rpx;
background: linear-gradient(135deg, #52C41A 0%, #73D13D 100%);
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
box-shadow: 0 8rpx 24rpx rgba(82, 196, 26, 0.3);
.currency-small {
font-size: 32rpx;
font-weight: 600;
margin-right: 4rpx;
// text-align: ;
}
.amount-large {
font-size: 52rpx;
font-weight: 700;
margin-right: 16rpx;
}
.pay-text {
font-size: 32rpx;
font-weight: 600;
}
&:active {
opacity: 0.9;
transform: scale(0.98);
}
}
.cancel-btn {
width: 100%;
//height: 84rpx;
margin-top: 16rpx;
//border-radius: 42rpx;
//border: 2rpx solid #d9d9d9;
//background: #fff;
color: #666;
font-size: 24rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
&:active {
opacity: 0.8;
}
}
}
}
</style>
+141
View File
@@ -0,0 +1,141 @@
<template>
<view class="return-map-page">
<view class="image-wrapper">
<movable-area class="movable-area">
<movable-view class="movable-view" direction="all" :scale="true" :scale-min="1" :scale-max="4"
:scale-value="scale" @scale="onScaleChange">
<image :src="imageUrl" mode="widthFix" class="map-image" lazy-load="true"></image>
</movable-view>
</movable-area>
</view>
<view class="bottom-bar">
<view class="btn" @click="resetScale">{{ $t('common.reset') }}</view>
<view class="btn primary" @click="previewImage">{{ $t('common.preview') }}</view>
</view>
</view>
</template>
<script setup>
import {
ref
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
useI18n
} from '@/utils/i18n.js'
const {
t
} = useI18n()
const imageUrl = ref('')
const scale = ref(1)
onLoad((options) => {
if (options && options.imageUrl) {
try {
imageUrl.value = decodeURIComponent(options.imageUrl)
} catch (e) {
imageUrl.value = options.imageUrl
}
}
if (!imageUrl.value) {
uni.showToast({
title: t('common.loadFailed'),
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
uni.setNavigationBarTitle({
title: t('order.returnLocationMap')
})
})
const onScaleChange = (e) => {
if (e && typeof e.detail?.scale === 'number') {
scale.value = e.detail.scale
}
}
const resetScale = () => {
scale.value = 1
}
const previewImage = () => {
if (!imageUrl.value) return
uni.previewImage({
urls: [imageUrl.value]
})
}
</script>
<style lang="scss" scoped>
.return-map-page {
min-height: 100vh;
background-color: #000;
display: flex;
flex-direction: column;
padding-bottom: env(safe-area-inset-bottom);
box-sizing: border-box;
}
.image-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 20rpx;
box-sizing: border-box;
}
.movable-area {
width: 100%;
height: 100%;
}
.movable-view {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.map-image {
width: 100%;
display: block;
border-radius: 16rpx;
}
.bottom-bar {
padding: 16rpx 30rpx 24rpx;
display: flex;
justify-content: flex-end;
gap: 24rpx;
background: rgba(0, 0, 0, 0.8);
}
.btn {
min-width: 160rpx;
height: 72rpx;
border-radius: 36rpx;
border: 2rpx solid #ffffff;
color: #ffffff;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
&.primary {
background: #07c160;
border-color: #07c160;
}
}
</style>
@@ -85,6 +85,7 @@
<script>
import { queryById } from '@/config/api/order.js'
import { withdrawDeposit } from '@/config/api/user.js'
import {
URL
}from"@/config/url.js"
@@ -234,17 +235,9 @@ export default {
try {
uni.showLoading({ title: this.$t('common.processing') });
const res = await uni.request({
url: `${URL || 'http://127.0.0.1:8080'}/app/withdraw/add/${this.orderInfo.orderNo}`,
method: 'GET',
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
})
const res = await withdrawDeposit(this.orderInfo.orderNo)
if (res.statusCode === 200 && res.data.code === 200) {
if (res && res.code === 200) {
uni.showToast({
title: this.$t('order.refundSuccess'),
icon: 'success'
@@ -259,7 +252,7 @@ export default {
this.loadOrderInfo();
}, 1500);
} else {
throw new Error(res.data.msg || this.$t('order.refundFailed'));
throw new Error(res?.msg || this.$t('order.refundFailed'));
}
} catch (error) {
console.error('退款申请错误:', error);
+399
View File
@@ -0,0 +1,399 @@
<template>
<view class="success-container">
<!-- 支付成功状态和订单信息 -->
<view class="status-order-card">
<!-- 支付成功状态 -->
<view class="status-section">
<view class="status-icon success"></view>
<view class="status-text">{{ $t('success.paymentSuccess') }}</view>
<view class="status-desc">{{ $t('success.paymentSuccessDesc') }}</view>
</view>
<!-- 分割线 -->
<view class="section-divider"></view>
<!-- 订单信息 -->
<view class="order-section">
<view class="card-title">{{ $t('success.orderInfo') }}</view>
<view class="info-item">
<text class="label">{{ $t('order.orderNo') }}</text>
<text class="value">{{ orderInfo.orderNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('order.deviceNo') }}</text>
<text class="value">{{ orderInfo.deviceNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('success.paymentAmount') }}</text>
<text class="value">{{ orderInfo.amount || '0.00' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('success.paymentTime') }}</text>
<text class="value">{{ orderInfo.payTime || '-' }}</text>
</view>
</view>
</view>
<!-- 设备状态 -->
<view class="device-status">
<view class="status-message">{{ deviceMessage }}</view>
<view class="loading-animation" v-if="isLoading">
<view class="loading-circle"></view>
</view>
</view>
<!-- 操作按钮 -->
<view class="button-group">
<view class="secondary-btn" @click="goToHome">{{ $t('success.backToHome') }}</view>
<view class="primary-btn" @click="goToOrderList">{{ $t('success.viewOrder') }}</view>
</view>
</view>
</template>
<script setup>
import {
ref,
getCurrentInstance
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
queryById,
getOrderByOrderNo
} from '@/config/api/order.js'
// 访 $t
const {
proxy
} = getCurrentInstance()
//
const orderId = ref('')
const orderInfo = ref({})
const isLoading = ref(true)
const deviceMessage = ref('')
const hasTriggeredDevice = ref(false)
//
onLoad((options) => {
//
uni.setNavigationBarTitle({
title: proxy.$t('success.paymentSuccess')
})
deviceMessage.value = proxy.$t('success.preparingDevice')
// #ifdef H5
if (uni.getStorageSync('pendingPaymentNo')) {
orderId.value = options.orderId
loadOrderInfo()
//
uni.$once('orderSuccess:' + orderId.value, () => {
console.log('已经触发过弹出逻辑,不再重复触发')
hasTriggeredDevice.value = true
})
} else {
uni.showToast({
title: proxy.$t('order.orderNotExist'),
icon: 'none'
})
setTimeout(() => {
goToHome()
}, 1500)
}
// #endif
// #ifndef H5
if (options && options.orderId) {
orderId.value = options.orderId
loadOrderInfo()
//
uni.$once('orderSuccess:' + orderId.value, () => {
console.log('已经触发过弹出逻辑,不再重复触发')
hasTriggeredDevice.value = true
})
} else {
uni.showToast({
title: proxy.$t('order.orderNotExist'),
icon: 'none'
})
setTimeout(() => {
goToHome()
}, 1500)
}
// #endif
})
//
const loadOrderInfo = async () => {
try {
uni.showLoading({
title: proxy.$t('common.loading')
})
const res = await queryById(orderId.value)
if (res.code === 200 && res.data) {
const orderData = res.data
orderInfo.value = {
orderNo: orderData.orderNo || orderData.orderId,
deviceNo: orderData.deviceNo,
amount: orderData.payAmount || orderData.amount,
payTime: orderData.payTime || formatTime(new Date())
}
//
if (orderData.orderStatus === 'IN_USED') {
// 使
deviceMessage.value = '设备已弹出,请取走您的风扇'
isLoading.value = false
//
if (!hasTriggeredDevice.value) {
uni.$emit('orderSuccess:' + orderId.value)
hasTriggeredDevice.value = true
}
} else {
//
deviceMessage.value = proxy.$t('success.paymentSuccessDesc')
isLoading.value = false
}
} else {
throw new Error('获取订单信息失败')
}
uni.hideLoading()
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || proxy.$t('order.getOrderFailed'),
icon: 'none'
})
}
}
//
const formatTime = (date) => {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
const second = date.getSeconds().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
}
//
const goToHome = () => {
uni.reLaunch({
url: '/pages/index/index'
})
}
//
const goToOrderList = () => {
uni.redirectTo({
url: '/pages/order/index'
})
}
</script>
<style lang="scss" scoped>
.success-container {
padding: 20px;
padding-bottom: 180rpx;
background-color: #f5f5f5;
min-height: 100vh;
box-sizing: border-box;
}
.status-order-card {
background-color: #fff;
border-radius: 12px;
margin-bottom: 20px;
overflow: hidden;
.status-section {
padding: 30px;
text-align: center;
.status-icon {
width: 60px;
height: 60px;
margin: 0 auto 16px;
background-color: #07c160;
border-radius: 50%;
position: relative;
&::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 30px;
height: 20px;
border: 3px solid #fff;
border-top: none;
border-right: none;
transform-origin: center;
transform: translate(-50%, -70%) rotate(-45deg);
}
}
.status-text {
font-size: 24px;
font-weight: bold;
color: #07c160;
margin-bottom: 8px;
}
.status-desc {
font-size: 14px;
color: #666;
}
}
.section-divider {
height: 1px;
background-color: #f0f0f0;
margin: 0 20px;
}
.order-section {
padding: 20px;
.card-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 16px;
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 {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.label {
color: #666;
font-size: 14px;
}
.value {
color: #333;
font-size: 14px;
font-weight: 500;
}
}
}
}
.device-status {
background-color: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
text-align: center;
.status-message {
font-size: 16px;
color: #333;
margin-bottom: 12px;
}
.loading-animation {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
.loading-circle {
width: 30px;
height: 30px;
border-radius: 50%;
border: 3px solid #f0f0f0;
border-top-color: #07c160;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
}
.button-group {
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;
justify-content: flex-end;
align-items: center;
gap: 20rpx;
.primary-btn {
background-color: #07c160;
color: #fff;
border: none;
border-radius: 32rpx;
padding: 0 32rpx;
font-size: 24rpx;
height: 64rpx;
line-height: 64rpx;
white-space: nowrap;
&:active {
opacity: 0.8;
}
}
.secondary-btn {
background-color: #fff;
color: #07c160;
border: 2rpx solid #07c160;
border-radius: 32rpx;
padding: 0 32rpx;
font-size: 24rpx;
height: 64rpx;
line-height: 64rpx;
white-space: nowrap;
&:active {
background-color: #f5f5f5;
}
}
}
</style>
+323
View File
@@ -0,0 +1,323 @@
<template>
<view class="deposit-container">
<!-- 押金金额卡片 -->
<view class="deposit-card">
<view class="title">{{ $t('deposit.depositBalance') }}</view>
<view class="amount">¥{{ depositAmount }}</view>
<button class="withdraw-btn" @click="handleWithdraw" :disabled="depositAmount <= 0">{{ $t('deposit.withdraw') }}</button>
</view>
<!-- 提现说明 -->
<view class="notice-card">
<view class="notice-title">
<view class="dot"></view>
<text>{{ $t('deposit.withdrawNotice') }}</text>
</view>
<view class="notice-content">
<view class="notice-item">1. {{ $t('deposit.withdrawNotice1') }}</view>
<view class="notice-item">2. {{ $t('deposit.withdrawNotice2') }}</view>
<view class="notice-item">3. {{ $t('deposit.withdrawNotice3') }}</view>
</view>
</view>
<!-- 押金记录 -->
<view class="record-card" v-if="records.length > 0">
<view class="record-title">{{ $t('deposit.depositRecord') }}</view>
<view class="record-list">
<view class="record-item" v-for="(item, index) in records" :key="index">
<view class="record-info">
<text class="record-type">{{ item.typeText }}</text>
<text class="record-time">{{ item.time }}</text>
</view>
<text class="record-amount" :class="item.type === 'refund' ? 'refund' : ''">
{{ item.type === 'refund' ? '+' : '-' }}¥{{ item.amount }}
</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { getUserInfo } from '@/util/index.js'
import { withdrawDeposit } from '@/config/api/user.js'
import { queryById } from '@/config/api/order.js'
import { useI18n, showModalI18n } from '@/utils/i18n.js'
const { t } = useI18n()
const depositAmount = ref('0.00')
const orderNo = ref('')
const orderId = ref('')
const records = ref([])
onMounted(() => {
uni.setNavigationBarTitle({
title: t('deposit.title')
})
})
onShow(() => {
loadUserInfo()
})
const loadUserInfo = async () => {
try {
const res = await getUserInfo()
if (res.code === 200) {
depositAmount.value = res.data.balanceAmount || '0.00'
orderNo.value = res.data.latestOrderNo || ''
orderId.value = res.data.latestOrderId || ''
//
if (parseFloat(depositAmount.value) > 0 && orderNo.value) {
records.value = [
{
type: 'pay',
typeText: t('deposit.payRecord'),
time: formatDate(new Date()),
amount: depositAmount.value
}
]
} else {
records.value = []
}
}
} catch (error) {
uni.showToast({
title: t('user.getUserInfoFailed'),
icon: 'none'
})
}
}
const handleWithdraw = async () => {
if (parseFloat(depositAmount.value) <= 0) {
uni.showToast({
title: t('deposit.noBalance'),
icon: 'none'
})
return
}
showModalI18n({
title: t('deposit.confirmWithdraw'),
content: t('deposit.withdrawDesc'),
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: t('deposit.withdrawing')
})
try {
const result = await withdrawDeposit(orderNo.value)
if (result.code === 200) {
uni.hideLoading()
uni.showToast({
title: t('deposit.withdrawSubmitted'),
icon: 'success'
})
//
records.value.push({
type: 'refund',
typeText: t('deposit.refundRecord'),
time: formatDate(new Date()),
amount: depositAmount.value
})
// 0
depositAmount.value = '0.00'
//
setTimeout(() => {
loadUserInfo()
}, 1500)
} else {
throw new Error(result.msg || t('deposit.withdrawFailed'))
}
} catch (error) {
uni.hideLoading()
//
let errorMessage = t('deposit.withdrawFailed');
if (error.message) {
if (error.message.includes('尚未归还')) {
errorMessage = t('deposit.orderNotReturned');
} else if (error.message.includes('已退还')) {
errorMessage = t('deposit.alreadyRefunded');
} else if (error.message.includes('处理中')) {
errorMessage = t('deposit.refundProcessing');
} else if (error.message.includes('余额为0')) {
errorMessage = t('deposit.noBalance');
} else {
errorMessage = error.message;
}
}
showModalI18n({
title: t('deposit.withdrawFailed'),
content: errorMessage,
showCancel: false
})
}
}
}
})
}
const formatDate = (date) => {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
</script>
<style lang="scss" scoped>
.deposit-container {
min-height: 100vh;
background: #f8f8f8;
padding: 30rpx;
.deposit-card {
background: linear-gradient(135deg, #1976D2, #64B5F6);
border-radius: 20rpx;
padding: 40rpx;
color: #fff;
text-align: center;
box-shadow: 0 4rpx 20rpx rgba(25,118,210,0.2);
.title {
font-size: 28rpx;
opacity: 0.9;
margin-bottom: 20rpx;
}
.amount {
font-size: 72rpx;
font-weight: bold;
margin-bottom: 40rpx;
}
.withdraw-btn {
background: #fff;
color: #1976D2;
width: 80%;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 32rpx;
font-weight: 500;
margin: 0 auto;
&:active {
transform: scale(0.98);
}
&[disabled] {
background: rgba(255,255,255,0.6);
color: rgba(25,118,210,0.5);
}
}
}
.notice-card {
margin-top: 30rpx;
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
.notice-title {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.dot {
width: 12rpx;
height: 12rpx;
background: #1976D2;
border-radius: 50%;
margin-right: 10rpx;
}
text {
font-size: 30rpx;
font-weight: 500;
color: #333;
}
}
.notice-content {
.notice-item {
font-size: 26rpx;
color: #666;
line-height: 1.8;
padding-left: 22rpx;
}
}
}
.record-card {
margin-top: 30rpx;
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
.record-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 20rpx;
border-left: 8rpx solid #1976D2;
padding-left: 20rpx;
}
.record-list {
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.record-info {
.record-type {
font-size: 28rpx;
color: #333;
margin-bottom: 6rpx;
display: block;
}
.record-time {
font-size: 24rpx;
color: #999;
}
}
.record-amount {
font-size: 32rpx;
color: #333;
font-weight: 500;
&.refund {
color: #4CAF50;
}
}
}
}
}
}
</style>
@@ -12,7 +12,7 @@
} from 'vue'
import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n()
const { t } = useI18n()
//
const webUrl = ref('https://joininvestment.gxfs123.com/')
@@ -27,7 +27,7 @@
const handleError = (e) => {
console.error('web-view 加载错误:', e)
uni.showToast({
title: $t('join.pageLoadFailed'),
title: t('join.pageLoadFailed'),
icon: 'none',
duration: 2000
})
@@ -35,7 +35,7 @@
onMounted(() => {
uni.setNavigationBarTitle({
title: $t('join.title')
title: t('join.title')
})
console.log('招商页面加载,外部网址:', webUrl.value)
})
@@ -84,7 +84,7 @@
})
})
const brandName = 'WindPower'
const brandName = 'Isidaya'
const companyName = 'Shenzhen Lemu Zhiyun Technology Co., Ltd.'
const effectiveDate = '2025-10-13'
const disputeVenue = 'the location of the platform'
@@ -84,7 +84,7 @@
})
})
const brandName = '风电者'
const brandName = 'Isidaya'
const companyName = '深圳乐慕智云科技有限公司'
const effectiveDate = '2025-10-13'
const disputeVenue = '平台所在地'
@@ -36,9 +36,9 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useI18n } from '@/utils/i18n.js'
import { URL } from '@/config/url.js'
import { getCurrentAgreement } from '@/config/api/system.js'
const { t: $t } = useI18n()
const { t } = useI18n()
//
const loading = ref(true)
@@ -50,13 +50,6 @@
remark: ''
})
//
const convertLanguageCode = (lang) => {
// zh-CN -> zh-CN ()
// en-US -> en_US (线)
return lang.replace(/-/g, '_')
}
//
const loadAgreement = async () => {
loading.value = true
@@ -64,35 +57,22 @@
errorMessage.value = ''
try {
//
const currentLang = uni.getStorageSync('language') || 'zh-CN'
const languageCode = convertLanguageCode(currentLang)
console.log('加载用户协议,语言:', currentLang, '转换后:', languageCode)
console.log('加载用户协议')
//
const res = await uni.request({
url: `${URL}/device/agreementConfig/current`,
method: 'GET',
header: {
'Content-Language': languageCode,
'Authorization': 'Bearer ' + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
},
data: {
const res = await getCurrentAgreement({
agreementCode: 'USER_AGREEMENT',
appPlatform: 'wechat',
appType: 'user'
}
})
console.log('用户协议响应:', res)
if (res.statusCode === 200 && res.data.code === 200 && res.data.data) {
if (res && res.code === 200 && res.data) {
agreementData.value = {
title: res.data.data.title || $t('legal.agreement'),
content: res.data.data.content || '',
remark: res.data.data.remark || ''
title: res.data.title || t('legal.agreement'),
content: res.data.content || '',
remark: res.data.remark || ''
}
//
@@ -100,12 +80,12 @@
title: agreementData.value.title
})
} else {
throw new Error(res.data.msg || $t('common.loadFailed'))
throw new Error(res?.msg || t('common.loadFailed'))
}
} catch (err) {
console.error('加载用户协议失败:', err)
error.value = true
errorMessage.value = err.message || $t('common.loadFailed')
errorMessage.value = err.message || t('common.loadFailed')
} finally {
loading.value = false
}
@@ -69,7 +69,7 @@
})
})
const brandName = 'WindPower'
const brandName = 'Isidaya'
const companyName = 'Shenzhen Lemu Zhiyun Technology Co., Ltd.'
const effectiveDate = '2025-10-13'
</script>
@@ -69,7 +69,7 @@
})
})
const brandName = '风电者'
const brandName = 'Isidaya'
const companyName = '深圳乐慕智云科技有限公司'
const effectiveDate = '2025-10-13'
</script>
@@ -36,9 +36,9 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useI18n } from '@/utils/i18n.js'
import { URL } from '@/config/url.js'
import { getCurrentAgreement } from '@/config/api/system.js'
const { t: $t } = useI18n()
const { t } = useI18n()
//
const loading = ref(true)
@@ -50,13 +50,6 @@
remark: ''
})
//
const convertLanguageCode = (lang) => {
// zh-CN -> zh-CN ()
// en-US -> en_US (线)
return lang.replace(/-/g, '_')
}
//
const loadAgreement = async () => {
loading.value = true
@@ -64,35 +57,22 @@
errorMessage.value = ''
try {
//
const currentLang = uni.getStorageSync('language') || 'zh-CN'
const languageCode = convertLanguageCode(currentLang)
console.log('加载隐私政策,语言:', currentLang, '转换后:', languageCode)
console.log('加载隐私政策')
//
const res = await uni.request({
url: `${URL}/device/agreementConfig/current`,
method: 'GET',
header: {
'Content-Language': languageCode,
'Authorization': 'Bearer ' + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
},
data: {
const res = await getCurrentAgreement({
agreementCode: 'PRIVACY_POLICY',
appPlatform: 'wechat',
appType: 'user'
}
})
console.log('隐私政策响应:', res)
if (res.statusCode === 200 && res.data.code === 200 && res.data.data) {
if (res && res.code === 200 && res.data) {
agreementData.value = {
title: res.data.data.title || $t('legal.privacy'),
content: res.data.data.content || '',
remark: res.data.data.remark || ''
title: res.data.title || t('legal.privacy'),
content: res.data.content || '',
remark: res.data.remark || ''
}
//
@@ -100,12 +80,12 @@
title: agreementData.value.title
})
} else {
throw new Error(res.data.msg || $t('common.loadFailed'))
throw new Error(res?.msg || t('common.loadFailed'))
}
} catch (err) {
console.error('加载隐私政策失败:', err)
error.value = true
errorMessage.value = err.message || $t('common.loadFailed')
errorMessage.value = err.message || t('common.loadFailed')
} finally {
loading.value = false
}
+43
View File
@@ -0,0 +1,43 @@
# Terms & Conditions
**Last Update**: 2025-02-05
---
## Applicable Law
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.
## Payment Methods
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.
## Refund Policy
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.
2. **Order Cancellation**: Unused orders can be cancelled before use begins, and the deposit will be fully refunded.
3. **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.
4. **Membership Cards/Coupons**: Purchased membership cards and coupons generally do not support refunds. Please contact customer service for special cases.
## Service Terms
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.
## Liability Limitation
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.
## Dispute Resolution
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.
---
If you have questions about this agreement, please go to **My → Customer Service**.
+43
View File
@@ -0,0 +1,43 @@
# Syarat & Ketentuan
**Pembaruan Terakhir**: 2025-02-05
---
## Hukum yang Berlaku
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.
## Metode Pembayaran
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.
## Kebijakan Pengembalian Dana
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.
2. **Pembatalan Pesanan**: Pesanan yang tidak digunakan dapat dibatalkan sebelum penggunaan dimulai, dan deposit akan dikembalikan sepenuhnya.
3. **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.
4. **Kartu Keanggotaan/Kupon**: Kartu keanggotaan dan kupon yang dibeli umumnya tidak mendukung pengembalian dana. Silakan hubungi layanan pelanggan untuk kasus khusus.
## Ketentuan Layanan
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.
## Batasan Tanggung Jawab
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.
## Penyelesaian Sengketa
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.
---
Jika ada pertanyaan tentang perjanjian ini, harap pergi ke **Saya → Layanan Pelanggan** untuk konsultasi.
+43
View File
@@ -0,0 +1,43 @@
# 条款与细则
**最后更新**: 2025-02-05
---
## 适用法律
本服务条款受中华人民共和国法律管辖。用户使用本服务即表示同意接受中国法律的约束。任何因本服务引起的争议,应首先通过友好协商解决;协商不成的,任何一方均可向服务提供方所在地有管辖权的人民法院提起诉讼。
## 支付方式
我们支持多种支付方式,包括但不限于:微信支付、支付宝、微信支付分免押金等。用户在使用服务前需完成支付流程。支付成功后,系统将自动开启设备供用户使用。所有支付交易均通过安全加密通道进行,确保用户资金安全。
## 退款介绍
1. 押金退款:归还设备后,押金将在扣除相应租金后自动退还至原支付账户,预计0-7个工作日到账。
2. 订单取消:未使用的订单可在开始使用前取消,押金将全额退还。
3. 异常退款:如遇设备故障等特殊情况,用户可申请退款,我们将在核实后3-5个工作日内处理。
4. 会员卡/优惠券:已购买的会员卡和优惠券一般不支持退款,特殊情况请联系客服处理。
## 服务条款
用户在使用本服务时,应遵守以下规定:
1. 妥善保管租借的设备,不得故意损坏或私自占有;
2. 按时归还设备,避免产生额外费用;
3. 不得将设备用于非法用途;
4. 如发现设备故障,应及时联系客服处理。
违反上述规定的,我们有权终止服务并追究相应责任。
## 责任限制
在法律允许的最大范围内,我们对因使用或无法使用本服务而导致的任何间接、偶然、特殊或后果性损害不承担责任。我们的总责任不超过用户为使用本服务所支付的费用。对于因不可抗力、网络故障、第三方原因等导致的服务中断或延迟,我们不承担责任。
## 争议解决
如用户对服务有任何疑问或争议,请首先通过客服渠道联系我们,我们将在收到反馈后24小时内响应,并尽快协商解决。如协商不成,双方同意将争议提交至服务提供方所在地有管辖权的人民法院通过诉讼方式解决。在争议解决期间,双方应继续履行本协议中无争议的条款。
---
如对本协议有疑问,请前往「我的 - 客服」咨询。
+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>
+146
View File
@@ -0,0 +1,146 @@
<template>
<view>
</view>
</template>
<script>
import {
wxLogin,
} from '@/util/index'
import {
getMyIndexInfo
} from "@/config/api/user.js";
import {
queryHasOrder,
checkOrdersByStatus
} from "@/config/api/order.js";
export default {
data() {
return {
}
},
async onLoad(option) {
console.log('bagCheck onLoad option:', option);
//
uni.setNavigationBarTitle({
title: this.$t('device.checking')
})
try {
uni.showLoading({
title: this.$t('common.processing'),
mask: true
});
//
if (!option || !option.deviceNo) {
throw new Error(this.$t('device.deviceNoNotRecognized'));
}
const deviceNo = option.deviceNo;
// (in_used)(waiting_for_payment)
const statusesToCheck = ['in_used', 'waiting_for_payment'];
const res = await checkOrdersByStatus(deviceNo, statusesToCheck);
if (res.code === 200 && res.data && res.data.length > 0) {
//
const latestOrder = res.data[0]; //
if (latestOrder.orderStatus === 'in_used') {
// 使
console.log('检测到使用中订单,跳转归还页:', latestOrder.orderId);
uni.redirectTo({
url: `/pages/device/return?orderId=${latestOrder.orderId}`
});
} else if (latestOrder.orderStatus === 'waiting_for_payment') {
//
console.log('检测到待支付订单,跳转支付页:', latestOrder.orderId);
//
const packageTimeMinutes = latestOrder.packageTime || 60;
//
const packageTimeHours = (packageTimeMinutes / 60).toFixed(1);
//
const packagePrice = latestOrder.packagePrice || '0.00';
//
const hourlyRate = (parseFloat(packagePrice) / (packageTimeMinutes / 60)).toFixed(2);
//
const depositAmount = latestOrder.depositAmount || '99.00';
// +
const totalAmount = (parseFloat(depositAmount) + parseFloat(packagePrice)).toFixed(2);
uni.redirectTo({
url: `/pages/order/payment?orderId=${latestOrder.orderId}&packageTimeHours=${packageTimeHours}&packagePrice=${packagePrice}&hourlyRate=${hourlyRate}&totalAmount=${totalAmount}&depositAmount=${depositAmount}`
});
} else {
// statusesToCheck
console.log('检测到其他状态订单,跳转详情页:', latestOrder.orderId);
uni.redirectTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
});
}
} else {
//
console.log('未检测到相关订单,跳转详情页');
uni.redirectTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
});
}
} catch (error) {
// ""
if (error.message && (
error.message.includes('未识别到设备编号') ||
error.message.includes('网络请求失败') ||
error.message.includes('服务器错误')
)) {
console.error('扫码检查订单失败:', error);
uni.showToast({
title: error.message || this.$t('device.processFailed'),
icon: 'none',
duration: 2000
});
} else {
// ""
console.log('没有找到符合条件的订单或发生其他错误,直接跳转详情页');
}
//
setTimeout(() => {
if (option && option.deviceNo) {
uni.redirectTo({
url: `/pages/device/detail?deviceNo=${option.deviceNo}`
});
} else {
// uni.switchTab({
// url:'/pages/index/index'
// })
// deviceNo
uni.reLaunch({
url: '/pages/index/index'
});
}
}, 2000);
} finally {
uni.hideLoading();
}
},
methods: {
}
}
</script>
<style>
</style>
+78
View File
@@ -0,0 +1,78 @@
<template>
<view class="webview-container">
<!-- web-view 组件用于显示外部网页 -->
<web-view :src="webUrl" @message="handleMessage" @load="handleLoad" @error="handleError"></web-view>
</view>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue';
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
//
const webUrl = ref('')
// URL
onLoad((options) => {
if (options.url) {
// URL
webUrl.value = decodeURIComponent(options.url)
console.log('加载外部链接:', webUrl.value)
} else {
uni.showToast({
title: t('common.invalidUrl') || '无效的链接',
icon: 'none',
duration: 2000
})
// 2
setTimeout(() => {
uni.navigateBack()
}, 2000)
}
})
//
const handleLoad = (e) => {
console.log('网页加载完成:', e)
}
//
const handleError = (e) => {
console.error('网页加载错误:', e)
uni.showToast({
title: t('common.loadFailed') || '加载失败',
icon: 'none'
})
}
// web-view
const handleMessage = (e) => {
console.log('收到网页消息:', e)
//
}
</script>
<script>
export default {
//
onShareAppMessage() {
const $t = this.$t || ((key) => key)
return {
title: $t('share.title') || '分享',
path: '/pages/index/index'
}
}
}
</script>
<style lang="scss" scoped>
.webview-container {
width: 100vw;
height: 100vh;
overflow: hidden;
}
</style>
@@ -60,13 +60,13 @@
getExpressReturnDetail,
fillExpressTrackingNumber
} from '@/config/api/expressReturn.js'
import { useI18n } from '@/utils/i18n.js'
import { useI18n, showModalI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n()
const { t } = useI18n()
onMounted(() => {
uni.setNavigationBarTitle({
title: $t('express.fillExpress')
title: t('express.fillExpress')
})
})
@@ -96,7 +96,7 @@
if (!orderId.value) {
uni.showToast({
title: $t('express.orderNoMissing'),
title: t('express.orderNoMissing'),
icon: 'none'
})
setTimeout(() => {
@@ -111,7 +111,7 @@
const loadOrder = async () => {
try {
uni.showLoading({
title: $t('common.loading')
title: t('common.loading')
})
const res = await queryById(orderId.value)
if (res?.code === 200 && res.data) {
@@ -121,11 +121,11 @@
//
if (res.data.phone && !phone.value) phone.value = res.data.phone
} else {
throw new Error(res?.msg || $t('order.getOrderFailed'))
throw new Error(res?.msg || t('order.getOrderFailed'))
}
} catch (e) {
uni.showToast({
title: e.message || $t('express.loadFailed'),
title: e.message || t('express.loadFailed'),
icon: 'none'
})
} finally {
@@ -136,7 +136,7 @@
const loadRecordAndOrderByRecord = async () => {
try {
uni.showLoading({
title: $t('common.loading')
title: t('common.loading')
})
const res = await getExpressReturnDetail(recordId.value)
if (res?.code === 200 && res.data) {
@@ -146,11 +146,11 @@
}
if (res.data.userPhone && !phone.value) phone.value = res.data.userPhone
} else {
throw new Error(res?.msg || $t('express.getRecordFailed'))
throw new Error(res?.msg || t('express.getRecordFailed'))
}
} catch (e) {
uni.showToast({
title: e.message || $t('express.loadFailed'),
title: e.message || t('express.loadFailed'),
icon: 'none'
})
} finally {
@@ -165,11 +165,11 @@
const rec = res.data
if (rec.status === 0) {
recordId.value = rec.id
uni.showModal({
title: $t('common.tips'),
content: $t('express.existingReturnNotice'),
confirmText: $t('express.goToFill'),
cancelText: $t('common.cancel'),
showModalI18n({
title: t('common.tips'),
content: t('express.existingReturnNotice'),
confirmText: t('express.goToFill'),
cancelText: t('common.cancel'),
success: (r) => {
if (r.confirm) {
uni.redirectTo({
@@ -181,7 +181,7 @@
return
} else {
uni.showToast({
title: $t('express.alreadyHasRecord'),
title: t('express.alreadyHasRecord'),
icon: 'none'
})
setTimeout(() => {
@@ -201,14 +201,14 @@
const digits = (phone.value || '').replace(/\D/g, '')
if (!digits || digits.length < 5) {
uni.showToast({
title: $t('express.pleaseEnterValidPhone'),
title: t('express.pleaseEnterValidPhone'),
icon: 'none'
})
return false
}
if (isFillMode.value && !trackingNumber.value) {
uni.showToast({
title: $t('express.pleaseEnterTrackingNo'),
title: t('express.pleaseEnterTrackingNo'),
icon: 'none'
})
return false
@@ -221,7 +221,7 @@
submitting.value = true
try {
uni.showLoading({
title: isFillMode.value ? $t('common.filling') : $t('common.submitting')
title: isFillMode.value ? t('common.filling') : t('common.submitting')
})
let res
if (isFillMode.value) {
@@ -238,18 +238,18 @@
}
if (res && res.code === 200) {
uni.showToast({
title: isFillMode.value ? '补填成功' : '提交成功',
title: isFillMode.value ? t('express.fillSuccess') : t('express.submitSuccess'),
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 800)
} else {
throw new Error(res?.msg || (isFillMode.value ? '补填失败' : '提交失败'))
throw new Error(res?.msg || (isFillMode.value ? t('express.fillFailed') : t('express.submitFailed')))
}
} catch (e) {
uni.showToast({
title: e.message || (isFillMode.value ? '补填失败' : '提交失败'),
title: e.message || (isFillMode.value ? t('express.fillFailed') : t('express.submitFailed')),
icon: 'none'
})
} finally {
@@ -89,9 +89,9 @@
import { ref, onMounted } from 'vue'
import { getExpressReturnDetail } from '@/config/api/expressReturn.js'
import { getCustomerPhone } from '@/util/index.js'
import { useI18n } from '@/utils/i18n.js'
import { useI18n, showModalI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n()
const { t } = useI18n()
//
const detailData = ref({
@@ -131,21 +131,21 @@ const getStatusIcon = (status) => {
//
const getStatusText = (status) => {
const textMap = {
'completed': $t('express.returnCompleted'),
'processing': $t('express.processing'),
'pending': $t('express.pending')
'completed': t('express.returnCompleted'),
'processing': t('express.processing'),
'pending': t('express.pending')
}
return textMap[status] || $t('express.pending')
return textMap[status] || t('express.pending')
}
//
const getStatusDesc = (status) => {
const descMap = {
'completed': $t('express.returnCompletedDesc'),
'processing': $t('express.processingDesc'),
'pending': $t('express.pendingDesc')
'completed': t('express.returnCompletedDesc'),
'processing': t('express.processingDesc'),
'pending': t('express.pendingDesc')
}
return descMap[status] || $t('express.pendingDesc')
return descMap[status] || t('express.pendingDesc')
}
//
@@ -154,7 +154,7 @@ const handleCopyTracking = () => {
data: detailData.value.trackingNumber,
success: () => {
uni.showToast({
title: $t('express.trackingNoCopied'),
title: t('express.trackingNoCopied'),
icon: 'success'
})
}
@@ -164,11 +164,11 @@ const handleCopyTracking = () => {
//
const handleContactService = () => {
const customerPhone = getCustomerPhone()
uni.showModal({
title: $t('user.customerService'),
content: `${$t('help.phone')}${customerPhone}\n${$t('help.workingHours')}${$t('express.workingHours')}`,
confirmText: $t('express.call'),
cancelText: $t('common.cancel'),
showModalI18n({
title: t('user.customerService'),
content: `${t('help.phone')}${customerPhone}\n${t('help.workingHours')}${t('express.workingHours')}`,
confirmText: t('express.call'),
cancelText: t('common.cancel'),
success: (res) => {
if (res.confirm) {
uni.makePhoneCall({
@@ -182,7 +182,7 @@ const handleContactService = () => {
//
onMounted(async () => {
uni.setNavigationBarTitle({
title: $t('express.returnDetail')
title: t('express.returnDetail')
})
const pages = getCurrentPages()
@@ -190,7 +190,7 @@ onMounted(async () => {
const options = currentPage.options || {}
if (!options.id) return
try {
uni.showLoading({ title: $t('common.loading') })
uni.showLoading({ title: t('common.loading') })
const res = await getExpressReturnDetail(options.id)
if (res && res.code === 200 && res.data) {
const r = res.data
@@ -208,10 +208,10 @@ onMounted(async () => {
remark: r.remark || ''
}
} else {
throw new Error(res?.msg || $t('express.getDetailFailed'))
throw new Error(res?.msg || t('express.getDetailFailed'))
}
} catch (e) {
uni.showToast({ title: e.message || $t('express.loadFailed'), icon: 'none' })
uni.showToast({ title: e.message || t('express.loadFailed'), icon: 'none' })
} finally {
uni.hideLoading()
}
@@ -54,7 +54,7 @@ import { ref, onMounted } from 'vue'
import { getExpressReturnList } from '@/config/api/expressReturn.js'
import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n()
const { t } = useI18n()
const returnList = ref([])
const loading = ref(false)
@@ -62,13 +62,12 @@ const query = ref({ pageNum: 1, pageSize: 20 })
onMounted(() => {
uni.setNavigationBarTitle({
title: $t('express.returnRecord')
title: t('express.returnRecord')
})
loadList()
})
//
const recipientName = '风电者 18163601305'
//
const recipientAddress = '湖南省长沙市岳麓区麓谷街道新长海尖科技园A2栋623'
const loadList = async () => {
@@ -80,12 +79,12 @@ const loadList = async () => {
const rows = (res.data && (res.data.rows || res.data)) || []
returnList.value = rows.map(r => ({
id: r.id,
expressCompany: r.expressCompany || r.company || '待填写',
trackingNumber: r.logisticsTrackingNumber || r.trackingNumber || '待填写',
returnAddress: r.returnAddress || r.address || '待填写',
returnTime: r.expressFillTime || r.createTime || r.returnTime || '待填写',
packageType: r.packageType || '待填写',
weight: r.weight || '待填写',
expressCompany: r.expressCompany || r.company || t('express.toFill'),
trackingNumber: r.logisticsTrackingNumber || r.trackingNumber || t('express.toFill'),
returnAddress: r.returnAddress || r.address || t('express.toFill'),
returnTime: r.expressFillTime || r.createTime || r.returnTime || t('express.toFill'),
packageType: r.packageType || t('express.toFill'),
weight: r.weight || t('express.toFill'),
status: mapStatus(r.status),
rawStatus: r.status,
userPhone: r.userPhone,
@@ -93,10 +92,10 @@ const loadList = async () => {
remark: r.remark
}))
} else {
throw new Error(res?.msg || $t('express.getListFailed'))
throw new Error(res?.msg || t('express.getListFailed'))
}
} catch (e) {
uni.showToast({ title: e.message || $t('express.loadFailed'), icon: 'none' })
uni.showToast({ title: e.message || t('express.loadFailed'), icon: 'none' })
} finally {
loading.value = false
}
@@ -118,34 +117,33 @@ const getStatusClass = (status) => ({
}[status] || 'status-pending')
const getStatusText = (status) => ({
'completed': $t('express.billingPaused'),
'processing': $t('express.billingPaused'),
'pending': $t('express.billingPaused')
}[status] || $t('express.billingPaused'))
'completed': t('express.billingPaused'),
'processing': t('express.billingPaused'),
'pending': t('express.billingPaused')
}[status] || t('express.billingPaused'))
const getStatusBadge = (status) => ({
'completed': $t('express.completed'),
'processing': $t('express.processing'),
'pending': $t('express.pending')
}[status] || $t('express.pending'))
'completed': t('express.completed'),
'processing': t('express.processing'),
'pending': t('express.pending')
}[status] || t('express.pending'))
//
const copyAllInfo = () => {
const allInfo = `${$t('express.recipient')}${recipientName}\n${$t('express.recipientAddressLabel')}${recipientAddress}`
const allInfo = `${t('express.recipient')}${t('express.recipientName')}\n${t('express.recipientAddressLabel')}${recipientAddress}`
uni.setClipboardData({
data: allInfo,
success: () => {
uni.showToast({ title: $t('express.copySuccess'), icon: 'success' })
uni.showToast({ title: t('express.copySuccess'), icon: 'success' })
},
fail: () => {
uni.showToast({ title: $t('express.copyFailed'), icon: 'none' })
uni.showToast({ title: t('express.copyFailed'), icon: 'none' })
}
})
}
//
const handleItemClick = (item) => {
console.log('点击了归还记录:', item)
// (status=0 -> mapped 'pending')
if (item && item.rawStatus === 0) {
uni.navigateTo({ url: `/pages/expressReturn/addExpressReturn?id=${item.id}` })
@@ -92,19 +92,19 @@
getFeedbackDetail,
getFeedbackMessages,
sendFeedbackMessage
} from '../../config/api/feedback.js';
} from '@/config/api/feedback.js';
import {
useI18n
} from '@/utils/i18n.js'
const {
t: $t
t
} = useI18n()
//
onMounted(() => {
uni.setNavigationBarTitle({
title: $t('feedback.detail')
title: t('feedback.detail')
})
})
@@ -126,7 +126,7 @@
await loadDetail();
} else {
uni.showToast({
title: $t('feedback.idRequired'),
title: t('feedback.idRequired'),
icon: 'none'
});
setTimeout(() => {
@@ -170,7 +170,7 @@
try {
if (shouldShowLoading) {
uni.showLoading({
title: $t('common.loading')
title: t('common.loading')
});
}
@@ -180,7 +180,7 @@
await loadMessages(res.data.messages);
} else {
uni.showToast({
title: res.msg || $t('feedback.getDetailFailed'),
title: res.msg || t('feedback.getDetailFailed'),
icon: 'none'
});
setTimeout(() => {
@@ -190,7 +190,7 @@
} catch (error) {
console.error('获取投诉详情失败:', error);
uni.showToast({
title: $t('feedback.getDetailFailed'),
title: t('feedback.getDetailFailed'),
icon: 'none'
});
setTimeout(() => {
@@ -207,7 +207,7 @@
const submitReply = async () => {
if (!replyContent.value.trim()) {
uni.showToast({
title: $t('feedback.pleaseEnterReply'),
title: t('feedback.pleaseEnterReply'),
icon: 'none'
});
return;
@@ -215,7 +215,7 @@
try {
uni.showLoading({
title: $t('common.submitting')
title: t('common.submitting')
});
const res = await sendFeedbackMessage(feedbackId.value, {
@@ -224,7 +224,7 @@
if (res.code === 200) {
uni.showToast({
title: $t('feedback.replySuccess'),
title: t('feedback.replySuccess'),
icon: 'success'
});
replyContent.value = '';
@@ -234,14 +234,14 @@
});
} else {
uni.showToast({
title: res.msg || $t('feedback.replyFailed'),
title: res.msg || t('feedback.replyFailed'),
icon: 'none'
});
}
} catch (error) {
console.error('提交回复失败:', error);
uni.showToast({
title: $t('feedback.replyFailed'),
title: t('feedback.replyFailed'),
icon: 'none'
});
} finally {
@@ -252,11 +252,11 @@
//
const getStatusText = (status) => {
const statusMap = {
'pending': $t('feedback.pending'),
'in_progress': $t('feedback.processing'),
'resolved': $t('feedback.completed')
'pending': t('feedback.pending'),
'in_progress': t('feedback.processing'),
'resolved': t('feedback.completed')
};
return statusMap[status] || $t('feedback.pending');
return statusMap[status] || t('feedback.pending');
};
//
@@ -272,8 +272,8 @@
//
const getTypeText = (type) => {
const typeMap = {
'complain': $t('feedback.complain'),
'suggestion': $t('feedback.suggestion')
'complain': t('feedback.complain'),
'suggestion': t('feedback.suggestion')
};
return typeMap[type] || type || '-';
};
@@ -72,34 +72,37 @@
} from "@dcloudio/uni-app"
import {
addUserFeedback
} from '../../config/api/feedback'
} from '@/config/api/feedback'
import {
uploadOssResource
} from '../../config/api/user'
} from '@/config/api/user'
import {
useI18n
} from '@/utils/i18n.js'
const {
t: $t
t
} = useI18n()
//
const navigateToRecord = () => {
uni.navigateTo({
url: '/pages/feedback/list'
url: '/subPackages/service/feedback/list'
})
}
onMounted(() => {
uni.setNavigationBarTitle({
title: $t('feedback.title')
title: t('feedback.title')
})
})
onLoad(() => {
onLoad((options) => {
if (uni.getStorageSync("userInfo").phone) {
contact.value = uni.getStorageSync('userInfo').phone;
}
if(options.selectedType) {
selectedType.value = parseInt(options.selectedType);
}
})
//
@@ -134,7 +137,7 @@
const submitFeedback = async () => {
if (selectedType.value === -1) {
uni.showToast({
title: $t('feedback.pleaseSelectType'),
title: t('feedback.pleaseSelectType'),
icon: 'none'
})
return
@@ -142,7 +145,7 @@
if (!description.value.trim()) {
uni.showToast({
title: $t('feedback.pleaseDescribe'),
title: t('feedback.pleaseDescribe'),
icon: 'none'
})
return
@@ -150,7 +153,7 @@
if (!contact.value) {
uni.showToast({
title: $t('feedback.pleaseContact'),
title: t('feedback.pleaseContact'),
icon: 'none'
})
return
@@ -166,7 +169,7 @@
try {
//
uni.showLoading({
title: $t('feedback.uploading') || '上传中...',
title: t('feedback.uploading') || '上传中...',
mask: true
})
@@ -182,7 +185,7 @@
console.error(`文件 ${i + 1} 上传失败:`, err)
uni.hideLoading()
uni.showToast({
title: $t('feedback.imageUploadFailed'),
title: t('feedback.imageUploadFailed'),
icon: 'none'
})
return
@@ -205,7 +208,7 @@
//
if (res && (res.code === 200 || res === true || res?.success === true)) {
uni.showToast({
title: $t('feedback.submitSuccess'),
title: t('feedback.submitSuccess'),
icon: 'success'
})
setTimeout(() => {
@@ -213,7 +216,7 @@
}, 1500);
} else {
uni.showToast({
title: (res && (res.msg || res.message)) || $t('feedback.submitFailed'),
title: (res && (res.msg || res.message)) || t('feedback.submitFailed'),
icon: 'none'
})
}
@@ -221,7 +224,7 @@
console.error('feedback submit failed:', err)
uni.hideLoading()
uni.showToast({
title: $t('error.networkError') || '网络错误,请重试',
title: t('error.networkError') || '网络错误,请重试',
icon: 'none'
})
}
@@ -72,19 +72,19 @@
} from '@dcloudio/uni-app';
import {
getFeedbackList
} from '../../config/api/feedback.js';
} from '@/config/api/feedback.js';
import {
useI18n
} from '@/utils/i18n.js'
const {
t: $t
t
} = useI18n()
//
onMounted(() => {
uni.setNavigationBarTitle({
title: $t('feedback.recordList')
title: t('feedback.recordList')
})
})
@@ -100,25 +100,25 @@
//
const statusTabs = reactive([{
get text() {
return $t('common.all')
return t('common.all')
},
status: ''
},
{
get text() {
return $t('feedback.pending')
return t('feedback.pending')
},
status: 'pending'
},
{
get text() {
return $t('feedback.processing')
return t('feedback.processing')
},
status: 'in_progress'
},
{
get text() {
return $t('feedback.completed')
return t('feedback.completed')
},
status: 'resolved'
}
@@ -152,7 +152,7 @@
const status = statusTabs[currentTab.value].status;
const params = {
pageNum: currentPage.value,
pageSize: pageSize.value
pageSize: pageSize.value,
};
if (status) {
params.status = status;
@@ -176,14 +176,14 @@
}
} else {
uni.showToast({
title: res.msg || $t('feedback.getListFailed'),
title: res.msg || t('feedback.getListFailed'),
icon: 'none'
});
}
} catch (error) {
console.error('获取投诉列表失败:', error);
uni.showToast({
title: $t('feedback.getListFailed'),
title: t('feedback.getListFailed'),
icon: 'none'
});
} finally {
@@ -211,11 +211,11 @@
//
const getStatusText = (status) => {
const statusMap = {
'pending': $t('feedback.pending'),
'in_progress': $t('feedback.processing'),
'resolved': $t('feedback.completed')
'pending': t('feedback.pending'),
'in_progress': t('feedback.processing'),
'resolved': t('feedback.completed')
};
return statusMap[status] || $t('feedback.pending');
return statusMap[status] || t('feedback.pending');
};
//
@@ -231,8 +231,8 @@
//
const getTypeText = (type) => {
const typeMap = {
'complain': $t('feedback.complain'),
'suggestion': $t('feedback.suggestion')
'complain': t('feedback.complain'),
'suggestion': t('feedback.suggestion')
};
return typeMap[type] || type || '-';
};
@@ -270,7 +270,7 @@
//
const navigateToDetail = (item) => {
uni.navigateTo({
url: `/pages/feedback/detail?id=${item.id || item.feedbackId}`
url: `/subPackages/service/feedback/detail?id=${item.id || item.feedbackId}`
});
};
</script>

Some files were not shown because too many files have changed in this diff Show More