2 Commits

Author SHA1 Message Date
pcwl_yancheng 2a7c6c8e03 fix:阿里适配 2026-01-19 09:16:53 +08:00
pcwl_yancheng 76bdcd1aba fix:对接高德地图,修复地图bug 2025-12-23 10:39:41 +08:00
110 changed files with 6560 additions and 25721 deletions
-13
View File
@@ -1,13 +0,0 @@
{
"version" : "1.0",
"configurations" : [
{
"playground" : "standard",
"type" : "uni-app:app-android"
},
{
"playground" : "standard",
"type" : "uni-app:app-ios"
}
]
}
-130
View File
@@ -1,130 +0,0 @@
# 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>
+7 -44
View File
@@ -1,6 +1,6 @@
<script>
import {
wxLogin,
alipayLogin,
getUserInfo
} from './util/index'
@@ -11,52 +11,13 @@
// 注意:语言初始化已移至 main.js,确保每次 reLaunch 都能正确加载新语言
},
onShow: async function() {
// 检查启动路径,如果设置了启动路径则跳转
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)
}
console.log('========================================')
console.log('=== App onShow 被调用 ===')
console.log('时间戳:', new Date().toLocaleTimeString())
// 检查并更新语言(uni.reLaunch 会触发 onShow
try {
const savedLang = uni.getStorageSync('language')
if(savedLang){
uni.removeStorageSync('language');
}
console.log('App onShow - 缓存中的语言:', savedLang)
// 获取当前 i18n 实例并检查语言
@@ -78,6 +39,8 @@
} catch (e) {
console.error('App onShow - 语言检查失败:', e)
}
console.log('========================================')
},
onHide: function() {
console.log('App Hide')
@@ -86,7 +49,7 @@
// 保留方法但不调用
async autoLogin() {
try {
const loginResult = await wxLogin()
const loginResult = await alipayLogin()
// await getUserInfo()
} catch (error) {
console.error('自动登录失败:', error)
-187
View File
@@ -1,187 +0,0 @@
<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
@@ -1,263 +0,0 @@
<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 || '风电者2026新款' }}</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" lazy-load="true" />
<image class="empty-icon" src="/static/scan-icon.png" mode="aspectFit" />
<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 } = useI18n()
const { t: $t } = useI18n()
const props = defineProps({
show: { type: Boolean, default: false },
+83 -144
View File
@@ -1,11 +1,9 @@
<template>
<view class="map-container" :class="{ 'full-width': props.fullWidth }">
<view class="map-container" :class="{ 'full-width': props.fullWidth }" :style="{ '--map-height': props.customHeight || '78vh' }">
<!-- 地图容器 -->
<view class="map-wrapper">
<!-- 使用小程序原生地图组件 -->
<map
id="map"
class="native-map"
<!-- 支付宝小程序地图组件使用高德地图 -->
<map id="map" class="native-map"
:longitude="mapCenter.longitude"
:latitude="mapCenter.latitude"
:markers="mapMarkers"
@@ -13,81 +11,42 @@
:show-location="false"
@regionchange="onMapRegionChange"
@markertap="onMapMarkerTap"
@callouttap="onCalloutTap"
@tap="onMapTap"
@updated="onMapUpdated"
@error="onMapError"
>
@error="onMapError">
</map>
<!-- 覆盖在地图上的广告轮播使用 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 guide" @tap="handleGuide">
<cover-image
class="side-icon"
src="/static/use_help.png"
style="border-radius: 50%;"
></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>
<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/location.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-image class="side-icon" src="/static/customer-service.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-view>
</cover-view>
</map>
<!-- 地图加载状态 -->
<view class="map-loading" v-if="isLoading">
@@ -118,7 +77,7 @@
import { useI18n } from '../utils/i18n.js'
// 获取 i18n 实例
const { t } = useI18n()
const { t: $t } = useI18n()
// 引用折叠面板组件的ref
const collapseRef = ref(null)
@@ -178,8 +137,7 @@
'showList',
'markerTap',
'mapCenterChange',
'bannerClick',
'guide'
'bannerClick'
])
// 响应式数据
@@ -296,14 +254,45 @@
deep: true
})
// 启动广告轮播
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)
}
// 停止广告轮播
const stopBannerRotation = () => {
if (bannerTimer) {
console.log('停止广告轮播')
clearInterval(bannerTimer)
bannerTimer = null
}
}
// 监听广告图片变化,启动或停止轮播
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()
@@ -319,31 +308,22 @@
isLoading.value = false
}
// 地图区域变化事件(带防抖优化)
// 地图区域变化事件(支付宝小程序,带防抖优化)
const onMapRegionChange = (e) => {
// 只处理结束事件
if (!e || (e.type !== 'end' && e.type !== 'regionchange')) {
if (!e) {
return
}
const causedBy = e.causedBy || e.detail?.causedBy
// 获取触发原因和中心位置
const causedBy = e.detail?.causedBy || e.causedBy
const centerLocation = e.detail?.centerLocation || e.centerLocation || e.detail?.location
// 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 (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(() => {
@@ -353,13 +333,11 @@
}
mapCenter.value = newCenter;
// 触发父组件查询新位置的场地
emit('mapCenterChange', newCenter)
}, 500)
} else {
// 兜底方案:如果事件中没有centerLocation,才使用API获取
// 兜底方案:使用API获取地图中心
regionChangeTimer = setTimeout(() => {
if (mapContext.value) {
mapContext.value.getCenterLocation({
@@ -383,12 +361,17 @@
}
}
// 标记点点击事件
// 标记点点击事件(支付宝小程序)
const onMapMarkerTap = (e) => {
const markerId = e.detail?.markerId || e.markerId
if (!e) {
return
}
// 获取markerId
const markerId = e.detail?.markerId || e.markerId || e.detail?.marker?.id
// 查找对应的场地位置信息
if (props.filteredPositions && props.filteredPositions.length > 0) {
if (props.filteredPositions && props.filteredPositions.length > 0 && markerId) {
const position = props.filteredPositions[markerId - 1]
if (position) {
emit('markerTap', position)
@@ -396,14 +379,9 @@
}
}
// 标记点气泡点击事件
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 onMapTap = (e) => {
console.log('地图点击事件:', e)
}
// 地图错误事件
@@ -427,49 +405,23 @@ const handleSearch = () => {
const handleService = () => {
uni.navigateTo({
url: '/subPackages/service/help/index'
url: '/pages/help/index'
})
}
const handleGuide = () => {
emit('guide')
}
const handleJoinTap = () => {
uni.navigateTo({
url: '/subPackages/business/join/index'
url: '/pages/join/index'
})
}
// 处理广告点击
const handleBannerTap = () => {
console.log('点击地图广告:', currentBannerIndex.value, currentBannerImage.value)
// 触发父组件处理点击事件
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
}
// 默认跳转到合作加盟页面
handleJoinTap()
}
const handleScan = () => {
@@ -502,6 +454,7 @@ const handleSearch = () => {
// 初始化广告轮播
if (props.bannerImages && props.bannerImages.length > 1) {
console.log('onMounted: 初始化广告轮播')
startBannerRotation()
}
})
@@ -542,35 +495,22 @@ const handleSearch = () => {
/* 地图容器 */
.map-container {
flex: 1;
position: relative;
// position: fixed;
// top: 0;
left: 0;
right: 0;
bottom: 0;
width: 94vw;
// height: var(--map-height, calc(100% - 20rpx)); /* 使用变量或默认高度 */
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 {
@@ -659,14 +599,14 @@ const handleSearch = () => {
// min-width: 160rpx;
margin: auto;
// height: 72rpx;
background: rgba(255, 255, 255, 0.96);
border-radius: 24rpx;
// background: rgba(255, 255, 255, 0.96);
border-radius: 36rpx;
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
// box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.12);
padding: 13rpx;
padding: 20rpx;
border: 2rpx solid #e0e0e0;
&:active {
@@ -698,8 +638,8 @@ const handleSearch = () => {
}
.side-icon {
width: 40rpx;
height: 40rpx;
width: 44rpx;
height: 44rpx;
}
.index-swiper {
@@ -731,7 +671,6 @@ const handleSearch = () => {
justify-content: center;
gap: 8rpx;
z-index: 2;
pointer-events: none;
.indicator-dot {
width: 12rpx;
-687
View File
@@ -1,687 +0,0 @@
<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>
+18 -37
View File
@@ -8,28 +8,22 @@
</view>
<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" lazy-load="true"></image>
<view class="payment-badge alipay-score" v-if="order.payWay == 'alipay_score_pay'">
<image src="/static/images/alipay.svg" mode="aspectFit" class="badge-icon"></image>
<view class="badge-text">
<text>{{ $t('order.wxPayScore') }}</text>
<text>{{ $t('order.alipayScore') }}</text>
<text class="divider">|</text>
<text class="highlight">{{ $t('order.depositFree') }}</text>
</view>
</view>
<view class="payment-badge whitelist" v-else-if="order.payWay == 'wx_global_pay'">
<view class="payment-badge whitelist" v-else-if="order.payWay == 'alipay_global_pay'">
<text class="badge-text">{{ $t('order.whitelistOrder') }}</text>
</view>
<view class="payment-badge member" v-else-if="order.payWay == 'wx_member_pay'">
<view class="payment-badge member" v-else-if="order.payWay == 'alipay_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="badge-text">{{ $t('order.alipayPay') }}</text>
<text class="divider">|</text>
<text class="badge-text">{{ $t('order.depositPay') }}</text>
</view>
@@ -69,16 +63,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" lazy-load="true"></image>
<image src="/static/order_time.png" mode="aspectFit" class="icon-time"></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" lazy-load="true"></image>
<image src="/static/order_time.png" mode="aspectFit" class="icon-time"></image>
{{ usedDurationText }}
</view>
<view class="meta-item">
<image src="/static/order_price.png" mode="aspectFit" class="icon-price" lazy-load="true"></image>
<image src="/static/order_price.png" mode="aspectFit" class="icon-price"></image>
{{ displayAmount }}
</view>
</view>
@@ -104,7 +98,7 @@
import { computed } from 'vue';
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
const { t: $t } = useI18n()
const props = defineProps({
order: { type: Object, required: true },
@@ -113,7 +107,7 @@
const emit = defineEmits(['pay', 'cancel', 'return-device', 'details']);
const rawStatus = computed(() => props.order.orderStatus ?? props.order.status);
const rawStatus = computed(() => props.order.orderStatus != null ? props.order.orderStatus : props.order.status);
const normalizedStatus = computed(() => {
const s = rawStatus.value;
switch (s) {
@@ -134,19 +128,9 @@
}
});
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(() => {
if (isBillingPaused.value) return t('order.orderStatusBillingPaused')
return statusDef.value.text || ''
});
const statusText = computed(() => 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';
@@ -160,7 +144,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');
@@ -174,10 +158,8 @@
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')}`;
// 如果小于1分钟,显示"小于1分钟"
if (minutes < 1) return `${t('time.lessThan')}1${t('time.minute')}`;
return `${mins}${t('time.minute')}`;
if (hours > 0) return `${hours}${$t('time.hour')}${mins}${$t('time.minute')}`;
return `${mins}${$t('time.minute')}`;
});
function parseDate(str) {
@@ -222,7 +204,6 @@
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; }
@@ -291,8 +272,8 @@
border-radius: 8rpx;
white-space: nowrap;
&.wx-score {
background: rgba(7, 193, 96, 0.08);
&.alipay-score {
background: rgba(0, 122, 255, 0.08);
.badge-icon {
width: 32rpx;
@@ -302,7 +283,7 @@
.badge-text {
font-size: 22rpx;
color: #07c160;
color: #007AFF;
display: flex;
align-items: center;
-5021
View File
File diff suppressed because it is too large Load Diff
-41
View File
@@ -1,41 +0,0 @@
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'
})
}
+21 -3
View File
@@ -50,14 +50,32 @@ export const transformDeviceData = (device) => {
}
// 立即租借
export const rentPowerBank = (deviceNo, phone,payway) => {
export const rentPowerBank = (deviceNo, phone) => {
return request({
url: `/app/device/rentPowerBank?deviceNo=${deviceNo}`,
method: 'post',
data: {
// deviceNo,
phone,
payway
phone
}
})
}
// 确认支付并弹出风扇
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
@@ -1,41 +0,0 @@
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'
})
}
+17 -164
View File
@@ -10,16 +10,6 @@ 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({
@@ -51,6 +41,7 @@ export const createOrder = (data) => {
// 查询订单
export const queryById = (id) => {
console.log(`查询订单详情, orderId: ${id}`)
return request({
url: `/app/order/${id}`,
method: 'get',
@@ -58,15 +49,6 @@ 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({
@@ -78,6 +60,7 @@ export const cancelOrder = (data) => {
// 结束订单
export const overOrderById = (orderId) => {
console.log(`调用结束订单API, orderId: ${orderId}`)
return request({
url: `/app/order/close/${orderId}`,
method: 'get',
@@ -93,141 +76,21 @@ 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) => {
// 通过订单号创建支付宝支付订单(芝麻信用免押
export const getOrderByOrderNoScore = (orderNo) => {
console.log('通过订单号创建支付宝支付订单(芝麻信用免押)', 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) => {
// 通过订单号查询支付宝订单支付状态
export const getOrderByOrderNoScorePayStatus = (orderNo) => {
console.log('通过订单号查询支付宝订单支付状态', 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) => {
return request({
url: `/app/wx-payment/score/create/${orderNo}`,
method: 'get',
hideLoading: true
})
}
// 通过订单号获取支付分订单状态
export const getOrderByOrderNoScorePayStatus = (orderNo) => {
return request({
url: `/app/wx-payment/score/status/${orderNo}`,
method: 'get',
hideLoading: true
})
@@ -235,6 +98,7 @@ export const getOrderByOrderNoScorePayStatus = (orderNo) => {
// 更新订单套餐信息
export const updateOrderPackage = (data) => {
console.log('更新订单套餐信息:', data)
return request({
url: '/app/device/updateOrderPackage',
method: 'post',
@@ -242,26 +106,15 @@ export const updateOrderPackage = (data) => {
})
}
// 用户端删除商品订单(逻辑删除)
export const deleteProductOrder = (id) => {
/*
* 弃用
*/
export const getPotionsDetail = (data) => {
console.log(data);
return request({
url: `/app/product/order/${id}`,
method: 'delete'
url: '/device/position/positionDetails',
method: 'get',
data
})
}
// 用户端取消商品订单支付
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
@@ -1,75 +0,0 @@
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'
})
}
+4 -30
View File
@@ -27,38 +27,12 @@ export const getCommonByBrand = (brandName) => {
})
}
// 获取当前协议内容
export const getCurrentAgreement = (data) => {
// 查询激活的且临近关闭时间最近的活动
export const getActiveActivity = () => {
return request({
url: '/device/agreementConfig/current',
url: '/device/activity/agent/list',
method: 'get',
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
hideLoading: true
})
}
+3 -60
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,42 +10,6 @@ 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({
@@ -92,8 +56,7 @@ export const uploadUserAvatar = (filePath) => {
header: {
'appid': appid,
'Authorization': 'Bearer ' + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id'),
'Content-Language': (uni.getStorageSync('language') || 'zh-CN').replace(/-/g, '_')
'Clientid': uni.getStorageSync('client_id')
},
success: (res) => {
try {
@@ -120,8 +83,7 @@ export const uploadOssResource = (filePath) => {
header: {
'appid': appid,
'Authorization': 'Bearer ' + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id'),
'Content-Language': (uni.getStorageSync('language') || 'zh-CN').replace(/-/g, '_')
'Clientid': uni.getStorageSync('client_id')
},
success: (res) => {
try {
@@ -153,22 +115,3 @@ 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
@@ -1,77 +0,0 @@
/**
* 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)
}
+10 -31
View File
@@ -1,15 +1,8 @@
import {
URL,
appid,
ZFBappid
appid
} from './url'
// 根据运行平台选择正确的小程序 appid(后端通常依赖该 header 做平台识别)
let platformAppid = appid
// #ifdef MP-ALIPAY
platformAppid = ZFBappid
// #endif
// 获取多语言翻译文本
const getLoadingText = () => {
try {
@@ -36,14 +29,12 @@ 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': platformAppid,
'appid': appid,
'platform': 'alipay', // 标识支付宝小程序平台
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id'),
'Content-Language': (uni.getStorageSync('language') || 'zh-CN').replace(/-/g, '_')
'Clientid': uni.getStorageSync('client_id')
},
success(res) {
@@ -58,9 +49,7 @@ const request = (option) => {
return
}
reject({
msg: `请求失败,状态码:${res.statusCode}`
})
reject({msg: `请求失败,状态码:${res.statusCode}`})
return
}
@@ -72,22 +61,12 @@ 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: "/subPackages/user/login/index"
})
} catch (e) {
uni.reLaunch({
url: "/subPackages/user/login/index"
})
}
uni.reLaunch({ url: `/pages/login/index?redirect=${redirect}` })
} catch (e) {}
}
// 检查业务状态码
+3 -3
View File
@@ -1,8 +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.0.158:8080" //本地调试
// export const URL = "http://192.168.5.30:8080" //本地调试
// export const URL = "http://127.0.0.1:8080" //本地调试
export const appid = "wx2165f0be356ae7a9" //微信小程序appid
export const ZFBappid = "2021006117693332" //支付宝小程序appid
export const appid = "2021006117693332" //支付宝小程序appid
export const ZFBappid = "2021006117693332" //支付宝小程序appid(保留兼容)
+207
View File
@@ -0,0 +1,207 @@
# 支付宝支付接口文档
## 接口概述
本文档描述支付宝支付相关的API接口,包括创建支付订单和查询支付状态。
---
## 1. 创建支付宝支付订单
### 接口描述
创建支付宝支付订单,用于扫码预下单并返回二维码。
### 请求信息
#### 请求URL
```
GET /app/ali-payment/create/{orderNo}
```
#### 请求方式
`GET`
#### 请求参数
| 参数名 | 参数类型 | 是否必填 | 参数位置 | 参数说明 |
|--------|----------|----------|----------|----------|
| orderNo | String | 是 | Path | 订单号 |
#### 请求示例
```http
GET /app/ali-payment/create/ORD20231223001
```
### 响应信息
#### 响应参数
| 参数名 | 参数类型 | 参数说明 |
|--------|----------|----------|
| code | Integer | 响应状态码,200表示成功 |
| msg | String | 响应消息 |
| data | Object | 返回数据,包含支付宝支付相关信息 |
#### 响应示例
成功响应:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"qrCode": "https://qr.alipay.com/xxx",
"outTradeNo": "ORD20231223001",
"totalAmount": "99.00"
}
}
```
失败响应:
```json
{
"code": 500,
"msg": "订单不存在或已支付"
}
```
### 错误码说明
| 错误码 | 说明 |
|--------|------|
| 200 | 创建成功 |
| 400 | 参数错误,订单号不能为空 |
| 500 | 系统错误或订单状态异常 |
---
## 2. 查询订单支付状态
### 接口描述
查询指定订单的支付状态。
### 请求信息
#### 请求URL
```
GET /app/ali-payment/status/{orderNo}
```
#### 请求方式
`GET`
#### 请求参数
| 参数名 | 参数类型 | 是否必填 | 参数位置 | 参数说明 |
|--------|----------|----------|----------|----------|
| orderNo | String | 是 | Path | 订单号 |
#### 请求示例
```http
GET /app/ali-payment/status/ORD20231223001
```
### 响应信息
#### 响应参数
| 参数名 | 参数类型 | 参数说明 |
|--------|----------|----------|
| code | Integer | 响应状态码,200表示成功 |
| msg | String | 响应消息 |
| data | Object | 订单支付状态信息 |
| data.tradeStatus | String | 交易状态(WAIT_BUYER_PAY-等待支付,TRADE_SUCCESS-支付成功,TRADE_CLOSED-交易关闭) |
| data.tradeNo | String | 支付宝交易号 |
| data.totalAmount | String | 订单金额 |
| data.buyerPayAmount | String | 买家实付金额 |
#### 响应示例
支付成功响应:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"tradeStatus": "TRADE_SUCCESS",
"tradeNo": "2023122322001234567890",
"outTradeNo": "ORD20231223001",
"totalAmount": "99.00",
"buyerPayAmount": "99.00",
"buyerLogonId": "158****5620"
}
}
```
等待支付响应:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"tradeStatus": "WAIT_BUYER_PAY",
"outTradeNo": "ORD20231223001",
"totalAmount": "99.00"
}
}
```
失败响应:
```json
{
"code": 500,
"msg": "订单不存在"
}
```
### 支付状态说明
| 状态码 | 说明 |
|--------|------|
| WAIT_BUYER_PAY | 交易创建,等待买家付款 |
| TRADE_CLOSED | 未付款交易超时关闭,或支付完成后全额退款 |
| TRADE_SUCCESS | 交易支付成功 |
| TRADE_FINISHED | 交易结束,不可退款 |
---
## 公共说明
### 基础路径
```
http://your-domain.com/app/ali-payment
```
### 请求头
```
Content-Type: application/json
```
### 注意事项
1. **订单号格式**:订单号必须唯一,建议使用系统生成的订单编号
2. **幂等性**:同一订单号多次调用创建接口,返回相同的支付信息
3. **超时处理**:支付订单创建后,建议在15分钟内完成支付
4. **状态查询**:建议在支付完成后通过回调通知处理业务逻辑,状态查询接口用于补充查询
5. **安全性**:生产环境建议添加签名验证和请求频率限制
### 业务流程
```
1. 用户发起租借 → 系统创建订单
2. 调用创建支付接口 → 返回支付二维码
3. 用户扫码支付 → 支付宝处理支付
4. 支付宝回调通知 → 系统更新订单状态
5. 前端轮询状态接口 → 确认支付结果
6. 支付成功 → 触发开锁逻辑
```
---
## 联系方式
如有问题,请联系技术支持团队。
**文档版本**v1.0
**最后更新**2025-12-23
**维护人员**:开发团队
+16 -228
View File
@@ -11,7 +11,6 @@ export default {
filling: 'Filling...',
save: 'Save',
loadFailed: 'Load failed',
invalidUrl: 'Invalid URL',
statusCode: 'Status Code',
message: 'Message',
none: 'None',
@@ -49,20 +48,10 @@ 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',
functionDeveloping: 'Function under development',
saveImage: 'Save to Phone',
saveSuccess: 'Saved successfully',
saving: 'Saving...',
saveFailed: 'Save failed',
downloadFailed: 'Download failed',
reset: 'Reset',
preview: 'Preview'
noMore: 'No more'
},
nav: {
@@ -87,7 +76,6 @@ export default {
scanToUse: 'Scan',
personalCenter: 'Profile',
useGuide: 'Guide',
buyDevice: 'Custom Powewr',
navigate: 'Navigate',
relocate: 'Relocate',
search: 'Search',
@@ -101,9 +89,7 @@ export default {
invalidQRCode: 'Invalid QR code',
scanFailed: 'Scan failed',
noticeTitle: 'Notice',
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'
getLocationFailed: 'Location unavailable'
},
guide: {
@@ -111,7 +97,7 @@ export default {
step1Title: 'Scan QR Code',
step1Desc: 'Find a device and scan its QR code',
step2Title: 'No Deposit',
step2Desc: 'Rent with WeChat Pay Score, no deposit needed',
step2Desc: 'Rent with Sesame Credit, no deposit needed',
step3Title: 'Start Using',
step3Desc: 'Device unlocks, take out the fan',
step4Title: 'Return',
@@ -126,8 +112,7 @@ export default {
businessHours: 'Business Hours: ',
navigateHere: 'Navigate Here',
coordinateError: 'Invalid location coordinates',
notExist: 'Location does not exist',
supportCouponOrMember: 'Coupons & Cards Available'
notExist: 'Location does not exist'
},
device: {
@@ -149,8 +134,7 @@ export default {
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',
alipayScoreDesc: 'Sesame Credit | 550+ points enjoy',
checking: 'Checking',
deviceNoNotRecognized: 'Device number not recognized',
processFailed: 'Process failed, please try again later',
@@ -160,14 +144,11 @@ export default {
rentSuccess: 'Rent successful',
rentFailedRetry: 'Rent failed, please retry',
getPayParamsFailed: 'Failed to get payment parameters',
payScoreFailedCancelled: 'Pay score call failed, order cancelled',
canUsePromotion: 'Tips: Coupons and membership cards available',
goToBuy: 'Buy Now'
payScoreFailedCancelled: 'Credit payment failed, order cancelled'
},
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?',
@@ -175,7 +156,6 @@ export default {
orderNo: 'Order No.',
orderStatus: 'Order Status',
deviceNo: 'Device No.',
deviceName: 'Device Name',
rentLocation: 'Rent Location',
rentTime: 'Rent Time',
returnTime: 'Return Time',
@@ -188,8 +168,6 @@ export default {
deposit: 'Deposit',
rentFee: 'Rent Fee',
payNow: 'Pay Now',
myCoupons: 'Coupons',
myCards: 'Member Cards',
cancelOrder: 'Cancel Order',
quickReturn: 'Quick Return',
returnDevice: 'Return Device',
@@ -212,13 +190,11 @@ export default {
returnFailed: 'Return failed',
confirmCancel: 'Confirm to cancel order?',
confirmReturn: 'Confirm to return device?',
wxPayScore: 'WeChat Pay Score',
alipayScore: 'Sesame Credit',
alipayPay: 'Alipay',
depositFree: 'Deposit-free',
whitelistOrder: 'Whitelist Order',
memberOrder: 'Member Order',
aliPay: 'AliPay',
antomPay: 'Antom Pay',
wxPay: 'WeChat Pay',
depositPay: 'Deposit Pay',
paymentInProgress: 'Payment in Progress',
paymentFailedRetry: 'Payment failed, please try again',
@@ -227,21 +203,12 @@ 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',
@@ -272,37 +239,7 @@ export default {
paymentMethod: 'Payment Method',
perHour: 'per hour',
perMinute: 'per minute',
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'
perHalfHour: 'per half hour'
},
user: {
@@ -316,16 +253,12 @@ 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',
@@ -370,22 +303,7 @@ export default {
phoneSuccess: 'Success',
phoneError: 'Error',
phoneGetFailed: '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'
authCodeFailed: 'Auth failed'
},
permission: {
@@ -400,10 +318,7 @@ export default {
payment: {
paymentAmount: 'Amount',
paymentMethod: 'Method',
wechatPay: 'WeChat',
alipay: 'Alipay',
alipayHk: 'Alipay (Hong Kong)',
alipayId: 'Alipay (Indonesia)',
balance: 'Balance',
payNow: 'Pay',
paying: 'Processing...',
@@ -421,9 +336,7 @@ export default {
package: 'Package',
total: 'Total',
paymentFailedRetry: 'Payment failed, retry?',
createPayOrderFailed: 'Failed',
subscriptionSuccess: 'Subscription successful',
subscriptionFailed: 'Subscription failed, please try again'
createPayOrderFailed: 'Failed'
},
feedback: {
@@ -483,7 +396,6 @@ 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.',
@@ -494,9 +406,7 @@ 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.',
pauseBillingButton: 'Pause billing',
pauseBillingHint: 'You may request to pause billing during an active rental (see your order for details).'
faq5Answer: 'Submit feedback via "My - Feedback", or call customer service directly.'
},
settings: {
@@ -505,8 +415,6 @@ export default {
languageSetting: 'Language Setting',
chinese: '简体中文',
english: 'English',
indonesian: 'Bahasa Indonesia',
languageSwitched: 'Language switched, refreshing...',
notification: 'Notification',
privacy: 'Privacy',
about: 'About',
@@ -609,30 +517,10 @@ 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',
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"',
// 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.'
footerNoticePolicy: 'If you have questions about this policy, please go to "My-Customer Service"'
},
search: {
@@ -676,8 +564,7 @@ export default {
tomorrow: 'Tomorrow',
hours: 'hour(s)',
minutes: 'minute(s)',
halfHours: 'half hour(s)',
lessThan: 'less than'
halfHours: 'half hour(s)'
},
unit: {
@@ -752,8 +639,6 @@ 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'
@@ -777,104 +662,7 @@ 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',
productName: 'FengDianZhe 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: 'FengDianZhe 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...'
}
}
-879
View File
@@ -1,879 +0,0 @@
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 FengDianZhe'
},
app: {
name: 'FengDianZhe',
slogan: 'Kipas Angin & Power Bank Berbagi',
fullName: 'FengDianZhe - Kipas Angin & Power Bank Berbagi',
welcome: 'Selamat datang menggunakan FengDianZhe'
},
home: {
title: 'Kipas Angin & Power Bank Berbagi FengDianZhe',
nearbyDevices: 'Perangkat Terdekat',
scanToUse: 'Pindai untuk Menggunakan',
personalCenter: 'Pusat Pribadi',
useGuide: 'Panduan Penggunaan',
buyDevice: 'Kustomisasi Power Bank',
navigate: 'Navigasi',
relocate: 'Lokasi Ulang',
search: 'Cari',
service: 'Layanan Pelanggan',
searchPlaceholder: 'Cari lokasi terdekat',
nearbyDeviceLocation: 'Lokasi Perangkat Terdekat',
noNearbyDevice: 'Tidak ada perangkat terdekat',
relocating: 'Melokalisasi ulang...',
locateSuccess: 'Lokasi berhasil',
locateFailed: 'Lokasi gagal, harap periksa izin lokasi',
invalidQRCode: 'Kode QR perangkat tidak valid',
scanFailed: 'Pemindaian gagal',
noticeTitle: 'Pemberitahuan',
getLocationFailed: 'Gagal mendapatkan lokasi, menampilkan peta default',
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'
},
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',
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?',
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',
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: 'FengDianZhe 18163601305',
recipientAddress: 'Gedung A2, Lantai 623, Taman Sains dan Teknologi Xinchanghaijian, Jalan Lugu, Distrik Yuelu, Changsha, Provinsi Hunan',
copyAllInfo: 'Salin Semua Informasi',
recipient: 'Penerima',
recipientAddressLabel: 'Alamat Penerima',
copySuccess: 'Semua informasi telah disalin',
copyFailed: 'Gagal menyalin',
noReturnRecord: 'Tidak ada catatan pengembalian',
toFill: 'Menunggu Diisi',
userPhone: 'Nomor Telepon Pengguna',
billingPaused: 'Penagihan Dijeda',
completed: 'Selesai',
processing: 'Memproses',
getListFailed: 'Gagal mendapatkan daftar',
loadFailed: 'Gagal memuat',
returnCompleted: 'Pengembalian Selesai',
returnCompletedDesc: 'Ekspres Anda telah berhasil dikembalikan',
processingDesc: 'Sedang memproses permintaan pengembalian Anda',
pendingDesc: 'Menunggu pemrosesan aplikasi pengembalian',
expressInfo: 'Informasi Ekspres',
trackingNo: 'Nomor Pelacakan',
packageType: 'Jenis Paket',
packageWeight: 'Berat Paket',
returnInfo: 'Informasi Pengembalian',
returnAddress: 'Alamat Pengembalian',
returnTime: 'Waktu Pengembalian',
processTime: 'Waktu Pemrosesan',
completeTime: 'Waktu Penyelesaian',
remarkInfo: 'Informasi Catatan',
copyTrackingNo: 'Salin Nomor Pelacakan',
trackingNoCopied: 'Nomor pelacakan telah disalin',
workingHours: 'Senin hingga Minggu 09:00-22:00',
call: 'Hubungi',
returnDetail: 'Detail Pengembalian',
getDetailFailed: 'Gagal mendapatkan detail',
fillExpress: 'Pengembalian Ekspres',
openTime: 'Waktu Mulai',
fillExpressInfo: 'Isi Informasi Pengembalian Ekspres',
contactPhone: 'Nomor Telepon Kontak',
fillTrackingPlaceholder: 'Harap masukkan nomor ekspres yang perlu diisi',
trackingPlaceholder: 'Harap masukkan nomor ekspres (dapat dikosongkan terlebih dahulu)',
confirmFill: 'Konfirmasi Isi',
submitInfo: 'Kirim Informasi',
orderNoMissing: 'Nomor pesanan kurang',
getRecordFailed: 'Gagal mendapatkan catatan',
existingReturnNotice: 'Sudah ada aplikasi pengembalian ekspres, apakah akan pergi untuk mengisi nomor ekspres?',
goToFill: 'Pergi untuk Mengisi',
alreadyHasRecord: 'Sudah ada catatan pengembalian',
pleaseEnterValidPhone: 'Harap isi nomor telepon kontak yang valid',
pleaseEnterTrackingNo: 'Harap isi nomor ekspres',
filling: 'Mengisi...',
fillSuccess: 'Pengisian berhasil',
fillFailed: 'Pengisian gagal',
submitSuccess: 'Pengiriman berhasil',
submitFailed: 'Pengiriman gagal'
},
join: {
title: 'Kerja Sama dan Keanggotaan',
cooperationTitle: 'Metode Kerja Sama',
contactUs: 'Hubungi Kami',
phone: 'Nomor Telepon Kontak',
email: 'Email Kontak',
submit: 'Kirim Aplikasi',
name: 'Nama',
contactPhone: 'Informasi Kontak',
city: 'Kota',
intention: 'Niat Kerja Sama',
placeholder: 'Harap jelaskan secara singkat niat kerja sama Anda...',
submitSuccess: 'Pengiriman berhasil, kami akan segera menghubungi Anda',
submitFailed: 'Pengiriman gagal, harap coba lagi nanti',
pageLoadFailed: 'Gagal memuat halaman'
},
legal: {
agreement: 'Perjanjian Pengguna',
privacy: 'Kebijakan Privasi',
termsOfService: 'Ketentuan Layanan',
termsAndConditions: 'Syarat & Ketentuan',
lastUpdate: 'Pembaruan Terakhir',
applicableToService: 'Berlaku untuk layanan sewa kipas angin berbagi "FengDianZhe"',
footerNotice: 'Jika ada pertanyaan tentang perjanjian ini, harap pergi ke "Saya-Layanan Pelanggan" untuk konsultasi',
footerNoticePolicy: 'Jika ada pertanyaan tentang kebijakan ini, harap pergi ke "Saya-Layanan Pelanggan" untuk konsultasi',
// Konten Syarat dan Ketentuan
applicableLaw: 'Hukum yang Berlaku',
applicableLawContent: 'Ketentuan Layanan ini diatur oleh hukum Republik Rakyat Tiongkok. Dengan menggunakan layanan ini, Anda setuju untuk terikat oleh hukum Tiongkok. Setiap perselisihan yang timbul dari layanan ini harus diselesaikan terlebih dahulu melalui negosiasi bersahabat; jika negosiasi gagal, salah satu pihak dapat mengajukan gugatan ke Pengadilan Rakyat yang memiliki yurisdiksi atas lokasi penyedia layanan.',
paymentMethods: 'Metode Pembayaran',
paymentMethodsContent: 'Kami mendukung berbagai metode pembayaran, termasuk namun tidak terbatas pada: WeChat Pay, Alipay, WeChat Pay Score tanpa deposit, dll. Pengguna perlu menyelesaikan proses pembayaran sebelum menggunakan layanan. Setelah pembayaran berhasil, sistem akan secara otomatis membuka kunci perangkat untuk akses pengguna. Semua transaksi pembayaran dilakukan melalui saluran terenkripsi yang aman untuk memastikan keamanan dana pengguna.',
refundPolicy: 'Kebijakan Pengembalian Dana',
refundPolicyContent: '1. Pengembalian Deposit: Setelah mengembalikan perangkat, deposit akan secara otomatis dikembalikan ke akun pembayaran asli setelah dikurangi biaya sewa yang sesuai, diperkirakan tiba dalam 0-7 hari kerja.\n2. Pembatalan Pesanan: Pesanan yang tidak digunakan dapat dibatalkan sebelum penggunaan dimulai, dan deposit akan dikembalikan sepenuhnya.\n3. Pengembalian Dana Pengecualian: Dalam kasus keadaan khusus seperti kegagalan perangkat, pengguna dapat mengajukan pengembalian dana, yang akan kami proses dalam 3-5 hari kerja setelah verifikasi.\n4. Kartu Keanggotaan/Kupon: Kartu keanggotaan dan kupon yang dibeli umumnya tidak mendukung pengembalian dana. Silakan hubungi layanan pelanggan untuk kasus khusus.',
serviceTerms: 'Ketentuan Layanan',
serviceTermsContent: 'Saat menggunakan layanan ini, pengguna harus mematuhi peraturan berikut: 1) Jaga peralatan yang disewa dengan baik dan jangan sengaja merusak atau memilikinya secara pribadi; 2) Kembalikan peralatan tepat waktu untuk menghindari biaya tambahan; 3) Jangan gunakan peralatan untuk tujuan ilegal; 4) Jika ditemukan kegagalan peralatan, hubungi layanan pelanggan segera. Pelanggaran terhadap peraturan di atas dapat mengakibatkan penghentian layanan dan tanggung jawab.',
liabilityLimitation: 'Batasan Tanggung Jawab',
liabilityLimitationContent: 'Sejauh diizinkan oleh hukum, kami tidak bertanggung jawab atas kerusakan tidak langsung, insidental, khusus, atau konsekuensial yang timbul dari penggunaan atau ketidakmampuan menggunakan layanan ini. Total tanggung jawab kami tidak akan melebihi biaya yang dibayarkan oleh pengguna untuk menggunakan layanan ini. Kami tidak bertanggung jawab atas gangguan atau penundaan layanan yang disebabkan oleh force majeure, kegagalan jaringan, alasan pihak ketiga, dll.',
disputeResolution: 'Penyelesaian Sengketa',
disputeResolutionContent: 'Jika pengguna memiliki pertanyaan atau perselisihan tentang layanan, silakan hubungi kami terlebih dahulu melalui saluran layanan pelanggan. Kami akan merespons dalam 24 jam setelah menerima umpan balik dan bernegosiasi untuk penyelesaian sesegera mungkin. Jika negosiasi gagal, kedua belah pihak setuju untuk menyerahkan perselisihan ke Pengadilan Rakyat dengan yurisdiksi atas lokasi penyedia layanan untuk penyelesaian melalui litigasi. Selama periode penyelesaian sengketa, kedua belah pihak harus terus melaksanakan ketentuan yang tidak dipersengketakan dari perjanjian ini.'
},
search: {
title: 'Cari Perangkat',
placeholder: 'Harap masukkan nama lokasi atau alamat',
history: 'Riwayat Pencarian',
clear: 'Hapus Riwayat',
noResult: 'Tidak ada hasil pencarian',
searching: 'Mencari...',
invalidCoordinate: 'Koordinat lokasi ini tidak valid',
positionInfoError: 'Informasi lokasi abnormal'
},
share: {
title: 'FengDianZhe - Kipas Angin & Power Bank Berbagi',
path: '/pages/index/index'
},
error: {
networkError: 'Koneksi jaringan gagal',
serverError: 'Kesalahan server',
timeout: 'Permintaan timeout',
unknown: 'Kesalahan tidak diketahui',
tryAgain: 'Harap coba lagi nanti'
},
time: {
hour: 'jam',
minute: 'menit',
second: 'detik',
day: 'hari',
week: 'minggu',
month: 'bulan',
year: 'tahun',
justNow: 'Baru saja',
minutesAgo: 'menit yang lalu',
hoursAgo: 'jam yang lalu',
daysAgo: 'hari yang lalu',
yesterday: 'Kemarin',
today: 'Hari ini',
tomorrow: 'Besok',
hours: 'jam',
minutes: 'menit',
halfHours: 'setengah jam',
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',
productName: 'FengDianZhe Kipas Angin Berbagi + Power Bank + Seri Hand Warmer - Pink Sakura',
perUnit: '/buah',
buyNow: 'Beli Sekarang',
productDetail: 'Detail Kustomisasi',
features: {
battery: '8000Ahm',
batteryDesc: 'Baterai Kapasitas Besar',
wind: 'Kipas Angin Efisien',
temp: 'Kontrol Suhu Pintar',
charge: 'Pengisian Cepat'
},
description: 'FengDianZhe kipas angin berbagi, mengintegrasikan tiga fungsi dalam satu: kipas angin, power bank, dan hand warmer. Menggunakan baterai kapasitas besar 8000mAh, daya tahan lama. Desain kipas angin efisien, tiga tingkat angin dapat disesuaikan. Hand warmer kontrol suhu pintar, hangat di musim dingin dan sejuk di musim panas. Teknologi pengisian cepat, mendukung pengisian multi-perangkat. Warna pink sakura, modis dan indah, adalah teman perjalanan terbaik Anda.',
confirmPurchase: 'Konfirmasi Pembelian',
confirmPurchaseContent: 'Konfirmasi membeli produk ini, perlu membayar ¥{price}?',
purchaseSuccess: 'Pembelian berhasil',
purchaseFailed: 'Pembelian gagal',
processing: 'Sedang memproses...'
}
}
+1 -3
View File
@@ -1,10 +1,8 @@
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,
'id-ID': idID
'en-US': enUS
}
+23 -232
View File
@@ -11,7 +11,6 @@ export default {
filling: '补填中',
save: '保存',
loadFailed: '加载失败',
invalidUrl: '无效的链接',
statusCode: '状态码',
message: '消息',
none: '无',
@@ -49,19 +48,10 @@ export default {
loginRequired: '请先登录',
operationSuccess: '操作成功',
operationFailed: '操作失败',
sending: '发送中...',
loggingIn: '登录中...',
refresh: '刷新',
pull: '下拉刷新',
release: '释放刷新',
noMore: '没有更多了',
functionDeveloping: '功能开发中',
saveImage: '保存到手机',
saveSuccess: '保存成功',
saving: '保存中...',
saveFailed: '保存失败',
reset: '重置',
preview: '预览'
noMore: '没有更多了'
},
nav: {
@@ -70,7 +60,7 @@ export default {
orders: '订单',
settings: '设置',
back: '返回',
title: '风电者共享风扇&暖手充电宝'
title: '风电者共享风扇&暖手宝'
},
app: {
@@ -81,12 +71,11 @@ export default {
},
home: {
title: '风电者共享风扇&暖手充电宝',
title: '风电者共享风扇&暖手宝',
nearbyDevices: '附近设备',
scanToUse: '扫码使用',
personalCenter: '个人中心',
useGuide: '使用指南',
buyDevice: '产品定制',
navigate: '导航',
relocate: '重新定位',
search: '搜索',
@@ -100,9 +89,7 @@ export default {
invalidQRCode: '无效的设备二维码',
scanFailed: '扫码失败',
noticeTitle: '通知公告',
getLocationFailed: '获取位置失败,显示默认地图',
locationPermissionOffTip: '定位权限未打开,暂无法寻找附近设备。如需寻找附近设备,点击下方按钮开启定位。',
enableLocation: '开启定位'
getLocationFailed: '获取位置失败,显示默认地图'
},
guide: {
@@ -110,7 +97,7 @@ export default {
step1Title: '扫码使用',
step1Desc: '找到附近设备,扫描设备上的二维码',
step2Title: '免押金支付',
step2Desc: '无需支付押金,使用支付分免押即可完成租借',
step2Desc: '无需支付押金,使用芝麻信用免押即可完成租借',
step3Title: '开始使用',
step3Desc: '设备自动解锁,风扇弹出后取出即可开始使用',
step4Title: '归还设备',
@@ -125,8 +112,7 @@ export default {
businessHours: '营业时间:',
navigateHere: '导航去这',
coordinateError: '该场地坐标信息异常',
notExist: '场地不存在',
supportCouponOrMember: '可使用优惠券、会员卡'
notExist: '场地不存在'
},
device: {
@@ -148,8 +134,7 @@ export default {
autoChargeOvertime: '超出使用时间将自动按小时计费',
useInDesignatedArea: '请在指定区域内使用设备',
rentDepositFree: '免押金租借',
rentNow: '立即租借',
wxPayScoreDesc: '微信支付分 | 550分以上优享',
alipayScoreDesc: '芝麻信用免押 | 550分以上优享',
checking: '检查中',
deviceNoNotRecognized: '未识别到设备编号',
processFailed: '处理失败,请稍后重试',
@@ -159,14 +144,11 @@ export default {
rentSuccess: '租借成功',
rentFailedRetry: '租借失败,请重试',
getPayParamsFailed: '获取支付参数失败',
payScoreFailedCancelled: '支付调用失败,订单已取消',
canUsePromotion: '提示:可使用优惠券、会员卡',
goToBuy: '去购买'
payScoreFailedCancelled: '信用支付调用失败,订单已取消'
},
order: {
myOrders: '我的订单',
myDeviceOrders: '我的定制',
noOrderRecord: '暂无订单记录',
getOrderListFailed: '获取订单列表失败',
confirmCancelContent: '确定要取消此订单吗?',
@@ -174,7 +156,6 @@ export default {
orderNo: '订单号',
orderStatus: '订单状态',
deviceNo: '设备号',
deviceName: '设备名称',
rentLocation: '租借地点',
rentTime: '租借时间',
returnTime: '归还时间',
@@ -186,11 +167,9 @@ export default {
payAmount: '支付金额',
deposit: '押金',
rentFee: '租金',
myCards: '会员卡优惠',
myCoupons: '优惠券优惠',
payNow: '立即支付',
cancelOrder: '取消订单',
quickReturn: '附近可归还',
quickReturn: '快速归还',
returnDevice: '归还设备',
viewDetails: '查看详情',
orderCompleted: '订单已完成',
@@ -211,13 +190,11 @@ export default {
returnFailed: '归还失败',
confirmCancel: '确认取消订单?',
confirmReturn: '确认归还设备?',
wxPayScore: '微信支付分',
alipayScore: '芝麻信用免押',
depositFree: '免押租借',
whitelistOrder: '白名单订单',
memberOrder: '会员订单',
aliPay: '支付宝支付',
antomPay: '海外支付',
wxPay: '微信支付',
alipayPay: '支付宝支付',
depositPay: '押金租借',
paymentInProgress: '支付中',
paymentFailedRetry: '支付失败,请重新支付',
@@ -226,21 +203,12 @@ export default {
returnedThankYou: '您的风扇已归还,感谢使用',
used: '已使用',
rentInfo: '租借信息',
rentInfoExpand: '展开',
rentInfoCollapse: '收起',
fanNo: '风扇编号',
rentMethod: '租借方式',
returnLocation: '归还地点',
paid: '已支付',
canExpressReturn: '后可快递归还',
pauseBilling: '暂停计费',
pauseBillingSuccess: '暂停计费已生效',
pauseBillingFailed: '暂停计费失败,请稍后重试',
pauseBillingNotEligible: '当前订单暂不符合暂停计费条件',
billingPausedBadge: '计费已暂停',
orderStatusBillingPaused: '已暂停计费',
billingPausedDurationLabel: '暂停时长',
pausedExpressAvailable: '已可申请快递归还',
rentAgain: '再次租借',
backToHome: '返回首页',
feeAppeal: '费用申诉',
@@ -271,37 +239,7 @@ export default {
paymentMethod: '支付方式',
perHour: '每小时',
perMinute: '每分钟',
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:'暂停计费'
perHalfHour: '每半小时'
},
user: {
@@ -315,17 +253,13 @@ export default {
quickReturnDesc: '(直接查看使用中的订单)',
expressReturn: '快递归还记录',
myOrders: '我的订单',
myCards: '我的会员卡',
myCoupons: '我的优惠券',
customerService: '客服中心',
feedback: '投诉与建议',
businessLicense: '营业资质',
cooperation: '信息咨询',
cooperation: '合作加盟',
settings: '设置',
userAgreement: '《用户协议》',
settinguserAgreement: '用户协议',
privacyPolicy: '《隐私政策》',
settinguserprivacyPolicy: '隐私政策',
version: 'v',
logout: '退出登录',
confirmLogout: '确认退出登录?',
@@ -357,6 +291,7 @@ export default {
getUserInfoSuccess: '获取用户信息成功',
getUserInfoFailed: '获取用户信息失败',
pleaseUseInWechat: '请在微信小程序中使用此功能',
pleaseUseInAlipay: '请在支付宝小程序中使用此功能',
agreeToTerms: '我已阅读并同意',
pleaseAgreeToTerms: '请先阅读并同意《用户协议》和《隐私政策》',
loginSuccess: '登录成功',
@@ -369,22 +304,7 @@ export default {
phoneSuccess: '手机号获取成功',
phoneError: '获取手机号异常',
phoneGetFailed: '获取手机号失败',
authCodeFailed: '获取授权码失败',
phoneLogin: '手机号登录',
phonePlaceholder: '请输入手机号码',
codePlaceholder: '请输入验证码',
getCode: '获取验证码',
resend: '重新发送',
loginBtn: '立即登录',
phoneRequired: '请输入手机号',
phoneInvalid: '请输入正确的手机号',
codeRequired: '请输入验证码',
codeSent: '验证码已发送',
sendCodeFailed: '发送验证码失败',
regionNotSupported: '非大陆、香港、澳门用户请通过平台手机号授权登录',
onlyMainlandSupported: '当前仅支持中国大陆地区',
getServicePhoneFailed: '获取客服电话失败',
noAuthToken: '登录成功但未获取到授权凭证'
authCodeFailed: '获取授权码失败'
},
permission: {
@@ -399,10 +319,8 @@ export default {
payment: {
paymentAmount: '支付金额',
paymentMethod: '支付方式',
wechatPay: '微信支付',
alipayPay: '支付宝支付',
alipay: '支付宝',
alipayHk: 'Alipay 香港',
alipayId: 'Alipay 印尼',
balance: '余额支付',
payNow: '立即支付',
paying: '支付中...',
@@ -420,9 +338,7 @@ export default {
package: '套餐',
total: '合计',
paymentFailedRetry: '支付失败,请重试',
createPayOrderFailed: '创建支付订单失败',
subscriptionSuccess: '订阅成功',
subscriptionFailed: '订阅失败,请稍后重试'
createPayOrderFailed: '创建支付订单失败'
},
feedback: {
@@ -482,7 +398,6 @@ export default {
phone: '电话',
email: '邮箱',
workingHours: '工作时间',
workingHoursValue: '周一至周日 09:00-22:00',
functionDeveloping: '功能开发中',
faq1Question: '如何租借风扇?',
faq1Answer: '点击首页"扫码租借"按钮,使用微信扫描设备上的二维码,按提示完成支付即可使用。',
@@ -493,9 +408,7 @@ export default {
faq4Question: '押金多久能退还?',
faq4Answer: '归还设备后押金将自动发起退款,预计0-7个工作日到账。',
faq5Question: '设备无法正常使用怎么办?',
faq5Answer: '您可以通过"我的-投诉与建议"提交故障反馈,或直接拨打客服电话处理。',
pauseBillingButton: '暂停计费',
pauseBillingHint: '使用中有疑问可申请暂停计费(具体以订单页为准)'
faq5Answer: '您可以通过"我的-投诉与建议"提交故障反馈,或直接拨打客服电话处理。'
},
settings: {
@@ -504,8 +417,6 @@ export default {
languageSetting: '语言设置',
chinese: '简体中文',
english: 'English',
indonesian: 'Bahasa Indonesia',
languageSwitched: '语言已切换,正在刷新...',
notification: '通知',
privacy: '隐私',
about: '关于',
@@ -588,7 +499,7 @@ export default {
},
join: {
title: '信息咨询',
title: '合作加盟',
cooperationTitle: '合作方式',
contactUs: '联系我们',
phone: '联系电话',
@@ -608,30 +519,10 @@ export default {
agreement: '用户协议',
privacy: '隐私政策',
termsOfService: '服务条款',
termsAndConditions: '条款与细则',
lastUpdate: '最后更新',
applicableToService: '适用于"风电者"共享风扇租借服务',
footerNotice: '如对本协议有疑问,请前往"我的-客服"咨询',
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小时内响应,并尽快协商解决。如协商不成,双方同意将争议提交至服务提供方所在地有管辖权的人民法院通过诉讼方式解决。在争议解决期间,双方应继续履行本协议中无争议的条款。'
footerNoticePolicy: '如对本政策有疑问,请前往"我的-客服"咨询'
},
search: {
@@ -675,8 +566,7 @@ export default {
tomorrow: '明天',
hours: '小时',
minutes: '分钟',
halfHours: '半小时',
lessThan: '小于'
halfHours: '半小时'
},
unit: {
@@ -751,8 +641,6 @@ export default {
withdrawNotice2: '提现申请提交后预计0-7个工作日到账',
withdrawNotice3: '如超时未收到,请联系客服处理',
depositRecord: '押金记录',
payRecord: '支付记录',
refundRecord: '退还记录',
orderNotReturned: '当前订单尚未归还,请归还后再提现',
alreadyRefunded: '押金已退还,无需重复提现',
refundProcessing: '押金退还处理中,请耐心等待'
@@ -776,104 +664,7 @@ 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: '定制详情',
productName: '风电者共享风扇 + 充电宝 + 暖手宝系列-樱花粉',
perUnit: '/个',
buyNow: '立即购买',
productDetail: '定制详情',
features: {
battery: '8000Ahm',
batteryDesc: '大容量电池',
wind: '高效风扇',
temp: '智能控温',
charge: '快速充电'
},
description: '风电者共享风扇,集风扇、充电宝、暖手宝三合一功能。采用8000mAh大容量电池,续航持久。高效风扇设计,三档风力可调。智能控温暖手宝,冬暖夏凉。快速充电技术,支持多设备充电。樱花粉配色,时尚美观,是您出行的最佳伴侣。',
confirmPurchase: '确认购买',
confirmPurchaseContent: '确认购买该商品,需支付 ¥{price}?',
purchaseSuccess: '购买成功',
purchaseFailed: '购买失败',
processing: '正在处理...'
}
}
+29 -30
View File
@@ -4,34 +4,11 @@ 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'
// 初始化 console 控制
initConsoleControl()
// 检测是否为 H5 环境
const isH5Platform = () => {
try {
const systemInfo = uni.getSystemInfoSync()
return systemInfo.platform === 'web' || systemInfo.uniPlatform === 'web' ||
(typeof window !== 'undefined' && typeof document !== 'undefined')
} catch (e) {
// 如果获取系统信息失败,尝试通过全局对象判断
return typeof window !== 'undefined' && typeof document !== 'undefined'
}
}
// 获取系统语言
const getSystemLanguage = () => {
// H5 环境默认使用印尼语
if (isH5Platform()) {
return 'id-ID'
}
// 非 H5 环境根据系统语言判断
let language = 'en-US'
let language = 'zh-CN'
try {
const systemInfo = uni.getSystemInfoSync()
if (systemInfo && systemInfo.language) {
@@ -41,8 +18,6 @@ const getSystemLanguage = () => {
}
} catch (e) {
console.error('获取系统语言失败:', e)
// 默认使用中文
language = 'zh-CN'
}
return language
}
@@ -59,8 +34,7 @@ const getSavedLanguage = () => {
return systemLang
} catch (e) {
console.error('语言设置出错:', e)
// 出错时根据平台返回默认语言
return isH5Platform() ? 'id-ID' : 'zh-CN'
return 'zh-CN'
}
}
@@ -71,6 +45,10 @@ 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) {
@@ -81,30 +59,44 @@ 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,
'id-ID': idID
'en-US': enUS
},
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
}
export function createApp() {
console.log('========================================')
console.log('=== createApp 被调用 ===')
console.log('时间戳:', new Date().toLocaleTimeString())
console.log('========================================')
const app = createSSRApp(App)
@@ -134,6 +126,13 @@ 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
}
+8 -35
View File
@@ -43,39 +43,26 @@
"ios" : {},
"sdkConfigs" : {
"maps" : {
"amap" : {
"appkey_ios" : "4c513a688938fd89b88b296e867f66ec",
"appkey_android" : "4c513a688938fd89b88b296e867f66ec"
"qqmap" : {
"appkey_ios" : "RO5BZ-ECZ63-7US3C-RT5QW-TIDZE-2FF35",
"appkey_android" : "RO5BZ-ECZ63-7US3C-RT5QW-TIDZE-2FF35"
}
}
}
}
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "wx2165f0be356ae7a9",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true,
"permission" : {
"scope.getPhoneNumber" : {
"desc" : "您的手机号将用于登录和订单服务"
},
"scope.userLocation" : {
"desc" : "您的位置信息将用于获取附近的设备"
}
},
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
},
"mp-alipay" : {
"component2": true,
"transpile" : [ "uview-ui", "vue-i18n" ],
"skia" : true,
"usingComponents" : true,
"appid" : "2021006117693332",
"unipush" : {
"enable" : false
},
"permission" : {
"scope.userLocation" : {
"desc" : "您的位置信息将用于获取附近的设备"
}
}
},
"mp-baidu" : {
@@ -84,20 +71,6 @@
"mp-toutiao" : {
"usingComponents" : true
},
"h5" : {
"sdkConfigs" : {
"maps" : {
"qqmap" : {
"key" : "DJQBZ-WB53Q-WPS5B-4S6J7-53RMS-X4FJ2"
}
}
},
"router" : {
"mode" : "history",
"base" : "/"
},
"title" : "FDZPower"
},
"uniStatistics" : {
"enable" : false
},
-1
View File
@@ -3,7 +3,6 @@
"@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"
+186 -320
View File
@@ -5,8 +5,7 @@
"^uv-(.*)": "@climblee/uv-ui/components/uv-$1/uv-$1.vue"
}
},
"pages": [
{
"pages": [{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "",
@@ -16,6 +15,147 @@
"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": {
@@ -23,15 +163,23 @@
}
},
{
"path": "pages/scan/index",
"path": "pages/return/index",
"style": {
"navigationBarTitleText": "扫码使用",
"navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "white"
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/device/detail",
"path": "pages/order/success",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/order/return-success",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
@@ -46,6 +194,21 @@
"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": {
@@ -54,6 +217,22 @@
"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": {
@@ -61,319 +240,6 @@
"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
@@ -0,0 +1,341 @@
<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>
+91 -293
View File
@@ -1,25 +1,16 @@
<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/device_location.png" mode="aspectFit" class="location-icon" lazy-load="true"></image>
<image src="/static/images/location-map.svg" mode="aspectFit" class="location-icon"></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>
@@ -78,26 +69,15 @@
</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">
<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>
<button class="rent-button" :class="{ 'return-button': hasActiveOrder }"
@click="handleRent('alipay-score-pay')">
<text>{{ hasActiveOrder ? $t('order.returnDevice') : $t('device.rentDepositFree') }}</text>
</button>
<view class="alipay-credit">
<image src="/static/images/alipay.svg" mode="aspectFit" class="alipay-icon"></image>
<text class="credit-text">{{ $t('device.alipayScoreDesc') }}</text>
</view>
</view>
@@ -122,7 +102,6 @@
</view>
</view>
</view>
</view>
</template>
<script setup>
@@ -132,8 +111,7 @@
onMounted
} from 'vue'
import {
onLoad,
onUnload
onLoad
} from '@dcloudio/uni-app'
import {
getDeviceInfo,
@@ -142,26 +120,22 @@
import {
getOrderByOrderNoScore,
getOrderByOrderNo,
cancelOrder,
getInUseOrder,
getUnpaidOrder
cancelOrder
} from '@/config/api/order.js'
import {
initiateWeChatScorePayment,
initiateAlipayPayment,
getUserInfo,
getUserPhoneNumber
} from '@/util/index.js'
import {
useI18n
} 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({})
@@ -169,38 +143,15 @@
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)
@@ -213,43 +164,12 @@
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()
@@ -273,7 +193,7 @@
// 用户拒绝授权的情况
if (e.detail.errMsg && e.detail.errMsg.includes('deny')) {
uni.showToast({
title: t('auth.phoneRequired'),
title: $t('auth.phoneRequired'),
icon: 'none'
})
return
@@ -281,9 +201,9 @@
// 获取到授权code
if (e.detail.code) {
// uni.showLoading({
// title: t('auth.getting')
// })
uni.showLoading({
title: $t('auth.getting')
})
console.log('获取到的授权code:', e.detail.code)
@@ -292,7 +212,7 @@
getUserPhoneNumber(e.detail.code)
.then(res => {
console.log('获取手机号API响应原始数据:', JSON.stringify(res))
// uni.hideLoading()
uni.hideLoading()
// 不立即抛出错误,而是记录问题并继续处理
if (!res) {
@@ -314,43 +234,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')}`,
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,
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),
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'
})
}
@@ -358,9 +278,6 @@
// 检查登录状态和订单
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 || {}
@@ -381,10 +298,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'
}
}
@@ -397,16 +314,7 @@
maxHourPrice: '5.00',
}
}
}else{
// uni.reLaunch({
// url:'/pages/index/index'
// })
}
}catch(error){
console.error('获取设备信息失败:', error)
}
finally {
}
}
@@ -414,61 +322,51 @@
// 显示登录提示
const showLoginTip = () => {
uni.showModal({
title: t('common.tips'),
content: t('common.loginRequired'),
confirmText: t('auth.goToLogin'),
title: $t('common.tips'),
content: $t('common.loginRequired'),
confirmText: $t('auth.goToLogin'),
success: (res) => {
if (res.confirm) {
uni.navigateTo({
url: '/subPackages/user/login/index'
url: '/pages/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 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
}
const result = await uni.$api.checkActiveOrder()
// 检查是否有待支付的订单
const unpaidRes = await getUnpaidOrder()
if (unpaidRes && unpaidRes.code === 200 && unpaidRes.data) {
const order = unpaidRes.data
if (result.hasOrder) {
const order = result.order // 假设后端返回 order 对象
// 检查订单状态
if (order.status === 'waiting_for_payment') {
// 跳转支付页面,带上订单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 = () => {
const handleRent = (payWay) => {
if (!isLoggedIn.value) {
showLoginTip()
return
@@ -480,21 +378,8 @@
return
}
// 根据运行环境选择不同的租借/支付流程
// 微信小程序:走微信支付分免押租借
if (isWechatMiniProgram.value) {
submitRentOrder('wx-score-pay')
return
}
// 支付宝小程序:走押金租借,后续在支付页内调起支付宝支付
if (isAlipayMiniProgram.value) {
submitRentOrder('wx-pay')
return
}
// H5 等其他环境:统一走押金租借,支付页内根据平台选择支付方式(Antom 等)
submitRentOrder('wx-pay')
// 提交订单
submitRentOrder(payWay)
}
// 获取价格单位文本
@@ -507,7 +392,7 @@
return '30分钟'
}
// 按小时计费(默认)
return t('time.hour')
return $t('time.hour')
}
// 计算计费单位时间(分钟)
@@ -575,67 +460,27 @@
return `不足${unitMinutes}分钟按${unitMinutes}分钟计费,封顶${depositAmount}元,持续计费至${depositAmount}元视为买断`
}
// 获取租借按钮文本
const getRentButtonText = () => {
if (isWechatMiniProgram.value) {
return t('device.rentDepositFree')
} else {
return t('device.rentNow')
}
}
// 提交租借订单
const submitRentOrder = async (payWay) => {
try {
uni.showLoading({
title: t('common.processing')
title: $t('common.processing')
})
// --- 第一步:先请求订阅消息(必须在用户点击的同步上下文中)---
if (payWay === 'wx-score-pay') {
console.log('准备请求订阅消息(在异步操作之前),时间:', new Date().toLocaleTimeString());
try {
await new Promise((resolve, reject) => {
uni.requestSubscribeMessage({
tmplIds: ['o7OMTIcHnFBR7mvsggxFtdt8FfIgSl-v0swVUefGx6w'],
success: (subscribeRes) => {
console.log('订阅消息success回调,时间:', new Date()
.toLocaleTimeString(), subscribeRes);
resolve(subscribeRes);
},
fail: (subscribeErr) => {
console.log('订阅消息fail回调,时间:', new Date().toLocaleTimeString(),
subscribeErr);
// 订阅失败不影响主流程
resolve(subscribeErr);
}
});
});
console.log('订阅消息完成,时间:', new Date().toLocaleTimeString());
} catch (subscribeError) {
console.log('订阅消息异常', subscribeError);
}
}
// --- 订阅消息请求完成 ---
// --- 支付宝小程序不需要订阅消息,移除相关代码 ---
// 支付宝小程序使用消息推送,不需要订阅消息
console.log(deviceId.value);
// 调用设备租借接口
const rentResult = await rentPowerBank(deviceId.value, phoneNumber.value,payWay.value)
const rentResult = await rentPowerBank(deviceId.value, phoneNumber.value)
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') {
if (payWay == 'alipay-pay') {
// 当支付方式为押金支付时
uni.hideLoading()
const res = await getOrderByOrderNo(order.orderNo);
@@ -647,36 +492,35 @@
// 跳转到订单支付页面
uni.redirectTo({
url: `/subPackages/order/payment?orderId=${order.orderId}&packagePrice=${packagePrice}&totalAmount=${totalAmount}&depositAmount=${deposit}${deviceInfo.value && deviceInfo.value.feeConfig ? '&feeConfig=' + encodeURIComponent(deviceInfo.value.feeConfig) : ''}`
url: `/pages/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') {
// 当支付方式为支付支付时
} else if (payWay == 'alipay-score-pay') {
// 当支付方式为支付宝信用免押支付时
uni.hideLoading()
// 获取支付所需参数
// 获取支付宝信用免押所需参数
const res = await getOrderByOrderNoScore(order.orderNo);
uni.hideLoading()
if (res && res.code === 200) {
try {
// 调用微信支付分小程序
const payResult = await initiateWeChatScorePayment(res);
console.log('支付调用结果', payResult);
// 调用支付宝信用免押小程序
const payResult = await initiateAlipayPayment(res);
console.log('支付宝信用免押调用结果', payResult);
// 成功则跳转到等待页面
if (payResult.errCode == '0' && payResult.extraData && Object.keys(payResult.extraData)
.length > 0) {
console.log('支付分授权成功,准备跳转到等待页,时间:', new Date().toLocaleTimeString());
// 跳转到等待页面(订阅消息已经在前面完成了)
if (payResult && payResult.success !== false) {
console.log('支付宝信用免押授权成功,准备跳转到等待页,时间:', new Date().toLocaleTimeString());
// 跳转到等待页面
uni.redirectTo({
url: `/pages/waiting/index?orderNo=${order.orderNo}&orderId=${order.orderId}&deviceId=${deviceId.value}`
});
return;
} else {
console.log('支付未完成授权或用户取消extraData:', payResult.extraData);
console.log('支付宝信用免押未完成授权或用户取消:', payResult);
// 用户取消授权,需要取消订单
try {
uni.showLoading({
title: t('order.cancelling')
title: $t('order.cancelling')
});
const cancelRes = await cancelOrder({
orderId: order.orderNo
@@ -685,7 +529,7 @@
uni.hideLoading();
uni.showToast({
title: t('order.orderCancelled'),
title: $t('order.orderCancelled'),
icon: 'none',
duration: 2000
});
@@ -700,7 +544,7 @@
console.error('取消订单失败:', cancelError);
uni.hideLoading();
uni.showToast({
title: t('order.cancelFailedContactService'),
title: $t('order.cancelFailedContactService'),
icon: 'none'
});
}
@@ -711,7 +555,7 @@
// 支付分调用异常,也需要取消订单
try {
uni.showLoading({
title: t('order.cancelling')
title: $t('order.cancelling')
});
const cancelRes = await cancelOrder({
orderId: order.orderNo
@@ -724,7 +568,7 @@
}
uni.showToast({
title: t('device.payScoreFailedCancelled'),
title: $t('device.payScoreFailedCancelled'),
icon: 'none'
});
@@ -736,7 +580,7 @@
}
} else {
uni.showToast({
title: res?.msg || t('device.getPayParamsFailed'),
title: res?.msg || $t('device.getPayParamsFailed'),
icon: 'none'
});
}
@@ -744,7 +588,7 @@
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || t('device.rentFailedRetry'),
title: error.message || $t('device.rentFailedRetry'),
icon: 'none'
})
}
@@ -795,15 +639,15 @@
align-items: center;
.location-icon {
width: 32rpx;
height: 32rpx;
width: 40rpx;
height: 40rpx;
margin-right: 12rpx;
// background-color: #10d673;
background-color: #10d673;
border-radius: 50%;
}
.location-name {
font-size: 28rpx;
font-size: 32rpx;
color: #333;
font-weight: 500;
}
@@ -812,7 +656,7 @@
.device-status {
padding: 8rpx 24rpx;
border-radius: 30rpx;
font-size: 22rpx;
font-size: 24rpx;
&.available {
background-color: #d4f4dd;
@@ -839,10 +683,9 @@
.device-id {
display: flex;
align-items: center;
margin-bottom: 5rpx;
.id-label {
font-size: 26rpx;
font-size: 28rpx;
color: #999;
}
@@ -856,7 +699,7 @@
// 计费规则卡片
.pricing-card {
.pricing-banner {
background: #E6F7EC;
background: linear-gradient(135deg, #e8f5e9, #c8e6c9);
border-radius: 20rpx;
padding: 40rpx 30rpx;
margin-bottom: 24rpx;
@@ -870,21 +713,21 @@
margin-bottom: 16rpx;
.price-symbol {
font-size: 36rpx;
font-size: 48rpx;
font-weight: bold;
color: #07c160;
margin-right: 4rpx;
}
.price {
font-size: 64rpx;
font-size: 80rpx;
font-weight: bold;
color: #07c160;
line-height: 1;
}
.unit {
font-size: 28rpx;
font-size: 32rpx;
color: #07c160;
margin-left: 8rpx;
}
@@ -892,11 +735,11 @@
.cap-badge {
background-color: #07c160;
padding: 10rpx 28rpx;
padding: 10rpx 32rpx;
border-radius: 30rpx;
line-height: 1;
.cap-text {
font-size: 24rpx;
font-size: 26rpx;
color: #fff;
font-weight: 500;
}
@@ -968,51 +811,6 @@
}
}
// 促销提示框
.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;
@@ -1053,13 +851,13 @@
}
}
.wechat-credit {
.alipay-credit {
display: flex;
align-items: center;
justify-content: center;
margin-top: 16rpx;
.wx-icon {
.alipay-icon {
width: 48rpx;
height: 38rpx;
margin-right: 8rpx;
@@ -62,11 +62,11 @@
} from '@/config/api/expressReturn.js'
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
const { t: $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 {
@@ -166,10 +166,10 @@
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'),
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 ? t('express.fillSuccess') : t('express.submitSuccess'),
title: isFillMode.value ? '补填成功' : '提交成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 800)
} else {
throw new Error(res?.msg || (isFillMode.value ? t('express.fillFailed') : t('express.submitFailed')))
throw new Error(res?.msg || (isFillMode.value ? '补填失败' : '提交失败'))
}
} catch (e) {
uni.showToast({
title: e.message || (isFillMode.value ? t('express.fillFailed') : t('express.submitFailed')),
title: e.message || (isFillMode.value ? '补填失败' : '提交失败'),
icon: 'none'
})
} finally {
@@ -91,7 +91,7 @@ import { getExpressReturnDetail } from '@/config/api/expressReturn.js'
import { getCustomerPhone } from '@/util/index.js'
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
const { t: $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'
})
}
@@ -165,10 +165,10 @@ 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'),
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 } = useI18n()
const { t: $t } = useI18n()
const returnList = ref([])
const loading = ref(false)
@@ -62,7 +62,7 @@ const query = ref({ pageNum: 1, pageSize: 20 })
onMounted(() => {
uni.setNavigationBarTitle({
title: t('express.returnRecord')
title: $t('express.returnRecord')
})
loadList()
})
@@ -80,12 +80,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 || 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'),
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 || '待填写',
status: mapStatus(r.status),
rawStatus: r.status,
userPhone: r.userPhone,
@@ -93,10 +93,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,33 +118,34 @@ 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')}${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 || '-';
};
@@ -306,7 +306,7 @@
//
const getImageList = (item) => {
if (!item) return [];
const pictureSource = item.pictureUrls ?? item.picturePath;
const pictureSource = item.pictureUrls != null ? item.pictureUrls : item.picturePath;
if (!pictureSource) return [];
if (Array.isArray(pictureSource)) {
return pictureSource.filter(img => !!img);
@@ -72,37 +72,34 @@
} 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: '/subPackages/service/feedback/list'
url: '/pages/feedback/list'
})
}
onMounted(() => {
uni.setNavigationBarTitle({
title: t('feedback.title')
title: $t('feedback.title')
})
})
onLoad((options) => {
onLoad(() => {
if (uni.getStorageSync("userInfo").phone) {
contact.value = uni.getStorageSync('userInfo').phone;
}
if(options.selectedType) {
selectedType.value = parseInt(options.selectedType);
}
})
//
@@ -137,7 +134,7 @@
const submitFeedback = async () => {
if (selectedType.value === -1) {
uni.showToast({
title: t('feedback.pleaseSelectType'),
title: $t('feedback.pleaseSelectType'),
icon: 'none'
})
return
@@ -145,7 +142,7 @@
if (!description.value.trim()) {
uni.showToast({
title: t('feedback.pleaseDescribe'),
title: $t('feedback.pleaseDescribe'),
icon: 'none'
})
return
@@ -153,7 +150,7 @@
if (!contact.value) {
uni.showToast({
title: t('feedback.pleaseContact'),
title: $t('feedback.pleaseContact'),
icon: 'none'
})
return
@@ -169,7 +166,7 @@
try {
//
uni.showLoading({
title: t('feedback.uploading') || '上传中...',
title: $t('feedback.uploading') || '上传中...',
mask: true
})
@@ -185,7 +182,7 @@
console.error(`文件 ${i + 1} 上传失败:`, err)
uni.hideLoading()
uni.showToast({
title: t('feedback.imageUploadFailed'),
title: $t('feedback.imageUploadFailed'),
icon: 'none'
})
return
@@ -208,7 +205,7 @@
//
if (res && (res.code === 200 || res === true || res?.success === true)) {
uni.showToast({
title: t('feedback.submitSuccess'),
title: $t('feedback.submitSuccess'),
icon: 'success'
})
setTimeout(() => {
@@ -216,7 +213,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'
})
}
@@ -224,7 +221,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: `/subPackages/service/feedback/detail?id=${item.id || item.feedbackId}`
url: `/pages/feedback/detail?id=${item.id || item.feedbackId}`
});
};
</script>
+147
View File
@@ -0,0 +1,147 @@
<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>
+181 -725
View File
File diff suppressed because it is too large Load Diff
@@ -12,7 +12,7 @@
} from 'vue'
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
const { t: $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)
})
@@ -11,7 +11,7 @@
<view class="p">Before using {{ brandName }}, please carefully read and fully understand all contents of this Agreement, especially the terms highlighted in bold (including but not limited to liability limitations, dispute resolution, applicable law, protection of minors, etc.). By clicking "Login/Use" or actually using the service, you are deemed to have read and agreed to be bound by this Agreement.</view>
<view class="h1">II. Account and Login</view>
<view class="p">2.1 You can log in and use this service through WeChat authorization. To complete deposit-free rental and order settlement, you agree that we conduct credit assessment and post-order settlement based on WeChat Payment Score.</view>
<view class="p">2.1 You can log in and use this service through Alipay authorization. To complete deposit-free rental and order settlement, you agree that we conduct credit assessment and post-order settlement based on Sesame Credit.</view>
<view class="p">2.2 You should ensure that the information provided is true, accurate, and complete, and update it in a timely manner. Any service restrictions, order abnormalities, or losses caused by untrue information or failure to update in time shall be borne by you.</view>
<view class="p">2.3 You shall be responsible for all activities under the account, properly keep the device and account credentials, and shall not lend, rent, or otherwise provide them to others.</view>
@@ -20,10 +20,10 @@
<view class="p">3.2 Usage specifications: Please use the device properly, avoid water ingress, dropping, unauthorized disassembly or modification; do not approach open flames and high-temperature environments; avoid outdoor use in rainy days; children should use under supervision.</view>
<view class="p">3.3 Prohibited behaviors: Using the device for illegal or improper purposes; affecting the normal operation of the device or system in any way; circumventing billing or return processes through abnormal means.</view>
<view class="h1">IV. Billing and Settlement (Including WeChat Payment Score)</view>
<view class="h1">IV. Billing and Settlement (Including Sesame Credit)</view>
<view class="p"><text class="bold">4.1 Billing rules</text>: Subject to the real-time billing rules displayed in the mini program, which may include duration billing, capped prices, service fees, etc. Charges will be based on this after order generation.</view>
<view class="p"><text class="bold">4.2 WeChat Payment Score deposit-free</text>: If you activate and pass the credit assessment, you can enjoy deposit-free rental; if the assessment fails, pre-authorization or deposit may be required. Please refer to the page prompts for details.</view>
<view class="p">4.3 Settlement and deduction: After the order ends, we will complete the settlement based on actual usage and platform rules, and deduct through WeChat Payment Score/WeChat Pay.</view>
<view class="p"><text class="bold">4.2 Sesame Credit deposit-free</text>: If you activate and pass the credit assessment, you can enjoy deposit-free rental; if the assessment fails, pre-authorization or deposit may be required. Please refer to the page prompts for details.</view>
<view class="p">4.3 Settlement and deduction: After the order ends, we will complete the settlement based on actual usage and platform rules, and deduct through Sesame Credit/Alipay.</view>
<view class="p">4.4 Exceptions and disputes: If you have any objection to billing or settlement, please submit it through "My-Customer Service" within 48 hours after order completion; overdue submissions may affect processing results.</view>
<view class="h1">V. Device Return and Overdue Handling</view>
@@ -49,7 +49,7 @@
<view class="p">9.2 You should be responsible for your own use. Any losses caused by your violation of this Agreement or improper storage and use of equipment shall be borne by you or compensated to relevant parties.</view>
<view class="h1">X. Privacy and Personal Information Protection</view>
<view class="p">10.1 We strictly handle your personal information in accordance with the Privacy Policy, including WeChat login information, mobile phone number (obtained after your authorization), device and order information, location and outlet information, etc.</view>
<view class="p">10.1 We strictly handle your personal information in accordance with the Privacy Policy, including Alipay login information, mobile phone number (obtained after your authorization), device and order information, location and outlet information, etc.</view>
<view class="p">10.2 For details, please refer to the Privacy Policy in this mini program.</view>
<view class="h1">XI. Service Changes and Termination</view>
@@ -11,7 +11,7 @@
<view class="p">在使用{{ brandName }}请您务必仔细阅读并充分理解本协议全部内容尤其是以加粗方式提示的条款包括但不限于责任限制争议解决适用法律未成年人保护等您点击"登录/使用"或实际使用服务即视为您已阅读并同意受本协议约束</view>
<view class="h1">账号与登录</view>
<view class="p">2.1 您可通过微信授权登录使用本服务为完成免押租借与订单结算您同意我们基于微信支付分进行信用评估及订单后结等必要处理</view>
<view class="p">2.1 您可通过支付宝授权登录使用本服务为完成免押租借与订单结算您同意我们基于芝麻信用进行信用评估及订单后结等必要处理</view>
<view class="p">2.2 您应保证提供信息真实准确完整并及时更新因您提供的信息不真实或未及时更新导致的服务受限订单异常或损失由您自行承担</view>
<view class="p">2.3 您应对账户下的全部行为负责妥善保管设备与账户凭证不得转借出租或以其他方式提供给他人使用</view>
@@ -20,10 +20,10 @@
<view class="p">3.2 使用规范请合理使用设备避免进水摔落私自拆卸或改装请勿靠近明火与高温环境室外雨天请避免使用儿童应在监护下使用</view>
<view class="p">3.3 禁止行为将设备用于违法或不当用途以任何方式影响设备或系统的正常运行通过非正常手段规避计费或归还流程</view>
<view class="h1">计费与结算微信支付分</view>
<view class="h1">计费与结算芝麻信用</view>
<view class="p"><text class="bold">4.1 计费规则</text>以小程序展示的实时计费规则为准可能包含时长计费封顶价服务费等订单生成后将据此计费</view>
<view class="p"><text class="bold">4.2 微信支付分免押</text>若您开通并通过信用评估可享受免押租借如评估未通过可能需预授权或押金具体以页面提示为准</view>
<view class="p">4.3 结算与扣款订单结束后我们将基于实际使用情况与平台规则完成结算并通过微信支付分/微信支付进行扣款</view>
<view class="p"><text class="bold">4.2 芝麻信用免押</text>若您开通并通过信用评估可享受免押租借如评估未通过可能需预授权或押金具体以页面提示为准</view>
<view class="p">4.3 结算与扣款订单结束后我们将基于实际使用情况与平台规则完成结算并通过芝麻信用/支付宝支付进行扣款</view>
<view class="p">4.4 异常与争议如对计费或结算有异议请在订单完成后48小时内通过"我的-客服"提交逾期可能影响处理结果</view>
<view class="h1">设备归还与逾期处理</view>
@@ -36,9 +36,9 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useI18n } from '@/utils/i18n.js'
import { getCurrentAgreement } from '@/config/api/system.js'
import { URL } from '@/config/url.js'
const { t } = useI18n()
const { t: $t } = useI18n()
//
const loading = ref(true)
@@ -50,6 +50,13 @@
remark: ''
})
//
const convertLanguageCode = (lang) => {
// zh-CN -> zh-CN ()
// en-US -> en_US (线)
return lang.replace(/-/g, '_')
}
//
const loadAgreement = async () => {
loading.value = true
@@ -57,22 +64,35 @@
errorMessage.value = ''
try {
console.log('加载用户协议')
//
const currentLang = uni.getStorageSync('language') || 'zh-CN'
const languageCode = convertLanguageCode(currentLang)
console.log('加载用户协议,语言:', currentLang, '转换后:', languageCode)
//
const res = await getCurrentAgreement({
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: {
agreementCode: 'USER_AGREEMENT',
appPlatform: 'wechat',
appType: 'user'
}
})
console.log('用户协议响应:', res)
if (res && res.code === 200 && res.data) {
if (res.statusCode === 200 && res.data.code === 200 && res.data.data) {
agreementData.value = {
title: res.data.title || t('legal.agreement'),
content: res.data.content || '',
remark: res.data.remark || ''
title: res.data.data.title || $t('legal.agreement'),
content: res.data.data.content || '',
remark: res.data.data.remark || ''
}
//
@@ -80,12 +100,12 @@
title: agreementData.value.title
})
} else {
throw new Error(res?.msg || t('common.loadFailed'))
throw new Error(res.data.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
}
@@ -16,18 +16,18 @@
</view>
<view class="h1">II. Information We Collect</view>
<view class="p">2.1 Account information: WeChat login identifier (such as openId/unionId), nickname and avatar (with your authorization), mobile phone number (obtained through WeChat after your authorization).</view>
<view class="p">2.1 Account information: Alipay login identifier (such as userId), nickname and avatar (with your authorization), mobile phone number (obtained through Alipay after your authorization).</view>
<view class="p">2.2 Order and device information: rental records, usage duration, fees, return points, device status, abnormal records, etc.</view>
<view class="p">2.3 Location and outlet information: used to find nearby outlets and navigation after your authorization, and will not be obtained without authorization.</view>
<view class="p">2.4 Log information: To ensure service security and stability, we may record operation logs, network requests, and error information.</view>
<view class="h1">III. Purpose of Information Use</view>
<view class="p">3.1 Provide core functions: identity verification, deposit-free rental (WeChat Payment Score assessment), order billing and settlement, customer service and after-sales.</view>
<view class="p">3.1 Provide core functions: identity verification, deposit-free rental (Sesame Credit assessment), order billing and settlement, customer service and after-sales.</view>
<view class="p">3.2 Security risk control: prevent fraud, violations and risk control; ensure system and device security.</view>
<view class="p">3.3 Product optimization: statistics and analysis to improve experience (conducted after de-identification/anonymization).</view>
<view class="h1">IV. WeChat Payment Score and Payment</view>
<view class="p">4.1 To implement deposit-free rental, we will conduct necessary data interaction with WeChat Payment Score (such as credit assessment results and order settlement). Related data processing follows the rules of WeChat Pay and WeChat Payment Score.</view>
<view class="h1">IV. Sesame Credit and Payment</view>
<view class="p">4.1 To implement deposit-free rental, we will conduct necessary data interaction with Sesame Credit (such as credit assessment results and order settlement). Related data processing follows the rules of Alipay and Sesame Credit.</view>
<view class="p">4.2 If you fail the assessment, pre-authorization or deposit processing may be required, subject to page prompts.</view>
<view class="h1">V. Sharing, Transfer and Public Disclosure</view>
@@ -22,12 +22,12 @@
<view class="p">2.4 日志信息为保障服务安全与稳定我们可能记录操作日志网络请求与错误信息</view>
<view class="h1">信息使用目的</view>
<view class="p">3.1 提供核心功能身份验证免押租借微信支付分评估订单计费结算客服与售后</view>
<view class="p">3.1 提供核心功能身份验证免押租借芝麻信用评估订单计费结算客服与售后</view>
<view class="p">3.2 安全风控防范欺诈违规与风险控制保障系统与设备安全</view>
<view class="p">3.3 产品优化统计与分析以改进体验在去标识化/匿名化后进行</view>
<view class="h1">微信支付分与支付</view>
<view class="p">4.1 为实现免押租借我们将与微信支付分进行必要的数据交互如信用评估结果订单结算相关数据处理遵循微信支付与微信支付分的规则</view>
<view class="h1">芝麻信用与支付</view>
<view class="p">4.1 为实现免押租借我们将与芝麻信用进行必要的数据交互如信用评估结果订单结算相关数据处理遵循支付宝与芝麻信用的规则</view>
<view class="p">4.2 如您未通过评估可能需进行预授权或押金处理以页面提示为准</view>
<view class="h1">共享转移与公开披露</view>
@@ -36,9 +36,9 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useI18n } from '@/utils/i18n.js'
import { getCurrentAgreement } from '@/config/api/system.js'
import { URL } from '@/config/url.js'
const { t } = useI18n()
const { t: $t } = useI18n()
//
const loading = ref(true)
@@ -50,6 +50,13 @@
remark: ''
})
//
const convertLanguageCode = (lang) => {
// zh-CN -> zh-CN ()
// en-US -> en_US (线)
return lang.replace(/-/g, '_')
}
//
const loadAgreement = async () => {
loading.value = true
@@ -57,22 +64,35 @@
errorMessage.value = ''
try {
console.log('加载隐私政策')
//
const currentLang = uni.getStorageSync('language') || 'zh-CN'
const languageCode = convertLanguageCode(currentLang)
console.log('加载隐私政策,语言:', currentLang, '转换后:', languageCode)
//
const res = await getCurrentAgreement({
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: {
agreementCode: 'PRIVACY_POLICY',
appPlatform: 'wechat',
appType: 'user'
}
})
console.log('隐私政策响应:', res)
if (res && res.code === 200 && res.data) {
if (res.statusCode === 200 && res.data.code === 200 && res.data.data) {
agreementData.value = {
title: res.data.title || t('legal.privacy'),
content: res.data.content || '',
remark: res.data.remark || ''
title: res.data.data.title || $t('legal.privacy'),
content: res.data.data.content || '',
remark: res.data.data.remark || ''
}
//
@@ -80,12 +100,12 @@
title: agreementData.value.title
})
} else {
throw new Error(res?.msg || t('common.loadFailed'))
throw new Error(res.data.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
}
@@ -8,32 +8,16 @@
<view class="title">{{ $t('auth.loginTitle') }}</view>
<view class="subtitle">{{ $t('auth.loginDesc') }}</view>
<!-- 微信小程序一键手机号快捷登录 -->
<!-- #ifdef MP-WEIXIN -->
<!-- 支付宝一键手机号快捷登录推荐 -->
<button v-if="!isAgreed" class="btn primary" @click="handleLoginClick">
{{ $t('auth.getPhoneNumber') }}
</button>
<button v-else class="btn primary" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">
<button v-else class="btn primary" open-type="getAuthorize" scope="phoneNumber" @getAuthorize="onGetPhoneNumber">
{{ $t('auth.getPhoneNumber') }}
</button>
<!-- #endif -->
<!-- 支付宝小程序授权码快捷登录不支持 open-type=getPhoneNumber -->
<!-- #ifdef MP-ALIPAY -->
<button v-if="!isAgreed" class="btn primary" @click="handleLoginClick">
{{ $t('auth.loginBtn') }}
</button>
<button v-else class="btn primary" @click="onAlipayLogin">
{{ $t('auth.loginBtn') }}
</button>
<!-- #endif -->
<!-- H5不显示小程序快捷登录按钮 -->
<!-- 手机号验证码登录 -->
<button class="btn outline" @click="goToPhoneLogin" v-if="isHTML5">
{{ $t('auth.phoneLogin') }}
</button>
<!-- 支付宝登录不授权手机号时使用 -->
<!-- <button class="btn outline" @click="onAlipayLogin">仅支付宝登录</button> -->
<view class="agreement-box">
<checkbox-group @change="onAgreementChange">
@@ -41,9 +25,9 @@
<checkbox value="agreed" :checked="isAgreed" color="#07c160" class="agreement-checkbox" />
<text class="agreement-text">
{{ $t('auth.agreeToTerms') }}
<text class="link" @tap.stop="go('/subPackages/other/legal/agreement')">{{ $t('user.userAgreement') }}</text>
<text class="link" @tap.stop="go('/pages/legal/agreement')">{{ $t('user.userAgreement') }}</text>
{{ $t('common.and') }}
<text class="link" @tap.stop="go('/subPackages/other/legal/privacy')">{{ $t('user.privacyPolicy') }}</text>
<text class="link" @tap.stop="go('/pages/legal/privacy')">{{ $t('user.privacyPolicy') }}</text>
</text>
</label>
</checkbox-group>
@@ -54,26 +38,25 @@
<script setup>
import { ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { wxLogin, alipayLogin, getUserPhoneNumber, getUserInfo } from '@/util/index.js'
import { alipayLogin, getUserPhoneNumber, getUserInfo } from '../../util/index.js'
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
const { t: $t } = useI18n()
//
onMounted(() => {
uni.setNavigationBarTitle({
title: t('auth.loginTitle')
title: $t('auth.loginTitle')
})
})
const isHTML5 = ref(false) // HTML5
const redirect = ref('/pages/index/index')
const isAgreed = ref(false) //
//
const onAgreementChange = (e) => {
isAgreed.value = e.detail.value.includes('agreed')
console.log('协议勾选状态:', isAgreed.value, e.detail.value)
}
//
@@ -96,10 +79,10 @@
//
uni.showModal({
title: t('common.tips'),
content: t('auth.pleaseAgreeToTerms'),
confirmText: t('common.confirm'),
cancelText: t('common.cancel'),
title: $t('common.tips'),
content: $t('auth.pleaseAgreeToTerms'),
confirmText: $t('common.confirm'),
cancelText: $t('common.cancel'),
success: (res) => {
if (res.confirm) {
//
@@ -107,7 +90,7 @@
resolve()
} else {
//
reject(new Error(t('auth.pleaseAgreeToTerms')))
reject(new Error('需要同意协议才能登录'))
}
}
})
@@ -130,51 +113,41 @@
uni.reLaunch({ url: target })
}
// 使
// const onWeChatLogin = async () => {
// try {
// await checkAgreement()
// await wxLogin()
// uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
// await navigateAfterLogin()
// } catch (error) {
// if (error.message !== t('auth.pleaseAgreeToTerms')) {
// uni.showToast({ title: error.message || t('auth.loginFailed'), icon: 'none' })
// }
// }
// }
//
const onAlipayLogin = async () => {
try {
//
await checkAgreement()
await alipayLogin()
uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
uni.showToast({ title: $t('auth.loginSuccess'), icon: 'success' })
await navigateAfterLogin()
} catch (error) {
if (error.message !== t('auth.pleaseAgreeToTerms')) {
uni.showToast({ title: error.message || t('auth.loginFailed'), icon: 'none' })
if (error.message !== '需要同意协议才能登录') {
uni.showToast({ title: error.message || '登录失败', icon: 'none' })
}
}
}
// / my.getPhoneNumber
const onGetPhoneNumber = async (e) => {
if (!e || e.detail.errMsg !== 'getPhoneNumber:ok') {
uni.showToast({ title: t('auth.phoneCancelled'), icon: 'none' })
//
if (!e || !e.detail || !e.detail.response) {
uni.showToast({ title: $t('auth.phoneCancelled'), icon: 'none' })
return
}
console.log(e);
try {
// /app/user/quickLogin/ WECHAT_MINI
await wxLogin(e.detail.code)
// code phone
// await getUserPhoneNumber(e.detail.code)
uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
// token
await alipayLogin()
//
// e.detail.response
const authCode = e.detail.response?.response?.code || e.detail.response?.code
if (authCode) {
await getUserPhoneNumber(authCode)
}
uni.showToast({ title: $t('auth.loginSuccess'), icon: 'success' })
await navigateAfterLogin()
} catch (error) {
uni.showToast({ title: error.message || t('auth.loginFailed'), icon: 'none' })
uni.showToast({ title: error.message || $t('auth.loginFailed'), icon: 'none' })
}
}
@@ -182,29 +155,19 @@
if (opts && opts.redirect) {
try {
redirect.value = decodeURIComponent(opts.redirect)
} catch (err) {
} catch (_) {}
}
}
// #ifdef H5
isHTML5.value = true
// #endif
})
const go = (url) => {
uni.navigateTo({ url })
}
//
const goToPhoneLogin = () => {
uni.navigateTo({ url: '/subPackages/user/login/phone' })
}
</script>
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(180deg, #C8F4D9 0%, #FFFFFF 100%);
background: #f8f8f8;
padding: 80rpx 40rpx 40rpx;
display: flex;
flex-direction: column;
@@ -1,16 +1,15 @@
<template>
<view class="my-page">
<view class="user-card" @click="navigateTo('/subPackages/user/userProfile/index')">
<view class="user-card" @click="navigateTo('/pages/userProfile/index')">
<view class="avatar-box">
<image class="avatar" v-if="userInfo.avatar" :src="userInfo.avatar" mode="aspectFill" lazy-load="true">
</image>
<image v-else class="avatar" src="@/static/head.png" mode="aspectFill" lazy-load="true"></image>
<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>
</view>
<view class="user-text">
<view class="nickname">{{ userInfo.nickName || $t('user.clickToLogin') }}</view>
<view class="subtext">{{ userInfo.phone ? maskPhone(userInfo.phone) : $t('user.loginPrompt') }}</view>
</view>
<uv-icon type="right" size="16" color="#999"></uv-icon>
<uv-icon name="arrow-right" size="16" color="#999"></uv-icon>
</view>
@@ -30,65 +29,48 @@
<swiper class="banner-swiper" :indicator-dots="bannerImages.length > 1"
:autoplay="bannerImages.length > 1" :circular="true" :interval="3000">
<swiper-item v-for="(image, index) in bannerImages" :key="index">
<image class="banner-image" :src="image" mode="aspectFill" @click="handleBannerClick(index)"
lazy-load="true">
</image>
<image class="banner-image" :src="image" mode="aspectFill"
@click="handleBannerClick(index)"></image>
</swiper-item>
</swiper>
</view>
<!-- 默认图片当没有广告时显示 -->
<!-- <view class="banner-card" v-else @click="navigateTo('/pages/join/index')">
<image class="banner-image" src="/static/userCenter_swiper.png" mode="aspectFill" lazy-load="true"></image>
</view> -->
<view class="banner-card" v-else @click="navigateTo('/pages/join/index')">
<image class="banner-image" src="/static/userCenter_swiper.png" mode="aspectFill"></image>
</view>
<!-- <view class="section-title">常用服务</view> -->
<view class="list">
<view class="list-item" @click="handleQuickReturn">
<view class="left">
<image class="icon" src="/static/express_return.png" mode="aspectFit" lazy-load="true"></image>
<text class="title">{{ $t('user.quickReturn') }}<text
style="font-size: 18rpx;">{{ $t('user.quickReturnDesc') }}</text></text>
<image class="icon" src="/static/express_return.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.quickReturn') }}<text style="font-size: 18rpx;">{{ $t('user.quickReturnDesc') }}</text></text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="list-item" @click="navigateTo('/pages/expressReturn/index')" v-if="showMenuItem">
<view class="left">
<image class="icon" src="/static/express.png" mode="aspectFit" lazy-load="true"></image>
<image class="icon" src="/static/express.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.expressReturn') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="list-item" @click="navigateTo('/subPackages/order/index')">
<view class="list-item" @click="navigateTo('/pages/order/index')">
<view class="left">
<image class="icon" src="/static/orderList.png" mode="aspectFit" lazy-load="true"></image>
<image class="icon" src="/static/orderList.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.myOrders') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="list-item" @click="navigateTo('/subPackages/business/my-card')">
<view class="list-item" @click="navigateTo('/pages/help/index')">
<view class="left">
<image class="icon" src="/static/my_member.png" mode="aspectFit" lazy-load="true"></image>
<text class="title">{{ $t('user.myCards') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="list-item" @click="navigateTo('/subPackages/business/my-coupon')">
<view class="left">
<image class="icon" src="/static/my_coupon.png" mode="aspectFit" lazy-load="true"></image>
<text class="title">{{ $t('user.myCoupons') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="list-item" @click="navigateTo('/subPackages/service/help/index')">
<view class="left">
<image class="icon" src="/static/customer-service.png" mode="aspectFit" lazy-load="true">
</image>
<image class="icon" src="/static/customer-service.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.customerService') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="list-item" @click="navigateTo('/subPackages/service/feedback/index')">
<view class="list-item" @click="navigateTo('/pages/feedback/index')">
<view class="left">
<image class="icon" src="/static/suggess.png" mode="aspectFit" lazy-load="true"></image>
<image class="icon" src="/static/suggess.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.feedback') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
@@ -100,18 +82,16 @@
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view> -->
<!-- #ifndef MP-ALIPAY -->
<view class="list-item" @click="navigateTo('/subPackages/other/join/index')">
<view class="list-item" @click="navigateTo('/pages/join/index')">
<view class="left">
<image class="icon" src="/static/peopleInWork.png" mode="aspectFit" lazy-load="true"></image>
<image class="icon" src="/static/peopleInWork.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.cooperation') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<!-- #endif -->
<view class="list-item" @click="navigateTo('/subPackages/user/setting/index')">
<view class="list-item" @click="navigateTo('/pages/setting/index')">
<view class="left">
<image class="icon" src="/static/setting.png" mode="aspectFit" lazy-load="true"></image>
<image class="icon" src="/static/setting.png" mode="aspectFit"></image>
<text class="title">{{ $t('user.settings') }}</text>
</view>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
@@ -121,11 +101,9 @@
<view class="footer-agreements">
<view class="link-box">
<text class="link"
@click="navigateTo('/subPackages/other/legal/agreement')">{{ $t('user.userAgreement') }}</text>
<text class="link" @click="navigateTo('/pages/legal/agreement')">{{ $t('user.userAgreement') }}</text>
<text class="sep"></text>
<text class="link"
@click="navigateTo('/subPackages/other/legal/privacy')">{{ $t('user.privacyPolicy') }}</text>
<text class="link" @click="navigateTo('/pages/legal/privacy')">{{ $t('user.privacyPolicy') }}</text>
</view>
<view class="version">{{ $t('user.version') }}{{ appVersion }}</view>
</view>
@@ -135,7 +113,7 @@
<u-popup ref="authPopup" mode="center" border-radius="15" width="600rpx" @open="onPopupOpen" @close="onPopupClose">
<view class="auth-popup">
<view class="auth-title">授权登录</view>
<view class="auth-desc">获取您的微信头像昵称等公开信息</view>
<view class="auth-desc">获取您的支付宝头像昵称等公开信息</view>
<view class="auth-buttons">
<button class="cancel-btn" @click="closeAuthPopup">取消</button>
<button class="confirm-btn" @click="getUserProfile">确定</button>
@@ -157,26 +135,19 @@
} from '@dcloudio/uni-app';
import {
wxLogin,
alipayLogin,
getUserInfo
} from '@/util/index.js';
} from '../../util/index.js';
import {
uploadUserAvatar
} from '@/config/api/user.js'
} from '../../config/api/user.js'
import {
getCurrentAdvertisement
} from '@/config/api/system.js'
import {
getInUseOrder
} from '@/config/api/order.js'
import {
useI18n
} from '@/utils/i18n.js'
URL
} from '../../config/url.js'
import { useI18n } from '@/utils/i18n.js'
// 退
const {
t
} = useI18n()
const { t: $t } = useI18n()
//
const userInfo = ref({});
@@ -188,35 +159,47 @@
const showMenuItem = ref(false)
const bannerImages = ref([]) // 广
const bannerImageList = ref([]) // 广
//
const convertLanguageCode = (lang) => {
// zh-CN -> zh_CN (线)
// en-US -> en_US (线)
return lang.replace(/-/g, '_')
}
// 广
const getBannerImages = async () => {
try {
let appPlatform;
// #ifdef MP-WEIXIN
appPlatform = 'wechat'
// #endif
// #ifdef MP-ALIPAY
appPlatform = 'ali'
// #endif
//
const currentLang = uni.getStorageSync('language') || 'zh-CN'
const languageCode = convertLanguageCode(currentLang)
console.log('加载个人中心广告,语言:', currentLang, '转换后:', languageCode)
// 广
const res = await getCurrentAdvertisement({
appPlatform: appPlatform, //
appType: 'user', //
pictureLocation: 'userProfile_banner'
const res = await uni.request({
url: `${URL}/device/advertisementConfig/current`,
method: 'GET',
header: {
'Content-Language': languageCode
},
data: {
appPlatform: 'alipay', //
appType: 'user' //
}
})
if (res && res.code === 200 && res.data) {
// 使 imageList
const imageList = res.data.imageList || []
if (imageList.length > 0) {
bannerImageList.value = imageList
// URL
bannerImages.value = imageList.map(item => item.imageUrl)
console.log('个人中心广告响应:', res)
if (res.statusCode === 200 && res.data.code === 200 && res.data.data) {
// 使 files
const files = res.data.data.files || []
if (files.length > 0) {
bannerImages.value = files
console.log('个人中心广告加载成功,图片数量:', files.length)
}
} else {
console.warn('获取个人中心广告失败:', res?.msg || '未知错误')
console.warn('获取个人中心广告失败:', res.data?.msg || '未知错误')
}
} catch (error) {
console.error('获取个人中心广告失败:', error)
@@ -225,54 +208,17 @@
// 广
const handleBannerClick = (index) => {
if (!bannerImageList.value || !bannerImageList.value[index]) {
return
}
const config = bannerImageList.value[index]
//
if (config.linkType === 'miniapp' && config.appId) {
//
// #ifdef MP-WEIXIN
uni.navigateToMiniProgram({
appId: config.appId,
path: config.linkUrl || '',
success: () => {},
fail: (err) => {
console.error('跳转小程序失败:', err)
uni.showToast({
title: t('common.loadFailed'),
icon: 'none'
})
}
})
// #endif
// #ifndef MP-WEIXIN
uni.showToast({
title: t('auth.pleaseUseInWechat'),
icon: 'none'
})
// #endif
} else if (config.linkType === 'external' && config.linkUrl) {
// H5
uni.navigateTo({
url: `subPackages/other/webview/index?url=${encodeURIComponent(config.linkUrl)}`
})
} else if (config.linkType === 'internal' && config.linkUrl) {
//
uni.navigateTo({
url: config.linkUrl
})
}
console.log('点击广告:', index)
//
// navigateTo('/pages/join/index')
}
//
onMounted(() => {
uni.setNavigationBarTitle({
title: t('user.personalCenter')
title: $t('user.personalCenter')
})
// getInfo();
getInfo();
initVersion();
getBannerImages(); // 广
});
@@ -287,6 +233,7 @@
const getInfo = async () => {
try {
const res = await getUserInfo();
console.log('User info response:', res);
if (res.code == 401 || res.code == 40101) {
redirectToLogin()
@@ -310,8 +257,9 @@
deposit.value = res.data.balanceAmount || '0.00';
}
} catch (error) {
console.error('获取用户信息失败:', error);
uni.showToast({
title: t('user.getUserInfoFailed'),
title: $t('user.getUserInfoFailed'),
icon: 'none'
});
}
@@ -319,11 +267,12 @@
//
const initVersion = () => {
// #ifdef MP-WEIXIN
// #ifdef MP-ALIPAY
try {
const info = wx.getAccountInfoSync && wx.getAccountInfoSync();
if (info && info.miniProgram && info.miniProgram.version) {
appVersion.value = info.miniProgram.version;
//
const systemInfo = uni.getSystemInfoSync();
if (systemInfo && systemInfo.version) {
appVersion.value = systemInfo.version;
}
} catch (e) {}
// #endif
@@ -346,11 +295,11 @@
`${k}=${encodeURIComponent(current.options[k])}`).join('&') : ''
const redirect = encodeURIComponent(query ? `${route}?${query}` : route)
uni.reLaunch({
url: `/subPackages/user/login/index?redirect=${redirect}`
url: `/pages/login/index?redirect=${redirect}`
})
} catch (e) {
uni.reLaunch({
url: '/subPackages/user/login/index'
url: '/pages/login/index'
})
}
}
@@ -366,29 +315,43 @@
const handleQuickReturn = async () => {
try {
uni.showLoading({
title: t('common.loading')
title: $t('common.loading')
});
// 使
const res = await getInUseOrder();
const res = await uni.request({
url: `${URL}/app/order/inUse`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
});
uni.hideLoading();
if (res && res.code === 200 && res.data) {
const inUseOrder = res.data;
if (res.statusCode === 401 || res.data?.code === 401 || res.data?.code === 40101) {
redirectToLogin();
return;
}
if (res.statusCode === 200 && res.data.code === 200 && res.data.data) {
const inUseOrder = res.data.data;
//
uni.navigateTo({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${inUseOrder.deviceNo}`
});
} else {
uni.showToast({
title: t('order.noOrder'),
title: $t('order.noOrder'),
icon: 'none'
});
}
} catch (error) {
uni.hideLoading();
console.error('获取使用中订单失败:', error);
uni.showToast({
title: t('order.getOrderFailed'),
title: $t('order.getOrderFailed'),
icon: 'none'
});
}
@@ -406,12 +369,13 @@
redirectToLogin()
return
}
// #ifdef MP-WEIXIN
getUserProfile()
// #ifdef MP-ALIPAY
//
navigateTo('/pages/userProfile/index')
// #endif
// #ifndef MP-WEIXIN
// #ifndef MP-ALIPAY
uni.showToast({
title: t('auth.pleaseUseInWechat'),
title: $t('auth.pleaseUseInAlipay'),
icon: 'none'
})
// #endif
@@ -428,13 +392,13 @@
const avatarLocalPath = e?.detail?.avatarUrl
if (!avatarLocalPath) {
uni.showToast({
title: t('user.noAvatar'),
title: '未选择头像',
icon: 'none'
})
return
}
uni.showLoading({
title: t('common.uploading'),
title: $t('common.uploading'),
mask: true
})
const uploadRes = await uploadUserAvatar(avatarLocalPath)
@@ -447,13 +411,14 @@
uni.setStorageSync('userInfo', userInfo.value)
}
uni.showToast({
title: t('user.avatarUpdated'),
title: '头像已更新',
icon: 'success'
})
await getInfo()
} catch (err) {
console.error('选择/上传头像失败:', err)
uni.showToast({
title: t('user.avatarUploadFailed'),
title: '头像更新失败',
icon: 'none'
})
} finally {
@@ -471,77 +436,49 @@
//
const onPopupOpen = () => {
console.log('授权弹窗已打开');
isPopupVisible.value = true;
//
};
//
const onPopupClose = () => {
console.log('授权弹窗已关闭');
isPopupVisible.value = false;
//
};
//
// 使
const getUserProfile = () => {
// #ifdef MP-WEIXIN
uni.showLoading({
title: t('common.getting'),
mask: true
});
wx.getUserProfile({
desc: '用于完善会员资料',
success: (res) => {
console.log('获取用户信息成功:', res);
updateUserInfo(res.userInfo);
uploadAvatarAndRefresh(res.userInfo);
},
fail: (err) => {
console.error('获取用户信息失败:', err);
uni.showToast({
title: t('user.getUserInfoFailed'),
icon: 'none'
});
},
complete: () => {
uni.hideLoading();
closeAuthPopup();
}
});
// #ifdef MP-ALIPAY
//
navigateTo('/pages/userProfile/index')
// #endif
// #ifndef MP-WEIXIN
// #ifndef MP-ALIPAY
uni.showToast({
title: t('auth.pleaseUseInWechat'),
title: $t('auth.pleaseUseInAlipay'),
icon: 'none'
});
closeAuthPopup();
// #endif
};
//
const updateUserInfo = async (wxUserInfo) => {
//
const updateUserInfo = async (alipayUserInfo) => {
try {
//
const updatedInfo = {
...userInfo.value,
nickName: wxUserInfo.nickName,
avatar: wxUserInfo.avatarUrl
nickName: alipayUserInfo.nickName,
avatar: alipayUserInfo.avatarUrl
};
userInfo.value = updatedInfo;
uni.setStorageSync('userInfo', updatedInfo);
// API
// const updateRes = await updateUserInfoApi({
// openId: openId.value,
// nickName: wxUserInfo.nickName,
// avatarUrl: wxUserInfo.avatarUrl,
// gender: wxUserInfo.gender
// });
uni.showToast({
title: t('user.updateSuccess'),
title: $t('user.updateSuccess'),
icon: 'success'
});
@@ -550,24 +487,24 @@
} catch (error) {
console.error('更新用户信息失败:', error);
uni.showToast({
title: t('user.updateFailed'),
title: $t('user.updateFailed'),
icon: 'none'
});
}
};
//
const uploadAvatarAndRefresh = async (wxUserInfo) => {
const uploadAvatarAndRefresh = async (alipayUserInfo) => {
try {
const avatarUrl = wxUserInfo?.avatarUrl
const avatarUrl = alipayUserInfo?.avatarUrl
if (!avatarUrl) {
uni.showToast({
title: t('user.noAvatarUrl'),
title: '未获取到头像地址',
icon: 'none'
})
return
}
//
//
const tempFilePath = await new Promise((resolve, reject) => {
uni.downloadFile({
url: avatarUrl,
@@ -576,7 +513,7 @@
resolve(res.tempFilePath)
return
}
reject(new Error(t('user.avatarDownloadFailed')))
reject(new Error('头像下载失败'))
},
fail: reject
})
@@ -593,13 +530,14 @@
uni.setStorageSync('userInfo', userInfo.value)
}
uni.showToast({
title: t('user.avatarUpdated'),
title: '头像已更新',
icon: 'success'
})
await getInfo()
} catch (error) {
console.error('头像上传失败:', error)
uni.showToast({
title: t('user.avatarUploadFailed'),
title: '头像上传失败',
icon: 'none'
})
} finally {
@@ -618,7 +556,7 @@
//
const handleAboutUs = () => {
uni.showToast({
title: t('help.functionDeveloping'),
title: $t('help.functionDeveloping'),
icon: 'none'
});
};
@@ -626,7 +564,7 @@
//
const handlePrivacyPolicy = () => {
uni.showToast({
title: t('help.functionDeveloping'),
title: $t('help.functionDeveloping'),
icon: 'none'
});
};
+156 -1001
View File
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" lazy-load="true"></image>
<image src="/static/orderList.png" mode="aspectFill" class="empty-icon"></image>
</view>
<text class="empty-text">{{ $t('order.noOrderRecord') }}</text>
</view>
@@ -35,8 +35,7 @@
import {
ref,
reactive,
onMounted,
onUnmounted
onMounted
} from 'vue';
import OrderItemCard from '../../components/OrderItemCard.vue';
import {
@@ -46,9 +45,11 @@
getOrderList,
queryById,
getOrderByOrderNoScorePayStatus,
cancelOrder,
createWxPayment
cancelOrder
} from '../../config/api/order.js';
import {
confirmPaymentAndRent
} from '../../config/api/device.js';
import {
updateUserBalance
} from '../../config/api/user.js';
@@ -57,7 +58,14 @@
} from '../../config/url.js';
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
const { t: $t } = useI18n()
//
onMounted(() => {
uni.setNavigationBarTitle({
title: $t('order.myOrders')
})
})
//
const currentTab = ref(0);
@@ -66,62 +74,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']
}
]);
@@ -142,9 +150,7 @@
//
const formattedOrder = {
orderNo: orderData.orderNo || orderData.orderId,
orderId: orderData.orderId,
orderStatus: orderData.orderStatus,
orderNo: orderData.orderId,
status: orderData.orderStatus,
deviceId: orderData.deviceNo,
payWay: orderData.payWay,
@@ -152,8 +158,7 @@
endTime: orderData.endTime || '',
positionName: orderData.positionName || orderData.positionLocation || '',
deviceName: orderData.deviceName || '',
amount: orderData.payAmount || orderData.actualDeviceAmount || orderData.currentFee || orderData.residueAmount || '0.00',
pauseTime: orderData.pauseTime != null ? orderData.pauseTime : orderData.pause_time
amount: orderData.payAmount || orderData.actualDeviceAmount || orderData.currentFee || orderData.residueAmount || '0.00'
};
//
@@ -210,57 +215,33 @@
endTime: item.endTime || '',
positionName: item.positionName || item.positionLocation || '',
deviceName: item.deviceName || '',
amount: item.payAmount || item.actualDeviceAmount || item.currentFee || item.residueAmount || '0.00',
pauseTime: item.pauseTime != null ? item.pauseTime : item.pause_time
amount: item.payAmount || item.actualDeviceAmount || item.currentFee || item.residueAmount || '0.00'
};
});
}
} 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'
});
}
@@ -287,21 +268,29 @@
const handlePayment = async (order) => {
try {
uni.showLoading({
title: t('common.processing')
title: $t('common.processing')
});
//
const res = await createWxPayment(order.orderNo);
//
const res = await uni.request({
url: `${URL || 'http://127.0.0.1:8080'}/app/alipay-payment/create/${order.orderNo}`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
});
if (res && res.code === 200) {
const payParams = res.data;
if (res.statusCode === 200 && res.data.code === 200) {
const payParams = res.data.data;
//
//
await uni.requestPayment({
provider: 'alipay',
...payParams,
success: async () => {
uni.showToast({
title: t('payment.paymentSuccess'),
title: $t('payment.paymentSuccess'),
icon: 'success'
});
@@ -317,18 +306,18 @@
},
fail: (err) => {
console.error('支付失败:', err);
throw new Error(t('payment.paymentFailedRetry'));
throw new Error($t('payment.paymentFailedRetry'));
}
});
} else {
throw new Error(res?.msg || '创建支付订单失败');
throw new Error(res.data.msg || '创建支付订单失败');
}
uni.hideLoading();
} catch (error) {
uni.hideLoading();
uni.showToast({
title: error.message || t('payment.paymentFailed'),
title: error.message || $t('payment.paymentFailed'),
icon: 'none'
});
}
@@ -338,12 +327,12 @@
const handleCancelOrder = async (order) => {
try {
uni.showModal({
title: t('order.confirmCancel'),
content: t('order.confirmCancelContent'),
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({
@@ -353,14 +342,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'));
}
}
}
@@ -368,7 +357,7 @@
} catch (error) {
uni.hideLoading();
uni.showToast({
title: error.message || t('order.cancelFailed'),
title: error.message || $t('order.cancelFailed'),
icon: 'none'
});
}
+588
View File
@@ -0,0 +1,588 @@
<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/alipay-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({
provider: 'alipay',
...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/alipay-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>
@@ -85,7 +85,6 @@
<script>
import { queryById } from '@/config/api/order.js'
import { withdrawDeposit } from '@/config/api/user.js'
import {
URL
}from"@/config/url.js"
@@ -235,9 +234,17 @@ export default {
try {
uni.showLoading({ title: this.$t('common.processing') });
const res = await withdrawDeposit(this.orderInfo.orderNo)
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')
}
})
if (res && res.code === 200) {
if (res.statusCode === 200 && res.data.code === 200) {
uni.showToast({
title: this.$t('order.refundSuccess'),
icon: 'success'
@@ -252,7 +259,7 @@ export default {
this.loadOrderInfo();
}, 1500);
} else {
throw new Error(res?.msg || this.$t('order.refundFailed'));
throw new Error(res.data.msg || this.$t('order.refundFailed'));
}
} catch (error) {
console.error('退款申请错误:', error);
+107 -160
View File
@@ -1,19 +1,14 @@
<template>
<view class="success-container">
<!-- 支付成功状态和订单信息 -->
<view class="status-order-card">
<!-- 支付成功状态 -->
<view class="status-section">
<view class="status-card">
<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="order-card">
<view class="card-title">{{ $t('success.orderInfo') }}</view>
<view class="info-item">
<text class="label">{{ $t('order.orderNo') }}</text>
@@ -32,7 +27,6 @@
<text class="value">{{ orderInfo.payTime || '-' }}</text>
</view>
</view>
</view>
<!-- 设备状态 -->
<view class="device-status">
@@ -44,120 +38,84 @@
<!-- 操作按钮 -->
<view class="button-group">
<view class="secondary-btn" @click="goToHome">{{ $t('success.backToHome') }}</view>
<view class="primary-btn" @click="goToOrderList">{{ $t('success.viewOrder') }}</view>
<button class="primary-btn" @click="goToHome">{{ $t('success.backToHome') }}</button>
<button class="secondary-btn" @click="goToOrderList">{{ $t('success.viewOrder') }}</button>
</view>
</view>
</template>
<script setup>
import {
ref,
getCurrentInstance
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
queryById,
getOrderByOrderNo
} from '@/config/api/order.js'
<script>
import { queryById } from '@/config/api/order.js'
import { confirmPaymentAndRent } from '@/config/api/device.js'
// 获取当前实例以访问 $t 方法
const {
proxy
} = getCurrentInstance()
// 响应式数据
const orderId = ref('')
const orderInfo = ref({})
const isLoading = ref(true)
const deviceMessage = ref('')
const hasTriggeredDevice = ref(false)
// 页面加载
onLoad((options) => {
export default {
data() {
return {
orderId: '',
orderInfo: {},
isLoading: true,
deviceMessage: '',
hasTriggeredDevice: false
}
},
onLoad(options) {
// 设置页面标题
uni.setNavigationBarTitle({
title: proxy.$t('success.paymentSuccess')
title: this.$t('success.paymentSuccess')
})
deviceMessage.value = proxy.$t('success.preparingDevice')
this.deviceMessage = this.$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()
this.orderId = options.orderId
this.loadOrderInfo()
// 添加页面显示监听,防止页面切换后重复触发弹出
uni.$once('orderSuccess:' + orderId.value, () => {
uni.$once('orderSuccess:' + this.orderId, () => {
console.log('已经触发过弹出逻辑,不再重复触发')
hasTriggeredDevice.value = true
this.hasTriggeredDevice = true
})
} else {
uni.showToast({
title: proxy.$t('order.orderNotExist'),
title: this.$t('order.orderNotExist'),
icon: 'none'
})
setTimeout(() => {
goToHome()
this.goToHome()
}, 1500)
}
// #endif
})
// 加载订单信息
const loadOrderInfo = async () => {
},
methods: {
async loadOrderInfo() {
try {
uni.showLoading({
title: proxy.$t('common.loading')
title: this.$t('common.loading')
})
const res = await queryById(orderId.value)
const res = await queryById(this.orderId)
if (res.code === 200 && res.data) {
const orderData = res.data
orderInfo.value = {
this.orderInfo = {
orderNo: orderData.orderNo || orderData.orderId,
deviceNo: orderData.deviceNo,
amount: orderData.payAmount || orderData.amount,
payTime: orderData.payTime || formatTime(new Date())
payTime: orderData.payTime || this.formatTime(new Date())
}
// 检查订单状态
if (orderData.orderStatus === 'IN_USED') {
// 如果已经是使用中状态,可能说明开锁已经完成
deviceMessage.value = '设备已弹出,请取走您的风扇'
isLoading.value = false
this.deviceMessage = '设备已弹出,请取走您的风扇'
this.isLoading = false
// 如果是第一次加载页面且设备已弹出,记录状态,避免重复弹出
if (!hasTriggeredDevice.value) {
uni.$emit('orderSuccess:' + orderId.value)
hasTriggeredDevice.value = true
if (!this.hasTriggeredDevice) {
uni.$emit('orderSuccess:' + this.orderId)
this.hasTriggeredDevice = true
}
} else {
// 在此页面不再触发设备弹出操作,仅更新展示文案和加载状态
deviceMessage.value = proxy.$t('success.paymentSuccessDesc')
isLoading.value = false
// 正常触发弹出逻辑
this.triggerDeviceEject()
}
} else {
throw new Error('获取订单信息失败')
@@ -167,14 +125,53 @@
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || proxy.$t('order.getOrderFailed'),
title: error.message || this.$t('order.getOrderFailed'),
icon: 'none'
})
}
},
// 触发弹出风扇
async triggerDeviceEject() {
if (this.hasTriggeredDevice) {
console.log('已经触发过弹出风扇,不重复触发')
return
}
// 格式化时间
const formatTime = (date) => {
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 year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
@@ -182,41 +179,34 @@
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({
},
goToHome() {
uni.switchTab({
url: '/pages/index/index'
})
}
// 查看订单列表
const goToOrderList = () => {
},
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 {
.status-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;
@@ -255,34 +245,17 @@
}
}
.section-divider {
height: 1px;
background-color: #f0f0f0;
margin: 0 20px;
}
.order-section {
.order-card {
background-color: #fff;
border-radius: 12px;
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 {
@@ -291,10 +264,6 @@
align-items: center;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.label {
color: #666;
font-size: 14px;
@@ -303,8 +272,6 @@
.value {
color: #333;
font-size: 14px;
font-weight: 500;
}
}
}
}
@@ -338,42 +305,25 @@
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
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;
margin-top: 30px;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 20rpx;
// flex-direction: column;
gap: 16px;
.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;
border-radius: 24px;
padding: 12px;
font-size: 16px;
&:active {
opacity: 0.8;
@@ -383,13 +333,10 @@
.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;
border: 1px solid #07c160;
border-radius: 24px;
padding: 12px;
font-size: 16px;
&:active {
background-color: #f5f5f5;
@@ -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 } = useI18n()
const { t: $t } = useI18n()
const positionInfo = ref({})
const positionId = ref('')
@@ -112,7 +112,7 @@ const { t } = useI18n()
const loadPositionDetail = async () => {
try {
uni.showLoading({
title: t('common.loading')
title: $t('common.loading')
})
//
@@ -147,7 +147,7 @@ const { t } = useI18n()
positionInfo.value = position
} else {
uni.showToast({
title: t('location.notExist'),
title: $t('location.notExist'),
icon: 'none'
})
}
@@ -155,7 +155,7 @@ const { 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 } = 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 } = useI18n()
longitude < -180 || longitude > 180 ||
(latitude === 0 && longitude === 0)) {
uni.showToast({
title: t('location.coordinateError'),
title: $t('location.coordinateError'),
icon: 'none'
})
return
@@ -15,21 +15,21 @@
<!-- 支付方式标识 -->
<view class="device-right">
<!-- 微信支付分标识 -->
<view class="payment-badge wx-score" v-if="orderInfo.payWay == 'wx_score_pay'">
<image src="/static/images/wxpayflag.png" mode="aspectFit" class="badge-icon"></image>
<!-- 支付宝信用免押标识 -->
<view class="payment-badge alipay-score" v-if="orderInfo.payWay == 'alipay_score_pay'">
<image src="/static/images/alipay.svg" mode="aspectFit" class="badge-icon"></image>
<view class="badge-text">
<text>{{ $t('order.wxPayScore') }}</text>
<text>{{ $t('order.alipayScore') }}</text>
<text class="divider">|</text>
<text class="highlight">{{ $t('order.depositFree') }}</text>
</view>
</view>
<!-- 会员订单标识 -->
<view class="payment-badge member" v-else-if="orderInfo.payWay == 'wx_member_pay'">
<view class="payment-badge member" v-else-if="orderInfo.payWay == 'alipay_member_pay'">
<text class="badge-text">{{ $t('order.memberOrder') }}</text>
</view>
<!-- 微信支付押金标识 -->
<view class="payment-badge deposit" v-else-if="orderInfo.payWay == 'wx_pay'">
<!-- 支付宝支付押金标识 -->
<view class="payment-badge deposit" v-else-if="orderInfo.payWay == 'alipay_pay'">
<text class="badge-text">{{ $t('order.depositPay') }}</text>
</view>
</view>
@@ -143,8 +143,7 @@
<script>
import {
queryById,
cancelOrder,
getInUseOrder
cancelOrder
} from '@/config/api/order.js'
import {
getSystemConfig
@@ -323,7 +322,7 @@
this.countdownTimer = null
}
},
// iOS/
// iOS/
parseStartTimeToMs(timeStr) {
if (!timeStr) return NaN
if (typeof timeStr === 'number') {
@@ -711,12 +710,19 @@
}
// API使
const inUseRes = await getInUseOrder()
const inUseRes = await uni.request({
url: `${URL || 'http://127.0.0.1:8080'}/app/order/inUse`,
method: 'GET',
header: {
'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
}
})
console.log('通过设备号查询订单结果:', JSON.stringify(inUseRes))
if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
const inUseOrder = inUseRes.data
if (inUseRes.statusCode === 200 && inUseRes.data.code === 200 && inUseRes.data.data) {
const inUseOrder = inUseRes.data.data
console.log('使用中的订单:', inUseOrder)
// ID
-746
View File
@@ -1,746 +0,0 @@
<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>相册</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>手动输入</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>返回</text>
</view>
</view>
<!-- 手动输入弹窗 -->
<uv-popup ref="inputPopup" mode="center" round="16" :closeOnClickOverlay="true">
<view class="input-dialog">
<view class="dialog-title">手动输入设备号</view>
<input
v-model="manualDeviceNo"
placeholder="请输入设备上的编号"
class="device-input"
type="text"
/>
<view class="dialog-btns">
<button class="cancel-btn" @click="closeInput">取消</button>
<button class="confirm-btn" @click="confirmManualInput">确定</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';
const inputPopup = ref(null);
const manualDeviceNo = ref('');
const tipText = ref('正在初始化...');
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 = '正在初始化...';
console.log('=== 开始初始化扫码 ===');
// 检查浏览器支持
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('您的浏览器不支持摄像头访问');
}
// 等待 DOM 渲染
await new Promise(resolve => setTimeout(resolve, 500));
// 检查容器元素
const readerElement = document.getElementById('qr-reader');
if (!readerElement) {
throw new Error('扫码容器元素未找到');
}
console.log('✓ 扫码容器元素已找到');
// 创建 Html5Qrcode 实例
html5QrCode = new Html5Qrcode('qr-reader', {
verbose: false // 关闭详细日志
});
console.log('✓ Html5Qrcode 实例创建成功');
// 启动扫描
await startScanning();
} catch (err) {
console.error('❌ 初始化失败:', err);
handleInitError(err);
}
};
// 开始扫描
const startScanning = async () => {
try {
if (!html5QrCode) {
throw new Error('Html5Qrcode 实例不存在');
}
tipText.value = '正在启动摄像头...';
console.log('=== 开始启动扫描 ===');
console.log('html5QrCode 实例:', html5QrCode);
console.log('html5QrCode.start 方法:', typeof html5QrCode.start);
// 配置扫描参数
const config = 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 = '将二维码放入框内扫描';
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 = '将二维码放入框内扫描';
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 = '将二维码放入框内扫描';
console.log('✅ 使用默认摄像头启动成功');
setTimeout(() => {
hideDefaultUI();
}, 200);
} else {
throw new Error('未找到可用的摄像头');
}
} catch (err3) {
console.error('❌ 所有方式都失败:', err3);
throw err3;
}
}
}
};
// 隐藏默认UI
const hideDefaultUI = () => {
try {
const shaded = document.getElementById('qr-shaded-region');
if (shaded) {
shaded.style.border = 'none';
console.log('✓ 已隐藏默认边框');
}
} catch (err) {
console.warn('隐藏默认UI失败:', err);
}
};
// 扫描成功回调
const onScanSuccess = (decodedText, decodedResult) => {
// 防止重复处理
if (isProcessing) {
console.log('正在处理中,忽略重复扫码');
return;
}
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 = '初始化失败';
let errDetail = '';
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
errMsg = '摄像头权限被拒绝';
errDetail = '请在浏览器设置中允许访问摄像头';
}
else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError') {
errMsg = '未找到可用的摄像头';
errDetail = '请确保设备有摄像头';
}
else if (err.name === 'NotReadableError' || err.name === 'TrackStartError') {
errMsg = '摄像头被占用';
errDetail = '请关闭其他使用摄像头的应用';
}
else if (err.name === 'NotSupportedError') {
errMsg = '浏览器不支持';
errDetail = '请使用现代浏览器访问';
}
// else if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
// errMsg = '需要 HTTPS 环境';
// errDetail = '摄像头功能需要在安全环境下使用';
// }
else {
errMsg = err.message || '摄像头启动失败';
errDetail = '请尝试刷新页面或使用其他方式';
}
tipText.value = errMsg;
// 显示错误提示,提供备选方案
uni.showModal({
title: errMsg,
content: errDetail + '\n\n您可以:\n1. 从相册选择二维码图片\n2. 手动输入设备号',
showCancel: true,
cancelText: '返回',
confirmText: '手动输入',
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: '正在识别...' });
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: '未识别到二维码', icon: 'none' });
// 识别失败,重新启动摄像头扫描
if (wasScanning) {
setTimeout(async () => {
await startScanning();
}, 500);
}
}
} catch (err) {
console.error('图片识别失败:', err);
uni.hideLoading();
uni.showToast({ title: '识别失败', icon: 'none' });
// 识别失败,重新启动摄像头扫描
setTimeout(async () => {
try {
await startScanning();
} catch (e) {
console.error('重新启动扫描失败:', e);
}
}, 500);
}
};
input.click();
// #endif
// #ifndef H5
uni.chooseImage({
count: 1,
sourceType: ['album'],
success: (res) => {
uni.showToast({ title: '该功能仅在H5环境可用', 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: '请输入设备号', 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('扫码页面已挂载');
// 延迟初始化,确保 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>
+33 -60
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" lazy-load="true"></image>
<image src="/static/location.png" class="relocate-icon" mode="aspectFit"></image>
</view>
</view>
<view class="list-wrap">
@@ -22,19 +22,15 @@
: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>
@@ -45,28 +41,31 @@
<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="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>
</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>
{{ item.distance }}</view>
</view>
</view>
@@ -101,13 +100,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',
@@ -134,13 +133,8 @@
}
const formatDistance = (meters) => {
// 兼容支付宝小程序等环境:保证始终对 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`
if (meters < 1000) return `${Math.round(meters)}m`
return `${(meters / 1000).toFixed(1)}km`
}
const setTab = (name) => {
@@ -222,8 +216,8 @@
return {
...transformed,
canRent: activeTab.value === 'rent' ? true : (device.availablePowerBankCount > 0),
canReturn: activeTab.value === 'return' ? true : (device.availableEmptyGridCount > 0),
supportCouponOrMember: device.supportCouponOrMember || false
canReturn: activeTab.value === 'return' ? true : (device.availableEmptyGridCount >
0)
}
})
@@ -262,7 +256,7 @@ const init = async () => {
positionList.value = []
filteredPositions.value = []
uni.showToast({
title: t('home.getLocationFailed'),
title: $t('home.getLocationFailed'),
icon: 'none'
})
} finally {
@@ -284,7 +278,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
@@ -302,13 +296,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: `/subPackages/business/position/detail?positionId=${position.positionId}`
url: `/pages/position/detail?positionId=${position.positionId}`
})
}
</script>
@@ -398,8 +392,9 @@ const init = async () => {
}
.card {
display: flex;
flex-direction: column;
display: grid;
grid-template-columns: 120rpx 1fr 72rpx;
align-items: center;
gap: 16rpx;
padding: 20rpx;
border-radius: 20rpx;
@@ -418,21 +413,6 @@ 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;
@@ -462,7 +442,7 @@ const init = async () => {
font-size: 30rpx;
font-weight: 700;
color: #2A2A2A;
max-width: 100%;
max-width: 70%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -494,11 +474,9 @@ const init = async () => {
}
}
}
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
@@ -518,10 +496,6 @@ const init = async () => {
background: #E8F2FF;
color: #3578e5;
}
.tag.coupon {
background: #FFF9F0;
color: #D4A574;
}
.actions {
@@ -529,7 +503,6 @@ const init = async () => {
align-items: center;
justify-content: center;
flex-direction: column;
gap: 8rpx;
}
.nav {
+8 -11
View File
@@ -6,16 +6,16 @@
<script>
import {
wxLogin,
} from '@/util/index'
alipayLogin,
} 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: `/subPackages/service/return/index?orderId=${latestOrder.orderId}`
url: `/pages/device/return?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: `/subPackages/order/payment?orderId=${latestOrder.orderId}&packageTimeHours=${packageTimeHours}&packagePrice=${packagePrice}&hourlyRate=${hourlyRate}&totalAmount=${totalAmount}&depositAmount=${depositAmount}`
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/order/detail?deviceNo=${deviceNo}`
url: `/pages/device/detail?deviceNo=${deviceNo}`
});
}
} else {
@@ -122,11 +122,8 @@
url: `/pages/device/detail?deviceNo=${option.deviceNo}`
});
} else {
// uni.switchTab({
// url:'/pages/index/index'
// })
// 如果连deviceNo都没有,则返回首页
uni.reLaunch({ url: '/pages/index/index' });
uni.switchTab({ url: '/pages/index/index' });
}
}, 2000);
} finally {
@@ -10,16 +10,12 @@
</view>
</view>
<view class="group">
<view class="item" @click="navigateTo('/subPackages/other/legal/agreement')">
<text class="label">{{ $t('user.settinguserAgreement') }}</text>
<view class="item" @click="navigateTo('/pages/legal/agreement')">
<text class="label">{{ $t('user.userAgreement') }}</text>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="item" @click="navigateTo('/subPackages/other/legal/privacy')">
<text class="label">{{ $t('user.settinguserprivacyPolicy') }}</text>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
<view class="item" @click="navigateTo('/subPackages/other/legal/terms')">
<text class="label">{{ $t('legal.termsAndConditions') }}</text>
<view class="item" @click="navigateTo('/pages/legal/privacy')">
<text class="label">{{ $t('user.privacyPolicy') }}</text>
<uv-icon name="arrow-right" size="16" color="#c8c8c8"></uv-icon>
</view>
</view>
@@ -37,7 +33,7 @@ import { ref, computed, onMounted, getCurrentInstance } from 'vue'
import { userLogout } from '@/config/api/user.js'
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
const { t: $t } = useI18n()
// i18n
const instance = getCurrentInstance()
@@ -46,7 +42,7 @@ const globalI18n = instance?.appContext?.config?.globalProperties?.$i18n
//
onMounted(() => {
uni.setNavigationBarTitle({
title: t('settings.title')
title: $t('settings.title')
})
})
@@ -55,13 +51,7 @@ const currentLanguage = ref(uni.getStorageSync('language') || 'zh-CN')
//
const currentLanguageText = computed(() => {
if (currentLanguage.value === 'zh-CN') {
return t('settings.chinese')
} else if (currentLanguage.value === 'id-ID') {
return t('settings.indonesian')
} else {
return t('settings.english')
}
return currentLanguage.value === 'zh-CN' ? '简体中文' : 'English'
})
const navigateTo = (url) => {
@@ -70,40 +60,58 @@ const navigateTo = (url) => {
//
const showLanguageSelector = () => {
const languages = [
{ code: 'zh-CN', label: t('settings.chinese') },
{ code: 'en-US', label: t('settings.english') },
{ code: 'id-ID', label: t('settings.indonesian') }
]
uni.showActionSheet({
itemList: languages.map(lang => lang.label),
itemList: ['简体中文', 'English'],
success: (res) => {
const selectedLang = languages[res.tapIndex].code
if (selectedLang !== currentLanguage.value) {
const lang = res.tapIndex === 0 ? 'zh-CN' : 'en-US'
if (lang !== currentLanguage.value) {
console.log('========================================')
console.log('=== 用户选择切换语言 ===')
console.log('旧语言:', currentLanguage.value)
console.log('新语言:', lang)
// 1.
uni.setStorageSync('language', selectedLang)
uni.setStorageSync('language', lang)
console.log('✓ 语言已保存到缓存')
// 2. i18n
if (globalI18n) {
globalI18n.locale = selectedLang
console.log('✓ 正在更新 globalI18n.locale...')
console.log(' 更新前:', globalI18n.locale)
globalI18n.locale = lang
console.log(' 更新后:', globalI18n.locale)
console.log('✓ 测试翻译:', globalI18n.t('common.loading'))
} else {
console.warn('✗ globalI18n 不存在!')
console.warn(' instance:', !!instance)
console.warn(' appContext:', !!instance?.appContext)
console.warn(' globalProperties:', !!instance?.appContext?.config?.globalProperties)
}
// 3.
currentLanguage.value = selectedLang
currentLanguage.value = lang
console.log('========================================')
// 4.
uni.showToast({
title: t('settings.languageSwitched'),
title: lang === 'zh-CN' ? '语言已切换,正在刷新...' : 'Language switched, refreshing...',
icon: 'none',
duration: 800
})
// 5. i18n
setTimeout(() => {
console.log('=== 准备 reLaunch 到首页 ===')
// 使 reLaunch
uni.reLaunch({
url: '/pages/index/index'
url: '/pages/index/index',
success: () => {
console.log('✓ 页面已重新加载')
},
fail: (err) => {
console.error('✗ 页面重载失败:', err)
}
})
}, 800)
}
@@ -113,17 +121,17 @@ const showLanguageSelector = () => {
const handleLogout = async () => {
uni.showModal({
title: t('common.tips'),
content: t('user.confirmLogout'),
title: $t('common.tips'),
content: $t('user.confirmLogout'),
success: async (res) => {
if (res.confirm) {
const response = await userLogout();
if (response.code == 200) {
uni.showToast({ title: t('user.logoutSuccess'), icon: 'none' })
uni.showToast({ title: $t('user.logoutSuccess'), icon: 'none' })
setTimeout(() => {
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
uni.reLaunch({ url: '/subPackages/user/login/index' })
uni.reLaunch({ url: '/pages/login/index' })
}, 1200)
}
}
+240
View File
@@ -0,0 +1,240 @@
<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
@@ -0,0 +1,413 @@
<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-ALIPAY -->
<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-ALIPAY */
.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 } = useI18n()
const { t: $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.reLaunch({ url: '/pages/index/index' })
uni.switchTab({ 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.reLaunch({ url: '/pages/index/index' })
uni.switchTab({ url: '/pages/index/index' })
}, 800)
}, 60000)
}
onLoad((query) => {
uni.setNavigationBarTitle({
title: t('waiting.title')
title: $t('waiting.title')
})
if (query) {
if (query.orderNo) {
+7 -10
View File
@@ -17,9 +17,6 @@ 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)
@@ -86,6 +83,9 @@ 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,9 +491,6 @@ 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==}
@@ -746,7 +743,7 @@ snapshots:
'@jridgewell/gen-mapping@0.3.8':
dependencies:
'@jridgewell/set-array': 1.2.1
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/sourcemap-codec': 1.5.0
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/resolve-uri@3.1.2': {}
@@ -758,12 +755,14 @@ 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.5
'@jridgewell/sourcemap-codec': 1.5.0
'@parcel/watcher-android-arm64@2.5.1':
optional: true
@@ -1178,8 +1177,6 @@ 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.

Before

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because it is too large Load Diff
-783
View File
@@ -1,783 +0,0 @@
<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 || '风电者2026新款风扇、充电宝、暖手宝三合一' }}</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 } 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 = () => {
uni.showModal({
title: '提示',
content: '确定要取消这个订单吗?',
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 = () => {
uni.showModal({
title: '提示',
content: '确定要删除这个订单吗?',
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
@@ -1,864 +0,0 @@
<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 } 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 {
uni.showModal({
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) => {
uni.showModal({
title: '提示',
content: '确定要删除这个订单吗?',
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
@@ -1,479 +0,0 @@
<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
@@ -1,542 +0,0 @@
<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>
File diff suppressed because it is too large Load Diff
-897
View File
@@ -1,897 +0,0 @@
<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>
</view>
</template>
<script setup>
import {
ref,
computed,
reactive,
onMounted
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
queryById,
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
} 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)
// 支付方式相关(微信/支付宝/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 (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(() => '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;
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_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()
}
}
// 轮询定时器
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; // 最多轮询60次(5分钟)
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 中没有 orderId,尝试从缓存中获取(H5 环境)
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);
}
}
}
}
</style>
-141
View File
@@ -1,141 +0,0 @@
<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>
-399
View File
@@ -1,399 +0,0 @@
<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
@@ -1,323 +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.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 } 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
}
uni.showModal({
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;
}
}
uni.showModal({
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>
-135
View File
@@ -1,135 +0,0 @@
<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
@@ -1,146 +0,0 @@
<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
@@ -1,78 +0,0 @@
<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>
-213
View File
@@ -1,213 +0,0 @@
<template>
<view class="help-container">
<!-- 常见问题 -->
<view class="faq-section">
<view
v-for="(item, index) in faqList"
:key="index"
class="collapse-item"
>
<view
class="collapse-header"
@click="toggleCollapse(index)"
>
<text class="collapse-title">{{ $t(item.question) }}</text>
<text class="collapse-icon" :class="{ 'active': activeIndex === index }"></text>
</view>
<view
class="collapse-content"
:class="{ 'show': activeIndex === index }"
>
<view class="answer-content">
<text class="answer-text">{{ $t(item.answer) }}</text>
</view>
</view>
</view>
</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">{{ $t('help.workingHoursValue') }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { HELP_CONTENT } from '@/constants/help'
import { getCustomerPhone } from '@/util/index.js'
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
const faqList = ref(HELP_CONTENT.FAQ_LIST)
const customerPhone = ref(HELP_CONTENT.CONTACT.PHONE.VALUE)
const activeIndex = ref(null)
onLoad(() => {
uni.setNavigationBarTitle({
title: t('help.title')
})
customerPhone.value = getCustomerPhone()
})
const toggleCollapse = (index) => {
if (activeIndex.value === index) {
activeIndex.value = null
} else {
activeIndex.value = index
}
}
const makePhoneCall = () => {
uni.makePhoneCall({
phoneNumber: customerPhone.value
})
}
</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;
.collapse-item {
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.collapse-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
cursor: pointer;
transition: background-color 0.2s;
&:active {
background-color: #f5f5f5;
}
.collapse-title {
flex: 1;
font-size: 30rpx;
color: #333;
font-weight: 500;
line-height: 1.5;
}
.collapse-icon {
margin-left: 20rpx;
font-size: 24rpx;
color: #999;
transition: transform 0.3s;
transform: rotate(0deg);
&.active {
transform: rotate(180deg);
}
}
}
.collapse-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
&.show {
max-height: 2000rpx;
transition: max-height 0.3s ease-in;
}
.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>

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