22 Commits

Author SHA1 Message Date
pcwl_yancheng 120eba6c6a fix: payment page uses USD
Keep payment page currency display fixed to USD instead of changing with selected payment method.

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

Made-with: Cursor
2026-04-25 16:08:43 +08:00
pcwl_yancheng 2f618ef6ec 修复bug 2026-04-22 14:22:04 +08:00
pcwl_yancheng 09be26e2aa 新增暂停时长 2026-04-08 18:01:56 +08:00
pcwl_yancheng 9ca377907b 新增三码适配 2026-04-01 14:12:14 +08:00
pcwl_yancheng 337a92d8c1 支付宝上架 2026-03-26 09:47:22 +08:00
pcwl_yancheng 802aee59cb fix:兼容普通二维码跳转 2026-03-18 16:41:40 +08:00
pcwl_yancheng a79cf10bd4 fix:多平台兼容 2026-03-16 11:52:27 +08:00
pcwl_yancheng b3836b8bf2 支付宝兼容 2026-03-09 09:07:58 +08:00
pcwl_yancheng 069677957e fix:修复bug 2026-02-28 16:38:53 +08:00
pcwl_yancheng 3462a24d1e fix:修复bug 2026-02-26 09:16:35 +08:00
pcwl_yancheng 99872dd6df style:新增懒加载机制 2026-02-19 21:45:39 +08:00
pcwl_yancheng 7fd9c25ea8 fix:修复bug 2026-02-07 10:33:39 +08:00
pcwl_yancheng bb5a6dd100 fix:修复bug 2026-02-06 18:09:23 +08:00
pcwl_yancheng f476cee76d 新增h5qrcode依赖 2026-02-05 17:38:19 +08:00
pcwl_yancheng 5a13803743 fix:修复bug 2026-02-03 17:47:55 +08:00
pcwl_yancheng 9f66ee9658 fix:修复bug 2026-02-02 14:08:17 +08:00
pcwl_yancheng 6a1dff4b94 fix:修复bug 2026-01-22 10:52:58 +08:00
pcwl_yancheng b0daa7b59b add:新增会员、优惠券 2026-01-19 09:16:09 +08:00
pcwl_yancheng dbf7fa0c95 add:新增优化功能 2026-01-12 10:30:40 +08:00
pcwl_yancheng be01fb211e fix:修复bug;新增订单列表页面订单监控 2025-12-30 17:26:16 +08:00
pcwl_yancheng 9d4e229312 fix:修正优化首页、个人中心页面广告图url跳转 2025-12-15 17:37:52 +08:00
110 changed files with 25717 additions and 6556 deletions
+13
View File
@@ -0,0 +1,13 @@
{
"version" : "1.0",
"configurations" : [
{
"playground" : "standard",
"type" : "uni-app:app-android"
},
{
"playground" : "standard",
"type" : "uni-app:app-ios"
}
]
}
+130
View File
@@ -0,0 +1,130 @@
# AGENTS
<skills_system priority="1">
## Available Skills
<!-- SKILLS_TABLE_START -->
<usage>
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
How to use skills:
- Invoke: `npx openskills read <skill-name>` (run in your shell)
- For multiple: `npx openskills read skill-one,skill-two`
- The skill content will load with detailed instructions on how to complete the task
- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/)
Usage notes:
- Only use skills listed in <available_skills> below
- Do not invoke a skill that is already loaded in your context
- Each skill invocation is stateless
</usage>
<available_skills>
<skill>
<name>algorithmic-art</name>
<description>Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.</description>
<location>global</location>
</skill>
<skill>
<name>brand-guidelines</name>
<description>Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.</description>
<location>global</location>
</skill>
<skill>
<name>canvas-design</name>
<description>Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.</description>
<location>global</location>
</skill>
<skill>
<name>doc-coauthoring</name>
<description>Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision docs, or similar structured content. This workflow helps users efficiently transfer context, refine content through iteration, and verify the doc works for readers. Trigger when user mentions writing docs, creating proposals, drafting specs, or similar documentation tasks.</description>
<location>global</location>
</skill>
<skill>
<name>docx</name>
<description>"Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). Triggers include: any mention of \"Word doc\", \"word document\", \".docx\", or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a \"report\", \"memo\", \"letter\", \"template\", or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation."</description>
<location>global</location>
</skill>
<skill>
<name>frontend-design</name>
<description>Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.</description>
<location>global</location>
</skill>
<skill>
<name>internal-comms</name>
<description>A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.).</description>
<location>global</location>
</skill>
<skill>
<name>mcp-builder</name>
<description>Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).</description>
<location>global</location>
</skill>
<skill>
<name>pdf</name>
<description>Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill.</description>
<location>global</location>
</skill>
<skill>
<name>pptx</name>
<description>"Use this skill any time a .pptx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx filename, regardless of what they plan to do with the content afterward. If a .pptx file needs to be opened, created, or touched, use this skill."</description>
<location>global</location>
</skill>
<skill>
<name>skill-creator</name>
<description>Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.</description>
<location>global</location>
</skill>
<skill>
<name>slack-gif-creator</name>
<description>Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack."</description>
<location>global</location>
</skill>
<skill>
<name>template</name>
<description>Replace with description of the skill and when Claude should use it.</description>
<location>global</location>
</skill>
<skill>
<name>theme-factory</name>
<description>Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.</description>
<location>global</location>
</skill>
<skill>
<name>web-artifacts-builder</name>
<description>Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.</description>
<location>global</location>
</skill>
<skill>
<name>webapp-testing</name>
<description>Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.</description>
<location>global</location>
</skill>
<skill>
<name>xlsx</name>
<description>"Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like \"the xlsx in my downloads\") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved."</description>
<location>global</location>
</skill>
</available_skills>
<!-- SKILLS_TABLE_END -->
</skills_system>
+44 -7
View File
@@ -1,6 +1,6 @@
<script> <script>
import { import {
alipayLogin, wxLogin,
getUserInfo getUserInfo
} from './util/index' } from './util/index'
@@ -11,13 +11,52 @@
// 注意:语言初始化已移至 main.js,确保每次 reLaunch 都能正确加载新语言 // 注意:语言初始化已移至 main.js,确保每次 reLaunch 都能正确加载新语言
}, },
onShow: async function() { onShow: async function() {
console.log('========================================')
console.log('=== App onShow 被调用 ===') // 检查启动路径,如果设置了启动路径则跳转
console.log('时间戳:', new Date().toLocaleTimeString()) try {
const launchPath = uni.getStorageSync('launchPath')
if (launchPath) {
console.log('检测到启动路径:', launchPath)
// 获取当前页面栈
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentRoute = currentPage ? ('/' + currentPage.route) : ''
// 规范化路径格式进行比较
const normalizedLaunchPath = launchPath.replace(/\.html$/, '')
// 只要当前不在目标页面,就进行一次跳转
// 下单流程不会受影响,因为在下单离开设备详情页时不会写入 launchPath
if (currentRoute !== normalizedLaunchPath) {
console.log('当前页面:', currentRoute, '目标页面:', normalizedLaunchPath)
// 清除启动路径标记(在跳转前清除,避免重复触发)
uni.removeStorageSync('launchPath')
// 使用 reLaunch 跳转到启动路径(首页)
uni.reLaunch({
url: normalizedLaunchPath,
success: () => {
console.log('成功跳转到启动路径(reLaunch')
},
fail: (reLaunchErr) => {
console.error('跳转到启动路径失败:', reLaunchErr)
}
})
} else {
// 如果已经在目标页面,清除启动路径标记
console.log('当前已在目标页面,清除启动路径标记')
uni.removeStorageSync('launchPath')
}
}
} catch (e) {
console.error('App onShow - 启动路径检查失败:', e)
}
// 检查并更新语言(uni.reLaunch 会触发 onShow // 检查并更新语言(uni.reLaunch 会触发 onShow
try { try {
const savedLang = uni.getStorageSync('language') const savedLang = uni.getStorageSync('language')
if(savedLang){
uni.removeStorageSync('language');
}
console.log('App onShow - 缓存中的语言:', savedLang) console.log('App onShow - 缓存中的语言:', savedLang)
// 获取当前 i18n 实例并检查语言 // 获取当前 i18n 实例并检查语言
@@ -39,8 +78,6 @@
} catch (e) { } catch (e) {
console.error('App onShow - 语言检查失败:', e) console.error('App onShow - 语言检查失败:', e)
} }
console.log('========================================')
}, },
onHide: function() { onHide: function() {
console.log('App Hide') console.log('App Hide')
@@ -49,7 +86,7 @@
// 保留方法但不调用 // 保留方法但不调用
async autoLogin() { async autoLogin() {
try { try {
const loginResult = await alipayLogin() const loginResult = await wxLogin()
// await getUserInfo() // await getUserInfo()
} catch (error) { } catch (error) {
console.error('自动登录失败:', error) console.error('自动登录失败:', error)
+187
View File
@@ -0,0 +1,187 @@
<template>
<view class="skeleton-container">
<!-- 设备信息卡片骨架 -->
<view class="card skeleton-card">
<view class="device-location-skeleton">
<view class="location-left-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="32rpx" rowsHeight="32rpx"
borderRadius="50%"></uv-skeleton>
<view style="margin-left: 12rpx;">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="200rpx" rowsHeight="28rpx">
</uv-skeleton>
</view>
</view>
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="100rpx" rowsHeight="40rpx"
borderRadius="30rpx"></uv-skeleton>
</view>
<view class="device-info-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="300rpx" rowsHeight="26rpx">
</uv-skeleton>
</view>
<view class="device-info-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="350rpx" rowsHeight="26rpx">
</uv-skeleton>
</view>
</view>
<!-- 计费规则卡片骨架 -->
<view class="card skeleton-card">
<view class="card-header-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="150rpx" rowsHeight="32rpx">
</uv-skeleton>
</view>
<view class="pricing-banner-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="200rpx" rowsHeight="64rpx">
</uv-skeleton>
<view style="margin-top: 16rpx;">
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="120rpx" rowsHeight="40rpx"
borderRadius="30rpx"></uv-skeleton>
</view>
</view>
<view class="pricing-info-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="100%" rowsHeight="26rpx">
</uv-skeleton>
</view>
<view class="pricing-info-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="100%" rowsHeight="26rpx">
</uv-skeleton>
</view>
</view>
<!-- 使用说明卡片骨架 -->
<view class="card skeleton-card">
<view class="card-header-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="150rpx" rowsHeight="32rpx">
</uv-skeleton>
</view>
<view class="notice-item-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="12rpx" rowsHeight="12rpx"
borderRadius="50%"></uv-skeleton>
<view style="flex: 1; margin-left: 16rpx;">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="100%" rowsHeight="28rpx">
</uv-skeleton>
</view>
</view>
<view class="notice-item-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="12rpx" rowsHeight="12rpx"
borderRadius="50%"></uv-skeleton>
<view style="flex: 1; margin-left: 16rpx;">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="100%" rowsHeight="28rpx">
</uv-skeleton>
</view>
</view>
<view class="notice-item-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="12rpx" rowsHeight="12rpx"
borderRadius="50%"></uv-skeleton>
<view style="flex: 1; margin-left: 16rpx;">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="100%" rowsHeight="28rpx">
</uv-skeleton>
</view>
</view>
</view>
<!-- 促销提示框骨架 -->
<view class="promotion-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="100%" rowsHeight="60rpx"
borderRadius="22rpx"></uv-skeleton>
</view>
<!-- 底部按钮骨架 -->
<view class="footer-skeleton">
<uv-skeleton :loading="true" :animate="true" rows="0" rowsWidth="100%" rowsHeight="96rpx"
borderRadius="48rpx"></uv-skeleton>
<view style="margin-top: 16rpx;">
<uv-skeleton :loading="true" :animate="true" rows="1" rowsWidth="200rpx" rowsHeight="24rpx">
</uv-skeleton>
</view>
</view>
</view>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.skeleton-container {
min-height: 100vh;
background-color: #f5f7fa;
padding: 30rpx 30rpx 300rpx;
box-sizing: border-box;
}
.card {
background-color: #fff;
border-radius: 24rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
padding: 30rpx;
margin-bottom: 30rpx;
}
.skeleton-card {
.device-location-skeleton {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
.location-left-skeleton {
display: flex;
align-items: center;
}
}
.device-info-skeleton {
margin-bottom: 12rpx;
}
.card-header-skeleton {
margin-bottom: 24rpx;
}
.pricing-banner-skeleton {
background: #E6F7EC;
border-radius: 20rpx;
padding: 40rpx 30rpx;
margin-bottom: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.pricing-info-skeleton {
margin-bottom: 16rpx;
}
.notice-item-skeleton {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
}
}
.promotion-skeleton {
margin-bottom: 30rpx;
}
.footer-skeleton {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 24rpx 30rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
}
</style>
+263
View File
@@ -0,0 +1,263 @@
<template>
<view class="device-order-card" @click="onCardClick">
<!-- 头部标题和状态 -->
<view class="card-header">
<view class="header-left">
<view class="tag-bar"></view>
<text class="title">定制化订单</text>
</view>
<view class="status-badge">{{ statusText }}</view>
</view>
<!-- 产品信息区域 -->
<view class="product-section">
<image
:src="order.pictureUrl || order.productImage || '/static/default-product.png'"
mode="aspectFill"
class="product-image"
lazy-load="true"
></image>
<view class="product-info">
<view class="product-name">{{ order.productName || order.deviceName || '风电者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>
<view class="empty-state" v-if="!isLoading && (!positions || positions.length === 0)"> <view class="empty-state" v-if="!isLoading && (!positions || positions.length === 0)">
<image class="empty-icon" src="/static/scan-icon.png" mode="aspectFit" /> <image class="empty-icon" src="/static/scan-icon.png" mode="aspectFit" lazy-load="true" />
<text class="empty-text">{{ $t('home.noNearbyDevice') }}</text> <text class="empty-text">{{ $t('home.noNearbyDevice') }}</text>
</view> </view>
</view> </view>
@@ -40,7 +40,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from '@/utils/i18n.js' import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n() const { t } = useI18n()
const props = defineProps({ const props = defineProps({
show: { type: Boolean, default: false }, show: { type: Boolean, default: false },
+155 -94
View File
@@ -1,9 +1,11 @@
<template> <template>
<view class="map-container" :class="{ 'full-width': props.fullWidth }" :style="{ '--map-height': props.customHeight || '78vh' }"> <view class="map-container" :class="{ 'full-width': props.fullWidth }">
<!-- 地图容器 --> <!-- 地图容器 -->
<view class="map-wrapper"> <view class="map-wrapper">
<!-- 支付宝小程序地图组件使用高德地图 --> <!-- 使用小程序原生地图组件 -->
<map id="map" class="native-map" <map
id="map"
class="native-map"
:longitude="mapCenter.longitude" :longitude="mapCenter.longitude"
:latitude="mapCenter.latitude" :latitude="mapCenter.latitude"
:markers="mapMarkers" :markers="mapMarkers"
@@ -11,42 +13,81 @@
:show-location="false" :show-location="false"
@regionchange="onMapRegionChange" @regionchange="onMapRegionChange"
@markertap="onMapMarkerTap" @markertap="onMapMarkerTap"
@tap="onMapTap" @callouttap="onCalloutTap"
@updated="onMapUpdated" @updated="onMapUpdated"
@error="onMapError"> @error="onMapError"
</map> >
<!-- 覆盖在地图上的广告轮播使用 cover-view 以兼容小程序原生组件层级 -->
<!-- 覆盖在地图上的广告轮播使用 cover-view 以兼容小程序原生组件层级 --> <cover-view
<cover-view class="index-swiper" v-if="!props.hideControls && !props.hideMapOverlays && currentBannerImage"> class="index-swiper"
<cover-image :src="currentBannerImage" class="index-swiper-img" mode="aspectFill" @tap="handleBannerTap"></cover-image> v-if="!props.hideControls && !props.hideMapOverlays && currentBannerImage"
<!-- 轮播指示器 --> >
<cover-view class="banner-indicators" v-if="props.bannerImages.length > 1"> <cover-image
:src="currentBannerImage"
class="index-swiper-img"
mode="aspectFill"
@tap="handleBannerTap"
></cover-image>
<!-- 轮播指示器 -->
<cover-view <cover-view
v-for="(img, idx) in props.bannerImages" class="banner-indicators"
:key="idx" v-if="props.bannerImages.length > 1"
class="indicator-dot" >
:class="{ active: idx === currentBannerIndex }"> <cover-view
v-for="(img, idx) in props.bannerImages"
:key="idx"
class="indicator-dot"
:class="{ active: idx === currentBannerIndex }"
>
</cover-view>
</cover-view> </cover-view>
</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>
<!-- 地图侧边控制按钮重定位客服中心查看附近设备 --> <!-- 地图中心固定定位图标 -->
<cover-view class="map-side-controls" v-if="!props.hideControls && !props.hideMapOverlays"> <cover-view
<cover-view class="side-btn locate" @tap="handleRelocate"> class="center-location-marker"
<cover-image class="side-icon" src="/static/location.png"></cover-image> v-if="!props.hideMapOverlays"
>
<cover-image
src="/static/location-icon.png"
class="center-marker-icon"
></cover-image>
</cover-view> </cover-view>
<cover-view class="side-btn service" @tap="handleService">
<cover-image class="side-icon" src="/static/customer-service.png"></cover-image>
<!-- 侧边控制按钮 -->
<cover-view
class="map-side-controls"
v-if="!props.hideControls && !props.hideMapOverlays"
>
<cover-view class="side-btn guide" @tap="handleGuide">
<cover-image
class="side-icon"
src="/static/use_help.png"
style="border-radius: 50%;"
></cover-image>
</cover-view>
<cover-view class="side-btn 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-view>
<cover-view class="side-btn service" @tap="handleService">
<cover-image
class="side-icon"
src="/static/customer-service.png"
></cover-image>
</cover-view>
</cover-view> </cover-view>
<cover-view class="side-btn search" @tap="handleSearch"> </map>
<cover-image class="side-icon" src="/static/other_device.png"></cover-image>
</cover-view>
</cover-view>
<!-- 地图加载状态 --> <!-- 地图加载状态 -->
<view class="map-loading" v-if="isLoading"> <view class="map-loading" v-if="isLoading">
@@ -77,7 +118,7 @@
import { useI18n } from '../utils/i18n.js' import { useI18n } from '../utils/i18n.js'
// 获取 i18n 实例 // 获取 i18n 实例
const { t: $t } = useI18n() const { t } = useI18n()
// 引用折叠面板组件的ref // 引用折叠面板组件的ref
const collapseRef = ref(null) const collapseRef = ref(null)
@@ -137,7 +178,8 @@
'showList', 'showList',
'markerTap', 'markerTap',
'mapCenterChange', 'mapCenterChange',
'bannerClick' 'bannerClick',
'guide'
]) ])
// 响应式数据 // 响应式数据
@@ -254,45 +296,14 @@
deep: true 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) => { watch(() => props.bannerImages, (newImages, oldImages) => {
console.log('广告图片变化:', newImages?.length, '张')
// 先停止旧的轮播 // 先停止旧的轮播
stopBannerRotation() stopBannerRotation()
currentBannerIndex.value = 0 currentBannerIndex.value = 0
// 如果有多张图片,启动新的轮播 // 如果有多张图片,启动新的轮播
if (newImages && newImages.length > 1) { if (newImages && newImages.length > 1) {
console.log('启动广告轮播,共', newImages.length, '张图片')
// 使用 nextTick 确保 DOM 已更新 // 使用 nextTick 确保 DOM 已更新
nextTick(() => { nextTick(() => {
startBannerRotation() startBannerRotation()
@@ -308,22 +319,31 @@
isLoading.value = false isLoading.value = false
} }
// 地图区域变化事件(支付宝小程序,带防抖优化) // 地图区域变化事件(带防抖优化)
const onMapRegionChange = (e) => { const onMapRegionChange = (e) => {
if (!e) {
// 只处理结束事件
if (!e || (e.type !== 'end' && e.type !== 'regionchange')) {
return 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
if (causedBy === 'gesture' || causedBy === 'scale' || causedBy === 'drag' || causedBy === 'update') { // H5 环境下可能没有 causedBy,只要是 end 事件就处理
const isH5 = false;
// #ifdef H5
const h5Status = true;
// #endif
if (causedBy === 'gesture' || causedBy === 'scale' || causedBy === 'drag' || causedBy === 'update' || (typeof h5Status !== 'undefined' && e.type === 'end')) {
// 清除之前的定时器 // 清除之前的定时器
if (regionChangeTimer) { if (regionChangeTimer) {
clearTimeout(regionChangeTimer) clearTimeout(regionChangeTimer)
} }
// 直接从事件对象中获取最新的中心点位置
const centerLocation = e.detail?.centerLocation || e.centerLocation
if (centerLocation && centerLocation.longitude && centerLocation.latitude) { if (centerLocation && centerLocation.longitude && centerLocation.latitude) {
// 防抖:500ms后执行查询 // 防抖:500ms后执行查询
regionChangeTimer = setTimeout(() => { regionChangeTimer = setTimeout(() => {
@@ -333,11 +353,13 @@
} }
mapCenter.value = newCenter; mapCenter.value = newCenter;
// 触发父组件查询新位置的场地 // 触发父组件查询新位置的场地
emit('mapCenterChange', newCenter) emit('mapCenterChange', newCenter)
}, 500) }, 500)
} else { } else {
// 兜底方案:使用API获取地图中心 // 兜底方案:如果事件中没有centerLocation,才使用API获取
regionChangeTimer = setTimeout(() => { regionChangeTimer = setTimeout(() => {
if (mapContext.value) { if (mapContext.value) {
mapContext.value.getCenterLocation({ mapContext.value.getCenterLocation({
@@ -361,17 +383,12 @@
} }
} }
// 标记点点击事件(支付宝小程序) // 标记点点击事件
const onMapMarkerTap = (e) => { const onMapMarkerTap = (e) => {
if (!e) { const markerId = e.detail?.markerId || e.markerId
return
}
// 获取markerId
const markerId = e.detail?.markerId || e.markerId || e.detail?.marker?.id
// 查找对应的场地位置信息 // 查找对应的场地位置信息
if (props.filteredPositions && props.filteredPositions.length > 0 && markerId) { if (props.filteredPositions && props.filteredPositions.length > 0) {
const position = props.filteredPositions[markerId - 1] const position = props.filteredPositions[markerId - 1]
if (position) { if (position) {
emit('markerTap', position) emit('markerTap', position)
@@ -379,9 +396,14 @@
} }
} }
// 地图点击事件(支付宝小程序) // 标记点气泡点击事件
const onMapTap = (e) => { const onCalloutTap = (e) => {
console.log('地图点击事件:', e) const markerId = e.markerId
const marker = mapMarkers.value.find(item => item.id === markerId)
if (marker && marker.position) {
emit('markerTap', marker.position)
}
} }
// 地图错误事件 // 地图错误事件
@@ -405,23 +427,49 @@ const handleSearch = () => {
const handleService = () => { const handleService = () => {
uni.navigateTo({ uni.navigateTo({
url: '/pages/help/index' url: '/subPackages/service/help/index'
}) })
} }
const handleGuide = () => {
emit('guide')
}
const handleJoinTap = () => { const handleJoinTap = () => {
uni.navigateTo({ uni.navigateTo({
url: '/pages/join/index' url: '/subPackages/business/join/index'
}) })
} }
// 处理广告点击 // 处理广告点击
const handleBannerTap = () => { const handleBannerTap = () => {
console.log('点击地图广告:', currentBannerIndex.value, currentBannerImage.value)
// 触发父组件处理点击事件 // 触发父组件处理点击事件
emit('bannerClick', currentBannerIndex.value) emit('bannerClick', currentBannerIndex.value)
// 默认跳转到合作加盟页面 }
handleJoinTap()
// 启动广告轮播
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
}
} }
const handleScan = () => { const handleScan = () => {
@@ -454,7 +502,6 @@ const handleSearch = () => {
// 初始化广告轮播 // 初始化广告轮播
if (props.bannerImages && props.bannerImages.length > 1) { if (props.bannerImages && props.bannerImages.length > 1) {
console.log('onMounted: 初始化广告轮播')
startBannerRotation() startBannerRotation()
} }
}) })
@@ -495,22 +542,35 @@ const handleSearch = () => {
/* 地图容器 */ /* 地图容器 */
.map-container { .map-container {
flex: 1; flex: 1;
position: relative;
// position: fixed; // position: fixed;
// top: 0; // top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
width: 94vw; width: 94vw;
height: calc(100% - 20rpx); /* 减少高度,避免覆盖底部按钮 */ // height: var(--map-height, calc(100% - 20rpx)); /* 使用变量或默认高度 */
margin: 20rpx; margin: 20rpx;
margin-bottom: 0; /* 底部不需要边距 */ margin-bottom: 0; /* 底部不需要边距 */
border-radius: 20rpx; border-radius: 20rpx;
overflow: hidden; overflow: hidden;
display: flex;
flex-direction: column;
// #ifdef H5
height: 78vh;
// #endif
// #ifdef MP-WEIXIN
height: 72vh;
// #endif
&.full-width { &.full-width {
width: 100%; width: 100%;
margin: 0; margin: 0;
border-radius: 0; border-radius: 0;
height: 100%;
} }
.map-wrapper { .map-wrapper {
@@ -599,14 +659,14 @@ const handleSearch = () => {
// min-width: 160rpx; // min-width: 160rpx;
margin: auto; margin: auto;
// height: 72rpx; // height: 72rpx;
// background: rgba(255, 255, 255, 0.96); background: rgba(255, 255, 255, 0.96);
border-radius: 36rpx; border-radius: 24rpx;
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
// box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.12); // box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.12);
padding: 20rpx; padding: 13rpx;
border: 2rpx solid #e0e0e0; border: 2rpx solid #e0e0e0;
&:active { &:active {
@@ -638,8 +698,8 @@ const handleSearch = () => {
} }
.side-icon { .side-icon {
width: 44rpx; width: 40rpx;
height: 44rpx; height: 40rpx;
} }
.index-swiper { .index-swiper {
@@ -671,6 +731,7 @@ const handleSearch = () => {
justify-content: center; justify-content: center;
gap: 8rpx; gap: 8rpx;
z-index: 2; z-index: 2;
pointer-events: none;
.indicator-dot { .indicator-dot {
width: 12rpx; width: 12rpx;
+687
View File
@@ -0,0 +1,687 @@
<template>
<view class="map-container" :class="{ 'full-width': props.fullWidth }">
<!-- 地图容器 -->
<view class="map-wrapper">
<!-- 使用小程序原生地图组件 -->
<map
id="map"
class="native-map"
:longitude="mapCenter.longitude"
:latitude="mapCenter.latitude"
:markers="mapMarkers"
:scale="mapZoom"
:show-location="false"
@regionchange="onMapRegionChange"
@markertap="onMapMarkerTap"
@callouttap="onCalloutTap"
@updated="onMapUpdated"
@error="onMapError"
></map>
<!-- 支付宝小程序所有 cover-image cover-view 必须放在 map 标签外部 -->
<!-- 广告轮播直接使用 cover-image不能嵌套在 cover-view -->
<cover-image
v-if="!props.hideControls && !props.hideMapOverlays && currentBannerImage"
:src="currentBannerImage"
class="index-swiper-img-alipay"
mode="aspectFill"
@tap="handleBannerTap"
></cover-image>
<!-- 轮播指示器每个点都是独立的 cover-view避免嵌套 -->
<template v-if="!props.hideControls && !props.hideMapOverlays && props.bannerImages.length > 1">
<cover-view
v-for="(img, idx) in props.bannerImages"
:key="idx"
class="indicator-dot-alipay"
:class="{ active: idx === currentBannerIndex }"
:style="{
left: `calc(50% + ${(idx - (props.bannerImages.length - 1) / 2) * 20}rpx)`,
transform: 'translateX(-50%)'
}"
>
</cover-view>
</template>
<!-- 地图中心固定定位图标使用 cover-view 嵌套 cover-image -->
<cover-view
class="center-location-marker-alipay"
v-if="!props.hideMapOverlays"
>
<cover-image
src="/static/location-icon.png"
class="center-marker-icon-alipay"
></cover-image>
</cover-view>
<!-- 侧边控制按钮使用 cover-view 嵌套 cover-image -->
<cover-view
class="map-side-controls-alipay"
v-if="!props.hideControls && !props.hideMapOverlays"
>
<cover-view class="side-btn-alipay guide-alipay" @tap="handleGuide">
<cover-image
class="side-icon-alipay"
src="/static/use_help.png"
style="border-radius: 50%;"
></cover-image>
</cover-view>
<cover-view class="side-btn-alipay locate-alipay" @tap="handleRelocate">
<cover-image
class="side-icon-alipay"
src="/static/location.png"
></cover-image>
</cover-view>
<cover-view class="side-btn-alipay search-alipay" @tap="handleSearch">
<cover-image
class="side-icon-alipay"
src="/static/other_device.png"
></cover-image>
</cover-view>
<cover-view class="side-btn-alipay service-alipay" @tap="handleService">
<cover-image
class="side-icon-alipay"
src="/static/customer-service.png"
></cover-image>
</cover-view>
</cover-view>
<!-- 地图加载状态 -->
<view class="map-loading" v-if="isLoading">
<view class="loading-content">
<view class="loading-spinner"></view>
<text>{{ $t('common.loadingMap') }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
getCurrentInstance
} from 'vue'
// 导入地图工具函数
import {
calculateDistanceSync
} from '../utils/mapUtils.js'
// 导入国际化
import { useI18n } from '../utils/i18n.js'
// 获取 i18n 实例
const { t } = useI18n()
// 引用折叠面板组件的ref
const collapseRef = ref(null)
// Props
const props = defineProps({
userLocation: {
type: Object,
default: null
},
positionList: {
type: Array,
default: () => []
},
filteredPositions: {
type: Array,
default: () => []
},
searchKeyword: {
type: String,
default: ''
},
noticeText: {
type: String,
default: ''
},
enableMarkers: {
type: Boolean,
default: false
},
customHeight: {
type: String,
default: '' // 自定义高度,如 '48vh', '400rpx' 等
},
hideControls: {
type: Boolean,
default: false // 是否隐藏侧边控制按钮
},
fullWidth: {
type: Boolean,
default: false // 是否全宽显示(去掉 margin 和固定宽度)
},
hideMapOverlays: {
type: Boolean,
default: false // 是否隐藏地图上的覆盖层元素(如中心定位图标、轮播图等)
},
bannerImages: {
type: Array,
default: () => [] // 广告图片列表
}
})
// Emits
const emit = defineEmits([
'relocate',
'scan',
'showList',
'markerTap',
'mapCenterChange',
'bannerClick',
'guide'
])
// 响应式数据
const isLoading = ref(true)
const mapCenter = ref({
longitude: 116.397128,
latitude: 39.916527
})
const mapZoom = ref(17)
const mapMarkers = ref([]) // 用于地图组件的markers
const mapContext = ref(null) // 地图上下文
const currentBannerIndex = ref(0) // 当前显示的广告索引
let bannerTimer = null // 广告轮播定时器
// 计算当前显示的广告图片
const currentBannerImage = computed(() => {
if (props.bannerImages && props.bannerImages.length > 0) {
return props.bannerImages[currentBannerIndex.value]
}
// 降级:如果没有广告,显示默认图片
return ''
})
// 验证坐标有效性
const isValidCoordinate = (lat, lng) => {
const latitude = parseFloat(lat)
const longitude = parseFloat(lng)
return !isNaN(latitude) && !isNaN(longitude) &&
latitude >= -90 && latitude <= 90 &&
longitude >= -180 && longitude <= 180 &&
!(latitude === 0 && longitude === 0) // 排除 0,0 这种无效坐标
}
// 防抖定时器
let regionChangeTimer = null
// 方法
const updateMapMarkers = () => {
const markers = []
// 只添加周边场地位置点,中心定位图标改用固定的cover-view显示
if (props.enableMarkers && props.filteredPositions && props.filteredPositions.length > 0) {
props.filteredPositions.forEach((pos, index) => {
if (pos.longitude && pos.latitude && isValidCoordinate(pos.latitude, pos.longitude)) {
const lat = parseFloat(pos.latitude)
const lng = parseFloat(pos.longitude)
markers.push({
id: index + 1,
latitude: lat,
longitude: lng,
iconPath: '/static/markes_fdz.png',
width: 40,
height: 40,
callout: {
content: pos.name,
fontSize: 12,
borderRadius: 8,
bgColor: '#ffffff',
padding: 8,
display: 'BYCLICK'
}
})
}
})
}
mapMarkers.value = markers
isLoading.value = false
}
// 移动地图到指定位置(直接更新地图中心坐标)
const moveToLocation = (location) => {
if (!location || !location.longitude || !location.latitude) {
return
}
const newCenter = {
longitude: Number(location.longitude),
latitude: Number(location.latitude)
}
// 直接更新地图中心,触发地图组件的视图更新
mapCenter.value = newCenter
// 更新标记点
updateMapMarkers()
}
// 监听用户位置变化
watch(() => props.userLocation, (newLocation, oldLocation) => {
if (newLocation && newLocation.longitude && newLocation.latitude) {
// 检查位置是否真的变化了(避免重复更新)
const isChanged = !oldLocation ||
oldLocation.longitude !== newLocation.longitude ||
oldLocation.latitude !== newLocation.latitude
if (isChanged) {
mapCenter.value = {
longitude: newLocation.longitude,
latitude: newLocation.latitude
}
updateMapMarkers()
}
}
}, {
immediate: true,
deep: true
})
// 监听位置列表变化
watch(() => props.filteredPositions, (newPositions) => {
updateMapMarkers()
}, {
deep: true
})
// 监听广告图片变化,启动或停止轮播
watch(() => props.bannerImages, (newImages, oldImages) => {
// 先停止旧的轮播
stopBannerRotation()
currentBannerIndex.value = 0
// 如果有多张图片,启动新的轮播
if (newImages && newImages.length > 1) {
// 使用 nextTick 确保 DOM 已更新
nextTick(() => {
startBannerRotation()
})
}
}, {
immediate: true,
deep: true
})
// 地图加载完成事件
const onMapUpdated = () => {
isLoading.value = false
}
// 地图区域变化事件(带防抖优化)
const onMapRegionChange = (e) => {
// 只处理结束事件
if (!e || (e.type !== 'end' && e.type !== 'regionchange')) {
return
}
const causedBy = e.causedBy || e.detail?.causedBy
if (causedBy === 'gesture' || causedBy === 'scale' || causedBy === 'drag' || causedBy === 'update') {
// 清除之前的定时器
if (regionChangeTimer) {
clearTimeout(regionChangeTimer)
}
// 直接从事件对象中获取最新的中心点位置
const centerLocation = e.detail?.centerLocation || e.centerLocation
if (centerLocation && centerLocation.longitude && centerLocation.latitude) {
// 防抖:500ms后执行查询
regionChangeTimer = setTimeout(() => {
const newCenter = {
longitude: Number(centerLocation.longitude),
latitude: Number(centerLocation.latitude)
}
mapCenter.value = newCenter;
// 触发父组件查询新位置的场地
emit('mapCenterChange', newCenter)
}, 500)
} else {
// 兜底方案:如果事件中没有centerLocation,才使用API获取
regionChangeTimer = setTimeout(() => {
if (mapContext.value) {
mapContext.value.getCenterLocation({
success: (res) => {
if (res && res.longitude && res.latitude) {
const newCenter = {
longitude: res.longitude,
latitude: res.latitude
}
mapCenter.value = newCenter
emit('mapCenterChange', newCenter)
}
},
fail: (err) => {
console.error('获取地图中心失败:', err)
}
})
}
}, 500)
}
}
}
// 标记点点击事件
const onMapMarkerTap = (e) => {
const markerId = e.detail?.markerId || e.markerId
// 查找对应的场地位置信息
if (props.filteredPositions && props.filteredPositions.length > 0) {
const position = props.filteredPositions[markerId - 1]
if (position) {
emit('markerTap', position)
}
}
}
// 标记点气泡点击事件
const onCalloutTap = (e) => {
const markerId = e.markerId
const marker = mapMarkers.value.find(item => item.id === markerId)
if (marker && marker.position) {
emit('markerTap', marker.position)
}
}
// 地图错误事件
const onMapError = (error) => {
console.error('地图加载失败:', error)
isLoading.value = false
}
const handleRelocate = () => {
// 直接委托父级处理定位并移动地图,避免内部重复弹 loading
try {
emit('relocate')
} catch (e) {}
}
const handleSearch = () => {
try {
uni.navigateTo({ url: '/pages/search/index' })
} catch (e) {}
}
const handleService = () => {
uni.navigateTo({
url: '/subPackages/service/help/index'
})
}
const handleGuide = () => {
emit('guide')
}
// 处理广告点击
const handleBannerTap = () => {
// 触发父组件处理点击事件
emit('bannerClick', currentBannerIndex.value)
}
// 启动广告轮播
const startBannerRotation = () => {
// 如果只有一张或没有图片,不需要轮播
if (!props.bannerImages || props.bannerImages.length <= 1) {
return
}
// 清除旧的定时器
stopBannerRotation()
// 每3秒切换一次
bannerTimer = setInterval(() => {
const nextIndex = (currentBannerIndex.value + 1) % props.bannerImages.length
currentBannerIndex.value = nextIndex
}, 3000)
}
// 停止广告轮播
const stopBannerRotation = () => {
if (bannerTimer) {
clearInterval(bannerTimer)
bannerTimer = null
}
}
// 生命周期钩子
onMounted(() => {
// 初始化地图上下文
nextTick(() => {
// 需要使用nextTick确保地图组件已经渲染
const inst = getCurrentInstance()
const vm = (inst && (inst.proxy || inst)) || undefined
try {
mapContext.value = uni.createMapContext('map', vm)
} catch (e) {
// 兼容:如果第二参不被支持,退回单参
mapContext.value = uni.createMapContext('map')
}
updateMapMarkers()
// 初始化折叠面板
if (collapseRef.value) {
collapseRef.value.init()
}
// 初始化广告轮播
if (props.bannerImages && props.bannerImages.length > 1) {
startBannerRotation()
}
})
})
onUnmounted(() => {
// 清理工作
if (regionChangeTimer) {
clearTimeout(regionChangeTimer)
regionChangeTimer = null
}
// 停止广告轮播
stopBannerRotation()
mapContext.value = null
})
// 暴露给父组件的方法
defineExpose({
mapCenter: computed(() => mapCenter.value),
moveToLocation,
updateMapMarkers,
initCollapse: () => {
if (collapseRef.value) {
collapseRef.value.init()
}
}
})
</script>
<style lang="scss" scoped>
/* 地图容器 */
.map-container {
flex: 1;
position: relative;
left: 0;
right: 0;
bottom: 0;
width: 94vw;
margin: 20rpx;
margin-bottom: 0; /* 底部不需要边距 */
border-radius: 20rpx;
overflow: hidden;
display: flex;
flex-direction: column;
height: 72vh;
&.full-width {
width: 100%;
margin: 0;
border-radius: 0;
height: 100%;
}
.map-wrapper {
width: 100%;
height: 100%;
position: relative;
overflow: visible; /* 支付宝小程序:允许覆盖层显示在地图外部 */
border-radius: 0;
.native-map {
width: 100%;
height: 100%;
display: block;
border-radius: 0;
}
}
.map-loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 8rpx solid #f3f3f3;
border-top: 8rpx solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
text {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
}
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* 支付宝小程序专用样式 - 所有覆盖层元素相对于 map-wrapper 定位 */
/* 广告图片样式 */
.index-swiper-img-alipay {
position: absolute;
top: 20rpx;
left: 50%;
transform: translateX(-50%);
width: 90vw;
height: 120rpx;
border-radius: 20rpx;
z-index: 100; /* 确保在地图之上 */
}
/* 轮播指示器独立定位 */
.indicator-dot-alipay {
position: absolute;
top: 130rpx; /* 广告图片 top(20rpx) + height(120rpx) - 10rpx = 130rpx */
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.5);
transition: all 0.3s ease;
pointer-events: none;
z-index: 101; /* 确保在广告图片之上 */
&.active {
width: 24rpx;
border-radius: 6rpx;
background-color: rgba(255, 255, 255, 0.9);
}
}
/* 地图中心定位图标样式 */
.center-location-marker-alipay {
position: absolute;
left: 50%;
top: 50%;
z-index: 99; /* 确保在地图之上 */
width: 60rpx;
height: 80rpx;
margin-left: -30rpx;
margin-top: -80rpx;
pointer-events: none;
background: transparent;
.center-marker-icon-alipay {
width: 60rpx;
height: 60rpx;
display: block;
}
}
/* 侧边控制按钮容器 */
.map-side-controls-alipay {
position: absolute;
right: 20rpx;
bottom: 160rpx; /* 向上移动,避免被底部按钮遮挡 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
height: 300rpx;
margin: auto;
gap: 12rpx;
z-index: 102; /* 确保在最上层 */
background: transparent;
.side-btn-alipay {
margin: auto;
background: rgba(255, 255, 255, 0.96);
border-radius: 24rpx;
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 13rpx;
border: 2rpx solid #e0e0e0;
&:active {
transform: scale(0.95);
}
.side-icon-alipay {
width: 40rpx;
height: 40rpx;
z-index:1000;
}
}
}
</style>
+37 -18
View File
@@ -8,22 +8,28 @@
</view> </view>
<view class="header-right"> <view class="header-right">
<!-- 支付方式标识移到头部右侧 --> <!-- 支付方式标识移到头部右侧 -->
<view class="payment-badge alipay-score" v-if="order.payWay == 'alipay_score_pay'"> <view class="payment-badge wx-score" v-if="order.payWay == 'wx_score_pay'">
<image src="/static/images/alipay.svg" mode="aspectFit" class="badge-icon"></image> <image src="/static/images/wxpayflag.png" mode="aspectFit" class="badge-icon" lazy-load="true"></image>
<view class="badge-text"> <view class="badge-text">
<text>{{ $t('order.alipayScore') }}</text> <text>{{ $t('order.wxPayScore') }}</text>
<text class="divider">|</text> <text class="divider">|</text>
<text class="highlight">{{ $t('order.depositFree') }}</text> <text class="highlight">{{ $t('order.depositFree') }}</text>
</view> </view>
</view> </view>
<view class="payment-badge whitelist" v-else-if="order.payWay == 'alipay_global_pay'"> <view class="payment-badge whitelist" v-else-if="order.payWay == 'wx_global_pay'">
<text class="badge-text">{{ $t('order.whitelistOrder') }}</text> <text class="badge-text">{{ $t('order.whitelistOrder') }}</text>
</view> </view>
<view class="payment-badge member" v-else-if="order.payWay == 'alipay_member_pay'"> <view class="payment-badge member" v-else-if="order.payWay == 'wx_member_pay'">
<text class="badge-text">{{ $t('order.memberOrder') }}</text> <text class="badge-text">{{ $t('order.memberOrder') }}</text>
</view> </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> <view class="payment-badge deposit" v-else>
<text class="badge-text">{{ $t('order.alipayPay') }}</text> <text class="badge-text">{{ $t('order.wxPay') }}</text>
<text class="divider">|</text> <text class="divider">|</text>
<text class="badge-text">{{ $t('order.depositPay') }}</text> <text class="badge-text">{{ $t('order.depositPay') }}</text>
</view> </view>
@@ -63,16 +69,16 @@
<view class="order-footer"> <view class="order-footer">
<view class="footer-left"> <view class="footer-left">
<view v-if="isInUse" class="renting"> <view v-if="isInUse" class="renting">
<image src="/static/order_time.png" mode="aspectFit" class="icon-time"></image> <image src="/static/order_time.png" mode="aspectFit" class="icon-time" lazy-load="true"></image>
{{ $t('order.renting') }} {{ $t('order.renting') }}
</view> </view>
<view v-else-if="isFinished" class="meta"> <view v-else-if="isFinished" class="meta">
<view class="meta-item"> <view class="meta-item">
<image src="/static/order_time.png" mode="aspectFit" class="icon-time"></image> <image src="/static/order_time.png" mode="aspectFit" class="icon-time" lazy-load="true"></image>
{{ usedDurationText }} {{ usedDurationText }}
</view> </view>
<view class="meta-item"> <view class="meta-item">
<image src="/static/order_price.png" mode="aspectFit" class="icon-price"></image> <image src="/static/order_price.png" mode="aspectFit" class="icon-price" lazy-load="true"></image>
{{ displayAmount }} {{ displayAmount }}
</view> </view>
</view> </view>
@@ -98,7 +104,7 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from '@/utils/i18n.js' import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n() const { t } = useI18n()
const props = defineProps({ const props = defineProps({
order: { type: Object, required: true }, order: { type: Object, required: true },
@@ -107,7 +113,7 @@
const emit = defineEmits(['pay', 'cancel', 'return-device', 'details']); const emit = defineEmits(['pay', 'cancel', 'return-device', 'details']);
const rawStatus = computed(() => props.order.orderStatus != null ? props.order.orderStatus : props.order.status); const rawStatus = computed(() => props.order.orderStatus ?? props.order.status);
const normalizedStatus = computed(() => { const normalizedStatus = computed(() => {
const s = rawStatus.value; const s = rawStatus.value;
switch (s) { switch (s) {
@@ -128,9 +134,19 @@
} }
}); });
const hasPauseTime = computed(() => {
const pt = props.order.pauseTime
return pt !== undefined && pt !== null && String(pt).trim() !== ''
})
const isBillingPaused = computed(() => normalizedStatus.value === 'in_used' && hasPauseTime.value)
const statusDef = computed(() => props.orderStatusMap?.[rawStatus.value] || props.orderStatusMap?.[normalizedStatus.value] || {}); const statusDef = computed(() => props.orderStatusMap?.[rawStatus.value] || props.orderStatusMap?.[normalizedStatus.value] || {});
const statusText = computed(() => statusDef.value.text || ''); const statusText = computed(() => {
if (isBillingPaused.value) return t('order.orderStatusBillingPaused')
return statusDef.value.text || ''
});
const statusChipClass = computed(() => { const statusChipClass = computed(() => {
if (isBillingPaused.value) return 'chip-paused'
const cls = statusDef.value.class || ''; const cls = statusDef.value.class || '';
if (cls.includes('status-using')) return 'chip-using'; if (cls.includes('status-using')) return 'chip-using';
if (cls.includes('status-waiting')) return 'chip-waiting'; if (cls.includes('status-waiting')) return 'chip-waiting';
@@ -144,7 +160,7 @@
const isFinished = computed(() => normalizedStatus.value === 'used_done'); const isFinished = computed(() => normalizedStatus.value === 'used_done');
const isCancelled = computed(() => normalizedStatus.value === 'order_cancelled'); 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'); const displayAmount = computed(() => props.order.amount || props.order.payAmount || props.order.actualDeviceAmount || props.order.currentFee || '0.00');
@@ -158,8 +174,10 @@
const minutes = Math.floor(diffMs / 60000); const minutes = Math.floor(diffMs / 60000);
const hours = Math.floor(minutes / 60); const hours = Math.floor(minutes / 60);
const mins = minutes % 60; const mins = minutes % 60;
if (hours > 0) return `${hours}${$t('time.hour')}${mins}${$t('time.minute')}`; if (hours > 0) return `${hours}${t('time.hour')}${mins}${t('time.minute')}`;
return `${mins}${$t('time.minute')}`; // 如果小于1分钟,显示"小于1分钟"
if (minutes < 1) return `${t('time.lessThan')}1${t('time.minute')}`;
return `${mins}${t('time.minute')}`;
}); });
function parseDate(str) { function parseDate(str) {
@@ -204,6 +222,7 @@
overflow: hidden; overflow: hidden;
.chip-text { display: inline-block; transform: skewX(15deg); } .chip-text { display: inline-block; transform: skewX(15deg); }
&.chip-using { background: rgba(7,193,96,0.12); color: #07c160; } &.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-waiting { background: rgba(255,152,0,0.12); color: #FF9800; }
&.chip-finished { background: rgba(76,175,80,0.12); color: #4CAF50; } &.chip-finished { background: rgba(76,175,80,0.12); color: #4CAF50; }
&.chip-cancelled { background: rgba(158,158,158,0.12); color: #9E9E9E; } &.chip-cancelled { background: rgba(158,158,158,0.12); color: #9E9E9E; }
@@ -272,8 +291,8 @@
border-radius: 8rpx; border-radius: 8rpx;
white-space: nowrap; white-space: nowrap;
&.alipay-score { &.wx-score {
background: rgba(0, 122, 255, 0.08); background: rgba(7, 193, 96, 0.08);
.badge-icon { .badge-icon {
width: 32rpx; width: 32rpx;
@@ -283,7 +302,7 @@
.badge-text { .badge-text {
font-size: 22rpx; font-size: 22rpx;
color: #007AFF; color: #07c160;
display: flex; display: flex;
align-items: center; align-items: center;
+5021
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
import request from '../http'
// 按场地查询可用优惠券
export const getCouponsByPosition = (positionId) => {
return request({
url: `/device/coupon/app/position/${positionId}`,
method: 'get'
})
}
// 创建优惠券支付订单
export const createCouponPayment = (couponId, paymentPlatform) => {
return request({
url: '/app/coupon/pay',
method: 'post',
data: {
couponId,
// 支付平台类型:WECHAT / ALIPAY / ANTOM(不传则后端默认 WECHAT
...(paymentPlatform ? { paymentPlatform } : {})
}
})
}
// 查询用户优惠券
export const getUserCoupons = (status) => {
return request({
url: '/device/userPurchase/app/my',
method: 'get',
data: {
status
}
})
}
export const cancelCouponPayment = (orderNo) => {
return request({
url: `/device/userPurchase/app/cancel/${orderNo}`,
method: 'post'
})
}
+3 -21
View File
@@ -50,32 +50,14 @@ export const transformDeviceData = (device) => {
} }
// 立即租借 // 立即租借
export const rentPowerBank = (deviceNo, phone) => { export const rentPowerBank = (deviceNo, phone,payway) => {
return request({ return request({
url: `/app/device/rentPowerBank?deviceNo=${deviceNo}`, url: `/app/device/rentPowerBank?deviceNo=${deviceNo}`,
method: 'post', method: 'post',
data: { data: {
// deviceNo, // deviceNo,
phone phone,
payway
} }
}) })
} }
// 确认支付并弹出风扇
export const confirmPaymentAndRent = (orderId) => {
console.log(`确认支付并弹出风扇, orderId: ${orderId}`)
return request({
url: `/app/device/confirmPaymentAndRent?orderId=${orderId}`,
method: 'GET'
})
}
// 强制打开空格子
export const forcefOpenEmptyGrid = (deviceNo) => {
console.log(`强制打开空格子, deviceNo: ${deviceNo}`)
return request({
url: `/app/device/forcef/${deviceNo}`,
method: 'post'
})
}
+41
View File
@@ -0,0 +1,41 @@
import request from '../http'
// 创建会员卡支付订单
export const createMemberCardPayment = (memberCardId, paymentPlatform) => {
return request({
url: '/app/member/pay',
method: 'post',
data: {
memberCardId,
// 支付平台类型:WECHAT / ALIPAY / ANTOM(不传则后端默认 WECHAT
...(paymentPlatform ? { paymentPlatform } : {})
}
})
}
// 根据场地ID查询可用会员卡列表
export const getMemberCardsByPosition = (positionId) => {
return request({
url: `/device/memberCard/app/position/${positionId}`,
method: 'get'
})
}
//根据状态查询个人会员卡
export const getMemberCardsByStatus = () => {
return request({
url: `/device/userMemberCard/app/my`,
method: 'get'
})
}
// 取消会员卡支付订单
export const cancelMemberCardPayment = (orderNo) => {
return request({
url: `/device/userMemberCard/app/cancel/${orderNo}`,
method: 'post'
})
}
+164 -17
View File
@@ -10,6 +10,16 @@ export const getOrderList = (data) => {
}) })
} }
// 用户端查询商品订单列表
export const getProductOrderList = (data) => {
return request({
url: '/app/product/order/list',
method: 'get',
data,
hideLoading: true
})
}
// 查询是否有订单 // 查询是否有订单
export const queryHasOrder = (deviceNo) => { export const queryHasOrder = (deviceNo) => {
return request({ return request({
@@ -41,7 +51,6 @@ export const createOrder = (data) => {
// 查询订单 // 查询订单
export const queryById = (id) => { export const queryById = (id) => {
console.log(`查询订单详情, orderId: ${id}`)
return request({ return request({
url: `/app/order/${id}`, url: `/app/order/${id}`,
method: 'get', method: 'get',
@@ -49,6 +58,15 @@ export const queryById = (id) => {
}) })
} }
// 用户查询商品订单详情
export const getProductOrderDetail = (id) => {
return request({
url: `/app/product/order/${id}`,
method: 'get',
hideLoading: true
})
}
// 取消订单 // 取消订单
export const cancelOrder = (data) => { export const cancelOrder = (data) => {
return request({ return request({
@@ -60,7 +78,6 @@ export const cancelOrder = (data) => {
// 结束订单 // 结束订单
export const overOrderById = (orderId) => { export const overOrderById = (orderId) => {
console.log(`调用结束订单API, orderId: ${orderId}`)
return request({ return request({
url: `/app/order/close/${orderId}`, url: `/app/order/close/${orderId}`,
method: 'get', method: 'get',
@@ -76,21 +93,141 @@ export const getOrderByOrderNo = (orderNo) => {
}) })
} }
// 通过订单号创建支付宝支付订单(芝麻信用免押 // 充电宝未弹出反馈(快捷反馈
export const getOrderByOrderNoScore = (orderNo) => { export const reportDeviceNoEject = (data) => {
console.log('通过订单号创建支付宝支付订单(芝麻信用免押)', orderNo); return request({
url: '/app/order/report-no-eject',
method: 'post',
data
})
}
// 充电宝转为自用
export const convertToOwned = (orderId) => {
return request({
url: `/app/order/convert-to-owned/${orderId}`,
method: 'post'
})
}
// 不想还了转为自用(按最高费用)
export const closeWithMaxFee = (orderNo) => {
return request({
url: `/app/order/closeWithMaxFee/${orderNo}`,
method: 'post'
})
}
// 创建微信支付订单
export const createWxPayment = (orderNo) => {
return request({
url: `/app/wx-payment/create/${orderNo}`,
method: 'get'
})
}
// 创建支付宝支付订单(租借押金 H5 支付)
// 对应文档《支付宝接口文档》:GET /app/ali-payment/create/{orderNo}
export const createAliPayment = (orderNo) => {
return request({ return request({
url: `/app/ali-payment/create/${orderNo}`, 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', method: 'get',
hideLoading: true hideLoading: true
}) })
} }
// 通过订单号查询支付宝订单支付状态 // 对订单执行暂停计费
export const getOrderByOrderNoScorePayStatus = (orderNo) => { export const requestPauseBilling = (orderId) => {
console.log('通过订单号查询支付宝订单支付状态', orderNo); return request({
url: `/app/order/pauseBilling/${orderId}`,
method: 'post',
hideLoading: true
})
}
// 获取待支付订单
export const getUnpaidOrder = () => {
return request({
url: '/app/order/unpaid',
method: 'get'
})
}
// 查询微信支付状态
export const getWxPaymentStatus = (orderNo) => {
return request({
url: `/app/wx-payment/status/${orderNo}`,
method: 'get'
})
}
// 查询支付宝支付状态
// 对应文档:GET /app/ali-payment/status/{orderNo}
export const getAliPaymentStatus = (orderNo) => {
return request({ return request({
url: `/app/ali-payment/status/${orderNo}`, 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', method: 'get',
hideLoading: true hideLoading: true
}) })
@@ -98,7 +235,6 @@ export const getOrderByOrderNoScorePayStatus = (orderNo) => {
// 更新订单套餐信息 // 更新订单套餐信息
export const updateOrderPackage = (data) => { export const updateOrderPackage = (data) => {
console.log('更新订单套餐信息:', data)
return request({ return request({
url: '/app/device/updateOrderPackage', url: '/app/device/updateOrderPackage',
method: 'post', method: 'post',
@@ -106,15 +242,26 @@ export const updateOrderPackage = (data) => {
}) })
} }
/* // 用户端删除商品订单(逻辑删除)
* 弃用 export const deleteProductOrder = (id) => {
*/
export const getPotionsDetail = (data) => {
console.log(data);
return request({ return request({
url: '/device/position/positionDetails', url: `/app/product/order/${id}`,
method: 'get', method: 'delete'
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
@@ -0,0 +1,75 @@
import request from '../http'
/**
* 商品列表查询接口
* @param {Object} params - 查询参数
* @param {string} params.productName - 商品名称(可选,模糊查询)
* @param {number} params.pageNum - 页码(默认1
* @param {number} params.pageSize - 每页数量(默认10
* @returns {Promise} 分页的商品列表
*/
export const getProductList = ({ productName = '', pageNum = 1, pageSize = 10 }) => {
return request({
url: '/app/product/list',
method: 'get',
params: {
productName,
pageNum,
pageSize
}
})
}
/**
* 商品详情查询接口
* @param {string|number} id - 商品ID
* @returns {Promise} 商品详细信息,包含规格列表(skuList)
*/
export const getProductDetail = (id) => {
return request({
url: `/app/product/${id}`,
method: 'get'
})
}
/**
* 创建商品支付订单(多支付平台)
* 对应《商品购买多支付平台方案》:
* paymentPlatform: WECHAT / ALIPAY / ANTOM
* 其他字段见文档
*/
export const createProductOrder = (data) => {
return request({
url: '/app/product/pay',
method: 'post',
data
})
}
/**
* 商品订单退款
* @param {Object} data - 退款数据
* @param {number} data.productOrderId - 商品订单ID
* @param {number} data.refundAmount - 退款金额(可选,不传则全额退款)
* @param {string} data.refundReason - 退款原因(可选)
* @returns {Promise} 退款结果
*/
export const refundProductOrder = (data) => {
return request({
url: '/app/product/refund',
method: 'post',
data
})
}
/**
* 获取用户收货地址
* @returns {Promise} 用户收货地址信息
*/
export const getUserAddress = () => {
return request({
url: '/app/product/address',
method: 'get'
})
}
+33 -7
View File
@@ -27,12 +27,38 @@ export const getCommonByBrand = (brandName) => {
}) })
} }
// 查询激活的且临近关闭时间最近的活动 // 获取当前协议内容
export const getActiveActivity = () => { export const getCurrentAgreement = (data) => {
return request({ return request({
url: '/device/activity/agent/list', url: '/device/agreementConfig/current',
method: 'get', method: 'get',
hideLoading: true data
}) })
}
// 获取当前广告内容
export const getCurrentAdvertisement = (data) => {
return request({
url: '/device/advertisementConfig/current',
method: 'get',
data
})
}
// 获取当前公告内容
export const getCurrentAnnouncement = (data) => {
return request({
url: '/device/announcementConfig/current',
method: 'get',
data
})
}
export const getActiveActivity = (data) => {
return request({
url: '/device/activeActivity/current',
method: 'get',
data
})
} }
+60 -3
View File
@@ -1,7 +1,7 @@
import request from '../http' import request from '../http'
import { URL, appid } from '../url' import { URL, appid } from '../url'
// 用户登录 // 旧登录接口(兼容保留,后端将逐步废弃)
export const login = (data) => { export const login = (data) => {
return request({ return request({
url: '/app/user/login', url: '/app/user/login',
@@ -10,6 +10,42 @@ export const login = (data) => {
}) })
} }
// 统一快捷登录接口 /app/user/quickLogin
// 对应文档《快捷登录最终方案》中的 QuickLoginDto
// loginType: WECHAT / ALIPAY / SMS
// appid: 平台应用ID
// openId: 第三方 openId(微信必传)
// code: 授权码(微信手机号授权码 / 支付宝 authCode
// phonenumber: 短信登录手机号
// smsCode: 短信验证码
export const quickLogin = (data) => {
return request({
url: '/app/user/quickLogin',
method: 'post',
data
})
}
// 发送验证码
export const sendVerifyCode = (phonenumber) => {
return request({
url: '/app/user/sms/code',
method: 'get',
data: { phonenumber }
})
}
// 手机号+验证码登录
export const loginWithCode = (phonenumber, smsCode) => {
// 兼容保留:统一走 quickLoginSMS
return quickLogin({
loginType: 'SMS',
appid,
phonenumber,
smsCode
})
}
// 用户退出登录 // 用户退出登录
export const userLogout = (data) => { export const userLogout = (data) => {
return request({ return request({
@@ -56,7 +92,8 @@ export const uploadUserAvatar = (filePath) => {
header: { header: {
'appid': appid, 'appid': appid,
'Authorization': 'Bearer ' + uni.getStorageSync('token'), 'Authorization': 'Bearer ' + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id') 'Clientid': uni.getStorageSync('client_id'),
'Content-Language': (uni.getStorageSync('language') || 'zh-CN').replace(/-/g, '_')
}, },
success: (res) => { success: (res) => {
try { try {
@@ -83,7 +120,8 @@ export const uploadOssResource = (filePath) => {
header: { header: {
'appid': appid, 'appid': appid,
'Authorization': 'Bearer ' + uni.getStorageSync('token'), 'Authorization': 'Bearer ' + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id') 'Clientid': uni.getStorageSync('client_id'),
'Content-Language': (uni.getStorageSync('language') || 'zh-CN').replace(/-/g, '_')
}, },
success: (res) => { success: (res) => {
try { try {
@@ -115,3 +153,22 @@ export const withdrawDeposit = (orderNo) => {
}) })
} }
// 获取微信用户手机号
export const getWxUserPhoneNumber = (data) => {
return request({
url: '/app/user/getPhoneNumber',
method: 'post',
data
})
}
// 获取支付宝用户手机号(复用同一后端接口,由后端按 appid / 参数结构区分平台)
// 期望后端返回:{ code:200, data:{ phoneNumber: 'xxx' } }
export const getAliUserPhoneNumber = (data) => {
return request({
url: '/app/user/alipay/getPhone',
method: 'post',
data
})
}
+77
View File
@@ -0,0 +1,77 @@
/**
* Console 日志配置
* 用于控制是否在控制台打印日志
*/
// 配置项:true 表示打印日志,false 表示不打印日志
export const CONSOLE_CONFIG = {
// 是否启用 console.log
enableLog: true,
// 是否启用 console.warn
enableWarn: false,
// 是否启用 console.error
enableError: false,
// 是否启用 console.info
enableInfo: false
}
// 保存原始的 console 方法
const originalConsole = {
log: console.log,
warn: console.warn,
error: console.error,
info: console.info
}
/**
* 初始化 console 控制
* 根据配置决定是否打印日志
*/
export function initConsoleControl() {
// 重写 console.log
console.log = function(...args) {
if (CONSOLE_CONFIG.enableLog) {
originalConsole.log.apply(console, args)
}
}
// 重写 console.warn
console.warn = function(...args) {
if (CONSOLE_CONFIG.enableWarn) {
originalConsole.warn.apply(console, args)
}
}
// 重写 console.error
console.error = function(...args) {
if (CONSOLE_CONFIG.enableError) {
originalConsole.error.apply(console, args)
}
}
// 重写 console.info
console.info = function(...args) {
if (CONSOLE_CONFIG.enableInfo) {
originalConsole.info.apply(console, args)
}
}
}
/**
* 恢复原始的 console 方法
*/
export function restoreConsole() {
console.log = originalConsole.log
console.warn = originalConsole.warn
console.error = originalConsole.error
console.info = originalConsole.info
}
/**
* 动态设置 console 配置
* @param {Object} config - 配置对象
*/
export function setConsoleConfig(config) {
Object.assign(CONSOLE_CONFIG, config)
}
+33 -12
View File
@@ -1,8 +1,15 @@
import { import {
URL, URL,
appid appid,
ZFBappid
} from './url' } from './url'
// 根据运行平台选择正确的小程序 appid(后端通常依赖该 header 做平台识别)
let platformAppid = appid
// #ifdef MP-ALIPAY
platformAppid = ZFBappid
// #endif
// 获取多语言翻译文本 // 获取多语言翻译文本
const getLoadingText = () => { const getLoadingText = () => {
try { try {
@@ -29,12 +36,14 @@ const request = (option) => {
method: option.method, method: option.method,
data: option.data, data: option.data,
header: { 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[
...option.headers, "Content-Type"] : (option.method && option.method.toUpperCase() === 'POST' ?
'appid': appid, 'application/json' : 'application/x-www-form-urlencoded'),
'platform': 'alipay', // 标识支付宝小程序平台 ...option.headers,
'appid': platformAppid,
'Authorization': "Bearer " + uni.getStorageSync('token'), 'Authorization': "Bearer " + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id') 'Clientid': uni.getStorageSync('client_id'),
'Content-Language': (uni.getStorageSync('language') || 'zh-CN').replace(/-/g, '_')
}, },
success(res) { success(res) {
@@ -49,7 +58,9 @@ const request = (option) => {
return return
} }
reject({msg: `请求失败,状态码:${res.statusCode}`}) reject({
msg: `请求失败,状态码:${res.statusCode}`
})
return return
} }
@@ -61,12 +72,22 @@ const request = (option) => {
// 计算重定向地址 // 计算重定向地址
const pages = getCurrentPages() const pages = getCurrentPages()
const current = pages && pages.length ? pages[pages.length - 1] : null const current = pages && pages.length ? pages[pages.length - 1] : null
const route = current && current.route ? ('/' + current.route) : '/pages/index/index' const route = current && current.route ? ('/' + current.route) :
const query = current && current.options ? Object.keys(current.options).map(k => `${k}=${encodeURIComponent(current.options[k])}`).join('&') : '' '/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) const redirect = encodeURIComponent(query ? `${route}?${query}` : route)
// console.log(redirect, "===========");
// 跳转到登录页 // 跳转到登录页
uni.reLaunch({ url: `/pages/login/index?redirect=${redirect}` }) uni.reLaunch({
} catch (e) {} url: "/subPackages/user/login/index"
})
} catch (e) {
uni.reLaunch({
url: "/subPackages/user/login/index"
})
}
} }
// 检查业务状态码 // 检查业务状态码
@@ -76,7 +97,7 @@ const request = (option) => {
// 判断是否需要忽略数据为空的错误 // 判断是否需要忽略数据为空的错误
if (option.ignoreEmptyError && if (option.ignoreEmptyError &&
(res.data.code === 500 && res.data.msg && (res.data.code === 500 && res.data.msg &&
(res.data.msg.includes('未找到') || res.data.msg.includes('不存在')))) { (res.data.msg.includes('未找到') || res.data.msg.includes('不存在')))) {
// 对于指定需要忽略的错误,返回一个标准的"成功但数据为空"的响应 // 对于指定需要忽略的错误,返回一个标准的"成功但数据为空"的响应
resolve({ resolve({
code: 200, code: 200,
+3 -3
View File
@@ -1,8 +1,8 @@
// export const URL = "https://my.gxfs123.com/api" //正式服务器-弃用 // export const URL = "https://my.gxfs123.com/api" //正式服务器-弃用
export const URL = "https://manager.fdzpower.com/api" //正式服务器 export const URL = "https://manager.fdzpower.com/api" //正式服务器
// export const URL = "https://fansdev.gxfs123.com/api" //测试服务器 // export const URL = "https://fansdev.gxfs123.com/api" //测试服务器
// export const URL = "http://192.168.5.30:8080" //本地调试 // export const URL = "http://192.168.0.158:8080" //本地调试
// export const URL = "http://127.0.0.1:8080" //本地调试 // export const URL = "http://127.0.0.1:8080" //本地调试
export const appid = "2021006117693332" //支付宝小程序appid export const appid = "wx2165f0be356ae7a9" //微信小程序appid
export const ZFBappid = "2021006117693332" //支付宝小程序appid(保留兼容) export const ZFBappid = "2021006117693332" //支付宝小程序appid
-207
View File
@@ -1,207 +0,0 @@
# 支付宝支付接口文档
## 接口概述
本文档描述支付宝支付相关的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
**维护人员**:开发团队
+852 -640
View File
File diff suppressed because it is too large Load Diff
+879
View File
@@ -0,0 +1,879 @@
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...'
}
}
+3 -1
View File
@@ -1,8 +1,10 @@
import zhCN from './zh-CN.js' import zhCN from './zh-CN.js'
import enUS from './en-US.js' import enUS from './en-US.js'
import idID from './id-ID.js'
export default { export default {
'zh-CN': zhCN, 'zh-CN': zhCN,
'en-US': enUS 'en-US': enUS,
'id-ID': idID
} }
+851 -642
View File
File diff suppressed because it is too large Load Diff
+30 -29
View File
@@ -4,11 +4,34 @@ import { createSSRApp } from 'vue'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import zhCN from './locale/zh-CN.js' import zhCN from './locale/zh-CN.js'
import enUS from './locale/en-US.js' import enUS from './locale/en-US.js'
import idID from './locale/id-ID.js'
import uView from '@climblee/uv-ui' import uView from '@climblee/uv-ui'
import { initConsoleControl } from './config/console.js'
// 初始化 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 = () => { const getSystemLanguage = () => {
let language = 'zh-CN' // H5 环境默认使用印尼语
if (isH5Platform()) {
return 'id-ID'
}
// 非 H5 环境根据系统语言判断
let language = 'en-US'
try { try {
const systemInfo = uni.getSystemInfoSync() const systemInfo = uni.getSystemInfoSync()
if (systemInfo && systemInfo.language) { if (systemInfo && systemInfo.language) {
@@ -18,6 +41,8 @@ const getSystemLanguage = () => {
} }
} catch (e) { } catch (e) {
console.error('获取系统语言失败:', e) console.error('获取系统语言失败:', e)
// 默认使用中文
language = 'zh-CN'
} }
return language return language
} }
@@ -34,7 +59,8 @@ const getSavedLanguage = () => {
return systemLang return systemLang
} catch (e) { } catch (e) {
console.error('语言设置出错:', e) console.error('语言设置出错:', e)
return 'zh-CN' // 出错时根据平台返回默认语言
return isH5Platform() ? 'id-ID' : 'zh-CN'
} }
} }
@@ -45,10 +71,6 @@ function getI18nInstance() {
// 每次都重新读取当前语言 // 每次都重新读取当前语言
const currentLang = getSavedLanguage() 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) { if (i18nInstance && i18nInstance.global.locale !== currentLang) {
@@ -59,44 +81,30 @@ function getI18nInstance() {
// 直接更新 locale(这应该会触发所有组件重新渲染) // 直接更新 locale(这应该会触发所有组件重新渲染)
i18nInstance.global.locale = currentLang 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 return i18nInstance
} }
// 首次创建实例 // 首次创建实例
if (!i18nInstance) { if (!i18nInstance) {
console.log('=== 首次创建 i18n 实例 ===')
i18nInstance = createI18n({ i18nInstance = createI18n({
legacy: true, // 使用 Legacy API 模式,支持全局 $t legacy: true, // 使用 Legacy API 模式,支持全局 $t
locale: currentLang, locale: currentLang,
fallbackLocale: 'zh-CN', fallbackLocale: 'zh-CN',
messages: { messages: {
'zh-CN': zhCN, 'zh-CN': zhCN,
'en-US': enUS 'en-US': enUS,
'id-ID': idID
}, },
silentTranslationWarn: true, silentTranslationWarn: true,
silentFallbackWarn: true silentFallbackWarn: true
}) })
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 return i18nInstance
} }
export function createApp() { export function createApp() {
console.log('========================================')
console.log('=== createApp 被调用 ===')
console.log('时间戳:', new Date().toLocaleTimeString())
console.log('========================================')
const app = createSSRApp(App) const app = createSSRApp(App)
@@ -126,13 +134,6 @@ export function createApp() {
} }
} }
console.log('=== Vue 3 应用创建完成 ===')
console.log('最终 locale:', i18n.global.locale)
console.log('app.config.globalProperties.$t 存在?', !!app.config.globalProperties.$t)
console.log('app.config.globalProperties.$i18n 存在?', !!app.config.globalProperties.$i18n)
console.log('测试 $t 调用:', i18n.global.t('common.loading'))
console.log('===========================')
return { return {
app app
} }
+36 -9
View File
@@ -43,26 +43,39 @@
"ios" : {}, "ios" : {},
"sdkConfigs" : { "sdkConfigs" : {
"maps" : { "maps" : {
"qqmap" : { "amap" : {
"appkey_ios" : "RO5BZ-ECZ63-7US3C-RT5QW-TIDZE-2FF35", "appkey_ios" : "4c513a688938fd89b88b296e867f66ec",
"appkey_android" : "RO5BZ-ECZ63-7US3C-RT5QW-TIDZE-2FF35" "appkey_android" : "4c513a688938fd89b88b296e867f66ec"
} }
} }
} }
} }
}, },
"quickapp" : {}, "quickapp" : {},
"mp-weixin" : {
"appid" : "wx2165f0be356ae7a9",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true,
"permission" : {
"scope.getPhoneNumber" : {
"desc" : "您的手机号将用于登录和订单服务"
},
"scope.userLocation" : {
"desc" : "您的位置信息将用于获取附近的设备"
}
},
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
},
"mp-alipay" : { "mp-alipay" : {
"component2": true, "component2" : true,
"transpile" : [ "uview-ui", "vue-i18n" ],
"skia" : true,
"usingComponents" : true, "usingComponents" : true,
"appid" : "2021006117693332", "appid" : "2021006117693332",
"unipush" : { "unipush" : {
"enable" : false "enable" : false
},
"permission" : {
"scope.userLocation" : {
"desc" : "您的位置信息将用于获取附近的设备"
}
} }
}, },
"mp-baidu" : { "mp-baidu" : {
@@ -71,6 +84,20 @@
"mp-toutiao" : { "mp-toutiao" : {
"usingComponents" : true "usingComponents" : true
}, },
"h5" : {
"sdkConfigs" : {
"maps" : {
"qqmap" : {
"key" : "DJQBZ-WB53Q-WPS5B-4S6J7-53RMS-X4FJ2"
}
}
},
"router" : {
"mode" : "history",
"base" : "/"
},
"title" : "FDZPower"
},
"uniStatistics" : { "uniStatistics" : {
"enable" : false "enable" : false
}, },
+1
View File
@@ -3,6 +3,7 @@
"@climblee/uv-ui": "^1.1.20", "@climblee/uv-ui": "^1.1.20",
"axios": "^1.7.9", "axios": "^1.7.9",
"axios-miniprogram-adapter": "0.3.4", "axios-miniprogram-adapter": "0.3.4",
"html5-qrcode": "^2.3.8",
"uniapp-axios-adapter": "^0.3.2", "uniapp-axios-adapter": "^0.3.2",
"uview-ui": "1.8.8", "uview-ui": "1.8.8",
"vue-i18n": "9" "vue-i18n": "9"
+320 -186
View File
@@ -5,7 +5,8 @@
"^uv-(.*)": "@climblee/uv-ui/components/uv-$1/uv-$1.vue" "^uv-(.*)": "@climblee/uv-ui/components/uv-$1/uv-$1.vue"
} }
}, },
"pages": [{ "pages": [
{
"path": "pages/index/index", "path": "pages/index/index",
"style": { "style": {
"navigationBarTitleText": "", "navigationBarTitleText": "",
@@ -15,147 +16,6 @@
"enableShareTimeline": true "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", "path": "pages/serve/bagCheck/index",
"style": { "style": {
@@ -163,23 +23,15 @@
} }
}, },
{ {
"path": "pages/return/index", "path": "pages/scan/index",
"style": { "style": {
"navigationBarTitleText": "", "navigationBarTitleText": "扫码使用",
"navigationBarBackgroundColor": "#ffffff", "navigationBarBackgroundColor": "#000000",
"navigationBarTextStyle": "black" "navigationBarTextStyle": "white"
} }
}, },
{ {
"path": "pages/order/success", "path": "pages/device/detail",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/order/return-success",
"style": { "style": {
"navigationBarTitleText": "", "navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff", "navigationBarBackgroundColor": "#ffffff",
@@ -194,21 +46,6 @@
"navigationBarTextStyle": "black" "navigationBarTextStyle": "black"
} }
}, },
{
"path": "pages/expressReturn/index",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "default"
}
},
{
"path": "pages/expressReturn/detail",
"style": {
"navigationBarTitleText": "",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{ {
"path": "pages/search/index", "path": "pages/search/index",
"style": { "style": {
@@ -217,22 +54,6 @@
"navigationBarTextStyle": "black" "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", "path": "pages/waiting/index",
"style": { "style": {
@@ -240,6 +61,319 @@
"navigationBarBackgroundColor": "#ffffff", "navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black" "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": { "globalStyle": {
-341
View File
@@ -1,341 +0,0 @@
<template>
<view class="deposit-container">
<!-- 押金金额卡片 -->
<view class="deposit-card">
<view class="title">{{ $t('deposit.depositBalance') }}</view>
<view class="amount">¥{{ depositAmount }}</view>
<button class="withdraw-btn" @click="handleWithdraw" :disabled="depositAmount <= 0">{{ $t('deposit.withdraw') }}</button>
</view>
<!-- 提现说明 -->
<view class="notice-card">
<view class="notice-title">
<view class="dot"></view>
<text>{{ $t('deposit.withdrawNotice') }}</text>
</view>
<view class="notice-content">
<view class="notice-item">1. {{ $t('deposit.withdrawNotice1') }}</view>
<view class="notice-item">2. {{ $t('deposit.withdrawNotice2') }}</view>
<view class="notice-item">3. {{ $t('deposit.withdrawNotice3') }}</view>
</view>
</view>
<!-- 押金记录 -->
<view class="record-card" v-if="records.length > 0">
<view class="record-title">{{ $t('deposit.depositRecord') }}</view>
<view class="record-list">
<view class="record-item" v-for="(item, index) in records" :key="index">
<view class="record-info">
<text class="record-type">{{ item.type }}</text>
<text class="record-time">{{ item.time }}</text>
</view>
<text class="record-amount" :class="item.type === '退还' ? 'refund' : ''">
{{ item.type === '退还' ? '+' : '-' }}¥{{ item.amount }}
</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { getUserInfo } from '../../util/index.js'
import { withdrawDeposit } from '../../config/api/user.js'
import { queryById } from '../../config/api/order.js'
export default {
data() {
return {
depositAmount: '0.00',
orderNo: '',
records: [],
orderId:''
}
},
onLoad() {
// 设置页面标题
uni.setNavigationBarTitle({
title: this.$t('deposit.title')
})
// this.loadUserInfo()
},
onShow() {
this.loadUserInfo()
},
methods: {
async loadUserInfo() {
try {
const res = await getUserInfo()
console.log('loadUserInfo',res);
if (res.code === 200) {
this.depositAmount = res.data.balanceAmount || '0.00'
this.orderNo = res.data.latestOrderNo || ''
this.orderId = res.data.latestOrderId||''
// 如果存在余额,获取押金记录
if (parseFloat(this.depositAmount) > 0 && this.orderNo) {
this.records = [
{
type: '支付',
time: this.formatDate(new Date()),
amount: this.depositAmount
}
]
} else {
this.records = []
}
}
} catch (error) {
console.error('获取用户信息失败:', error)
uni.showToast({
title: this.$t('user.getUserInfoFailed'),
icon: 'none'
})
}
},
async handleWithdraw() {
if (parseFloat(this.depositAmount) <= 0) {
uni.showToast({
title: this.$t('deposit.noBalance'),
icon: 'none'
})
return
}
if(this.orderId.length!=0||this.orderNo.length!=0){
const res = await queryById(Number(this.orderId))
console.log(res);
}
// if(this.orderNo.length!=0){
// uni.showToast({
// title:'当前存在进行中的订单',
// icon:'none'
// })
// return
// }
uni.showModal({
title: this.$t('deposit.confirmWithdraw'),
content: this.$t('deposit.withdrawDesc'),
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: this.$t('deposit.withdrawing')
})
try {
console.log('发起提现请求,订单号:', this.orderNo)
const result = await withdrawDeposit(this.orderNo)
console.log('提现响应:', result)
if (result.code === 200) {
uni.hideLoading()
uni.showToast({
title: this.$t('deposit.withdrawSubmitted'),
icon: 'success'
})
// 更新余额为0
this.depositAmount = '0.00'
this.records.push({
type: '退还',
time: this.formatDate(new Date()),
amount: this.depositAmount
})
// 重新加载用户信息
setTimeout(() => {
this.loadUserInfo()
}, 1500)
} else {
throw new Error(result.msg || this.$t('deposit.withdrawFailed'))
}
} catch (error) {
console.error('提现失败:', error)
uni.hideLoading()
// 更详细的错误处理
let errorMessage = this.$t('deposit.withdrawFailed');
// 如果有具体错误信息,使用它
if (error.message) {
// 常见错误消息处理
if (error.message.includes('尚未归还')) {
errorMessage = this.$t('deposit.orderNotReturned');
} else if (error.message.includes('已退还')) {
errorMessage = this.$t('deposit.alreadyRefunded');
} else if (error.message.includes('处理中')) {
errorMessage = this.$t('deposit.refundProcessing');
} else if (error.message.includes('余额为0')) {
errorMessage = this.$t('deposit.noBalance');
} else {
// 使用后端返回的具体错误消息
errorMessage = error.message;
}
}
// 显示错误提示
uni.showModal({
title: this.$t('deposit.withdrawFailed'),
content: errorMessage,
showCancel: false
})
}
}
}
})
},
formatDate(date) {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
}
}
</script>
<style lang="scss" scoped>
.deposit-container {
min-height: 100vh;
background: #f8f8f8;
padding: 30rpx;
.deposit-card {
background: linear-gradient(135deg, #1976D2, #64B5F6);
border-radius: 20rpx;
padding: 40rpx;
color: #fff;
text-align: center;
box-shadow: 0 4rpx 20rpx rgba(25,118,210,0.2);
.title {
font-size: 28rpx;
opacity: 0.9;
margin-bottom: 20rpx;
}
.amount {
font-size: 72rpx;
font-weight: bold;
margin-bottom: 40rpx;
}
.withdraw-btn {
background: #fff;
color: #1976D2;
width: 80%;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 32rpx;
font-weight: 500;
margin: 0 auto;
&:active {
transform: scale(0.98);
}
&[disabled] {
background: rgba(255,255,255,0.6);
color: rgba(25,118,210,0.5);
}
}
}
.notice-card {
margin-top: 30rpx;
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
.notice-title {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.dot {
width: 12rpx;
height: 12rpx;
background: #1976D2;
border-radius: 50%;
margin-right: 10rpx;
}
text {
font-size: 30rpx;
font-weight: 500;
color: #333;
}
}
.notice-content {
.notice-item {
font-size: 26rpx;
color: #666;
line-height: 1.8;
padding-left: 22rpx;
}
}
}
.record-card {
margin-top: 30rpx;
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
.record-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 20rpx;
border-left: 8rpx solid #1976D2;
padding-left: 20rpx;
}
.record-list {
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.record-info {
.record-type {
font-size: 28rpx;
color: #333;
margin-bottom: 6rpx;
display: block;
}
.record-time {
font-size: 24rpx;
color: #999;
}
}
.record-amount {
font-size: 32rpx;
color: #333;
font-weight: 500;
&.refund {
color: #4CAF50;
}
}
}
}
}
}
</style>
+344 -142
View File
@@ -1,16 +1,25 @@
<template> <template>
<view class="container"> <view class="container">
<!-- 骨架屏 -->
<DeviceDetailSkeleton v-if="loading&&!deviceInfo" />
<!-- 实际内容 -->
<view v-else>
<!-- 设备信息卡片 --> <!-- 设备信息卡片 -->
<view class="card device-info-card"> <view class="card device-info-card">
<view class="device-location"> <view class="device-location">
<view class="location-left"> <view class="location-left">
<image src="/static/images/location-map.svg" mode="aspectFit" class="location-icon"></image> <image src="/static/device_location.png" mode="aspectFit" class="location-icon" lazy-load="true"></image>
<text class="location-name">{{ deviceLocation }}</text> <text class="location-name">{{ deviceLocation }}</text>
</view> </view>
<view class="device-status" :class="deviceStatus.class"> <view class="device-status" :class="deviceStatus.class">
<text class="status-text">{{ deviceStatus.text }}</text> <text class="status-text">{{ deviceStatus.text }}</text>
</view> </view>
</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"> <view class="device-id">
<text class="id-label">{{ $t('device.deviceNo') }}</text> <text class="id-label">{{ $t('device.deviceNo') }}</text>
<text class="id-value">{{ deviceId }}</text> <text class="id-value">{{ deviceId }}</text>
@@ -23,16 +32,16 @@
<text class="card-title">{{ $t('device.pricingRules') }}</text> <text class="card-title">{{ $t('device.pricingRules') }}</text>
</view> </view>
<view class="pricing-banner"> <view class="pricing-banner">
<view class="pricing-main"> <view class="pricing-main">
<text class="price-symbol">¥</text> <text class="price-symbol">¥</text>
<text class="price">{{ deviceFeeConfig.maxHourPrice || '5.00' }}</text> <text class="price">{{ deviceFeeConfig.maxHourPrice || '5.00' }}</text>
<text class="unit">/{{ getPriceUnit() }}</text> <text class="unit">/{{ getPriceUnit() }}</text>
</view>
<view class="cap-badge">
<text class="cap-text">{{ deviceInfo.depositAmount || '99' }}{{ $t('device.capLimit') }}</text>
</view>
</view> </view>
<view class="cap-badge">
<text class="cap-text">{{ deviceInfo.depositAmount || '99' }}{{ $t('device.capLimit') }}</text>
</view>
</view>
<view class="pricing-info"> <view class="pricing-info">
<view class="info-icon"> <view class="info-icon">
@@ -69,15 +78,26 @@
</view> </view>
</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="footer">
<button class="rent-button" :class="{ 'return-button': hasActiveOrder }" <view class="rent-button" :class="{ 'return-button': hasActiveOrder }"
@click="handleRent('alipay-score-pay')"> @click="handleRent">
<text>{{ hasActiveOrder ? $t('order.returnDevice') : $t('device.rentDepositFree') }}</text> <text>{{ hasActiveOrder ? $t('order.returnDevice') : getRentButtonText() }}</text>
</button> </view>
<view class="alipay-credit"> <!-- 微信支付分标识仅在微信小程序环境显示 -->
<image src="/static/images/alipay.svg" mode="aspectFit" class="alipay-icon"></image> <view class="wechat-credit" v-if="isWechatMiniProgram">
<text class="credit-text">{{ $t('device.alipayScoreDesc') }}</text> <image src="/static/images/wxpayflag.png" mode="aspectFit" class="wx-icon" lazy-load="true"></image>
<text class="credit-text">{{ $t('device.wxPayScoreDesc') }}</text>
</view> </view>
</view> </view>
@@ -101,6 +121,7 @@
</view> </view>
</view> </view>
</view> </view>
</view>
</view> </view>
</template> </template>
@@ -111,7 +132,8 @@
onMounted onMounted
} from 'vue' } from 'vue'
import { import {
onLoad onLoad,
onUnload
} from '@dcloudio/uni-app' } from '@dcloudio/uni-app'
import { import {
getDeviceInfo, getDeviceInfo,
@@ -120,22 +142,26 @@
import { import {
getOrderByOrderNoScore, getOrderByOrderNoScore,
getOrderByOrderNo, getOrderByOrderNo,
cancelOrder cancelOrder,
getInUseOrder,
getUnpaidOrder
} from '@/config/api/order.js' } from '@/config/api/order.js'
import { import {
initiateAlipayPayment, initiateWeChatScorePayment,
getUserInfo, getUserInfo,
getUserPhoneNumber getUserPhoneNumber
} from '@/util/index.js' } from '@/util/index.js'
import { import {
useI18n useI18n
} from '@/utils/i18n.js' } from '@/utils/i18n.js'
import DeviceDetailSkeleton from '@/components/DeviceDetailSkeleton.vue'
const { const {
t: $t t
} = useI18n() } = useI18n()
// 响应式状态 // 响应式状态
const loading = ref(true)
const deviceInfo = ref({}) const deviceInfo = ref({})
const deviceId = ref('') const deviceId = ref('')
const deviceFeeConfig = ref({}) const deviceFeeConfig = ref({})
@@ -143,15 +169,38 @@
const deviceLocation = ref('一号教学楼大厅') const deviceLocation = ref('一号教学楼大厅')
const hasActiveOrder = ref(false) const hasActiveOrder = ref(false)
const deviceStatus = reactive({ const deviceStatus = reactive({
text: $t('device.available'), text: t('device.available'),
class: 'available' class: 'available'
}) })
const isLoggedIn = ref(true) const isLoggedIn = ref(true)
const phoneNumber = ref('') const phoneNumber = ref('')
const showPhoneAuthPopup = ref(false) const showPhoneAuthPopup = ref(false)
const isWechatMiniProgram = ref(false)
const isAlipayMiniProgram = ref(false)
const isH5 = ref(false)
// 生命周期 onLoad 钩子 // 生命周期 onLoad 钩子
onLoad(async (options) => { 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')) { if (options.deviceNo != uni.getStorageSync('deviceId') || !uni.getStorageSync('deviceId')) {
deviceId.value = options.deviceNo deviceId.value = options.deviceNo
uni.setStorageSync('deviceId', options.deviceNo) uni.setStorageSync('deviceId', options.deviceNo)
@@ -164,12 +213,43 @@
onMounted(async () => { onMounted(async () => {
uni.setNavigationBarTitle({ 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 checkUserPhone()
await fetchDeviceInfo() 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 () => { const checkUserPhone = async () => {
try { try {
const userInfoRes = await getUserInfo() const userInfoRes = await getUserInfo()
@@ -193,7 +273,7 @@
// 用户拒绝授权的情况 // 用户拒绝授权的情况
if (e.detail.errMsg && e.detail.errMsg.includes('deny')) { if (e.detail.errMsg && e.detail.errMsg.includes('deny')) {
uni.showToast({ uni.showToast({
title: $t('auth.phoneRequired'), title: t('auth.phoneRequired'),
icon: 'none' icon: 'none'
}) })
return return
@@ -201,9 +281,9 @@
// 获取到授权code // 获取到授权code
if (e.detail.code) { if (e.detail.code) {
uni.showLoading({ // uni.showLoading({
title: $t('auth.getting') // title: t('auth.getting')
}) // })
console.log('获取到的授权code:', e.detail.code) console.log('获取到的授权code:', e.detail.code)
@@ -212,7 +292,7 @@
getUserPhoneNumber(e.detail.code) getUserPhoneNumber(e.detail.code)
.then(res => { .then(res => {
console.log('获取手机号API响应原始数据:', JSON.stringify(res)) console.log('获取手机号API响应原始数据:', JSON.stringify(res))
uni.hideLoading() // uni.hideLoading()
// 不立即抛出错误,而是记录问题并继续处理 // 不立即抛出错误,而是记录问题并继续处理
if (!res) { if (!res) {
@@ -234,43 +314,43 @@
showPhoneAuthPopup.value = false showPhoneAuthPopup.value = false
uni.showToast({ uni.showToast({
title: $t('auth.phoneSuccess'), title: t('auth.phoneSuccess'),
icon: 'success' icon: 'success'
}) })
} else { } else {
// 记录详细信息,不抛出错误 // 记录详细信息,不抛出错误
console.warn('获取手机号响应异常:', res.msg || '未知错误') console.warn('获取手机号响应异常:', res.msg || '未知错误')
uni.showModal({ uni.showModal({
title: $t('auth.phoneError'), title: t('auth.phoneError'),
content: `${$t('common.statusCode')}: ${res.code}, ${$t('common.message')}: ${res.msg || $t('common.none')}`, content: `${t('common.statusCode')}: ${res.code}, ${t('common.message')}: ${res.msg || t('common.none')}`,
showCancel: false showCancel: false
}) })
} }
}) })
.catch(err => { .catch(err => {
uni.hideLoading() // uni.hideLoading()
console.error('获取手机号码失败(catch):', err) console.error('获取手机号码失败(catch):', err)
// 显示更详细的错误信息 // 显示更详细的错误信息
let errMsg = err.message || err.toString() let errMsg = err.message || err.toString()
uni.showModal({ uni.showModal({
title: $t('auth.phoneGetFailed'), title: t('auth.phoneGetFailed'),
content: $t('common.errorInfo') + ': ' + errMsg, content: t('common.errorInfo') + ': ' + errMsg,
showCancel: false showCancel: false
}) })
}) })
} catch (outerError) { } catch (outerError) {
uni.hideLoading() // uni.hideLoading()
console.error('获取手机号外部错误:', outerError) console.error('获取手机号外部错误:', outerError)
uni.showModal({ uni.showModal({
title: $t('common.unexpectedError'), title: t('common.unexpectedError'),
content: $t('common.processException') + ': ' + (outerError.message || outerError), content: t('common.processException') + ': ' + (outerError.message || outerError),
showCancel: false showCancel: false
}) })
} }
} else { } else {
uni.showToast({ uni.showToast({
title: $t('auth.authCodeFailed'), title: t('auth.authCodeFailed'),
icon: 'none' icon: 'none'
}) })
} }
@@ -278,43 +358,55 @@
// 检查登录状态和订单 // 检查登录状态和订单
const fetchDeviceInfo = async () => { const fetchDeviceInfo = async () => {
const res = await getDeviceInfo(deviceId.value) try {
if (res.code == 200) { loading.value = true
deviceInfo.value = res.data.device || {} // console.log(deviceId.value);
const res = await getDeviceInfo(deviceId.value)
if (res.code == 200) {
deviceInfo.value = res.data.device || {}
// 保存 position 信息 // 保存 position 信息
if (res.data.position) { if (res.data.position) {
positionInfo.value = res.data.position positionInfo.value = res.data.position
}
// 更新设备位置信息
if (deviceInfo.value.deviceLocation) {
deviceLocation.value = deviceInfo.value.deviceLocation
} else if (res.data.position && res.data.position.name) {
deviceLocation.value = res.data.position.name
}
// 更新设备状态
if (deviceInfo.value.status) {
if (deviceInfo.value.status === 'online') {
deviceStatus.text = $t('device.available')
deviceStatus.class = 'available'
} else if (deviceInfo.value.status === 'offline') {
deviceStatus.text = $t('device.offline')
deviceStatus.class = 'offline'
} }
}
if (deviceInfo.value.feeConfig) { // 更新设备位置信息
deviceFeeConfig.value = JSON.parse(deviceInfo.value.feeConfig)[0] || {} if (deviceInfo.value.deviceLocation) {
console.log('deviceFeeConfig', deviceFeeConfig.value); deviceLocation.value = deviceInfo.value.deviceLocation
} else { } else if (res.data.position && res.data.position.name) {
deviceFeeConfig.value = { deviceLocation.value = res.data.position.name
maxHourPrice: '5.00', }
// 更新设备状态
if (deviceInfo.value.status) {
if (deviceInfo.value.status === 'online') {
deviceStatus.text = t('device.available')
deviceStatus.class = 'available'
} else if (deviceInfo.value.status === 'offline') {
deviceStatus.text = t('device.offline')
deviceStatus.class = 'offline'
}
}
if (deviceInfo.value.feeConfig) {
deviceFeeConfig.value = JSON.parse(deviceInfo.value.feeConfig)[0] || {}
console.log('deviceFeeConfig', deviceFeeConfig.value);
} else {
deviceFeeConfig.value = {
maxHourPrice: '5.00',
}
}
}else{
// uni.reLaunch({
// url:'/pages/index/index'
// })
} }
}catch(error){
console.error('获取设备信息失败:', error)
} }
finally {
} }
} }
@@ -322,51 +414,61 @@
// 显示登录提示 // 显示登录提示
const showLoginTip = () => { const showLoginTip = () => {
uni.showModal({ uni.showModal({
title: $t('common.tips'), title: t('common.tips'),
content: $t('common.loginRequired'), content: t('common.loginRequired'),
confirmText: $t('auth.goToLogin'), confirmText: t('auth.goToLogin'),
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
uni.navigateTo({ uni.navigateTo({
url: '/pages/login/index' url: '/subPackages/user/login/index'
}) })
} }
} }
}) })
} }
// 跳转到优惠专区
const goToPurchase = () => {
const positionId = positionInfo.value?.positionId || positionInfo.value?.id
uni.navigateTo({
url: `/subPackages/business/purchase/index?positionId=${positionId}`
})
}
// 检查订单状态 // 检查订单状态
const checkOrderStatus = async () => { const checkOrderStatus = async () => {
try { try {
// 调用接口检查是否有进行中的订单 // 调用接口检查是否有进行中的订单
const result = await uni.$api.checkActiveOrder() const inUseRes = await getInUseOrder()
if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
const order = inUseRes.data
// 如果有正在进行的订单,跳转到归还页面,带上设备ID
uni.redirectTo({
url: `/pages/order/detail?orderId=${order.orderId}`
})
return
}
if (result.hasOrder) { // 检查是否有待支付的订单
const order = result.order // 假设后端返回 order 对象 const unpaidRes = await getUnpaidOrder()
if (unpaidRes && unpaidRes.code === 200 && unpaidRes.data) {
// 检查订单状态 const order = unpaidRes.data
if (order.status === 'waiting_for_payment') { // 跳转支付页面,带上订单ID
// 跳转支付页面,带上订单ID uni.redirectTo({
uni.redirectTo({ url: `/pages/order/payment?orderId=${order.orderId}&deviceId=${deviceId.value}`
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) { } catch (error) {
console.error('检查订单状态失败:', error)
uni.showToast({ uni.showToast({
title: $t('order.getOrderStatusFailed'), title: t('order.getOrderStatusFailed'),
icon: 'none' icon: 'none'
}) })
} }
} }
// 处理租借操作 // 处理租借操作
const handleRent = (payWay) => { const handleRent = () => {
if (!isLoggedIn.value) { if (!isLoggedIn.value) {
showLoginTip() showLoginTip()
return return
@@ -378,8 +480,21 @@
return return
} }
// 提交订单 // 根据运行环境选择不同的租借/支付流程
submitRentOrder(payWay) // 微信小程序:走微信支付分免押租借
if (isWechatMiniProgram.value) {
submitRentOrder('wx-score-pay')
return
}
// 支付宝小程序:走押金租借,后续在支付页内调起支付宝支付
if (isAlipayMiniProgram.value) {
submitRentOrder('wx-pay')
return
}
// H5 等其他环境:统一走押金租借,支付页内根据平台选择支付方式(Antom 等)
submitRentOrder('wx-pay')
} }
// 获取价格单位文本 // 获取价格单位文本
@@ -388,11 +503,11 @@
// 按分钟计费 // 按分钟计费
if (deviceInfo.value && deviceInfo.value.feeType === 'minute') { if (deviceInfo.value && deviceInfo.value.feeType === 'minute') {
return '分钟' return '分钟'
}else if(deviceInfo.value && deviceFeeConfig.value.hourPrice == '0.5'){ } else if (deviceInfo.value && deviceFeeConfig.value.hourPrice == '0.5') {
return '30分钟' return '30分钟'
} }
// 按小时计费(默认) // 按小时计费(默认)
return $t('time.hour') return t('time.hour')
} }
// 计算计费单位时间(分钟) // 计算计费单位时间(分钟)
@@ -460,27 +575,67 @@
return `不足${unitMinutes}分钟按${unitMinutes}分钟计费,封顶${depositAmount}元,持续计费至${depositAmount}元视为买断` return `不足${unitMinutes}分钟按${unitMinutes}分钟计费,封顶${depositAmount}元,持续计费至${depositAmount}元视为买断`
} }
// 获取租借按钮文本
const getRentButtonText = () => {
if (isWechatMiniProgram.value) {
return t('device.rentDepositFree')
} else {
return t('device.rentNow')
}
}
// 提交租借订单 // 提交租借订单
const submitRentOrder = async (payWay) => { const submitRentOrder = async (payWay) => {
try { try {
uni.showLoading({ 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); console.log(deviceId.value);
// 调用设备租借接口 // 调用设备租借接口
const rentResult = await rentPowerBank(deviceId.value, phoneNumber.value) const rentResult = await rentPowerBank(deviceId.value, phoneNumber.value,payWay.value)
if (rentResult.code !== 200) { if (rentResult.code !== 200) {
throw new Error(rentResult.msg || $t('device.rentFailed')) throw new Error(rentResult.msg || t('device.rentFailed'))
} }
// 获取后端返回的订单信息 // 获取后端返回的订单信息
const order = rentResult.data const order = rentResult.data
console.log('订单信息', order); console.log('订单信息', order);
if (payWay == 'alipay-pay') { // 标记:本次是从设备详情页发起的下单流程,离开页面时不设置启动路径
try {
uni.setStorageSync('skipSetLaunchPathOnce', true)
} catch (e) {
console.warn('设置 skipSetLaunchPathOnce 失败:', e)
}
if (payWay == 'wx-pay') {
// 当支付方式为押金支付时 // 当支付方式为押金支付时
uni.hideLoading() uni.hideLoading()
const res = await getOrderByOrderNo(order.orderNo); const res = await getOrderByOrderNo(order.orderNo);
@@ -492,35 +647,36 @@
// 跳转到订单支付页面 // 跳转到订单支付页面
uni.redirectTo({ uni.redirectTo({
url: `/pages/order/payment?orderId=${order.orderId}&packagePrice=${packagePrice}&totalAmount=${totalAmount}&depositAmount=${deposit}${deviceInfo.value && deviceInfo.value.feeConfig ? '&feeConfig=' + encodeURIComponent(deviceInfo.value.feeConfig) : ''}` url: `/subPackages/order/payment?orderId=${order.orderId}&packagePrice=${packagePrice}&totalAmount=${totalAmount}&depositAmount=${deposit}${deviceInfo.value && deviceInfo.value.feeConfig ? '&feeConfig=' + encodeURIComponent(deviceInfo.value.feeConfig) : ''}`
}) })
} else if (payWay == 'alipay-score-pay') { } else if (payWay == 'wx-score-pay') {
// 当支付方式为支付宝信用免押支付时 // 当支付方式为支付支付时
uni.hideLoading() uni.hideLoading()
// 获取支付宝信用免押所需参数 // 获取支付所需参数
const res = await getOrderByOrderNoScore(order.orderNo); const res = await getOrderByOrderNoScore(order.orderNo);
uni.hideLoading() uni.hideLoading()
if (res && res.code === 200) { if (res && res.code === 200) {
try { try {
// 调用支付宝信用免押小程序 // 调用微信支付分小程序
const payResult = await initiateAlipayPayment(res); const payResult = await initiateWeChatScorePayment(res);
console.log('支付宝信用免押调用结果', payResult); console.log('支付调用结果', payResult);
// 成功则跳转到等待页面 // 成功则跳转到等待页面
if (payResult && payResult.success !== false) { if (payResult.errCode == '0' && payResult.extraData && Object.keys(payResult.extraData)
console.log('支付宝信用免押授权成功,准备跳转到等待页,时间:', new Date().toLocaleTimeString()); .length > 0) {
// 跳转到等待页面 console.log('支付分授权成功,准备跳转到等待页,时间:', new Date().toLocaleTimeString());
// 跳转到等待页面(订阅消息已经在前面完成了)
uni.redirectTo({ uni.redirectTo({
url: `/pages/waiting/index?orderNo=${order.orderNo}&orderId=${order.orderId}&deviceId=${deviceId.value}` url: `/pages/waiting/index?orderNo=${order.orderNo}&orderId=${order.orderId}&deviceId=${deviceId.value}`
}); });
return; return;
} else { } else {
console.log('支付宝信用免押未完成授权或用户取消:', payResult); console.log('支付未完成授权或用户取消extraData:', payResult.extraData);
// 用户取消授权,需要取消订单 // 用户取消授权,需要取消订单
try { try {
uni.showLoading({ uni.showLoading({
title: $t('order.cancelling') title: t('order.cancelling')
}); });
const cancelRes = await cancelOrder({ const cancelRes = await cancelOrder({
orderId: order.orderNo orderId: order.orderNo
@@ -529,7 +685,7 @@
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({
title: $t('order.orderCancelled'), title: t('order.orderCancelled'),
icon: 'none', icon: 'none',
duration: 2000 duration: 2000
}); });
@@ -544,7 +700,7 @@
console.error('取消订单失败:', cancelError); console.error('取消订单失败:', cancelError);
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({
title: $t('order.cancelFailedContactService'), title: t('order.cancelFailedContactService'),
icon: 'none' icon: 'none'
}); });
} }
@@ -555,7 +711,7 @@
// 支付分调用异常,也需要取消订单 // 支付分调用异常,也需要取消订单
try { try {
uni.showLoading({ uni.showLoading({
title: $t('order.cancelling') title: t('order.cancelling')
}); });
const cancelRes = await cancelOrder({ const cancelRes = await cancelOrder({
orderId: order.orderNo orderId: order.orderNo
@@ -568,7 +724,7 @@
} }
uni.showToast({ uni.showToast({
title: $t('device.payScoreFailedCancelled'), title: t('device.payScoreFailedCancelled'),
icon: 'none' icon: 'none'
}); });
@@ -580,7 +736,7 @@
} }
} else { } else {
uni.showToast({ uni.showToast({
title: res?.msg || $t('device.getPayParamsFailed'), title: res?.msg || t('device.getPayParamsFailed'),
icon: 'none' icon: 'none'
}); });
} }
@@ -588,7 +744,7 @@
} catch (error) { } catch (error) {
uni.hideLoading() uni.hideLoading()
uni.showToast({ uni.showToast({
title: error.message || $t('device.rentFailedRetry'), title: error.message || t('device.rentFailedRetry'),
icon: 'none' icon: 'none'
}) })
} }
@@ -639,15 +795,15 @@
align-items: center; align-items: center;
.location-icon { .location-icon {
width: 40rpx; width: 32rpx;
height: 40rpx; height: 32rpx;
margin-right: 12rpx; margin-right: 12rpx;
background-color: #10d673; // background-color: #10d673;
border-radius: 50%; border-radius: 50%;
} }
.location-name { .location-name {
font-size: 32rpx; font-size: 28rpx;
color: #333; color: #333;
font-weight: 500; font-weight: 500;
} }
@@ -656,7 +812,7 @@
.device-status { .device-status {
padding: 8rpx 24rpx; padding: 8rpx 24rpx;
border-radius: 30rpx; border-radius: 30rpx;
font-size: 24rpx; font-size: 22rpx;
&.available { &.available {
background-color: #d4f4dd; background-color: #d4f4dd;
@@ -683,9 +839,10 @@
.device-id { .device-id {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 5rpx;
.id-label { .id-label {
font-size: 28rpx; font-size: 26rpx;
color: #999; color: #999;
} }
@@ -699,7 +856,7 @@
// 计费规则卡片 // 计费规则卡片
.pricing-card { .pricing-card {
.pricing-banner { .pricing-banner {
background: linear-gradient(135deg, #e8f5e9, #c8e6c9); background: #E6F7EC;
border-radius: 20rpx; border-radius: 20rpx;
padding: 40rpx 30rpx; padding: 40rpx 30rpx;
margin-bottom: 24rpx; margin-bottom: 24rpx;
@@ -713,21 +870,21 @@
margin-bottom: 16rpx; margin-bottom: 16rpx;
.price-symbol { .price-symbol {
font-size: 48rpx; font-size: 36rpx;
font-weight: bold; font-weight: bold;
color: #07c160; color: #07c160;
margin-right: 4rpx; margin-right: 4rpx;
} }
.price { .price {
font-size: 80rpx; font-size: 64rpx;
font-weight: bold; font-weight: bold;
color: #07c160; color: #07c160;
line-height: 1; line-height: 1;
} }
.unit { .unit {
font-size: 32rpx; font-size: 28rpx;
color: #07c160; color: #07c160;
margin-left: 8rpx; margin-left: 8rpx;
} }
@@ -735,11 +892,11 @@
.cap-badge { .cap-badge {
background-color: #07c160; background-color: #07c160;
padding: 10rpx 32rpx; padding: 10rpx 28rpx;
border-radius: 30rpx; border-radius: 30rpx;
line-height: 1;
.cap-text { .cap-text {
font-size: 26rpx; font-size: 24rpx;
color: #fff; color: #fff;
font-weight: 500; font-weight: 500;
} }
@@ -811,6 +968,51 @@
} }
} }
// 促销提示框
.promotion-tip {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background: rgba(255, 244, 227, 1);
border-radius: 22rpx;
margin-bottom: 30rpx;
transition: all 0.3s;
&:active {
opacity: 0.8;
transform: scale(0.98);
}
.tip-left {
display: flex;
align-items: center;
.tip-text {
font-size: 26rpx;
color: #A16300;
font-weight: 400;
}
}
.tip-right {
display: flex;
align-items: center;
gap: 12rpx;
.buy-text {
font-size: 26rpx;
color: #A16300;
font-weight: 500;
}
.arrow-icon {
width: 20rpx;
height: 20rpx;
}
}
}
// 底部操作区 // 底部操作区
.footer { .footer {
position: fixed; position: fixed;
@@ -851,22 +1053,22 @@
} }
} }
.alipay-credit { .wechat-credit {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: 16rpx; margin-top: 16rpx;
.alipay-icon { .wx-icon {
width: 48rpx; width: 48rpx;
height: 38rpx; height: 38rpx;
margin-right: 8rpx; margin-right: 8rpx;
} }
.credit-text { .credit-text {
font-size: 24rpx; font-size: 24rpx;
color: #999; color: #999;
} }
} }
} }
-147
View File
@@ -1,147 +0,0 @@
<template>
<view class="help-container">
<!-- 常见问题 -->
<view class="faq-section">
<uv-collapse :border="false">
<uv-collapse-item
v-for="(item, index) in faqList"
:key="index"
:title="$t(item.question)"
:name="index"
>
<view class="answer-content">
<text class="answer-text">{{ $t(item.answer) }}</text>
</view>
</uv-collapse-item>
</uv-collapse>
</view>
<!-- 联系客服 -->
<view class="contact-card">
<view class="contact-title">{{ $t('help.contactUs') }}</view>
<view class="contact-content">
<view class="contact-item">
<text class="label">{{ $t('help.phone') }}</text>
<text class="value" @click="makePhoneCall">{{ customerPhone }}</text>
</view>
<view class="contact-item">
<text class="label">{{ $t('help.workingHours') }}</text>
<text class="value">{{ HELP_CONTENT.CONTACT.SERVICE_TIME.VALUE }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { HELP_CONTENT } from '@/constants/help'
import { getCustomerPhone } from '@/util/index.js'
export default {
data() {
return {
HELP_CONTENT,
faqList: HELP_CONTENT.FAQ_LIST,
customerPhone: HELP_CONTENT.CONTACT.PHONE.VALUE // 默认客服电话
}
},
onLoad() {
// 设置页面标题
uni.setNavigationBarTitle({
title: this.$t('help.title')
})
// 从缓存读取客服电话
this.customerPhone = getCustomerPhone()
},
methods: {
makePhoneCall() {
uni.makePhoneCall({
phoneNumber: this.customerPhone
})
}
}
}
</script>
<style lang="scss" scoped>
.help-container {
min-height: 100vh;
background: #f8f8f8;
padding: 30rpx;
.faq-section {
background: #fff;
border-radius: 20rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
overflow: hidden;
.answer-content {
padding: 20rpx 30rpx 30rpx;
background: #f9f9f9;
.answer-text {
font-size: 28rpx;
color: #666;
line-height: 1.8;
display: block;
}
}
}
.contact-card {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.04);
.contact-title {
position: relative;
z-index: 4;
display: inline-block;
font-size: 32rpx;
color: #333;
font-weight: 500;
margin-bottom: 20rpx;
padding-bottom: 8rpx;
width: fit-content;
&::after {
z-index: -1;
content: '';
position: absolute;
left: 0;
bottom: 8rpx;
width: 88%;
height: 16rpx;
border-radius: 20rpx;
background: #07C160;
}
}
.contact-content {
.contact-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
.label {
font-size: 28rpx;
color: #666;
}
.value {
font-size: 28rpx;
color: #333;
font-weight: 500;
&:active {
opacity: 0.7;
}
}
}
}
}
}
</style>
+734 -190
View File
File diff suppressed because it is too large Load Diff
+1086 -241
View File
File diff suppressed because it is too large Load Diff
-588
View File
@@ -1,588 +0,0 @@
<template>
<view class="payment-container">
<!-- 订单状态 -->
<view class="status-card">
<view class="status-icon" :class="orderStatus.class"></view>
<view class="status-text">{{ orderStatus.text }}</view>
<view class="status-desc">{{ orderStatus.desc }}</view>
</view>
<!-- 订单信息 -->
<view class="order-card">
<view class="card-title">{{ $t('payment.orderInfo') }}</view>
<view class="info-item">
<text class="label">{{ $t('order.orderNo') }}</text>
<text class="value">{{ orderInfo.orderNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('order.deviceNo') }}</text>
<text class="value">{{ orderInfo.deviceNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('payment.createTime') }}</text>
<text class="value">{{ orderInfo.createTime || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('payment.contactPhone') }}</text>
<text class="value">{{ orderInfo.phone || '-' }}</text>
</view>
</view>
<!-- 费用信息 -->
<view class="price-card">
<view class="card-title">{{ $t('payment.feeInfo') }}</view>
<view class="price-item">
<text class="label">{{ $t('payment.deposit') }}</text>
<text class="value">{{ orderInfo.deposit || '99.00' }}</text>
</view>
<view class="price-item">
<text class="label">{{ $t('payment.package') }}</text>
<text class="value">{{ packageInfo.price }}{{ $t('unit.yuan') }}/{{ packageInfo.time }}{{ $t('time.hour') }}</text>
</view>
<view class="price-item total">
<text class="label">{{ $t('payment.total') }}</text>
<text class="value">{{ totalAmount }}</text>
</view>
</view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="total-amount">
<text>{{ $t('payment.total') }}</text>
<text class="amount">{{ totalAmount }}</text>
</view>
<view class="pay-btn" @click="handlePayment">{{ $t('payment.payNow') }}</view>
</view>
</view>
</template>
<script>
import { queryById, updateOrderPackage } from '@/config/api/order.js'
import { getDeviceInfo } from '@/config/api/device.js'
import { updateUserBalance } from '@/config/api/user.js'
import {
URL
}from"@/config/url.js"
export default {
data() {
return {
orderId: null,
deviceNo: null,
orderInfo: {},
packageInfo: {
time: '',
price: '0.00'
},
deviceInfo: null,
passedTotalAmount: null,
passedDepositAmount: null,
orderStatus: {
get text() { return this.$t('payment.waitingForPayment') },
get desc() { return this.$t('payment.pleasePayIn15Min') },
class: 'waiting'
}
}
},
computed: {
totalAmount() {
if (this.passedTotalAmount !== null) {
return parseFloat(this.passedTotalAmount).toFixed(2);
}
const deposit = parseFloat(this.orderInfo.deposit || this.passedDepositAmount || 99)
const packagePrice = parseFloat(this.packageInfo.price || 0)
return (deposit + packagePrice).toFixed(2)
},
// 计算每小时的价格
hourlyRate() {
const price = parseFloat(this.packageInfo.price || 0);
let time = parseFloat(this.packageInfo.time || 1);
// 如果时间单位不是小时(例如分钟),需要转换
if (this.packageInfo.time && this.packageInfo.time.includes('分钟')) {
time = time / 60; // 将分钟转换为小时
} else if (this.packageInfo.time && this.packageInfo.time.includes('次')) {
// 按次计费时,暂时显示单次价格
return price.toFixed(2);
}
// 避免除以零
if (time <= 0) time = 1;
// 计算每小时价格
return (price / time).toFixed(2);
}
},
onLoad(options) {
// 设置页面标题
uni.setNavigationBarTitle({
title: this.$t('payment.orderPayment')
})
if (options && options.orderId) {
this.orderId = options.orderId
if (options.totalAmount) {
this.passedTotalAmount = options.depositAmount;
}
if (options.depositAmount) {
this.passedDepositAmount = options.depositAmount;
}
// 如果URL中包含了feeConfig参数,保存它
if (options.feeConfig) {
try {
console.log('从URL获取到feeConfig:', options.feeConfig)
const feeConfigStr = decodeURIComponent(options.feeConfig)
// 创建一个临时的deviceInfo对象保存feeConfig
this.deviceInfo = { feeConfig: feeConfigStr }
} catch (e) {
console.error('解析URL中的feeConfig失败:', e)
}
}
this.loadOrderInfo()
} else {
uni.showToast({
title: this.$t('order.orderNotExist'),
icon: 'none'
})
setTimeout(() => {
uni.redirectTo({
url: '/pages/index/index'
})
}, 1500)
}
},
methods: {
// 加载订单信息
async loadOrderInfo() {
try {
uni.showLoading({
title: this.$t('common.loading')
})
const res = await queryById(this.orderId)
if (res.code === 200 && res.data) {
const orderData = res.data
// 处理创建时间,确保显示的是格式化后的时间
let formattedTime;
try {
// 如果orderData.createTime存在并且是有效的日期字符串/时间戳,则格式化它
if (orderData.createTime) {
formattedTime = this.formatTime(new Date(orderData.createTime));
} else {
// 如果createTime不存在,使用当前时间作为创建时间
formattedTime = this.formatTime(new Date());
}
} catch (e) {
console.error('时间格式化错误:', e);
formattedTime = this.formatTime(new Date());
}
this.orderInfo = {
orderNo: orderData.orderNo || orderData.orderId,
deviceNo: orderData.deviceNo,
createTime: formattedTime,
phone: orderData.phone,
deposit: this.passedDepositAmount || orderData.depositAmount || '99.00',
}
// 直接从订单数据中获取套餐信息
if (orderData.packageTime && orderData.packagePrice) {
// 将分钟转换为小时
const timeInHours = (parseFloat(orderData.packageTime) / 60).toFixed(1);
this.packageInfo = {
time: timeInHours.toString(),
price: orderData.packagePrice.toString()
}
}
// 获取设备信息(但不再用于设置套餐信息)
this.deviceNo = orderData.deviceNo;
await this.loadDeviceInfo();
} else {
throw new Error('获取订单信息失败')
}
uni.hideLoading()
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || '获取订单信息失败',
icon: 'none'
})
}
},
// 加载设备信息
async loadDeviceInfo() {
if (!this.deviceNo) return;
try {
const res = await getDeviceInfo(this.deviceNo);
if (res.code === 200 && res.data) {
this.deviceInfo = res.data.device;
// 设置存款金额
if (this.deviceInfo && this.deviceInfo.depositAmount) {
this.orderInfo.deposit = this.deviceInfo.depositAmount;
}
}
} catch (error) {
console.error('获取设备信息失败:', error);
}
},
// 处理支付
async handlePayment() {
try {
uni.showLoading({
title: this.$t('common.processing')
})
// 调用后端创建支付宝支付订单接口
const res = await uni.request({
url: `${URL || 'http://127.0.0.1:8080'}/app/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>
+355 -302
View File
@@ -1,346 +1,399 @@
<template> <template>
<view class="success-container"> <view class="success-container">
<!-- 支付成功状态 --> <!-- 支付成功状态和订单信息 -->
<view class="status-card"> <view class="status-order-card">
<view class="status-icon success"></view> <!-- 支付成功状态 -->
<view class="status-text">{{ $t('success.paymentSuccess') }}</view> <view class="status-section">
<view class="status-desc">{{ $t('success.paymentSuccessDesc') }}</view> <view class="status-icon success"></view>
</view> <view class="status-text">{{ $t('success.paymentSuccess') }}</view>
<view class="status-desc">{{ $t('success.paymentSuccessDesc') }}</view>
</view>
<!-- 订单信息 --> <!-- 分割线 -->
<view class="order-card"> <view class="section-divider"></view>
<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 class="device-status"> <view class="order-section">
<view class="status-message">{{ deviceMessage }}</view> <view class="card-title">{{ $t('success.orderInfo') }}</view>
<view class="loading-animation" v-if="isLoading"> <view class="info-item">
<view class="loading-circle"></view> <text class="label">{{ $t('order.orderNo') }}</text>
</view> <text class="value">{{ orderInfo.orderNo || '-' }}</text>
</view> </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="button-group"> <view class="device-status">
<button class="primary-btn" @click="goToHome">{{ $t('success.backToHome') }}</button> <view class="status-message">{{ deviceMessage }}</view>
<button class="secondary-btn" @click="goToOrderList">{{ $t('success.viewOrder') }}</button> <view class="loading-animation" v-if="isLoading">
</view> <view class="loading-circle"></view>
</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> </template>
<script> <script setup>
import { queryById } from '@/config/api/order.js' import {
import { confirmPaymentAndRent } from '@/config/api/device.js' ref,
getCurrentInstance
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
queryById,
getOrderByOrderNo
} from '@/config/api/order.js'
export default { // 获取当前实例以访问 $t 方法
data() { const {
return { proxy
orderId: '', } = getCurrentInstance()
orderInfo: {},
isLoading: true,
deviceMessage: '',
hasTriggeredDevice: false
}
},
onLoad(options) {
// 设置页面标题
uni.setNavigationBarTitle({
title: this.$t('success.paymentSuccess')
})
this.deviceMessage = this.$t('success.preparingDevice') // 响应式数据
const orderId = ref('')
const orderInfo = ref({})
const isLoading = ref(true)
const deviceMessage = ref('')
const hasTriggeredDevice = ref(false)
if (options && options.orderId) { // 页面加载
this.orderId = options.orderId onLoad((options) => {
this.loadOrderInfo() // 设置页面标题
uni.setNavigationBarTitle({
title: proxy.$t('success.paymentSuccess')
})
// 添加页面显示监听,防止页面切换后重复触发弹出 deviceMessage.value = proxy.$t('success.preparingDevice')
uni.$once('orderSuccess:' + this.orderId, () => {
console.log('已经触发过弹出逻辑,不再重复触发')
this.hasTriggeredDevice = true
})
} else {
uni.showToast({
title: this.$t('order.orderNotExist'),
icon: 'none'
})
setTimeout(() => {
this.goToHome()
}, 1500)
}
},
methods: {
async loadOrderInfo() {
try {
uni.showLoading({
title: this.$t('common.loading')
})
const res = await queryById(this.orderId) // #ifdef H5
if (res.code === 200 && res.data) { if (uni.getStorageSync('pendingPaymentNo')) {
const orderData = res.data orderId.value = options.orderId
this.orderInfo = { loadOrderInfo()
orderNo: orderData.orderNo || orderData.orderId,
deviceNo: orderData.deviceNo,
amount: orderData.payAmount || orderData.amount,
payTime: orderData.payTime || this.formatTime(new Date())
}
// 检查订单状态 // 添加页面显示监听,防止页面切换后重复触发弹出
if (orderData.orderStatus === 'IN_USED') { uni.$once('orderSuccess:' + orderId.value, () => {
// 如果已经是使用中状态,可能说明开锁已经完成 console.log('已经触发过弹出逻辑,不再重复触发')
this.deviceMessage = '设备已弹出,请取走您的风扇' hasTriggeredDevice.value = true
this.isLoading = false })
} 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()
// 如果是第一次加载页面且设备已弹出,记录状态,避免重复弹出 // 添加页面显示监听,防止页面切换后重复触发弹出
if (!this.hasTriggeredDevice) { uni.$once('orderSuccess:' + orderId.value, () => {
uni.$emit('orderSuccess:' + this.orderId) console.log('已经触发过弹出逻辑,不再重复触发')
this.hasTriggeredDevice = true hasTriggeredDevice.value = true
} })
} else { } else {
// 正常触发弹出逻辑 uni.showToast({
this.triggerDeviceEject() title: proxy.$t('order.orderNotExist'),
} icon: 'none'
} else { })
throw new Error('获取订单信息失败') setTimeout(() => {
} goToHome()
}, 1500)
}
// #endif
})
uni.hideLoading() // 加载订单信息
} catch (error) { const loadOrderInfo = async () => {
uni.hideLoading() try {
uni.showToast({ uni.showLoading({
title: error.message || this.$t('order.getOrderFailed'), title: proxy.$t('common.loading')
icon: 'none' })
})
}
},
// 触发弹出风扇 const res = await queryById(orderId.value)
async triggerDeviceEject() { if (res.code === 200 && res.data) {
if (this.hasTriggeredDevice) { const orderData = res.data
console.log('已经触发过弹出风扇,不重复触发') orderInfo.value = {
return orderNo: orderData.orderNo || orderData.orderId,
} deviceNo: orderData.deviceNo,
amount: orderData.payAmount || orderData.amount,
payTime: orderData.payTime || formatTime(new Date())
}
this.hasTriggeredDevice = true // 检查订单状态
uni.$emit('orderSuccess:' + this.orderId) if (orderData.orderStatus === 'IN_USED') {
this.isLoading = true // 如果已经是使用中状态,可能说明开锁已经完成
this.deviceMessage = this.$t('success.preparingDevice') deviceMessage.value = '设备已弹出,请取走您的风扇'
isLoading.value = false
try { // 如果是第一次加载页面且设备已弹出,记录状态,避免重复弹出
console.log(`准备触发弹出风扇,orderId: ${this.orderId}`) 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()
const result = await confirmPaymentAndRent(this.orderId) } catch (error) {
console.log('确认支付并弹出风扇结果:', JSON.stringify(result)) uni.hideLoading()
uni.showToast({
title: error.message || proxy.$t('order.getOrderFailed'),
icon: 'none'
})
}
}
if (result && result.code === 200) { // 格式化时间
this.deviceMessage = this.$t('success.deviceReady') const formatTime = (date) => {
uni.showToast({ const year = date.getFullYear()
title: this.$t('success.deviceReady'), const month = (date.getMonth() + 1).toString().padStart(2, '0')
icon: 'success' const day = date.getDate().toString().padStart(2, '0')
}) const hour = date.getHours().toString().padStart(2, '0')
} else { const minute = date.getMinutes().toString().padStart(2, '0')
throw new Error((result && result.msg) || this.$t('success.deviceFailed')) const second = date.getSeconds().toString().padStart(2, '0')
} return `${year}-${month}-${day} ${hour}:${minute}:${second}`
} 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 goToHome = () => {
const month = (date.getMonth() + 1).toString().padStart(2, '0') uni.reLaunch({
const day = date.getDate().toString().padStart(2, '0') url: '/pages/index/index'
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 goToOrderList = () => {
goToHome() { uni.redirectTo({
uni.switchTab({ url: '/pages/order/index'
url: '/pages/index/index' })
}) }
},
goToOrderList() {
uni.redirectTo({
url: '/pages/order/index'
})
}
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.success-container { .success-container {
padding: 20px; padding: 20px;
background-color: #f5f5f5; padding-bottom: 180rpx;
min-height: 100vh; background-color: #f5f5f5;
} min-height: 100vh;
box-sizing: border-box;
}
.status-card { .status-order-card {
background-color: #fff; background-color: #fff;
border-radius: 12px; border-radius: 12px;
padding: 30px; margin-bottom: 20px;
text-align: center; overflow: hidden;
margin-bottom: 20px;
.status-icon { .status-section {
width: 60px; padding: 30px;
height: 60px; text-align: center;
margin: 0 auto 16px;
background-color: #07c160;
border-radius: 50%;
position: relative;
&::after { .status-icon {
content: ''; width: 60px;
position: absolute; height: 60px;
left: 50%; margin: 0 auto 16px;
top: 50%; background-color: #07c160;
transform: translate(-50%, -50%); border-radius: 50%;
width: 30px; position: relative;
height: 20px;
border: 3px solid #fff;
border-top: none;
border-right: none;
transform-origin: center;
transform: translate(-50%, -70%) rotate(-45deg);
}
}
.status-text { &::after {
font-size: 24px; content: '';
font-weight: bold; position: absolute;
color: #07c160; left: 50%;
margin-bottom: 8px; 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-desc { .status-text {
font-size: 14px; font-size: 24px;
color: #666; font-weight: bold;
} color: #07c160;
} margin-bottom: 8px;
}
.order-card { .status-desc {
background-color: #fff; font-size: 14px;
border-radius: 12px; color: #666;
padding: 20px; }
margin-bottom: 20px; }
.card-title { .section-divider {
font-size: 16px; height: 1px;
font-weight: bold; background-color: #f0f0f0;
margin-bottom: 16px; margin: 0 20px;
color: #333; }
}
.info-item { .order-section {
display: flex; padding: 20px;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.label { .card-title {
color: #666; font-size: 16px;
font-size: 14px; font-weight: bold;
} margin-bottom: 16px;
color: #333;
padding-left: 12px;
position: relative;
.value { &::before {
color: #333; content: '';
font-size: 14px; position: absolute;
} left: 0;
} top: 50%;
} transform: translateY(-50%);
width: 4px;
height: 16px;
background-color: #07c160;
border-radius: 2px;
}
}
.device-status { .info-item {
background-color: #fff; display: flex;
border-radius: 12px; justify-content: space-between;
padding: 20px; align-items: center;
margin-bottom: 20px; margin-bottom: 12px;
text-align: center;
.status-message { &:last-child {
font-size: 16px; margin-bottom: 0;
color: #333; }
margin-bottom: 12px;
}
.loading-animation { .label {
display: flex; color: #666;
justify-content: center; font-size: 14px;
align-items: center; }
height: 40px;
.loading-circle { .value {
width: 30px; color: #333;
height: 30px; font-size: 14px;
border-radius: 50%; font-weight: 500;
border: 3px solid #f0f0f0; }
border-top-color: #07c160; }
animation: spin 1s linear infinite; }
} }
@keyframes spin { .device-status {
0% { transform: rotate(0deg); } background-color: #fff;
100% { transform: rotate(360deg); } border-radius: 12px;
} padding: 20px;
} margin-bottom: 20px;
} text-align: center;
.button-group { .status-message {
margin-top: 30px; font-size: 16px;
display: flex; color: #333;
// flex-direction: column; margin-bottom: 12px;
gap: 16px; }
.primary-btn { .loading-animation {
background-color: #07c160; display: flex;
color: #fff; justify-content: center;
border: none; align-items: center;
border-radius: 24px; height: 40px;
padding: 12px;
font-size: 16px;
&:active { .loading-circle {
opacity: 0.8; width: 30px;
} height: 30px;
} border-radius: 50%;
border: 3px solid #f0f0f0;
border-top-color: #07c160;
animation: spin 1s linear infinite;
}
.secondary-btn { @keyframes spin {
background-color: #fff; 0% {
color: #07c160; transform: rotate(0deg);
border: 1px solid #07c160; }
border-radius: 24px;
padding: 12px;
font-size: 16px;
&:active { 100% {
background-color: #f5f5f5; 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> </style>
+746
View File
@@ -0,0 +1,746 @@
<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>
+97 -70
View File
@@ -6,7 +6,7 @@
@relocate="init" @markerTap="goToPositionDetail" /> @relocate="init" @markerTap="goToPositionDetail" />
<!-- 定位按钮 --> <!-- 定位按钮 -->
<view class="relocate-btn" @click="init"> <view class="relocate-btn" @click="init">
<image src="/static/location.png" class="relocate-icon" mode="aspectFit"></image> <image src="/static/location.png" class="relocate-icon" mode="aspectFit" lazy-load="true"></image>
</view> </view>
</view> </view>
<view class="list-wrap"> <view class="list-wrap">
@@ -22,51 +22,52 @@
:class="{ available: isRentable(item), invalid: !isValidCoordinate(item.latitude, item.longitude) }" :class="{ available: isRentable(item), invalid: !isValidCoordinate(item.latitude, item.longitude) }"
v-for="(item, index) in filteredPositions" :key="item.positionId || index" v-for="(item, index) in filteredPositions" :key="item.positionId || index"
@click="goToPositionDetail(item)"> @click="goToPositionDetail(item)">
<view class="thumb"> <!-- 第一行三列布局 -->
<image v-if="item.deviceImg" :src="item.deviceImg" class="thumb-img" mode="aspectFill"> <view class="card-row-first">
</image> <!-- 第一列缩略图 -->
<image v-else src="/static/device-info.png" class="thumb-img" mode="aspectFit"></image> <view class="thumb">
</view> <image v-if="item.deviceImg" :src="item.deviceImg" class="thumb-img" mode="aspectFill">
<view class="info"> </image>
<view class="row top"> <image v-else src="/static/device-info.png" class="thumb-img" mode="aspectFit"></image>
<view class="name">{{ item.name }}</view> </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>
</view>
<view class="row meta" v-if="item.workTime && item.workTime !== '0'">
<text class="time">{{ $t('location.businessHours') }}{{ item.workTime }}</text>
</view>
<view class="row meta" v-if="!isValidCoordinate(item.latitude, item.longitude)">
<text class="time" style="color: #ff6b6b;">{{ $t('location.coordinateError') }}</text>
</view>
</view> </view>
<view class="row sub" v-if="item.location">
<text class="addr">{{ item.location }}</text> <!-- 第三列操作 -->
</view> <view class="actions">
<view class="row meta" v-if="item.workTime && item.workTime !== '0'"> <view class="nav" :class="{ disabled: !isValidCoordinate(item.latitude, item.longitude) }"
<text class="time">{{ $t('location.businessHours') }}{{ item.workTime }}</text> @click.stop="navigateToPosition(item)">
</view> <image src="/static/luxian.png" class="action-icon" mode="aspectFit"></image>
<view class="row meta" v-if="!isValidCoordinate(item.latitude, item.longitude)"> </view>
<text class="time" style="color: #ff6b6b;">{{ $t('location.coordinateError') }}</text> <view class="distance"
</view> v-if="item.distance && isValidCoordinate(item.latitude, item.longitude)">
<view class="row meta" {{ item.distance }}
v-if="item.availablePowerBankCount !== undefined && item.availablePowerBankCount !== null"> </view>
<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>
</view>
<!-- 第二行标签 -->
<view class="card-row-second">
<view class="tags"> <view class="tags">
<view class="tag rent" v-if="isRentable(item)">{{ $t('location.rent') }}</view> <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 return" v-if="isReturnable(item)">{{ $t('location.return') }}</view>
<view class="tag coupon" v-if="item.supportCouponOrMember">{{ $t('location.supportCouponOrMember') }}</view>
</view> </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>
<view class="empty-state" v-if="!isLoading && (!positionList || positionList.length === 0)"> <view class="empty-state" v-if="!isLoading && (!positionList || positionList.length === 0)">
@@ -100,13 +101,13 @@
} from '@/utils/i18n.js' } from '@/utils/i18n.js'
const { const {
t: $t t
} = useI18n() } = useI18n()
onMounted(() => { onMounted(() => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: $t('search.title') title: t('search.title')
}) })
// uni.showLoading({ // uni.showLoading({
// title:'11111', // title:'11111',
@@ -133,8 +134,13 @@
} }
const formatDistance = (meters) => { const formatDistance = (meters) => {
if (meters < 1000) return `${Math.round(meters)}m` // 兼容支付宝小程序等环境:保证始终对 Number 调用 toFixed
return `${(meters / 1000).toFixed(1)}km` let m = meters
if (typeof m === 'bigint') m = Number(m)
m = Number(m)
if (!Number.isFinite(m) || m < 0) return ''
if (m < 1000) return `${Math.round(m)}m`
return `${(m / 1000).toFixed(1)}km`
} }
const setTab = (name) => { const setTab = (name) => {
@@ -216,8 +222,8 @@
return { return {
...transformed, ...transformed,
canRent: activeTab.value === 'rent' ? true : (device.availablePowerBankCount > 0), canRent: activeTab.value === 'rent' ? true : (device.availablePowerBankCount > 0),
canReturn: activeTab.value === 'return' ? true : (device.availableEmptyGridCount > canReturn: activeTab.value === 'return' ? true : (device.availableEmptyGridCount > 0),
0) supportCouponOrMember: device.supportCouponOrMember || false
} }
}) })
@@ -256,7 +262,7 @@ const init = async () => {
positionList.value = [] positionList.value = []
filteredPositions.value = [] filteredPositions.value = []
uni.showToast({ uni.showToast({
title: $t('home.getLocationFailed'), title: t('home.getLocationFailed'),
icon: 'none' icon: 'none'
}) })
} finally { } finally {
@@ -278,7 +284,7 @@ const init = async () => {
const navigateToPosition = (position) => { const navigateToPosition = (position) => {
if (!isValidCoordinate(position.latitude, position.longitude)) { if (!isValidCoordinate(position.latitude, position.longitude)) {
uni.showToast({ uni.showToast({
title: $t('search.invalidCoordinate'), title: t('search.invalidCoordinate'),
icon: 'none' icon: 'none'
}) })
return return
@@ -296,13 +302,13 @@ const init = async () => {
const goToPositionDetail = (position) => { const goToPositionDetail = (position) => {
if (!position.positionId) { if (!position.positionId) {
uni.showToast({ uni.showToast({
title: $t('search.positionInfoError'), title: t('search.positionInfoError'),
icon: 'none' icon: 'none'
}) })
return return
} }
uni.navigateTo({ uni.navigateTo({
url: `/pages/position/detail?positionId=${position.positionId}` url: `/subPackages/business/position/detail?positionId=${position.positionId}`
}) })
} }
</script> </script>
@@ -392,9 +398,8 @@ const init = async () => {
} }
.card { .card {
display: grid; display: flex;
grid-template-columns: 120rpx 1fr 72rpx; flex-direction: column;
align-items: center;
gap: 16rpx; gap: 16rpx;
padding: 20rpx; padding: 20rpx;
border-radius: 20rpx; border-radius: 20rpx;
@@ -413,6 +418,21 @@ const init = async () => {
background: #FFF9F9; 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 { .thumb {
width: 120rpx; width: 120rpx;
height: 120rpx; height: 120rpx;
@@ -442,7 +462,7 @@ const init = async () => {
font-size: 30rpx; font-size: 30rpx;
font-weight: 700; font-weight: 700;
color: #2A2A2A; color: #2A2A2A;
max-width: 70%; max-width: 100%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@@ -474,28 +494,34 @@ const init = async () => {
} }
} }
} }
}
.tags { .tags {
display: flex; display: flex;
gap: 10rpx; flex-wrap: wrap;
} gap: 10rpx;
}
.tag { .tag {
padding: 6rpx 14rpx; padding: 6rpx 14rpx;
border-radius: 16rpx; border-radius: 16rpx;
font-size: 22rpx; font-size: 22rpx;
font-weight: 600; font-weight: 600;
} }
.tag.rent { .tag.rent {
background: #E8F5E8; background: #E8F5E8;
color: #3EAB64; color: #3EAB64;
} }
.tag.return { .tag.return {
background: #E8F2FF; background: #E8F2FF;
color: #3578e5; color: #3578e5;
} }
.tag.coupon {
background: #FFF9F0;
color: #D4A574;
} }
.actions { .actions {
@@ -503,6 +529,7 @@ const init = async () => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
gap: 8rpx;
} }
.nav { .nav {
+11 -8
View File
@@ -6,16 +6,16 @@
<script> <script>
import { import {
alipayLogin, wxLogin,
} from '../../../util/index' } from '@/util/index'
import { import {
getMyIndexInfo getMyIndexInfo
} from "../../../config/api/user.js"; } from "@/config/api/user.js";
import { import {
queryHasOrder, queryHasOrder,
checkOrdersByStatus checkOrdersByStatus
} from "../../../config/api/order.js"; } from "@/config/api/order.js";
export default { export default {
data() { data() {
@@ -56,7 +56,7 @@
// 如果是使用中的订单,跳转到归还页面 // 如果是使用中的订单,跳转到归还页面
console.log('检测到使用中订单,跳转归还页:', latestOrder.orderId); console.log('检测到使用中订单,跳转归还页:', latestOrder.orderId);
uni.redirectTo({ uni.redirectTo({
url: `/pages/device/return?orderId=${latestOrder.orderId}` url: `/subPackages/service/return/index?orderId=${latestOrder.orderId}`
}); });
} else if (latestOrder.orderStatus === 'waiting_for_payment') { } else if (latestOrder.orderStatus === 'waiting_for_payment') {
// 如果是待支付订单,跳转到支付页面,并传递必要信息 // 如果是待支付订单,跳转到支付页面,并传递必要信息
@@ -81,13 +81,13 @@
const totalAmount = (parseFloat(depositAmount) + parseFloat(packagePrice)).toFixed(2); const totalAmount = (parseFloat(depositAmount) + parseFloat(packagePrice)).toFixed(2);
uni.redirectTo({ uni.redirectTo({
url: `/pages/order/payment?orderId=${latestOrder.orderId}&packageTimeHours=${packageTimeHours}&packagePrice=${packagePrice}&hourlyRate=${hourlyRate}&totalAmount=${totalAmount}&depositAmount=${depositAmount}` url: `/subPackages/order/payment?orderId=${latestOrder.orderId}&packageTimeHours=${packageTimeHours}&packagePrice=${packagePrice}&hourlyRate=${hourlyRate}&totalAmount=${totalAmount}&depositAmount=${depositAmount}`
}); });
} else { } else {
// 其他状态(理论上不应该到这里,除非statusesToCheck配置错误),默认到详情页 // 其他状态(理论上不应该到这里,除非statusesToCheck配置错误),默认到详情页
console.log('检测到其他状态订单,跳转详情页:', latestOrder.orderId); console.log('检测到其他状态订单,跳转详情页:', latestOrder.orderId);
uni.redirectTo({ uni.redirectTo({
url: `/pages/device/detail?deviceNo=${deviceNo}` url: `/pages/order/detail?deviceNo=${deviceNo}`
}); });
} }
} else { } else {
@@ -122,8 +122,11 @@
url: `/pages/device/detail?deviceNo=${option.deviceNo}` url: `/pages/device/detail?deviceNo=${option.deviceNo}`
}); });
} else { } else {
// uni.switchTab({
// url:'/pages/index/index'
// })
// 如果连deviceNo都没有,则返回首页 // 如果连deviceNo都没有,则返回首页
uni.switchTab({ url: '/pages/index/index' }); uni.reLaunch({ url: '/pages/index/index' });
} }
}, 2000); }, 2000);
} finally { } finally {
-240
View File
@@ -1,240 +0,0 @@
<template>
<view class="container">
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="avatar">
<image :src="userInfo.avatar || '/static/images/default-avatar.png'" mode="aspectFill"></image>
</view>
<view class="user-info">
<text class="nickname">{{ userInfo.nickName || $t('user.notLoggedIn') }}</text>
<text class="phone">{{ userInfo.phone || $t('user.phoneNotBound') }}</text>
</view>
</view>
<!-- 余额卡片 -->
<view class="balance-card">
<view class="balance-title">{{ $t('userProfile.balance') }}</view>
<view class="balance-amount">{{ userInfo.balanceAmount || '0.00' }}</view>
<view class="balance-desc">{{ $t('user.balanceDesc') }}</view>
</view>
<!-- 功能菜单 -->
<view class="menu-list">
<view class="menu-item" @click="navigateTo('/pages/order/index')">
<text class="menu-icon">📋</text>
<text class="menu-text">{{ $t('user.myOrders') }}</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="navigateTo('/pages/feedback/index')">
<text class="menu-icon">💬</text>
<text class="menu-text">{{ $t('user.feedback') }}</text>
<text class="menu-arrow">></text>
</view>
<view class="menu-item" @click="navigateTo('/pages/help/index')">
<text class="menu-icon"></text>
<text class="menu-text">{{ $t('help.title') }}</text>
<text class="menu-arrow">></text>
</view>
</view>
<!-- 退出登录按钮 -->
<view class="logout-btn" @click="handleLogout" v-if="isLogin">
<text>{{ $t('user.logout') }}</text>
</view>
</view>
</template>
<script>
import { getUserInfo } from '@/api/user'
import { URL } from '@/config/url'
export default {
data() {
return {
userInfo: {},
isLogin: false
}
},
onLoad() {
// 设置页面标题
uni.setNavigationBarTitle({
title: this.$t('user.personalCenter')
})
},
onShow() {
this.loadUserInfo()
},
methods: {
async loadUserInfo() {
try {
const res = await getUserInfo()
if (res.code === 401 || res.code === 40101) {
// 无提示跳转至登录
try {
const pages = getCurrentPages()
const current = pages && pages.length ? pages[pages.length - 1] : null
const route = current && current.route ? ('/' + current.route) : '/pages/index/index'
const query = current && current.options ? Object.keys(current.options).map(k => `${k}=${encodeURIComponent(current.options[k])}`).join('&') : ''
const redirect = encodeURIComponent(query ? `${route}?${query}` : route)
uni.reLaunch({ url: `/pages/login/index?redirect=${redirect}` })
} catch (e) {
uni.reLaunch({ url: '/pages/login/index' })
}
} else if (res.code === 200) {
this.userInfo = res.data
this.isLogin = true
} else {
this.isLogin = false
}
} catch (error) {
console.error('加载用户信息失败:', error)
this.isLogin = false
}
},
navigateTo(url) {
uni.navigateTo({ url })
},
handleLogout() {
uni.showModal({
title: this.$t('common.tips'),
content: this.$t('user.confirmLogout'),
success: (res) => {
if (res.confirm) {
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
this.isLogin = false
uni.showToast({
title: this.$t('user.logoutSuccess'),
icon: 'success'
})
setTimeout(() => {
uni.redirectTo({
url: '/pages/login/index'
})
}, 500)
}
}
})
}
}
}
</script>
<style>
.container {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.user-card {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
display: flex;
align-items: center;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
overflow: hidden;
margin-right: 30rpx;
}
.avatar image {
width: 100%;
height: 100%;
}
.user-info {
flex: 1;
}
.nickname {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
display: block;
}
.phone {
font-size: 28rpx;
color: #666;
}
.balance-card {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.balance-title {
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
}
.balance-amount {
font-size: 48rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.balance-desc {
font-size: 24rpx;
color: #999;
}
.menu-list {
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.menu-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-icon {
font-size: 36rpx;
margin-right: 20rpx;
}
.menu-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
.menu-arrow {
font-size: 28rpx;
color: #999;
}
.logout-btn {
margin-top: 40rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
text-align: center;
color: #ff4d4f;
font-size: 28rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
</style>
-413
View File
@@ -1,413 +0,0 @@
<template>
<view class="profile-page">
<view class="avatar-section">
<view class="avatar-container">
<image class="avatar" v-if="userInfo.avatar" :src="userInfo.avatar" mode="aspectFill"></image>
<image v-else class="avatar" src="@/static/head.png" mode="aspectFill"></image>
<!-- 覆盖在头像上的支付宝选择头像授权按钮仅小程序生效 -->
<!-- #ifdef MP-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 { getOrderByOrderNoScorePayStatus, cancelOrder } from '@/config/api/order.js'
import { useI18n } from '@/utils/i18n.js' import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n() const { t } = useI18n()
const progress = ref(0) const progress = ref(0)
const leftRotateDeg = ref(0) const leftRotateDeg = ref(0)
@@ -91,9 +91,9 @@
await cancelOrder({ orderId: orderNo.value }) await cancelOrder({ orderId: orderNo.value })
} }
} catch (e) {} } catch (e) {}
uni.showToast({ title: $t('waiting.rentFailed'), icon: 'none' }) uni.showToast({ title: t('waiting.rentFailed'), icon: 'none' })
setTimeout(() => { setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' }) uni.reLaunch({ url: '/pages/index/index' })
}, 800) }, 800)
} }
@@ -129,16 +129,16 @@
// 超时保护:例如 60 秒 // 超时保护:例如 60 秒
timeoutTimer = setTimeout(() => { timeoutTimer = setTimeout(() => {
stopAllTimers() stopAllTimers()
uni.showToast({ title: $t('waiting.timeout'), icon: 'none' }) uni.showToast({ title: t('waiting.timeout'), icon: 'none' })
setTimeout(() => { setTimeout(() => {
uni.switchTab({ url: '/pages/index/index' }) uni.reLaunch({ url: '/pages/index/index' })
}, 800) }, 800)
}, 60000) }, 60000)
} }
onLoad((query) => { onLoad((query) => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: $t('waiting.title') title: t('waiting.title')
}) })
if (query) { if (query) {
if (query.orderNo) { if (query.orderNo) {
+10 -7
View File
@@ -17,6 +17,9 @@ importers:
axios-miniprogram-adapter: axios-miniprogram-adapter:
specifier: 0.3.4 specifier: 0.3.4
version: 0.3.4 version: 0.3.4
html5-qrcode:
specifier: ^2.3.8
version: 2.3.8
uniapp-axios-adapter: uniapp-axios-adapter:
specifier: ^0.3.2 specifier: ^0.3.2
version: 0.3.2(axios@1.10.0) version: 0.3.2(axios@1.10.0)
@@ -83,9 +86,6 @@ packages:
'@jridgewell/source-map@0.3.6': '@jridgewell/source-map@0.3.6':
resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/sourcemap-codec@1.5.5': '@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
@@ -491,6 +491,9 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
html5-qrcode@2.3.8:
resolution: {integrity: sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==}
immutable@5.1.3: immutable@5.1.3:
resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==} resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==}
@@ -743,7 +746,7 @@ snapshots:
'@jridgewell/gen-mapping@0.3.8': '@jridgewell/gen-mapping@0.3.8':
dependencies: dependencies:
'@jridgewell/set-array': 1.2.1 '@jridgewell/set-array': 1.2.1
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
'@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/resolve-uri@3.1.2': {}
@@ -755,14 +758,12 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.8 '@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
'@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.25': '@jridgewell/trace-mapping@0.3.25':
dependencies: dependencies:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.5
'@parcel/watcher-android-arm64@2.5.1': '@parcel/watcher-android-arm64@2.5.1':
optional: true optional: true
@@ -1177,6 +1178,8 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
html5-qrcode@2.3.8: {}
immutable@5.1.3: {} immutable@5.1.3: {}
is-extglob@2.1.1: is-extglob@2.1.1:
Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because it is too large Load Diff
+783
View File
@@ -0,0 +1,783 @@
<template>
<view class="order-detail-container">
<!-- 状态标题 -->
<!-- <view class="status-header">
<view class="status-icon">
<uv-icon name="checkbox-mark" size="40" color="#07c160"></uv-icon>
</view>
<text class="status-text">{{ statusText }}</text>
</view> -->
<!-- 产品信息卡片 -->
<view class="product-card">
<image
:src="orderDetail.pictureUrl || orderDetail.productImage || '/static/default-product.png'"
mode="aspectFill"
class="product-image"
></image>
<view class="product-info">
<view class="product-name">{{ orderDetail.productName || orderDetail.deviceName || '风电者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
@@ -0,0 +1,864 @@
<template>
<view class="order-container">
<!-- 状态切换 -->
<view class="status-tabs">
<view v-for="(tab, index) in orderStatusTabs" :key="index" class="tab-item"
:class="{ active: currentTab === index }" @click="switchTab(index)">
{{ tab.text }}
</view>
</view>
<!-- 订单列表 -->
<view class="order-list">
<view class="empty-state" v-if="orderList.length === 0">
<view class="empty-icon">
<image src="/static/orderList.png" mode="aspectFill" class="empty-icon" lazy-load="true"></image>
</view>
<text class="empty-text">{{ $t('order.noOrderRecord') }}</text>
</view>
<DeviceOrderItemCard
v-for="(order, index) in orderList"
:key="index"
:order="order"
@click="navigateToDeviceOrderDetail"
@delete="handleDeleteOrder"
@pay="handlePayment"
/>
</view>
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
onUnmounted
} from 'vue';
import DeviceOrderItemCard from '../../components/DeviceOrderItemCard.vue';
import {
onLoad,
onShow
} from '@dcloudio/uni-app';
import {
getOrderList,
getProductOrderList,
queryById,
getOrderByOrderNoScorePayStatus,
cancelOrder,
createWxPayment,
deleteProductOrder,
cancelProductOrder,
} from '../../config/api/order.js';
import{
createProductOrder
}from "@/config/api/product.js"
import {
updateUserBalance
} from '../../config/api/user.js';
import {
URL
} from '../../config/url.js';
import { useI18n } 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
@@ -0,0 +1,479 @@
<template>
<view class="my-card-page">
<!-- 会员卡列表 -->
<view class="card-list" v-if="cardList.length > 0">
<view v-for="card in cardList" :key="card.id" style="position: relative;background-color: #f5f5f5;"
@click="viewCardDetail(card)" :style="card.cardType==='COUNT'?'height: 240rpx;':'height: 240rpx;'">
<view
style="height: 120rpx;background-color: #ffffff;z-index: 999;border-radius: 25rpx;padding: 32rpx;position: absolute;top: 0;left: 0;right: 0;">
<!-- 卡片头部标题和日期 -->
<view class="card-header">
<text class="card-name">{{ card.name }}</text>
<view class="card-date">
<text class="date-text" v-if="card.status !== 'expired'">{{ card.endDate }}{{
$t('myCard.expire') }}</text>
<text class="date-text expired" v-else>{{ $t('myCard.expiredOn') }}{{ card.endDate }}</text>
</view>
</view>
<!-- 地区信息 -->
<view class="card-region">
<text
class="region-text">{{ $t('myCard.onlyForRegionBefore') }}{{ card.positionName }}{{ $t('myCard.onlyForRegionAfter') }}</text>
<!-- 状态标签 / 去使用按钮 -->
<view v-if="card.status !== 'expired'" class="status-tag active"
style="display: flex; align-items: center; gap: 4rpx; background-color: #FFE2B8; border-radius: 20rpx; padding: 6rpx 20rpx;"
@click.stop="handleUseCard(card)">
<text class="status-text" style="color: #A16300;">{{ $t('myCard.toUse') }}</text>
<!-- <uv-icon name="scan" size="12" color="#D4A574"></uv-icon> -->
</view>
<view v-else class="status-tag" :class="getStatusClass(card.status)">
<text class="status-text">{{ getStatusText(card.status) }}</text>
</view>
</view>
</view>
<!-- 使用情况和操作按钮 -->
<view style="position: absolute; bottom: -20rpx; left: 0; right: 0; padding: 20rpx;z-index:1;">
<view class="card-footer">
<!-- 次卡信息 -->
<view v-if="card.cardType === 'COUNT'" class="card-usage-info">
<text class="usage-text">{{ $t('myCard.remainingTimes') }}{{ card.remainingCount }}{{
$t('myCard.times') }}</text>
</view>
<!-- 时长卡信息 -->
<view v-else class="card-usage-info">
<text class="usage-text">每日限用次数{{card.dailyLimitCount}}</text>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<!-- 续卡按钮仅次卡显示 -->
<view v-if="card.cardType === 'COUNT'" class="renew-btn" @click.stop="renewCard(card)">
<text class="renew-text">{{ $t('myCard.renew') }}</text>
<uv-icon name="arrow-right" size="14" color="#D4A574"></uv-icon>
</view>
<view v-else class="renew-btn">
<text class="renew-text">单次限时{{card.singleLimitMinutes}}分钟</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<image class="empty-icon" src="/static/empty-card.png" mode="aspectFit"></image>
<text class="empty-text">{{ $t('myCard.noCards') }}</text>
<view class="buy-btn" @click="goToBuy">
<text class="buy-text">{{ $t('myCard.buyNow') }}</text>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted
} from 'vue'
import {
useI18n
} from '@/utils/i18n.js'
import {
getMemberCardsByStatus
} from '@/config/api/member.js'
import {
getQueryString
} from '@/util/index.js'
import {
getDeviceInfo
} from '@/config/api/device.js'
import {
getInUseOrder,
getUnpaidOrder
} from '@/config/api/order.js'
const {
t
} = useI18n()
// 会员卡列表
const cardList = ref([])
// 获取会员卡列表
const getCardList = async () => {
try {
const response = await getMemberCardsByStatus()
// 处理API返回的数据,转换为模板需要的格式
if (response.code === 200 && response.data) {
cardList.value = response.data.map(item => ({
id: item.id,
name: item.cardName,
cardType: item.cardType, // TIME 或 COUNT
// 次卡相关
totalCount: item.totalCount,
remainingCount: item.remainingCount,
singleLimitMinutesForCount: item.singleLimitMinutesForCount,
// 时长卡相关
cycleDays: item.cycleDays,
dailyLimitCount: item.dailyLimitCount,
singleLimitMinutes: item.singleLimitMinutes,
currentCycleStartTime: item.currentCycleStartTime,
currentCycleUsedCount: item.currentCycleUsedCount,
// 通用信息
status: item.status,
startDate: item.startTime?.split(' ')[0] || item.startTime,
endDate: item.endTime?.split(' ')[0] || item.endTime,
positionId: item.positionId,
positionName: item.positionName,
purchasePrice: item.purchasePrice,
remark: item.remark
}))
} else {
cardList.value = []
}
} catch (error) {
console.error('获取会员卡列表失败:', error)
uni.showToast({
title: t('myCard.getListFailed'),
icon: 'none'
})
}
}
// 计算进度条宽度
const getProgressWidth = (used, total) => {
if (!total || total === 0) return '0%'
const percentage = (used / total) * 100
return `${Math.min(percentage, 100)}%`
}
// 获取状态类名
const getStatusClass = (status) => {
const statusMap = {
'unused': 'active',
'expired': 'expired',
'used': 'used',
'active': 'active' // 兼容原始状态
}
return statusMap[status] || 'active' // 默认为active
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'unused': t('myCard.active'), // unused表示未使用,即活跃状态
'expired': t('myCard.expired'),
'used': t('myCard.used'),
'active': t('myCard.active') // 兼容原始状态
}
return statusMap[status] || t('myCard.active') // 默认为active
}
// 查看卡详情
const viewCardDetail = (card) => {
// TODO: 跳转到卡详情页面
// uni.showToast({
// title: t('common.functionDeveloping'),
// icon: 'none'
// })
}
// 续卡
const renewCard = (card) => {
uni.navigateTo({
url: `/pages/purchase/index?positionId=${card.positionId}`
})
}
// 去使用会员卡
const handleUseCard = async (card) => {
try {
const scanResult = await new Promise((resolve, reject) => {
uni.scanCode({
success: resolve,
fail: reject
})
})
console.log('扫码结果:', scanResult);
let deviceNo;
// 兼容不同平台的扫码结果
if (scanResult.scanType === 'QR_CODE' || scanResult.scanType === 'qrCode') {
deviceNo = getQueryString(scanResult.result, 'deviceNo')
} else if (scanResult.path) {
deviceNo = getQueryString(scanResult.path, 'deviceNo')
} else {
deviceNo = scanResult.result
}
if (!deviceNo) {
uni.showToast({
title: t('home.invalidQRCode'),
icon: 'none'
})
return
}
uni.showLoading({
title: t('common.getting')
})
// 检查是否有使用中的订单
const inUseRes = await getInUseOrder()
if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
uni.hideLoading()
const inUseOrder = inUseRes.data
uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
})
return
}
// 检查是否有待支付订单
const orderRes = await getUnpaidOrder()
if (orderRes && orderRes.code === 200 && orderRes.data) {
uni.hideLoading()
const unpaidOrder = orderRes.data
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
})
return
}
// 获取设备信息并跳转详情
const deviceInfoRes = await getDeviceInfo(deviceNo)
uni.hideLoading()
if (deviceInfoRes.code === 200 && deviceInfoRes.data && deviceInfoRes.data.device) {
const deviceInfo = deviceInfoRes.data.device
let url = `/pages/device/detail?deviceNo=${deviceNo}`
if (deviceInfo.feeConfig) {
url += `&feeConfig=${encodeURIComponent(deviceInfo.feeConfig)}`
}
uni.navigateTo({
url
})
} else {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} catch (error) {
console.error('扫码处理失败:', error)
if (error && error.errMsg !== 'scanCode:fail cancel') {
uni.showToast({
title: t('home.scanFailed'),
icon: 'none'
})
}
}
}
// 去购买
const goToBuy = () => {
uni.navigateTo({
url: '/pages/purchase/index'
})
}
onMounted(() => {
uni.setNavigationBarTitle({
title: t('user.myCards')
})
getCardList()
})
</script>
<style lang="scss" scoped>
.my-card-page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 24rpx;
}
.card-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.card-item {
// background-color: #ffffff;
border-radius: 25rpx;
padding: 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
position: relative;
z-index: 5;
}
// 卡片头部
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
}
.card-name {
font-size: 36rpx;
font-weight: 600;
color: #333333;
line-height: 50rpx;
}
.card-date {
.date-text {
font-size: 24rpx;
color: #999999;
line-height: 34rpx;
&.expired {
color: #999999;
}
}
}
// 地区信息
.card-region {
margin-bottom: 24rpx;
align-items: center;
display: flex;
justify-content: space-between;
gap: 16rpx;
.region-text {
font-size: 26rpx;
color: #666666;
line-height: 36rpx;
}
}
// 卡片底部
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
padding-top: 50rpx;
position: absolute;
background: rgba(255, 244, 227, 1);
z-index: 1;
right: 0;
bottom: 0;
left: 0;
border-radius: 0 0 25rpx 25rpx;
/* text-align: 30rpx ; */
}
.card-usage-info {
flex: 1;
.usage-text {
font-size: 26rpx;
color: #D4A574;
font-weight: 500;
line-height: 36rpx;
}
}
.card-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
// 续卡按钮
.renew-btn {
display: flex;
align-items: center;
gap: 4rpx;
padding: 8rpx 16rpx;
// background-color: #FFF9F0;
border-radius: 8rpx;
.renew-text {
font-size: 24rpx;
color: #D4A574;
line-height: 34rpx;
}
.arrow {
font-size: 24rpx;
color: #D4A574;
}
}
// 状态标签
.status-tag {
padding: 8rpx 20rpx;
border-radius: 8rpx;
.status-text {
font-size: 24rpx;
line-height: 34rpx;
}
&.active {
// background-color: #FFF9F0;
.status-text {
color: #D4A574;
}
}
&.expired {
// background-color: #F5F5F5;
.status-text {
color: #999999;
}
}
&.used {
// background-color: #F5F5F5;
.status-text {
color: #999999;
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 40rpx;
}
.buy-btn {
padding: 20rpx 60rpx;
background-color: #B8741A;
border-radius: 48rpx;
.buy-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
}
}
</style>
+542
View File
@@ -0,0 +1,542 @@
<template>
<view class="my-coupon-page">
<!-- Tab 切换 -->
<!-- <view class="tab-container">
<view class="tab-item" :class="{ active: currentTab === 'available' }" @click="switchTab('available')">
<text class="tab-text">{{ $t('myCoupon.available') }}</text>
</view>
<view class="tab-item" :class="{ active: currentTab === 'used' }" @click="switchTab('used')">
<text class="tab-text">{{ $t('myCoupon.used') }}</text>
</view>
<view class="tab-item" :class="{ active: currentTab === 'expired' }" @click="switchTab('expired')">
<text class="tab-text">{{ $t('myCoupon.expired') }}</text>
</view>
</view> -->
<!-- 优惠券列表 -->
<view class="coupon-list" v-if="filteredCoupons.length > 0">
<view v-for="coupon in filteredCoupons" :key="coupon.id" class="coupon-item-wrapper">
<view class="coupon-item" :class="getCouponClass(coupon.status)">
<!-- 虚线上下圆形缺口 -->
<view class="coupon-circle-top"></view>
<view class="coupon-circle-bottom"></view>
<view class="coupon-left">
<view class="coupon-value">
<text v-if="coupon.type === 'cash'" class="coupon-unit">¥</text>
<text class="coupon-amount">{{ coupon.type === 'discount' ? coupon.discount : coupon.value }}</text>
<text v-if="coupon.type === 'discount'" class="coupon-unit"></text>
</view>
<view style="display: flex;flex-direction: column;">
<text class="coupon-condition">{{ coupon.condition }}</text>
<text class="coupon-validity-left">{{ coupon.validity }}</text>
</view>
</view>
<view class="coupon-divider"></view>
<view class="coupon-right">
<!-- <text class="coupon-name">{{ coupon.name }}</text> -->
<!-- <text class="coupon-region" v-if="coupon.positionName"
style="font-size: 22rpx; color: #999; margin-top: 4rpx;">
{{ $t('myCoupon.onlyForRegionBefore') }}{{ coupon.positionName }}{{ $t('myCoupon.onlyForRegionAfter') }}
</text> -->
<view class="use-btn" v-if="coupon.status == 'unused'" @click="useCoupon(coupon)">
<text class="use-text">{{ $t('myCoupon.useNow') }}</text>
</view>
<text class="coupon-status" v-else>{{ getStatusText(coupon.status) }}</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<image class="empty-icon" src="/static/empty-coupon.png" mode="aspectFit"></image>
<text class="empty-text">{{ getEmptyText() }}</text>
<view class="buy-btn" @click="goToBuy" v-if="currentTab === 'available'">
<text class="buy-text">{{ $t('myCoupon.buyNow') }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useI18n } from '@/utils/i18n.js'
import { getUserCoupons } from '@/config/api/coupon.js'
import { getQueryString } from '@/util/index.js'
import { getDeviceInfo } from '@/config/api/device.js'
import { getInUseOrder, getUnpaidOrder } from '@/config/api/order.js'
const { t } = useI18n()
// 当前选中的 Tab
const currentTab = ref('available')
// // Tab 与 API 状态的映射
// const tabToStatusMap = {
// available: 'unused',
// used: 'used',
// expired: 'expired'
// }
// 优惠券列表
const couponList = ref([])
// 过滤后的优惠券
const filteredCoupons = computed(() => {
return couponList.value;
})
// 获取优惠券列表
const getCouponList = async () => {
try {
// const apiStatus = tabToStatusMap[currentTab.value]
const res = await getUserCoupons('')
if (res.code === 200 && res.data) {
// 将后端数据转换为前端需要的格式
couponList.value = (res.data || []).map(item => {
// 判断优惠券类型:discount_coupon 折扣券,deduction_coupon 抵扣券
const isCashCoupon = item.couponType === 'deduction_coupon'
// 格式化使用条件
let condition = '无门槛'
if (item.usableCondition && item.usableCondition > 0) {
condition = `${item.usableCondition}可用`
}
// 格式化有效期
let validity = ''
if (currentTab.value === 'used') {
// 已使用显示使用时间
validity = item.couponStartTime ? `使用时间 ${item.couponStartTime.split(' ')[0]}` : ''
} else if (item.couponEndTime) {
validity = `${item.couponEndTime.split(' ')[0]} 过期`
}
console.log(item.status);
return {
id: item.id,
name: item.couponName || '优惠券',
type: isCashCoupon ? 'cash' : 'discount',
value: item.deductAmount ? parseFloat(item.deductAmount) : 0,
discount: item.discountRate ? parseFloat(item.discountRate) * 10 : null,
condition: condition,
validity: validity,
status: item.status,
positionName: item.positionName
}
})
} else {
couponList.value = []
}
} catch (error) {
console.error('获取优惠券列表失败:', error)
couponList.value = []
uni.showToast({
title: t('myCoupon.getListFailed'),
icon: 'none'
})
}
}
// 切换 Tab
const switchTab = (tab) => {
currentTab.value = tab
getCouponList()
}
// 获取优惠券样式类名
const getCouponClass = (status) => {
return status === 'unused' ? '' : 'disabled'
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'used': t('myCoupon.usedStatus'),
'expired': t('myCoupon.expiredStatus'),
'refunded':t('myCoupon.refundedStatus')
}
console.log("获取状态文本:"+statusMap[status]);
return statusMap[status] || ''
}
// 获取空状态文本
const getEmptyText = () => {
const textMap = {
'available': t('myCoupon.noAvailableCoupons'),
'used': t('myCoupon.noUsedCoupons'),
'expired': t('myCoupon.noExpiredCoupons')
}
return textMap[currentTab.value] || ''
}
// 使用优惠券
const useCoupon = async (coupon) => {
try {
const scanResult = await new Promise((resolve, reject) => {
uni.scanCode({
success: resolve,
fail: reject
})
})
console.log('扫码结果:', scanResult);
let deviceNo;
// 兼容不同平台的扫码结果
if (scanResult.scanType === 'QR_CODE' || scanResult.scanType === 'qrCode') {
deviceNo = getQueryString(scanResult.result, 'deviceNo')
} else if (scanResult.path) {
deviceNo = getQueryString(scanResult.path, 'deviceNo')
} else {
deviceNo = scanResult.result
}
if (!deviceNo) {
uni.showToast({
title: t('home.invalidQRCode'),
icon: 'none'
})
return
}
uni.showLoading({
title: t('common.getting')
})
// 检查是否有使用中的订单
const inUseRes = await getInUseOrder()
if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
uni.hideLoading()
const inUseOrder = inUseRes.data
uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
})
return
}
// 检查是否有待支付订单
const orderRes = await getUnpaidOrder()
if (orderRes && orderRes.code === 200 && orderRes.data) {
uni.hideLoading()
const unpaidOrder = orderRes.data
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
})
return
}
// 获取设备信息并跳转详情
const deviceInfoRes = await getDeviceInfo(deviceNo)
uni.hideLoading()
if (deviceInfoRes.code === 200 && deviceInfoRes.data && deviceInfoRes.data.device) {
const deviceInfo = deviceInfoRes.data.device
let url = `/pages/device/detail?deviceNo=${deviceNo}`
if (deviceInfo.feeConfig) {
url += `&feeConfig=${encodeURIComponent(deviceInfo.feeConfig)}`
}
uni.navigateTo({
url
})
} else {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} catch (error) {
console.error('扫码处理失败:', error)
if (error && error.errMsg !== 'scanCode:fail cancel') {
uni.showToast({
title: t('home.scanFailed'),
icon: 'none'
})
}
}
}
// 去购买
const goToBuy = () => {
uni.navigateTo({
url: '/pages/purchase/index?tab=coupon'
})
}
onMounted(() => {
uni.setNavigationBarTitle({
title: t('user.myCoupons')
})
getCouponList()
})
</script>
<style lang="scss" scoped>
// 优惠券样式变量封装
$coupon-theme-color: #A16300;
$coupon-divider-color: #B8741A;
$coupon-bg-faded: #f5f5f5;
$coupon-active-bg-start: #FFF4E6;
$coupon-active-bg-end: #FFE8CC;
$coupon-divider-left: 65%;
$coupon-circle-radius: 16rpx;
.my-coupon-page {
min-height: 100vh;
background-color: #f5f5f5;
}
/* Tab 切换 */
.tab-container {
position: sticky;
top: 0;
display: flex;
background-color: #ffffff;
z-index: 999;
padding: 20rpx 0;
}
.tab-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
position: relative;
.tab-text {
font-size: 28rpx;
color: #666;
font-weight: 500;
}
&.active {
.tab-text {
color: #333;
font-weight: 600;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 6rpx;
background-color: #FFA928;
border-radius: 3rpx;
}
}
}
.coupon-list {
padding: 20rpx;
display: flex;
flex-direction: column;
gap: 20rpx;
}
.coupon-item-wrapper {
position: relative;
}
.coupon-item {
background: #FFF4E3;
border-radius: 20rpx;
padding: 40rpx 30rpx;
display: flex;
align-items: stretch;
position: relative;
overflow: visible;
min-height: 180rpx;
box-sizing: border-box;
border: 2rpx solid transparent;
transition: all 0.3s;
&.selected {
border-color: $coupon-divider-color;
box-shadow: 0 4rpx 20rpx rgba(184, 116, 26, 0.2);
.coupon-circle-top,
.coupon-circle-bottom {
background-color: $coupon-active-bg-start;
border: 2rpx solid $coupon-divider-color;
}
}
&.disabled {
background: linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%);
opacity: 0.6;
.coupon-value,
.coupon-condition,
.coupon-name {
color: #999;
}
.coupon-circle-top,
.coupon-circle-bottom {
background-color: #f5f5f5;
}
}
}
/* 虚线顶部圆形缺口 */
.coupon-circle-top {
position: absolute;
left: $coupon-divider-left+4%;
top: -$coupon-circle-radius;
transform: translateX(-50%);
width: $coupon-circle-radius * 2;
height: $coupon-circle-radius * 2;
border-radius: 50%;
background-color: $coupon-bg-faded;
z-index: 10;
}
/* 虚线底部圆形缺口 */
.coupon-circle-bottom {
position: absolute;
left: $coupon-divider-left+4%;
bottom: -$coupon-circle-radius;
transform: translateX(-50%);
width: $coupon-circle-radius * 2;
height: $coupon-circle-radius * 2;
border-radius: 50%;
background-color: $coupon-bg-faded;
z-index: 10;
}
.coupon-left {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
gap: 8rpx;
}
.coupon-value {
display: flex;
align-items: flex-end; // 单位在脚
color: $coupon-theme-color;
line-height: 1;
width:120rpx;
}
.coupon-amount {
font-size: 56rpx; // 值要大
font-weight: 700;
}
.coupon-unit {
font-size: 24rpx; // 单位小
font-weight: 500;
margin-bottom: 6rpx; // 微调单位垂直位置,使其更贴合“脚”
margin-left: 4rpx;
margin-right: 4rpx;
}
.coupon-condition {
font-size: 24rpx;
color: #000;
font-weight: 600;
}
.coupon-validity-left {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
}
.coupon-divider {
width: 2rpx;
height: 100%;
position: absolute;
left: $coupon-divider-left;
transform: translateX(-50%);
top: 0;
bottom: 0;
align-self: stretch;
background: repeating-linear-gradient(to bottom,
$coupon-divider-color 0rpx,
$coupon-divider-color 8rpx,
transparent 8rpx,
transparent 16rpx);
margin: 0 30rpx;
}
.coupon-right {
// flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
align-items: center;
margin: auto 0;
width: 160rpx;
// align-content: center;
// transform: translateY(50%);
}
.coupon-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.coupon-validity {
font-size: 22rpx;
color: #999;
}
.use-btn {
// margin-top: 10rpx;
// padding: 12rpx 28rpx;
// background-color: #B8741A;
// border-radius: 40rpx;
.use-text {
font-size: 28rpx;
color: #A16300;
font-weight: 600;
}
}
.coupon-status {
margin-top: 10rpx;
font-size: 24rpx;
color: #999;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 40rpx;
}
.buy-btn {
padding: 20rpx 60rpx;
background-color: #B8741A;
border-radius: 48rpx;
.buy-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
}
}
</style>
@@ -71,10 +71,10 @@ import {
import { import {
getNearbyDevices, getNearbyDevices,
transformDeviceData transformDeviceData
} from '../../config/api/device.js' } from '@/config/api/device.js'
import { useI18n } from '../../utils/i18n.js' import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n() const { t } = useI18n()
const positionInfo = ref({}) const positionInfo = ref({})
const positionId = ref('') const positionId = ref('')
@@ -112,7 +112,7 @@ const { t: $t } = useI18n()
const loadPositionDetail = async () => { const loadPositionDetail = async () => {
try { try {
uni.showLoading({ uni.showLoading({
title: $t('common.loading') title: t('common.loading')
}) })
// //
@@ -147,7 +147,7 @@ const { t: $t } = useI18n()
positionInfo.value = position positionInfo.value = position
} else { } else {
uni.showToast({ uni.showToast({
title: $t('location.notExist'), title: t('location.notExist'),
icon: 'none' icon: 'none'
}) })
} }
@@ -155,7 +155,7 @@ const { t: $t } = useI18n()
} catch (e) { } catch (e) {
console.error('加载设备详情失败:', e) console.error('加载设备详情失败:', e)
uni.showToast({ uni.showToast({
title: $t('common.loadFailed'), title: t('common.loadFailed'),
icon: 'none' icon: 'none'
}) })
} finally { } finally {
@@ -166,7 +166,7 @@ const { t: $t } = useI18n()
const navigateToPosition = () => { const navigateToPosition = () => {
if (!positionInfo.value.latitude || !positionInfo.value.longitude) { if (!positionInfo.value.latitude || !positionInfo.value.longitude) {
uni.showToast({ uni.showToast({
title: $t('location.coordinateError'), title: t('location.coordinateError'),
icon: 'none' icon: 'none'
}) })
return return
@@ -181,7 +181,7 @@ const { t: $t } = useI18n()
longitude < -180 || longitude > 180 || longitude < -180 || longitude > 180 ||
(latitude === 0 && longitude === 0)) { (latitude === 0 && longitude === 0)) {
uni.showToast({ uni.showToast({
title: $t('location.coordinateError'), title: t('location.coordinateError'),
icon: 'none' icon: 'none'
}) })
return return
File diff suppressed because it is too large Load Diff
@@ -12,7 +12,7 @@
<view class="order-list"> <view class="order-list">
<view class="empty-state" v-if="orderList.length === 0"> <view class="empty-state" v-if="orderList.length === 0">
<view class="empty-icon"> <view class="empty-icon">
<image src="/static/orderList.png" mode="aspectFill" class="empty-icon"></image> <image src="/static/orderList.png" mode="aspectFill" class="empty-icon" lazy-load="true"></image>
</view> </view>
<text class="empty-text">{{ $t('order.noOrderRecord') }}</text> <text class="empty-text">{{ $t('order.noOrderRecord') }}</text>
</view> </view>
@@ -35,7 +35,8 @@
import { import {
ref, ref,
reactive, reactive,
onMounted onMounted,
onUnmounted
} from 'vue'; } from 'vue';
import OrderItemCard from '../../components/OrderItemCard.vue'; import OrderItemCard from '../../components/OrderItemCard.vue';
import { import {
@@ -45,11 +46,9 @@
getOrderList, getOrderList,
queryById, queryById,
getOrderByOrderNoScorePayStatus, getOrderByOrderNoScorePayStatus,
cancelOrder cancelOrder,
createWxPayment
} from '../../config/api/order.js'; } from '../../config/api/order.js';
import {
confirmPaymentAndRent
} from '../../config/api/device.js';
import { import {
updateUserBalance updateUserBalance
} from '../../config/api/user.js'; } from '../../config/api/user.js';
@@ -58,14 +57,7 @@
} from '../../config/url.js'; } from '../../config/url.js';
import { useI18n } from '@/utils/i18n.js' import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n() const { t } = useI18n()
//
onMounted(() => {
uni.setNavigationBarTitle({
title: $t('order.myOrders')
})
})
// //
const currentTab = ref(0); const currentTab = ref(0);
@@ -74,62 +66,62 @@
// //
const orderStatusMap = reactive({ const orderStatusMap = reactive({
'0': { '0': {
get text() { return $t('order.waitingForPayment') }, get text() { return t('order.waitingForPayment') },
class: 'status-waiting' class: 'status-waiting'
}, },
'1': { '1': {
get text() { return $t('order.inUse') }, get text() { return t('order.inUse') },
class: 'status-using' class: 'status-using'
}, },
'2': { '2': {
get text() { return $t('order.finished') }, get text() { return t('order.finished') },
class: 'status-finished' class: 'status-finished'
}, },
'3': { '3': {
get text() { return $t('order.cancelled') }, get text() { return t('order.cancelled') },
class: 'status-cancelled' class: 'status-cancelled'
}, },
'waiting_for_payment': { 'waiting_for_payment': {
get text() { return $t('order.waitingForPayment') }, get text() { return t('order.waitingForPayment') },
class: 'status-waiting' class: 'status-waiting'
}, },
'in_used': { 'in_used': {
get text() { return $t('order.inUse') }, get text() { return t('order.inUse') },
class: 'status-using' class: 'status-using'
}, },
'used_done': { 'used_done': {
get text() { return $t('order.finished') }, get text() { return t('order.finished') },
class: 'status-finished' class: 'status-finished'
}, },
'order_cancelled': { 'order_cancelled': {
get text() { return $t('order.cancelled') }, get text() { return t('order.cancelled') },
class: 'status-cancelled' class: 'status-cancelled'
}, },
'express_return': { 'express_return': {
get text() { return $t('express.title') }, get text() { return t('express.title') },
class: 'status-express-return' class: 'status-express-return'
} }
}); });
// //
const orderStatusTabs = reactive([{ const orderStatusTabs = reactive([{
get text() { return $t('common.all') }, get text() { return t('common.all') },
status: [] status: []
}, },
{ {
get text() { return $t('order.waitingForPayment') }, get text() { return t('order.waitingForPayment') },
status: ['waiting_for_payment'] status: ['waiting_for_payment']
}, },
{ {
get text() { return $t('order.inUse') }, get text() { return t('order.inUse') },
status: ['in_used'] status: ['in_used']
}, },
{ {
get text() { return $t('order.finished') }, get text() { return t('order.finished') },
status: ['used_done'] status: ['used_done']
}, },
{ {
get text() { return $t('order.cancelled') }, get text() { return t('order.cancelled') },
status: ['order_cancelled'] status: ['order_cancelled']
} }
]); ]);
@@ -150,7 +142,9 @@
// //
const formattedOrder = { const formattedOrder = {
orderNo: orderData.orderId, orderNo: orderData.orderNo || orderData.orderId,
orderId: orderData.orderId,
orderStatus: orderData.orderStatus,
status: orderData.orderStatus, status: orderData.orderStatus,
deviceId: orderData.deviceNo, deviceId: orderData.deviceNo,
payWay: orderData.payWay, payWay: orderData.payWay,
@@ -158,7 +152,8 @@
endTime: orderData.endTime || '', endTime: orderData.endTime || '',
positionName: orderData.positionName || orderData.positionLocation || '', positionName: orderData.positionName || orderData.positionLocation || '',
deviceName: orderData.deviceName || '', deviceName: orderData.deviceName || '',
amount: orderData.payAmount || orderData.actualDeviceAmount || orderData.currentFee || orderData.residueAmount || '0.00' amount: orderData.payAmount || orderData.actualDeviceAmount || orderData.currentFee || orderData.residueAmount || '0.00',
pauseTime: orderData.pauseTime != null ? orderData.pauseTime : orderData.pause_time
}; };
// //
@@ -215,33 +210,57 @@
endTime: item.endTime || '', endTime: item.endTime || '',
positionName: item.positionName || item.positionLocation || '', positionName: item.positionName || item.positionLocation || '',
deviceName: item.deviceName || '', deviceName: item.deviceName || '',
amount: item.payAmount || item.actualDeviceAmount || item.currentFee || item.residueAmount || '0.00' amount: item.payAmount || item.actualDeviceAmount || item.currentFee || item.residueAmount || '0.00',
pauseTime: item.pauseTime != null ? item.pauseTime : item.pause_time
}; };
}); });
} }
} catch (error) { } catch (error) {
console.error('获取订单列表失败:', error); console.error('获取订单列表失败:', error);
uni.showToast({ uni.showToast({
title: $t('order.getOrderListFailed'), title: t('order.getOrderListFailed'),
icon: 'none' 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) => { const getOrderStatus = async (order) => {
try { try {
const res = await getOrderByOrderNoScorePayStatus(order.orderNo); const res = await getOrderByOrderNoScorePayStatus(order.orderNo);
if (res.code === 200) { if (res.code === 200) {
uni.showToast({ uni.showToast({
title: $t('order.syncSuccess'), title: t('order.syncSuccess'),
icon: 'success' icon: 'success'
}); });
await loadOrderList(orderStatusTabs[currentTab.value].status); await loadOrderList(orderStatusTabs[currentTab.value].status);
} }
} catch (error) { } catch (error) {
uni.showToast({ uni.showToast({
title: $t('order.syncFailed'), title: t('order.syncFailed'),
icon: 'none' icon: 'none'
}); });
} }
@@ -268,29 +287,21 @@
const handlePayment = async (order) => { const handlePayment = async (order) => {
try { try {
uni.showLoading({ uni.showLoading({
title: $t('common.processing') title: t('common.processing')
}); });
// //
const res = await uni.request({ const res = await createWxPayment(order.orderNo);
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.statusCode === 200 && res.data.code === 200) { if (res && res.code === 200) {
const payParams = res.data.data; const payParams = res.data;
// //
await uni.requestPayment({ await uni.requestPayment({
provider: 'alipay',
...payParams, ...payParams,
success: async () => { success: async () => {
uni.showToast({ uni.showToast({
title: $t('payment.paymentSuccess'), title: t('payment.paymentSuccess'),
icon: 'success' icon: 'success'
}); });
@@ -306,18 +317,18 @@
}, },
fail: (err) => { fail: (err) => {
console.error('支付失败:', err); console.error('支付失败:', err);
throw new Error($t('payment.paymentFailedRetry')); throw new Error(t('payment.paymentFailedRetry'));
} }
}); });
} else { } else {
throw new Error(res.data.msg || '创建支付订单失败'); throw new Error(res?.msg || '创建支付订单失败');
} }
uni.hideLoading(); uni.hideLoading();
} catch (error) { } catch (error) {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({
title: error.message || $t('payment.paymentFailed'), title: error.message || t('payment.paymentFailed'),
icon: 'none' icon: 'none'
}); });
} }
@@ -327,12 +338,12 @@
const handleCancelOrder = async (order) => { const handleCancelOrder = async (order) => {
try { try {
uni.showModal({ uni.showModal({
title: $t('order.confirmCancel'), title: t('order.confirmCancel'),
content: $t('order.confirmCancelContent'), content: t('order.confirmCancelContent'),
success: async (res) => { success: async (res) => {
if (res.confirm) { if (res.confirm) {
uni.showLoading({ uni.showLoading({
title: $t('common.processing') title: t('common.processing')
}); });
const result = await cancelOrder({ const result = await cancelOrder({
@@ -342,14 +353,14 @@
if (result) { if (result) {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({
title: $t('order.cancelSuccess'), title: t('order.cancelSuccess'),
icon: 'success' icon: 'success'
}); });
// //
await loadOrderList(); await loadOrderList();
} else { } else {
throw new Error(result.msg || $t('order.cancelFailed')); throw new Error(result.msg || t('order.cancelFailed'));
} }
} }
} }
@@ -357,7 +368,7 @@
} catch (error) { } catch (error) {
uni.hideLoading(); uni.hideLoading();
uni.showToast({ uni.showToast({
title: error.message || $t('order.cancelFailed'), title: error.message || t('order.cancelFailed'),
icon: 'none' icon: 'none'
}); });
} }
+897
View File
@@ -0,0 +1,897 @@
<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
@@ -0,0 +1,141 @@
<template>
<view class="return-map-page">
<view class="image-wrapper">
<movable-area class="movable-area">
<movable-view class="movable-view" direction="all" :scale="true" :scale-min="1" :scale-max="4"
:scale-value="scale" @scale="onScaleChange">
<image :src="imageUrl" mode="widthFix" class="map-image" lazy-load="true"></image>
</movable-view>
</movable-area>
</view>
<view class="bottom-bar">
<view class="btn" @click="resetScale">{{ $t('common.reset') }}</view>
<view class="btn primary" @click="previewImage">{{ $t('common.preview') }}</view>
</view>
</view>
</template>
<script setup>
import {
ref
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
useI18n
} from '@/utils/i18n.js'
const {
t
} = useI18n()
const imageUrl = ref('')
const scale = ref(1)
onLoad((options) => {
if (options && options.imageUrl) {
try {
imageUrl.value = decodeURIComponent(options.imageUrl)
} catch (e) {
imageUrl.value = options.imageUrl
}
}
if (!imageUrl.value) {
uni.showToast({
title: t('common.loadFailed'),
icon: 'none'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
uni.setNavigationBarTitle({
title: t('order.returnLocationMap')
})
})
const onScaleChange = (e) => {
if (e && typeof e.detail?.scale === 'number') {
scale.value = e.detail.scale
}
}
const resetScale = () => {
scale.value = 1
}
const previewImage = () => {
if (!imageUrl.value) return
uni.previewImage({
urls: [imageUrl.value]
})
}
</script>
<style lang="scss" scoped>
.return-map-page {
min-height: 100vh;
background-color: #000;
display: flex;
flex-direction: column;
padding-bottom: env(safe-area-inset-bottom);
box-sizing: border-box;
}
.image-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 20rpx;
box-sizing: border-box;
}
.movable-area {
width: 100%;
height: 100%;
}
.movable-view {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.map-image {
width: 100%;
display: block;
border-radius: 16rpx;
}
.bottom-bar {
padding: 16rpx 30rpx 24rpx;
display: flex;
justify-content: flex-end;
gap: 24rpx;
background: rgba(0, 0, 0, 0.8);
}
.btn {
min-width: 160rpx;
height: 72rpx;
border-radius: 36rpx;
border: 2rpx solid #ffffff;
color: #ffffff;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
&.primary {
background: #07c160;
border-color: #07c160;
}
}
</style>
@@ -85,6 +85,7 @@
<script> <script>
import { queryById } from '@/config/api/order.js' import { queryById } from '@/config/api/order.js'
import { withdrawDeposit } from '@/config/api/user.js'
import { import {
URL URL
}from"@/config/url.js" }from"@/config/url.js"
@@ -234,17 +235,9 @@ export default {
try { try {
uni.showLoading({ title: this.$t('common.processing') }); uni.showLoading({ title: this.$t('common.processing') });
const res = await uni.request({ const res = await withdrawDeposit(this.orderInfo.orderNo)
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.statusCode === 200 && res.data.code === 200) { if (res && res.code === 200) {
uni.showToast({ uni.showToast({
title: this.$t('order.refundSuccess'), title: this.$t('order.refundSuccess'),
icon: 'success' icon: 'success'
@@ -259,7 +252,7 @@ export default {
this.loadOrderInfo(); this.loadOrderInfo();
}, 1500); }, 1500);
} else { } else {
throw new Error(res.data.msg || this.$t('order.refundFailed')); throw new Error(res?.msg || this.$t('order.refundFailed'));
} }
} catch (error) { } catch (error) {
console.error('退款申请错误:', error); console.error('退款申请错误:', error);
+399
View File
@@ -0,0 +1,399 @@
<template>
<view class="success-container">
<!-- 支付成功状态和订单信息 -->
<view class="status-order-card">
<!-- 支付成功状态 -->
<view class="status-section">
<view class="status-icon success"></view>
<view class="status-text">{{ $t('success.paymentSuccess') }}</view>
<view class="status-desc">{{ $t('success.paymentSuccessDesc') }}</view>
</view>
<!-- 分割线 -->
<view class="section-divider"></view>
<!-- 订单信息 -->
<view class="order-section">
<view class="card-title">{{ $t('success.orderInfo') }}</view>
<view class="info-item">
<text class="label">{{ $t('order.orderNo') }}</text>
<text class="value">{{ orderInfo.orderNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('order.deviceNo') }}</text>
<text class="value">{{ orderInfo.deviceNo || '-' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('success.paymentAmount') }}</text>
<text class="value">{{ orderInfo.amount || '0.00' }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('success.paymentTime') }}</text>
<text class="value">{{ orderInfo.payTime || '-' }}</text>
</view>
</view>
</view>
<!-- 设备状态 -->
<view class="device-status">
<view class="status-message">{{ deviceMessage }}</view>
<view class="loading-animation" v-if="isLoading">
<view class="loading-circle"></view>
</view>
</view>
<!-- 操作按钮 -->
<view class="button-group">
<view class="secondary-btn" @click="goToHome">{{ $t('success.backToHome') }}</view>
<view class="primary-btn" @click="goToOrderList">{{ $t('success.viewOrder') }}</view>
</view>
</view>
</template>
<script setup>
import {
ref,
getCurrentInstance
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
queryById,
getOrderByOrderNo
} from '@/config/api/order.js'
// 获取当前实例以访问 $t 方法
const {
proxy
} = getCurrentInstance()
// 响应式数据
const orderId = ref('')
const orderInfo = ref({})
const isLoading = ref(true)
const deviceMessage = ref('')
const hasTriggeredDevice = ref(false)
// 页面加载
onLoad((options) => {
// 设置页面标题
uni.setNavigationBarTitle({
title: proxy.$t('success.paymentSuccess')
})
deviceMessage.value = proxy.$t('success.preparingDevice')
// #ifdef H5
if (uni.getStorageSync('pendingPaymentNo')) {
orderId.value = options.orderId
loadOrderInfo()
// 添加页面显示监听,防止页面切换后重复触发弹出
uni.$once('orderSuccess:' + orderId.value, () => {
console.log('已经触发过弹出逻辑,不再重复触发')
hasTriggeredDevice.value = true
})
} else {
uni.showToast({
title: proxy.$t('order.orderNotExist'),
icon: 'none'
})
setTimeout(() => {
goToHome()
}, 1500)
}
// #endif
// #ifndef H5
if (options && options.orderId) {
orderId.value = options.orderId
loadOrderInfo()
// 添加页面显示监听,防止页面切换后重复触发弹出
uni.$once('orderSuccess:' + orderId.value, () => {
console.log('已经触发过弹出逻辑,不再重复触发')
hasTriggeredDevice.value = true
})
} else {
uni.showToast({
title: proxy.$t('order.orderNotExist'),
icon: 'none'
})
setTimeout(() => {
goToHome()
}, 1500)
}
// #endif
})
// 加载订单信息
const loadOrderInfo = async () => {
try {
uni.showLoading({
title: proxy.$t('common.loading')
})
const res = await queryById(orderId.value)
if (res.code === 200 && res.data) {
const orderData = res.data
orderInfo.value = {
orderNo: orderData.orderNo || orderData.orderId,
deviceNo: orderData.deviceNo,
amount: orderData.payAmount || orderData.amount,
payTime: orderData.payTime || formatTime(new Date())
}
// 检查订单状态
if (orderData.orderStatus === 'IN_USED') {
// 如果已经是使用中状态,可能说明开锁已经完成
deviceMessage.value = '设备已弹出,请取走您的风扇'
isLoading.value = false
// 如果是第一次加载页面且设备已弹出,记录状态,避免重复弹出
if (!hasTriggeredDevice.value) {
uni.$emit('orderSuccess:' + orderId.value)
hasTriggeredDevice.value = true
}
} else {
// 在此页面不再触发设备弹出操作,仅更新展示文案和加载状态
deviceMessage.value = proxy.$t('success.paymentSuccessDesc')
isLoading.value = false
}
} else {
throw new Error('获取订单信息失败')
}
uni.hideLoading()
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || proxy.$t('order.getOrderFailed'),
icon: 'none'
})
}
}
// 格式化时间
const formatTime = (date) => {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
const second = date.getSeconds().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
}
// 返回首页
const goToHome = () => {
uni.reLaunch({
url: '/pages/index/index'
})
}
// 查看订单列表
const goToOrderList = () => {
uni.redirectTo({
url: '/pages/order/index'
})
}
</script>
<style lang="scss" scoped>
.success-container {
padding: 20px;
padding-bottom: 180rpx;
background-color: #f5f5f5;
min-height: 100vh;
box-sizing: border-box;
}
.status-order-card {
background-color: #fff;
border-radius: 12px;
margin-bottom: 20px;
overflow: hidden;
.status-section {
padding: 30px;
text-align: center;
.status-icon {
width: 60px;
height: 60px;
margin: 0 auto 16px;
background-color: #07c160;
border-radius: 50%;
position: relative;
&::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 30px;
height: 20px;
border: 3px solid #fff;
border-top: none;
border-right: none;
transform-origin: center;
transform: translate(-50%, -70%) rotate(-45deg);
}
}
.status-text {
font-size: 24px;
font-weight: bold;
color: #07c160;
margin-bottom: 8px;
}
.status-desc {
font-size: 14px;
color: #666;
}
}
.section-divider {
height: 1px;
background-color: #f0f0f0;
margin: 0 20px;
}
.order-section {
padding: 20px;
.card-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 16px;
color: #333;
padding-left: 12px;
position: relative;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background-color: #07c160;
border-radius: 2px;
}
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.label {
color: #666;
font-size: 14px;
}
.value {
color: #333;
font-size: 14px;
font-weight: 500;
}
}
}
}
.device-status {
background-color: #fff;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
text-align: center;
.status-message {
font-size: 16px;
color: #333;
margin-bottom: 12px;
}
.loading-animation {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
.loading-circle {
width: 30px;
height: 30px;
border-radius: 50%;
border: 3px solid #f0f0f0;
border-top-color: #07c160;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
}
.button-group {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
z-index: 10;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 20rpx;
.primary-btn {
background-color: #07c160;
color: #fff;
border: none;
border-radius: 32rpx;
padding: 0 32rpx;
font-size: 24rpx;
height: 64rpx;
line-height: 64rpx;
white-space: nowrap;
&:active {
opacity: 0.8;
}
}
.secondary-btn {
background-color: #fff;
color: #07c160;
border: 2rpx solid #07c160;
border-radius: 32rpx;
padding: 0 32rpx;
font-size: 24rpx;
height: 64rpx;
line-height: 64rpx;
white-space: nowrap;
&:active {
background-color: #f5f5f5;
}
}
}
</style>
+323
View File
@@ -0,0 +1,323 @@
<template>
<view class="deposit-container">
<!-- 押金金额卡片 -->
<view class="deposit-card">
<view class="title">{{ $t('deposit.depositBalance') }}</view>
<view class="amount">¥{{ depositAmount }}</view>
<button class="withdraw-btn" @click="handleWithdraw" :disabled="depositAmount <= 0">{{ $t('deposit.withdraw') }}</button>
</view>
<!-- 提现说明 -->
<view class="notice-card">
<view class="notice-title">
<view class="dot"></view>
<text>{{ $t('deposit.withdrawNotice') }}</text>
</view>
<view class="notice-content">
<view class="notice-item">1. {{ $t('deposit.withdrawNotice1') }}</view>
<view class="notice-item">2. {{ $t('deposit.withdrawNotice2') }}</view>
<view class="notice-item">3. {{ $t('deposit.withdrawNotice3') }}</view>
</view>
</view>
<!-- 押金记录 -->
<view class="record-card" v-if="records.length > 0">
<view class="record-title">{{ $t('deposit.depositRecord') }}</view>
<view class="record-list">
<view class="record-item" v-for="(item, index) in records" :key="index">
<view class="record-info">
<text class="record-type">{{ item.typeText }}</text>
<text class="record-time">{{ item.time }}</text>
</view>
<text class="record-amount" :class="item.type === 'refund' ? 'refund' : ''">
{{ item.type === 'refund' ? '+' : '-' }}¥{{ item.amount }}
</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { getUserInfo } from '@/util/index.js'
import { withdrawDeposit } from '@/config/api/user.js'
import { queryById } from '@/config/api/order.js'
import { useI18n } 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>
@@ -12,7 +12,7 @@
} from 'vue' } from 'vue'
import { useI18n } from '@/utils/i18n.js' import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n() const { t } = useI18n()
// //
const webUrl = ref('https://joininvestment.gxfs123.com/') const webUrl = ref('https://joininvestment.gxfs123.com/')
@@ -27,7 +27,7 @@
const handleError = (e) => { const handleError = (e) => {
console.error('web-view 加载错误:', e) console.error('web-view 加载错误:', e)
uni.showToast({ uni.showToast({
title: $t('join.pageLoadFailed'), title: t('join.pageLoadFailed'),
icon: 'none', icon: 'none',
duration: 2000 duration: 2000
}) })
@@ -35,7 +35,7 @@
onMounted(() => { onMounted(() => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: $t('join.title') title: t('join.title')
}) })
console.log('招商页面加载,外部网址:', webUrl.value) 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="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="h1">II. Account and Login</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.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.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.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> <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,11 +20,11 @@
<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.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="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 Sesame Credit)</view> <view class="h1">IV. Billing and Settlement (Including WeChat Payment Score)</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.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 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"><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 Sesame Credit/Alipay.</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">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="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> <view class="h1">V. Device Return and Overdue Handling</view>
<view class="p">5.1 Return method: Return at designated outlets according to mini program instructions, or send back through the "Express Return" function. Non-designated methods may lead to order abnormalities and additional fees.</view> <view class="p">5.1 Return method: Return at designated outlets according to mini program instructions, or send back through the "Express Return" function. Non-designated methods may lead to order abnormalities and additional fees.</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="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="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 Alipay 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 WeChat 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="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> <view class="h1">XI. Service Changes and Termination</view>
@@ -11,7 +11,7 @@
<view class="p">在使用{{ brandName }}请您务必仔细阅读并充分理解本协议全部内容尤其是以加粗方式提示的条款包括但不限于责任限制争议解决适用法律未成年人保护等您点击"登录/使用"或实际使用服务即视为您已阅读并同意受本协议约束</view> <view class="p">在使用{{ brandName }}请您务必仔细阅读并充分理解本协议全部内容尤其是以加粗方式提示的条款包括但不限于责任限制争议解决适用法律未成年人保护等您点击"登录/使用"或实际使用服务即视为您已阅读并同意受本协议约束</view>
<view class="h1">账号与登录</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.2 您应保证提供信息真实准确完整并及时更新因您提供的信息不真实或未及时更新导致的服务受限订单异常或损失由您自行承担</view>
<view class="p">2.3 您应对账户下的全部行为负责妥善保管设备与账户凭证不得转借出租或以其他方式提供给他人使用</view> <view class="p">2.3 您应对账户下的全部行为负责妥善保管设备与账户凭证不得转借出租或以其他方式提供给他人使用</view>
@@ -20,11 +20,11 @@
<view class="p">3.2 使用规范请合理使用设备避免进水摔落私自拆卸或改装请勿靠近明火与高温环境室外雨天请避免使用儿童应在监护下使用</view> <view class="p">3.2 使用规范请合理使用设备避免进水摔落私自拆卸或改装请勿靠近明火与高温环境室外雨天请避免使用儿童应在监护下使用</view>
<view class="p">3.3 禁止行为将设备用于违法或不当用途以任何方式影响设备或系统的正常运行通过非正常手段规避计费或归还流程</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.1 计费规则</text>以小程序展示的实时计费规则为准可能包含时长计费封顶价服务费等订单生成后将据此计费</view>
<view class="p"><text class="bold">4.2 芝麻信用免押</text>若您开通并通过信用评估可享受免押租借如评估未通过可能需预授权或押金具体以页面提示为准</view> <view class="p"><text class="bold">4.2 微信支付分免押</text>若您开通并通过信用评估可享受免押租借如评估未通过可能需预授权或押金具体以页面提示为准</view>
<view class="p">4.3 结算与扣款订单结束后我们将基于实际使用情况与平台规则完成结算并通过芝麻信用/支付宝支付进行扣款</view> <view class="p">4.3 结算与扣款订单结束后我们将基于实际使用情况与平台规则完成结算并通过微信支付分/微信支付进行扣款</view>
<view class="p">4.4 异常与争议如对计费或结算有异议请在订单完成后48小时内通过"我的-客服"提交逾期可能影响处理结果</view> <view class="p">4.4 异常与争议如对计费或结算有异议请在订单完成后48小时内通过"我的-客服"提交逾期可能影响处理结果</view>
<view class="h1">设备归还与逾期处理</view> <view class="h1">设备归还与逾期处理</view>
<view class="p">5.1 归还方式按照小程序指引在指定网点归还或通过"快递归还"功能寄回非指定方式可能导致订单异常与额外费用</view> <view class="p">5.1 归还方式按照小程序指引在指定网点归还或通过"快递归还"功能寄回非指定方式可能导致订单异常与额外费用</view>
@@ -36,9 +36,9 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useI18n } from '@/utils/i18n.js' import { useI18n } from '@/utils/i18n.js'
import { URL } from '@/config/url.js' import { getCurrentAgreement } from '@/config/api/system.js'
const { t: $t } = useI18n() const { t } = useI18n()
// //
const loading = ref(true) const loading = ref(true)
@@ -50,13 +50,6 @@
remark: '' remark: ''
}) })
//
const convertLanguageCode = (lang) => {
// zh-CN -> zh-CN ()
// en-US -> en_US (线)
return lang.replace(/-/g, '_')
}
// //
const loadAgreement = async () => { const loadAgreement = async () => {
loading.value = true loading.value = true
@@ -64,35 +57,22 @@
errorMessage.value = '' errorMessage.value = ''
try { try {
// console.log('加载用户协议')
const currentLang = uni.getStorageSync('language') || 'zh-CN'
const languageCode = convertLanguageCode(currentLang)
console.log('加载用户协议,语言:', currentLang, '转换后:', languageCode)
// //
const res = await uni.request({ const res = await getCurrentAgreement({
url: `${URL}/device/agreementConfig/current`, agreementCode: 'USER_AGREEMENT',
method: 'GET', appPlatform: 'wechat',
header: { appType: 'user'
'Content-Language': languageCode,
'Authorization': 'Bearer ' + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
},
data: {
agreementCode: 'USER_AGREEMENT',
appPlatform: 'wechat',
appType: 'user'
}
}) })
console.log('用户协议响应:', res) console.log('用户协议响应:', res)
if (res.statusCode === 200 && res.data.code === 200 && res.data.data) { if (res && res.code === 200 && res.data) {
agreementData.value = { agreementData.value = {
title: res.data.data.title || $t('legal.agreement'), title: res.data.title || t('legal.agreement'),
content: res.data.data.content || '', content: res.data.content || '',
remark: res.data.data.remark || '' remark: res.data.remark || ''
} }
// //
@@ -100,12 +80,12 @@
title: agreementData.value.title title: agreementData.value.title
}) })
} else { } else {
throw new Error(res.data.msg || $t('common.loadFailed')) throw new Error(res?.msg || t('common.loadFailed'))
} }
} catch (err) { } catch (err) {
console.error('加载用户协议失败:', err) console.error('加载用户协议失败:', err)
error.value = true error.value = true
errorMessage.value = err.message || $t('common.loadFailed') errorMessage.value = err.message || t('common.loadFailed')
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -16,18 +16,18 @@
</view> </view>
<view class="h1">II. Information We Collect</view> <view class="h1">II. Information We Collect</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.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.2 Order and device information: rental records, usage duration, fees, return points, device status, abnormal records, etc.</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.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="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="h1">III. Purpose of Information Use</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.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.2 Security risk control: prevent fraud, violations and risk control; ensure system and device security.</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="p">3.3 Product optimization: statistics and analysis to improve experience (conducted after de-identification/anonymization).</view>
<view class="h1">IV. Sesame Credit and Payment</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 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.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="p">4.2 If you fail the assessment, pre-authorization or deposit processing may be required, subject to page prompts.</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> <view class="h1">V. Sharing, Transfer and Public Disclosure</view>
@@ -22,12 +22,12 @@
<view class="p">2.4 日志信息为保障服务安全与稳定我们可能记录操作日志网络请求与错误信息</view> <view class="p">2.4 日志信息为保障服务安全与稳定我们可能记录操作日志网络请求与错误信息</view>
<view class="h1">信息使用目的</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.2 安全风控防范欺诈违规与风险控制保障系统与设备安全</view>
<view class="p">3.3 产品优化统计与分析以改进体验在去标识化/匿名化后进行</view> <view class="p">3.3 产品优化统计与分析以改进体验在去标识化/匿名化后进行</view>
<view class="h1">芝麻信用与支付</view> <view class="h1">微信支付分与支付</view>
<view class="p">4.1 为实现免押租借我们将与芝麻信用进行必要的数据交互如信用评估结果订单结算相关数据处理遵循支付宝与芝麻信用的规则</view> <view class="p">4.1 为实现免押租借我们将与微信支付分进行必要的数据交互如信用评估结果订单结算相关数据处理遵循微信支付与微信支付分的规则</view>
<view class="p">4.2 如您未通过评估可能需进行预授权或押金处理以页面提示为准</view> <view class="p">4.2 如您未通过评估可能需进行预授权或押金处理以页面提示为准</view>
<view class="h1">共享转移与公开披露</view> <view class="h1">共享转移与公开披露</view>
@@ -36,9 +36,9 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useI18n } from '@/utils/i18n.js' import { useI18n } from '@/utils/i18n.js'
import { URL } from '@/config/url.js' import { getCurrentAgreement } from '@/config/api/system.js'
const { t: $t } = useI18n() const { t } = useI18n()
// //
const loading = ref(true) const loading = ref(true)
@@ -50,13 +50,6 @@
remark: '' remark: ''
}) })
//
const convertLanguageCode = (lang) => {
// zh-CN -> zh-CN ()
// en-US -> en_US (线)
return lang.replace(/-/g, '_')
}
// //
const loadAgreement = async () => { const loadAgreement = async () => {
loading.value = true loading.value = true
@@ -64,35 +57,22 @@
errorMessage.value = '' errorMessage.value = ''
try { try {
// console.log('加载隐私政策')
const currentLang = uni.getStorageSync('language') || 'zh-CN'
const languageCode = convertLanguageCode(currentLang)
console.log('加载隐私政策,语言:', currentLang, '转换后:', languageCode)
// //
const res = await uni.request({ const res = await getCurrentAgreement({
url: `${URL}/device/agreementConfig/current`, agreementCode: 'PRIVACY_POLICY',
method: 'GET', appPlatform: 'wechat',
header: { appType: 'user'
'Content-Language': languageCode,
'Authorization': 'Bearer ' + uni.getStorageSync('token'),
'Clientid': uni.getStorageSync('client_id')
},
data: {
agreementCode: 'PRIVACY_POLICY',
appPlatform: 'wechat',
appType: 'user'
}
}) })
console.log('隐私政策响应:', res) console.log('隐私政策响应:', res)
if (res.statusCode === 200 && res.data.code === 200 && res.data.data) { if (res && res.code === 200 && res.data) {
agreementData.value = { agreementData.value = {
title: res.data.data.title || $t('legal.privacy'), title: res.data.title || t('legal.privacy'),
content: res.data.data.content || '', content: res.data.content || '',
remark: res.data.data.remark || '' remark: res.data.remark || ''
} }
// //
@@ -100,12 +80,12 @@
title: agreementData.value.title title: agreementData.value.title
}) })
} else { } else {
throw new Error(res.data.msg || $t('common.loadFailed')) throw new Error(res?.msg || t('common.loadFailed'))
} }
} catch (err) { } catch (err) {
console.error('加载隐私政策失败:', err) console.error('加载隐私政策失败:', err)
error.value = true error.value = true
errorMessage.value = err.message || $t('common.loadFailed') errorMessage.value = err.message || t('common.loadFailed')
} finally { } finally {
loading.value = false loading.value = false
} }
+135
View File
@@ -0,0 +1,135 @@
<template>
<view class="terms-page">
<view class="content">
<view class="title">{{ $t('legal.termsAndConditions') }}</view>
<view class="section">
<view class="section-title">{{ $t('legal.applicableLaw') }}</view>
<view class="section-content">
<text>{{ $t('legal.applicableLawContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.paymentMethods') }}</view>
<view class="section-content">
<text>{{ $t('legal.paymentMethodsContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.refundPolicy') }}</view>
<view class="section-content">
<text>{{ $t('legal.refundPolicyContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.serviceTerms') }}</view>
<view class="section-content">
<text>{{ $t('legal.serviceTermsContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.liabilityLimitation') }}</view>
<view class="section-content">
<text>{{ $t('legal.liabilityLimitationContent') }}</text>
</view>
</view>
<view class="section">
<view class="section-title">{{ $t('legal.disputeResolution') }}</view>
<view class="section-content">
<text>{{ $t('legal.disputeResolutionContent') }}</text>
</view>
</view>
<view class="footer">
<text class="update-time">{{ $t('legal.lastUpdate') }}: 2025-02-05</text>
<text class="notice">{{ $t('legal.footerNotice') }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { onMounted } from 'vue'
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
onMounted(() => {
uni.setNavigationBarTitle({
title: t('legal.termsAndConditions')
})
})
</script>
<style lang="scss" scoped>
.terms-page {
min-height: 100vh;
background-color: #f6f6f6;
padding-bottom: env(safe-area-inset-bottom);
}
.content {
background-color: #ffffff;
padding: 40rpx 30rpx;
margin: 20rpx;
border-radius: 16rpx;
}
.title {
font-size: 40rpx;
font-weight: bold;
color: #333333;
text-align: center;
margin-bottom: 40rpx;
}
.section {
margin-bottom: 40rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 20rpx;
padding-left: 20rpx;
border-left: 6rpx solid #07c160;
}
.section-content {
font-size: 28rpx;
color: #666666;
line-height: 1.8;
text-align: justify;
padding: 20rpx;
background-color: #f9f9f9;
border-radius: 8rpx;
}
.footer {
margin-top: 60rpx;
padding-top: 30rpx;
border-top: 1rpx solid #e5e5e5;
text-align: center;
}
.update-time {
display: block;
font-size: 24rpx;
color: #999999;
margin-bottom: 20rpx;
}
.notice {
display: block;
font-size: 26rpx;
color: #666666;
line-height: 1.6;
}
</style>
+146
View File
@@ -0,0 +1,146 @@
<template>
<view>
</view>
</template>
<script>
import {
wxLogin,
} from '@/util/index'
import {
getMyIndexInfo
} from "@/config/api/user.js";
import {
queryHasOrder,
checkOrdersByStatus
} from "@/config/api/order.js";
export default {
data() {
return {
}
},
async onLoad(option) {
console.log('bagCheck onLoad option:', option);
// 设置页面标题
uni.setNavigationBarTitle({
title: this.$t('device.checking')
})
try {
uni.showLoading({
title: this.$t('common.processing'),
mask: true
});
// 检查是否传入设备编号
if (!option || !option.deviceNo) {
throw new Error(this.$t('device.deviceNoNotRecognized'));
}
const deviceNo = option.deviceNo;
// 检查用户是否有进行中(in_used)或待支付(waiting_for_payment)的订单
const statusesToCheck = ['in_used', 'waiting_for_payment'];
const res = await checkOrdersByStatus(deviceNo, statusesToCheck);
if (res.code === 200 && res.data && res.data.length > 0) {
// 找到相关订单,取最新的一条
const latestOrder = res.data[0]; // 假设返回结果按时间倒序
if (latestOrder.orderStatus === 'in_used') {
// 如果是使用中的订单,跳转到归还页面
console.log('检测到使用中订单,跳转归还页:', latestOrder.orderId);
uni.redirectTo({
url: `/pages/device/return?orderId=${latestOrder.orderId}`
});
} else if (latestOrder.orderStatus === 'waiting_for_payment') {
// 如果是待支付订单,跳转到支付页面,并传递必要信息
console.log('检测到待支付订单,跳转支付页:', latestOrder.orderId);
// 获取套餐时间(分钟)
const packageTimeMinutes = latestOrder.packageTime || 60;
// 套餐小时数
const packageTimeHours = (packageTimeMinutes / 60).toFixed(1);
// 套餐价格
const packagePrice = latestOrder.packagePrice || '0.00';
// 计算每小时费率
const hourlyRate = (parseFloat(packagePrice) / (packageTimeMinutes / 60)).toFixed(2);
// 押金金额
const depositAmount = latestOrder.depositAmount || '99.00';
// 计算总金额(套餐+押金)
const totalAmount = (parseFloat(depositAmount) + parseFloat(packagePrice)).toFixed(2);
uni.redirectTo({
url: `/pages/order/payment?orderId=${latestOrder.orderId}&packageTimeHours=${packageTimeHours}&packagePrice=${packagePrice}&hourlyRate=${hourlyRate}&totalAmount=${totalAmount}&depositAmount=${depositAmount}`
});
} else {
// 其他状态(理论上不应该到这里,除非statusesToCheck配置错误),默认到详情页
console.log('检测到其他状态订单,跳转详情页:', latestOrder.orderId);
uni.redirectTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
});
}
} else {
// 没有找到相关状态的订单,跳转到设备详情页进行租借
console.log('未检测到相关订单,跳转详情页');
uni.redirectTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
});
}
} catch (error) {
// 只处理真正的错误,不是"没有订单"的情况
if (error.message && (
error.message.includes('未识别到设备编号') ||
error.message.includes('网络请求失败') ||
error.message.includes('服务器错误')
)) {
console.error('扫码检查订单失败:', error);
uni.showToast({
title: error.message || this.$t('device.processFailed'),
icon: 'none',
duration: 2000
});
} else {
// 对于其他错误,包括"没有找到订单",直接跳转到详情页,不显示错误消息
console.log('没有找到符合条件的订单或发生其他错误,直接跳转详情页');
}
// 无论什么情况,都跳转到一个可用页面
setTimeout(() => {
if (option && option.deviceNo) {
uni.redirectTo({
url: `/pages/device/detail?deviceNo=${option.deviceNo}`
});
} else {
// uni.switchTab({
// url:'/pages/index/index'
// })
// 如果连deviceNo都没有,则返回首页
uni.reLaunch({
url: '/pages/index/index'
});
}
}, 2000);
} finally {
uni.hideLoading();
}
},
methods: {
}
}
</script>
<style>
</style>
+78
View File
@@ -0,0 +1,78 @@
<template>
<view class="webview-container">
<!-- web-view 组件用于显示外部网页 -->
<web-view :src="webUrl" @message="handleMessage" @load="handleLoad" @error="handleError"></web-view>
</view>
</template>
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue';
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
// 网页链接
const webUrl = ref('')
// 页面加载时获取传入的 URL 参数
onLoad((options) => {
if (options.url) {
// 解码 URL
webUrl.value = decodeURIComponent(options.url)
console.log('加载外部链接:', webUrl.value)
} else {
uni.showToast({
title: t('common.invalidUrl') || '无效的链接',
icon: 'none',
duration: 2000
})
// 2秒后返回上一页
setTimeout(() => {
uni.navigateBack()
}, 2000)
}
})
// 网页加载完成
const handleLoad = (e) => {
console.log('网页加载完成:', e)
}
// 网页加载错误
const handleError = (e) => {
console.error('网页加载错误:', e)
uni.showToast({
title: t('common.loadFailed') || '加载失败',
icon: 'none'
})
}
// 接收网页消息(web-view 向小程序发送消息)
const handleMessage = (e) => {
console.log('收到网页消息:', e)
// 可以根据需要处理网页发来的消息
}
</script>
<script>
export default {
// 支持分享
onShareAppMessage() {
const $t = this.$t || ((key) => key)
return {
title: $t('share.title') || '分享',
path: '/pages/index/index'
}
}
}
</script>
<style lang="scss" scoped>
.webview-container {
width: 100vw;
height: 100vh;
overflow: hidden;
}
</style>
@@ -62,11 +62,11 @@
} from '@/config/api/expressReturn.js' } from '@/config/api/expressReturn.js'
import { useI18n } from '@/utils/i18n.js' import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n() const { t } = useI18n()
onMounted(() => { onMounted(() => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: $t('express.fillExpress') title: t('express.fillExpress')
}) })
}) })
@@ -96,7 +96,7 @@
if (!orderId.value) { if (!orderId.value) {
uni.showToast({ uni.showToast({
title: $t('express.orderNoMissing'), title: t('express.orderNoMissing'),
icon: 'none' icon: 'none'
}) })
setTimeout(() => { setTimeout(() => {
@@ -111,7 +111,7 @@
const loadOrder = async () => { const loadOrder = async () => {
try { try {
uni.showLoading({ uni.showLoading({
title: $t('common.loading') title: t('common.loading')
}) })
const res = await queryById(orderId.value) const res = await queryById(orderId.value)
if (res?.code === 200 && res.data) { if (res?.code === 200 && res.data) {
@@ -121,11 +121,11 @@
// //
if (res.data.phone && !phone.value) phone.value = res.data.phone if (res.data.phone && !phone.value) phone.value = res.data.phone
} else { } else {
throw new Error(res?.msg || $t('order.getOrderFailed')) throw new Error(res?.msg || t('order.getOrderFailed'))
} }
} catch (e) { } catch (e) {
uni.showToast({ uni.showToast({
title: e.message || $t('express.loadFailed'), title: e.message || t('express.loadFailed'),
icon: 'none' icon: 'none'
}) })
} finally { } finally {
@@ -136,7 +136,7 @@
const loadRecordAndOrderByRecord = async () => { const loadRecordAndOrderByRecord = async () => {
try { try {
uni.showLoading({ uni.showLoading({
title: $t('common.loading') title: t('common.loading')
}) })
const res = await getExpressReturnDetail(recordId.value) const res = await getExpressReturnDetail(recordId.value)
if (res?.code === 200 && res.data) { if (res?.code === 200 && res.data) {
@@ -146,11 +146,11 @@
} }
if (res.data.userPhone && !phone.value) phone.value = res.data.userPhone if (res.data.userPhone && !phone.value) phone.value = res.data.userPhone
} else { } else {
throw new Error(res?.msg || $t('express.getRecordFailed')) throw new Error(res?.msg || t('express.getRecordFailed'))
} }
} catch (e) { } catch (e) {
uni.showToast({ uni.showToast({
title: e.message || $t('express.loadFailed'), title: e.message || t('express.loadFailed'),
icon: 'none' icon: 'none'
}) })
} finally { } finally {
@@ -166,10 +166,10 @@
if (rec.status === 0) { if (rec.status === 0) {
recordId.value = rec.id recordId.value = rec.id
uni.showModal({ uni.showModal({
title: $t('common.tips'), title: t('common.tips'),
content: $t('express.existingReturnNotice'), content: t('express.existingReturnNotice'),
confirmText: $t('express.goToFill'), confirmText: t('express.goToFill'),
cancelText: $t('common.cancel'), cancelText: t('common.cancel'),
success: (r) => { success: (r) => {
if (r.confirm) { if (r.confirm) {
uni.redirectTo({ uni.redirectTo({
@@ -181,7 +181,7 @@
return return
} else { } else {
uni.showToast({ uni.showToast({
title: $t('express.alreadyHasRecord'), title: t('express.alreadyHasRecord'),
icon: 'none' icon: 'none'
}) })
setTimeout(() => { setTimeout(() => {
@@ -201,14 +201,14 @@
const digits = (phone.value || '').replace(/\D/g, '') const digits = (phone.value || '').replace(/\D/g, '')
if (!digits || digits.length < 5) { if (!digits || digits.length < 5) {
uni.showToast({ uni.showToast({
title: $t('express.pleaseEnterValidPhone'), title: t('express.pleaseEnterValidPhone'),
icon: 'none' icon: 'none'
}) })
return false return false
} }
if (isFillMode.value && !trackingNumber.value) { if (isFillMode.value && !trackingNumber.value) {
uni.showToast({ uni.showToast({
title: $t('express.pleaseEnterTrackingNo'), title: t('express.pleaseEnterTrackingNo'),
icon: 'none' icon: 'none'
}) })
return false return false
@@ -221,7 +221,7 @@
submitting.value = true submitting.value = true
try { try {
uni.showLoading({ uni.showLoading({
title: isFillMode.value ? $t('common.filling') : $t('common.submitting') title: isFillMode.value ? t('common.filling') : t('common.submitting')
}) })
let res let res
if (isFillMode.value) { if (isFillMode.value) {
@@ -238,18 +238,18 @@
} }
if (res && res.code === 200) { if (res && res.code === 200) {
uni.showToast({ uni.showToast({
title: isFillMode.value ? '补填成功' : '提交成功', title: isFillMode.value ? t('express.fillSuccess') : t('express.submitSuccess'),
icon: 'success' icon: 'success'
}) })
setTimeout(() => { setTimeout(() => {
uni.navigateBack() uni.navigateBack()
}, 800) }, 800)
} else { } else {
throw new Error(res?.msg || (isFillMode.value ? '补填失败' : '提交失败')) throw new Error(res?.msg || (isFillMode.value ? t('express.fillFailed') : t('express.submitFailed')))
} }
} catch (e) { } catch (e) {
uni.showToast({ uni.showToast({
title: e.message || (isFillMode.value ? '补填失败' : '提交失败'), title: e.message || (isFillMode.value ? t('express.fillFailed') : t('express.submitFailed')),
icon: 'none' icon: 'none'
}) })
} finally { } finally {
@@ -91,7 +91,7 @@ import { getExpressReturnDetail } from '@/config/api/expressReturn.js'
import { getCustomerPhone } from '@/util/index.js' import { getCustomerPhone } from '@/util/index.js'
import { useI18n } from '@/utils/i18n.js' import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n() const { t } = useI18n()
// //
const detailData = ref({ const detailData = ref({
@@ -131,21 +131,21 @@ const getStatusIcon = (status) => {
// //
const getStatusText = (status) => { const getStatusText = (status) => {
const textMap = { const textMap = {
'completed': $t('express.returnCompleted'), 'completed': t('express.returnCompleted'),
'processing': $t('express.processing'), 'processing': t('express.processing'),
'pending': $t('express.pending') 'pending': t('express.pending')
} }
return textMap[status] || $t('express.pending') return textMap[status] || t('express.pending')
} }
// //
const getStatusDesc = (status) => { const getStatusDesc = (status) => {
const descMap = { const descMap = {
'completed': $t('express.returnCompletedDesc'), 'completed': t('express.returnCompletedDesc'),
'processing': $t('express.processingDesc'), 'processing': t('express.processingDesc'),
'pending': $t('express.pendingDesc') '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, data: detailData.value.trackingNumber,
success: () => { success: () => {
uni.showToast({ uni.showToast({
title: $t('express.trackingNoCopied'), title: t('express.trackingNoCopied'),
icon: 'success' icon: 'success'
}) })
} }
@@ -165,10 +165,10 @@ const handleCopyTracking = () => {
const handleContactService = () => { const handleContactService = () => {
const customerPhone = getCustomerPhone() const customerPhone = getCustomerPhone()
uni.showModal({ uni.showModal({
title: $t('user.customerService'), title: t('user.customerService'),
content: `${$t('help.phone')}${customerPhone}\n${$t('help.workingHours')}${$t('express.workingHours')}`, content: `${t('help.phone')}${customerPhone}\n${t('help.workingHours')}${t('express.workingHours')}`,
confirmText: $t('express.call'), confirmText: t('express.call'),
cancelText: $t('common.cancel'), cancelText: t('common.cancel'),
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
uni.makePhoneCall({ uni.makePhoneCall({
@@ -182,7 +182,7 @@ const handleContactService = () => {
// //
onMounted(async () => { onMounted(async () => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: $t('express.returnDetail') title: t('express.returnDetail')
}) })
const pages = getCurrentPages() const pages = getCurrentPages()
@@ -190,7 +190,7 @@ onMounted(async () => {
const options = currentPage.options || {} const options = currentPage.options || {}
if (!options.id) return if (!options.id) return
try { try {
uni.showLoading({ title: $t('common.loading') }) uni.showLoading({ title: t('common.loading') })
const res = await getExpressReturnDetail(options.id) const res = await getExpressReturnDetail(options.id)
if (res && res.code === 200 && res.data) { if (res && res.code === 200 && res.data) {
const r = res.data const r = res.data
@@ -208,10 +208,10 @@ onMounted(async () => {
remark: r.remark || '' remark: r.remark || ''
} }
} else { } else {
throw new Error(res?.msg || $t('express.getDetailFailed')) throw new Error(res?.msg || t('express.getDetailFailed'))
} }
} catch (e) { } catch (e) {
uni.showToast({ title: e.message || $t('express.loadFailed'), icon: 'none' }) uni.showToast({ title: e.message || t('express.loadFailed'), icon: 'none' })
} finally { } finally {
uni.hideLoading() uni.hideLoading()
} }
@@ -54,7 +54,7 @@ import { ref, onMounted } from 'vue'
import { getExpressReturnList } from '@/config/api/expressReturn.js' import { getExpressReturnList } from '@/config/api/expressReturn.js'
import { useI18n } from '@/utils/i18n.js' import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n() const { t } = useI18n()
const returnList = ref([]) const returnList = ref([])
const loading = ref(false) const loading = ref(false)
@@ -62,7 +62,7 @@ const query = ref({ pageNum: 1, pageSize: 20 })
onMounted(() => { onMounted(() => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: $t('express.returnRecord') title: t('express.returnRecord')
}) })
loadList() loadList()
}) })
@@ -80,12 +80,12 @@ const loadList = async () => {
const rows = (res.data && (res.data.rows || res.data)) || [] const rows = (res.data && (res.data.rows || res.data)) || []
returnList.value = rows.map(r => ({ returnList.value = rows.map(r => ({
id: r.id, id: r.id,
expressCompany: r.expressCompany || r.company || '待填写', expressCompany: r.expressCompany || r.company || t('express.toFill'),
trackingNumber: r.logisticsTrackingNumber || r.trackingNumber || '待填写', trackingNumber: r.logisticsTrackingNumber || r.trackingNumber || t('express.toFill'),
returnAddress: r.returnAddress || r.address || '待填写', returnAddress: r.returnAddress || r.address || t('express.toFill'),
returnTime: r.expressFillTime || r.createTime || r.returnTime || '待填写', returnTime: r.expressFillTime || r.createTime || r.returnTime || t('express.toFill'),
packageType: r.packageType || '待填写', packageType: r.packageType || t('express.toFill'),
weight: r.weight || '待填写', weight: r.weight || t('express.toFill'),
status: mapStatus(r.status), status: mapStatus(r.status),
rawStatus: r.status, rawStatus: r.status,
userPhone: r.userPhone, userPhone: r.userPhone,
@@ -93,10 +93,10 @@ const loadList = async () => {
remark: r.remark remark: r.remark
})) }))
} else { } else {
throw new Error(res?.msg || $t('express.getListFailed')) throw new Error(res?.msg || t('express.getListFailed'))
} }
} catch (e) { } catch (e) {
uni.showToast({ title: e.message || $t('express.loadFailed'), icon: 'none' }) uni.showToast({ title: e.message || t('express.loadFailed'), icon: 'none' })
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -118,34 +118,33 @@ const getStatusClass = (status) => ({
}[status] || 'status-pending') }[status] || 'status-pending')
const getStatusText = (status) => ({ const getStatusText = (status) => ({
'completed': $t('express.billingPaused'), 'completed': t('express.billingPaused'),
'processing': $t('express.billingPaused'), 'processing': t('express.billingPaused'),
'pending': $t('express.billingPaused') 'pending': t('express.billingPaused')
}[status] || $t('express.billingPaused')) }[status] || t('express.billingPaused'))
const getStatusBadge = (status) => ({ const getStatusBadge = (status) => ({
'completed': $t('express.completed'), 'completed': t('express.completed'),
'processing': $t('express.processing'), 'processing': t('express.processing'),
'pending': $t('express.pending') 'pending': t('express.pending')
}[status] || $t('express.pending')) }[status] || t('express.pending'))
// //
const copyAllInfo = () => { 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({ uni.setClipboardData({
data: allInfo, data: allInfo,
success: () => { success: () => {
uni.showToast({ title: $t('express.copySuccess'), icon: 'success' }) uni.showToast({ title: t('express.copySuccess'), icon: 'success' })
}, },
fail: () => { fail: () => {
uni.showToast({ title: $t('express.copyFailed'), icon: 'none' }) uni.showToast({ title: t('express.copyFailed'), icon: 'none' })
} }
}) })
} }
// //
const handleItemClick = (item) => { const handleItemClick = (item) => {
console.log('点击了归还记录:', item)
// (status=0 -> mapped 'pending') // (status=0 -> mapped 'pending')
if (item && item.rawStatus === 0) { if (item && item.rawStatus === 0) {
uni.navigateTo({ url: `/pages/expressReturn/addExpressReturn?id=${item.id}` }) uni.navigateTo({ url: `/pages/expressReturn/addExpressReturn?id=${item.id}` })
@@ -92,19 +92,19 @@
getFeedbackDetail, getFeedbackDetail,
getFeedbackMessages, getFeedbackMessages,
sendFeedbackMessage sendFeedbackMessage
} from '../../config/api/feedback.js'; } from '@/config/api/feedback.js';
import { import {
useI18n useI18n
} from '@/utils/i18n.js' } from '@/utils/i18n.js'
const { const {
t: $t t
} = useI18n() } = useI18n()
// //
onMounted(() => { onMounted(() => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: $t('feedback.detail') title: t('feedback.detail')
}) })
}) })
@@ -126,7 +126,7 @@
await loadDetail(); await loadDetail();
} else { } else {
uni.showToast({ uni.showToast({
title: $t('feedback.idRequired'), title: t('feedback.idRequired'),
icon: 'none' icon: 'none'
}); });
setTimeout(() => { setTimeout(() => {
@@ -170,7 +170,7 @@
try { try {
if (shouldShowLoading) { if (shouldShowLoading) {
uni.showLoading({ uni.showLoading({
title: $t('common.loading') title: t('common.loading')
}); });
} }
@@ -180,7 +180,7 @@
await loadMessages(res.data.messages); await loadMessages(res.data.messages);
} else { } else {
uni.showToast({ uni.showToast({
title: res.msg || $t('feedback.getDetailFailed'), title: res.msg || t('feedback.getDetailFailed'),
icon: 'none' icon: 'none'
}); });
setTimeout(() => { setTimeout(() => {
@@ -190,7 +190,7 @@
} catch (error) { } catch (error) {
console.error('获取投诉详情失败:', error); console.error('获取投诉详情失败:', error);
uni.showToast({ uni.showToast({
title: $t('feedback.getDetailFailed'), title: t('feedback.getDetailFailed'),
icon: 'none' icon: 'none'
}); });
setTimeout(() => { setTimeout(() => {
@@ -207,7 +207,7 @@
const submitReply = async () => { const submitReply = async () => {
if (!replyContent.value.trim()) { if (!replyContent.value.trim()) {
uni.showToast({ uni.showToast({
title: $t('feedback.pleaseEnterReply'), title: t('feedback.pleaseEnterReply'),
icon: 'none' icon: 'none'
}); });
return; return;
@@ -215,7 +215,7 @@
try { try {
uni.showLoading({ uni.showLoading({
title: $t('common.submitting') title: t('common.submitting')
}); });
const res = await sendFeedbackMessage(feedbackId.value, { const res = await sendFeedbackMessage(feedbackId.value, {
@@ -224,7 +224,7 @@
if (res.code === 200) { if (res.code === 200) {
uni.showToast({ uni.showToast({
title: $t('feedback.replySuccess'), title: t('feedback.replySuccess'),
icon: 'success' icon: 'success'
}); });
replyContent.value = ''; replyContent.value = '';
@@ -234,14 +234,14 @@
}); });
} else { } else {
uni.showToast({ uni.showToast({
title: res.msg || $t('feedback.replyFailed'), title: res.msg || t('feedback.replyFailed'),
icon: 'none' icon: 'none'
}); });
} }
} catch (error) { } catch (error) {
console.error('提交回复失败:', error); console.error('提交回复失败:', error);
uni.showToast({ uni.showToast({
title: $t('feedback.replyFailed'), title: t('feedback.replyFailed'),
icon: 'none' icon: 'none'
}); });
} finally { } finally {
@@ -252,11 +252,11 @@
// //
const getStatusText = (status) => { const getStatusText = (status) => {
const statusMap = { const statusMap = {
'pending': $t('feedback.pending'), 'pending': t('feedback.pending'),
'in_progress': $t('feedback.processing'), 'in_progress': t('feedback.processing'),
'resolved': $t('feedback.completed') 'resolved': t('feedback.completed')
}; };
return statusMap[status] || $t('feedback.pending'); return statusMap[status] || t('feedback.pending');
}; };
// //
@@ -272,8 +272,8 @@
// //
const getTypeText = (type) => { const getTypeText = (type) => {
const typeMap = { const typeMap = {
'complain': $t('feedback.complain'), 'complain': t('feedback.complain'),
'suggestion': $t('feedback.suggestion') 'suggestion': t('feedback.suggestion')
}; };
return typeMap[type] || type || '-'; return typeMap[type] || type || '-';
}; };
@@ -306,7 +306,7 @@
// //
const getImageList = (item) => { const getImageList = (item) => {
if (!item) return []; if (!item) return [];
const pictureSource = item.pictureUrls != null ? item.pictureUrls : item.picturePath; const pictureSource = item.pictureUrls ?? item.picturePath;
if (!pictureSource) return []; if (!pictureSource) return [];
if (Array.isArray(pictureSource)) { if (Array.isArray(pictureSource)) {
return pictureSource.filter(img => !!img); return pictureSource.filter(img => !!img);
@@ -72,34 +72,37 @@
} from "@dcloudio/uni-app" } from "@dcloudio/uni-app"
import { import {
addUserFeedback addUserFeedback
} from '../../config/api/feedback' } from '@/config/api/feedback'
import { import {
uploadOssResource uploadOssResource
} from '../../config/api/user' } from '@/config/api/user'
import { import {
useI18n useI18n
} from '@/utils/i18n.js' } from '@/utils/i18n.js'
const { const {
t: $t t
} = useI18n() } = useI18n()
// //
const navigateToRecord = () => { const navigateToRecord = () => {
uni.navigateTo({ uni.navigateTo({
url: '/pages/feedback/list' url: '/subPackages/service/feedback/list'
}) })
} }
onMounted(() => { onMounted(() => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: $t('feedback.title') title: t('feedback.title')
}) })
}) })
onLoad(() => { onLoad((options) => {
if (uni.getStorageSync("userInfo").phone) { if (uni.getStorageSync("userInfo").phone) {
contact.value = uni.getStorageSync('userInfo').phone; contact.value = uni.getStorageSync('userInfo').phone;
} }
if(options.selectedType) {
selectedType.value = parseInt(options.selectedType);
}
}) })
// //
@@ -134,7 +137,7 @@
const submitFeedback = async () => { const submitFeedback = async () => {
if (selectedType.value === -1) { if (selectedType.value === -1) {
uni.showToast({ uni.showToast({
title: $t('feedback.pleaseSelectType'), title: t('feedback.pleaseSelectType'),
icon: 'none' icon: 'none'
}) })
return return
@@ -142,7 +145,7 @@
if (!description.value.trim()) { if (!description.value.trim()) {
uni.showToast({ uni.showToast({
title: $t('feedback.pleaseDescribe'), title: t('feedback.pleaseDescribe'),
icon: 'none' icon: 'none'
}) })
return return
@@ -150,7 +153,7 @@
if (!contact.value) { if (!contact.value) {
uni.showToast({ uni.showToast({
title: $t('feedback.pleaseContact'), title: t('feedback.pleaseContact'),
icon: 'none' icon: 'none'
}) })
return return
@@ -166,7 +169,7 @@
try { try {
// //
uni.showLoading({ uni.showLoading({
title: $t('feedback.uploading') || '上传中...', title: t('feedback.uploading') || '上传中...',
mask: true mask: true
}) })
@@ -182,7 +185,7 @@
console.error(`文件 ${i + 1} 上传失败:`, err) console.error(`文件 ${i + 1} 上传失败:`, err)
uni.hideLoading() uni.hideLoading()
uni.showToast({ uni.showToast({
title: $t('feedback.imageUploadFailed'), title: t('feedback.imageUploadFailed'),
icon: 'none' icon: 'none'
}) })
return return
@@ -205,7 +208,7 @@
// //
if (res && (res.code === 200 || res === true || res?.success === true)) { if (res && (res.code === 200 || res === true || res?.success === true)) {
uni.showToast({ uni.showToast({
title: $t('feedback.submitSuccess'), title: t('feedback.submitSuccess'),
icon: 'success' icon: 'success'
}) })
setTimeout(() => { setTimeout(() => {
@@ -213,7 +216,7 @@
}, 1500); }, 1500);
} else { } else {
uni.showToast({ uni.showToast({
title: (res && (res.msg || res.message)) || $t('feedback.submitFailed'), title: (res && (res.msg || res.message)) || t('feedback.submitFailed'),
icon: 'none' icon: 'none'
}) })
} }
@@ -221,7 +224,7 @@
console.error('feedback submit failed:', err) console.error('feedback submit failed:', err)
uni.hideLoading() uni.hideLoading()
uni.showToast({ uni.showToast({
title: $t('error.networkError') || '网络错误,请重试', title: t('error.networkError') || '网络错误,请重试',
icon: 'none' icon: 'none'
}) })
} }
@@ -72,19 +72,19 @@
} from '@dcloudio/uni-app'; } from '@dcloudio/uni-app';
import { import {
getFeedbackList getFeedbackList
} from '../../config/api/feedback.js'; } from '@/config/api/feedback.js';
import { import {
useI18n useI18n
} from '@/utils/i18n.js' } from '@/utils/i18n.js'
const { const {
t: $t t
} = useI18n() } = useI18n()
// //
onMounted(() => { onMounted(() => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: $t('feedback.recordList') title: t('feedback.recordList')
}) })
}) })
@@ -100,25 +100,25 @@
// //
const statusTabs = reactive([{ const statusTabs = reactive([{
get text() { get text() {
return $t('common.all') return t('common.all')
}, },
status: '' status: ''
}, },
{ {
get text() { get text() {
return $t('feedback.pending') return t('feedback.pending')
}, },
status: 'pending' status: 'pending'
}, },
{ {
get text() { get text() {
return $t('feedback.processing') return t('feedback.processing')
}, },
status: 'in_progress' status: 'in_progress'
}, },
{ {
get text() { get text() {
return $t('feedback.completed') return t('feedback.completed')
}, },
status: 'resolved' status: 'resolved'
} }
@@ -152,7 +152,7 @@
const status = statusTabs[currentTab.value].status; const status = statusTabs[currentTab.value].status;
const params = { const params = {
pageNum: currentPage.value, pageNum: currentPage.value,
pageSize: pageSize.value pageSize: pageSize.value,
}; };
if (status) { if (status) {
params.status = status; params.status = status;
@@ -176,14 +176,14 @@
} }
} else { } else {
uni.showToast({ uni.showToast({
title: res.msg || $t('feedback.getListFailed'), title: res.msg || t('feedback.getListFailed'),
icon: 'none' icon: 'none'
}); });
} }
} catch (error) { } catch (error) {
console.error('获取投诉列表失败:', error); console.error('获取投诉列表失败:', error);
uni.showToast({ uni.showToast({
title: $t('feedback.getListFailed'), title: t('feedback.getListFailed'),
icon: 'none' icon: 'none'
}); });
} finally { } finally {
@@ -211,11 +211,11 @@
// //
const getStatusText = (status) => { const getStatusText = (status) => {
const statusMap = { const statusMap = {
'pending': $t('feedback.pending'), 'pending': t('feedback.pending'),
'in_progress': $t('feedback.processing'), 'in_progress': t('feedback.processing'),
'resolved': $t('feedback.completed') 'resolved': t('feedback.completed')
}; };
return statusMap[status] || $t('feedback.pending'); return statusMap[status] || t('feedback.pending');
}; };
// //
@@ -231,8 +231,8 @@
// //
const getTypeText = (type) => { const getTypeText = (type) => {
const typeMap = { const typeMap = {
'complain': $t('feedback.complain'), 'complain': t('feedback.complain'),
'suggestion': $t('feedback.suggestion') 'suggestion': t('feedback.suggestion')
}; };
return typeMap[type] || type || '-'; return typeMap[type] || type || '-';
}; };
@@ -270,7 +270,7 @@
// //
const navigateToDetail = (item) => { const navigateToDetail = (item) => {
uni.navigateTo({ uni.navigateTo({
url: `/pages/feedback/detail?id=${item.id || item.feedbackId}` url: `/subPackages/service/feedback/detail?id=${item.id || item.feedbackId}`
}); });
}; };
</script> </script>
+213
View File
@@ -0,0 +1,213 @@
<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>
@@ -15,23 +15,23 @@
<!-- 支付方式标识 --> <!-- 支付方式标识 -->
<view class="device-right"> <view class="device-right">
<!-- 支付宝信用免押标识 --> <!-- 微信支付分标识 -->
<view class="payment-badge alipay-score" v-if="orderInfo.payWay == 'alipay_score_pay'"> <view class="payment-badge wx-score" v-if="orderInfo.payWay == 'wx_score_pay'">
<image src="/static/images/alipay.svg" mode="aspectFit" class="badge-icon"></image> <image src="/static/images/wxpayflag.png" mode="aspectFit" class="badge-icon"></image>
<view class="badge-text"> <view class="badge-text">
<text>{{ $t('order.alipayScore') }}</text> <text>{{ $t('order.wxPayScore') }}</text>
<text class="divider">|</text> <text class="divider">|</text>
<text class="highlight">{{ $t('order.depositFree') }}</text> <text class="highlight">{{ $t('order.depositFree') }}</text>
</view>
</view>
<!-- 会员订单标识 -->
<view class="payment-badge member" v-else-if="orderInfo.payWay == 'wx_member_pay'">
<text class="badge-text">{{ $t('order.memberOrder') }}</text>
</view>
<!-- 微信支付押金标识 -->
<view class="payment-badge deposit" v-else-if="orderInfo.payWay == 'wx_pay'">
<text class="badge-text">{{ $t('order.depositPay') }}</text>
</view> </view>
</view>
<!-- 会员订单标识 -->
<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 == 'alipay_pay'">
<text class="badge-text">{{ $t('order.depositPay') }}</text>
</view>
</view> </view>
</view> </view>
@@ -143,7 +143,8 @@
<script> <script>
import { import {
queryById, queryById,
cancelOrder cancelOrder,
getInUseOrder
} from '@/config/api/order.js' } from '@/config/api/order.js'
import { import {
getSystemConfig getSystemConfig
@@ -322,7 +323,7 @@
this.countdownTimer = null this.countdownTimer = null
} }
}, },
// iOS/ // iOS/
parseStartTimeToMs(timeStr) { parseStartTimeToMs(timeStr) {
if (!timeStr) return NaN if (!timeStr) return NaN
if (typeof timeStr === 'number') { if (typeof timeStr === 'number') {
@@ -710,19 +711,12 @@
} }
// API使 // API使
const inUseRes = await uni.request({ const inUseRes = await getInUseOrder()
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)) console.log('通过设备号查询订单结果:', JSON.stringify(inUseRes))
if (inUseRes.statusCode === 200 && inUseRes.data.code === 200 && inUseRes.data.data) { if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
const inUseOrder = inUseRes.data.data const inUseOrder = inUseRes.data
console.log('使用中的订单:', inUseOrder) console.log('使用中的订单:', inUseOrder)
// ID // ID
@@ -8,16 +8,32 @@
<view class="title">{{ $t('auth.loginTitle') }}</view> <view class="title">{{ $t('auth.loginTitle') }}</view>
<view class="subtitle">{{ $t('auth.loginDesc') }}</view> <view class="subtitle">{{ $t('auth.loginDesc') }}</view>
<!-- 支付宝一键手机号快捷登录推荐 --> <!-- 微信小程序一键手机号快捷登录 -->
<!-- #ifdef MP-WEIXIN -->
<button v-if="!isAgreed" class="btn primary" @click="handleLoginClick"> <button v-if="!isAgreed" class="btn primary" @click="handleLoginClick">
{{ $t('auth.getPhoneNumber') }} {{ $t('auth.getPhoneNumber') }}
</button> </button>
<button v-else class="btn primary" open-type="getAuthorize" scope="phoneNumber" @getAuthorize="onGetPhoneNumber"> <button v-else class="btn primary" open-type="getPhoneNumber" @getphonenumber="onGetPhoneNumber">
{{ $t('auth.getPhoneNumber') }} {{ $t('auth.getPhoneNumber') }}
</button> </button>
<!-- #endif -->
<!-- 支付宝登录不授权手机号时使用 --> <!-- 支付宝小程序授权码快捷登录不支持 open-type=getPhoneNumber -->
<!-- <button class="btn outline" @click="onAlipayLogin">仅支付宝登录</button> --> <!-- #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>
<view class="agreement-box"> <view class="agreement-box">
<checkbox-group @change="onAgreementChange"> <checkbox-group @change="onAgreementChange">
@@ -25,9 +41,9 @@
<checkbox value="agreed" :checked="isAgreed" color="#07c160" class="agreement-checkbox" /> <checkbox value="agreed" :checked="isAgreed" color="#07c160" class="agreement-checkbox" />
<text class="agreement-text"> <text class="agreement-text">
{{ $t('auth.agreeToTerms') }} {{ $t('auth.agreeToTerms') }}
<text class="link" @tap.stop="go('/pages/legal/agreement')">{{ $t('user.userAgreement') }}</text> <text class="link" @tap.stop="go('/subPackages/other/legal/agreement')">{{ $t('user.userAgreement') }}</text>
{{ $t('common.and') }} {{ $t('common.and') }}
<text class="link" @tap.stop="go('/pages/legal/privacy')">{{ $t('user.privacyPolicy') }}</text> <text class="link" @tap.stop="go('/subPackages/other/legal/privacy')">{{ $t('user.privacyPolicy') }}</text>
</text> </text>
</label> </label>
</checkbox-group> </checkbox-group>
@@ -38,25 +54,26 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { alipayLogin, getUserPhoneNumber, getUserInfo } from '../../util/index.js' import { wxLogin, alipayLogin, getUserPhoneNumber, getUserInfo } from '@/util/index.js'
import { useI18n } from '@/utils/i18n.js' import { useI18n } from '@/utils/i18n.js'
const { t: $t } = useI18n() const { t } = useI18n()
// //
onMounted(() => { onMounted(() => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: $t('auth.loginTitle') title: t('auth.loginTitle')
}) })
}) })
const isHTML5 = ref(false) // HTML5
const redirect = ref('/pages/index/index') const redirect = ref('/pages/index/index')
const isAgreed = ref(false) // const isAgreed = ref(false) //
// //
const onAgreementChange = (e) => { const onAgreementChange = (e) => {
isAgreed.value = e.detail.value.includes('agreed') isAgreed.value = e.detail.value.includes('agreed')
console.log('协议勾选状态:', isAgreed.value, e.detail.value)
} }
// //
@@ -79,10 +96,10 @@
// //
uni.showModal({ uni.showModal({
title: $t('common.tips'), title: t('common.tips'),
content: $t('auth.pleaseAgreeToTerms'), content: t('auth.pleaseAgreeToTerms'),
confirmText: $t('common.confirm'), confirmText: t('common.confirm'),
cancelText: $t('common.cancel'), cancelText: t('common.cancel'),
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
// //
@@ -90,7 +107,7 @@
resolve() resolve()
} else { } else {
// //
reject(new Error('需要同意协议才能登录')) reject(new Error(t('auth.pleaseAgreeToTerms')))
} }
} }
}) })
@@ -113,41 +130,51 @@
uni.reLaunch({ url: target }) 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 () => { const onAlipayLogin = async () => {
try { try {
//
await checkAgreement() await checkAgreement()
await alipayLogin()
await alipayLogin() uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
uni.showToast({ title: $t('auth.loginSuccess'), icon: 'success' })
await navigateAfterLogin() await navigateAfterLogin()
} catch (error) { } catch (error) {
if (error.message !== '需要同意协议才能登录') { if (error.message !== t('auth.pleaseAgreeToTerms')) {
uni.showToast({ title: error.message || '登录失败', icon: 'none' }) uni.showToast({ title: error.message || t('auth.loginFailed'), icon: 'none' })
} }
} }
} }
// / my.getPhoneNumber
const onGetPhoneNumber = async (e) => { const onGetPhoneNumber = async (e) => {
// if (!e || e.detail.errMsg !== 'getPhoneNumber:ok') {
if (!e || !e.detail || !e.detail.response) { uni.showToast({ title: t('auth.phoneCancelled'), icon: 'none' })
uni.showToast({ title: $t('auth.phoneCancelled'), icon: 'none' })
return return
} }
console.log(e);
try { try {
// token // /app/user/quickLogin/ WECHAT_MINI
await alipayLogin() await wxLogin(e.detail.code)
// // code phone
// e.detail.response // await getUserPhoneNumber(e.detail.code)
const authCode = e.detail.response?.response?.code || e.detail.response?.code uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
if (authCode) {
await getUserPhoneNumber(authCode)
}
uni.showToast({ title: $t('auth.loginSuccess'), icon: 'success' })
await navigateAfterLogin() await navigateAfterLogin()
} catch (error) { } catch (error) {
uni.showToast({ title: error.message || $t('auth.loginFailed'), icon: 'none' }) uni.showToast({ title: error.message || t('auth.loginFailed'), icon: 'none' })
} }
} }
@@ -155,19 +182,29 @@
if (opts && opts.redirect) { if (opts && opts.redirect) {
try { try {
redirect.value = decodeURIComponent(opts.redirect) redirect.value = decodeURIComponent(opts.redirect)
} catch (_) {} } catch (err) {
}
} }
// #ifdef H5
isHTML5.value = true
// #endif
}) })
const go = (url) => { const go = (url) => {
uni.navigateTo({ url }) uni.navigateTo({ url })
} }
//
const goToPhoneLogin = () => {
uni.navigateTo({ url: '/subPackages/user/login/phone' })
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.login-container { .login-container {
min-height: 100vh; min-height: 100vh;
background: #f8f8f8; background: linear-gradient(180deg, #C8F4D9 0%, #FFFFFF 100%);
padding: 80rpx 40rpx 40rpx; padding: 80rpx 40rpx 40rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+423
View File
@@ -0,0 +1,423 @@
<template>
<view class="login-container">
<view class="header">
<view class="title">Hello,</view>
<view class="subtitle">{{ $t('app.welcome') }}</view>
</view>
<!-- 国家区号选择器 -->
<view class="form-group">
<view class="phone-input-wrapper">
<view class="country-code" @click="showCountryPicker">
<text>{{ countryCode }}</text>
<text class="arrow"></text>
</view>
<view class="divider"></view>
<input
class="phone-input"
v-model="phone"
type="number"
:maxlength="countryCode === '+86' ? 11 : 12"
:placeholder="$t('auth.phonePlaceholder')"
/>
</view>
</view>
<!-- 验证码输入 -->
<view class="form-group">
<view class="code-input-wrapper">
<input
class="code-input"
v-model="verifyCode"
type="number"
maxlength="6"
:placeholder="$t('auth.codePlaceholder')"
/>
<view class="code-btn" @click="handleSendCode" :class="{ disabled: countdown > 0 }">
<text class="code-btn-text">{{ countdown > 0 ? `${countdown}s` : $t('auth.getCode') }}</text>
</view>
</view>
</view>
<!-- 区域提示 -->
<view class="region-notice">
<!-- <text>当前支持印尼+62和中国+86手机号登录</text> -->
</view>
<!-- 登录按钮 -->
<view class="login-btn" @click="handleLogin">
<text class="login-btn-text">{{ $t('auth.loginBtn') }}</text>
</view>
<!-- 协议勾选 -->
<view class="agreement-box">
<checkbox-group @change="onAgreementChange">
<label class="agreement-label">
<checkbox value="agreed" :checked="isAgreed" color="#07c160" class="agreement-checkbox" />
<text class="agreement-text">
{{ $t('auth.agreeToTerms') }}
<text class="link" @tap.stop="go('/pages/legal/agreement')">{{ $t('user.userAgreement') }}</text>
{{ $t('common.and') }}
<text class="link" @tap.stop="go('/pages/legal/privacy')">{{ $t('user.privacyPolicy') }}</text>
</text>
</label>
</checkbox-group>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { sendVerifyCode, quickLogin } from '@/config/api/user.js'
import { appid } from '@/config/url.js'
import { fetchAndCacheCustomerPhone } from '@/util/index.js'
import { useI18n } from '@/utils/i18n.js'
const { t } = useI18n()
// 设置页面标题
onMounted(() => {
uni.setNavigationBarTitle({
title: t('auth.phoneLogin')
})
})
const redirect = ref('/pages/index/index')
const isAgreed = ref(false) // 是否同意协议
const phone = ref('') // 手机号
const verifyCode = ref('') // 验证码
const countryCode = ref('+62') // 国家区号,默认印尼
const countdown = ref(0) // 验证码倒计时
let timer = null // 计时器
const countryOptions = [{
label: '印尼 +62',
value: '+62'
}, {
label: '中国 +86',
value: '+86'
}]
// 勾选协议变化
const onAgreementChange = (e) => {
isAgreed.value = e.detail.value.includes('agreed')
}
// 显示国家区号选择器
const showCountryPicker = () => {
uni.showActionSheet({
itemList: countryOptions.map(item => item.label),
success: ({
tapIndex
}) => {
const selected = countryOptions[tapIndex]
if (!selected || selected.value === countryCode.value) return
countryCode.value = selected.value
phone.value = ''
}
})
}
// 验证手机号格式
const validatePhone = () => {
if (!phone.value) {
uni.showToast({ title: t('auth.phoneRequired'), icon: 'none' })
return false
}
const phoneReg = countryCode.value === '+86' ? /^1[3-9]\d{9}$/ : /^8\d{7,11}$/
if (!phoneReg.test(phone.value)) {
uni.showToast({ title: t('auth.phoneInvalid'), icon: 'none' })
return false
}
return true
}
const getSubmitPhoneNumber = () => {
// 中国沿用原有 11 位手机号格式;印尼附带国家区号
return countryCode.value === '+86' ? phone.value : `${countryCode.value}${phone.value}`
}
// 发送验证码
const handleSendCode = async () => {
if (countdown.value > 0) return
if (!validatePhone()) return
try {
uni.showLoading({ title: t('common.sending') })
await sendVerifyCode(getSubmitPhoneNumber())
uni.hideLoading()
uni.showToast({ title: t('auth.codeSent'), icon: 'success' })
// 启动60秒倒计时
countdown.value = 60
timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer)
timer = null
}
}, 1000)
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || t('auth.sendCodeFailed'),
icon: 'none'
})
}
}
// 登录
const handleLogin = async () => {
if (!validatePhone()) return
if (!verifyCode.value) {
uni.showToast({ title: t('auth.codeRequired'), icon: 'none' })
return
}
if (!isAgreed.value) {
uni.showToast({ title: t('auth.pleaseAgreeToTerms'), icon: 'none' })
return
}
try {
uni.showLoading({ title: t('common.loggingIn') })
const res = await quickLogin({
loginType: 'SMS',
appid,
phonenumber: getSubmitPhoneNumber(),
smsCode: verifyCode.value
})
if (res && res.code !== 200) {
throw new Error(res.msg || res.message || t('auth.loginFailed'))
}
// 保存token和client_id
// 兼容多种返回格式:res.data.token, res.token, res.data.access_token
const token = res.token || (res.data && (res.data.token || res.data.access_token))
const clientId = res.client_id || (res.data && (res.data.client_id || res.data.clientId))
if (token) {
uni.setStorageSync('token', token)
if (clientId) {
uni.setStorageSync('client_id', clientId)
}
// 登录成功后获取并缓存客服电话
fetchAndCacheCustomerPhone().catch(err => {
console.error(t('auth.getServicePhoneFailed'), err)
})
} else {
throw new Error(t('auth.noAuthToken'))
}
uni.hideLoading()
uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
// 跳转到首页
setTimeout(() => {
uni.reLaunch({ url: redirect.value })
}, 1500)
} catch (error) {
uni.hideLoading()
uni.showToast({
title: error.message || t('auth.loginFailed'),
icon: 'none'
})
}
}
// 页面加载
onLoad((opts) => {
if (opts && opts.redirect) {
try {
redirect.value = decodeURIComponent(opts.redirect)
} catch (_) {}
}
})
const go = (url) => {
uni.navigateTo({ url })
}
// 清理定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer)
timer = null
}
})
</script>
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
background: linear-gradient(180deg, #C8F4D9 0%, #FFFFFF 100%);
padding: 0 48rpx;
box-sizing: border-box;
position: relative;
.header {
padding-top: 120rpx;
margin-bottom: 80rpx;
.title {
font-size: 64rpx;
font-weight: 700;
color: #000000;
line-height: 1.2;
margin-bottom: 8rpx;
}
.subtitle {
font-size: 64rpx;
font-weight: 700;
color: #000000;
line-height: 1.2;
}
}
.form-group {
margin-bottom: 32rpx;
.phone-input-wrapper {
background: #FFFFFF;
border-radius: 48rpx;
height: 96rpx;
display: flex;
align-items: center;
padding: 0 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.country-code {
display: flex;
align-items: center;
font-size: 32rpx;
color: #333333;
padding-right: 16rpx;
.arrow {
margin-left: 8rpx;
font-size: 20rpx;
color: #999999;
}
}
.divider {
width: 2rpx;
height: 40rpx;
background: #E5E5E5;
margin: 0 16rpx;
}
.phone-input {
flex: 1;
font-size: 32rpx;
color: #333333;
}
}
.code-input-wrapper {
background: #FFFFFF;
border-radius: 48rpx;
height: 96rpx;
display: flex;
align-items: center;
padding: 0 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.code-input {
flex: 1;
font-size: 32rpx;
color: #333333;
}
.code-btn {
padding-left: 24rpx;
border-left: 2rpx solid #E5E5E5;
&.disabled {
opacity: 0.5;
}
.code-btn-text {
font-size: 28rpx;
color: #07c160;
font-weight: 500;
white-space: nowrap;
}
}
}
}
.region-notice {
margin-bottom: 48rpx;
padding: 0 8rpx;
text {
font-size: 24rpx;
color: #666666;
line-height: 1.6;
}
}
.login-btn {
background: #07c160;
border-radius: 60rpx;
height: 112rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.3);
margin-bottom: 48rpx;
&:active {
opacity: 0.9;
}
.login-btn-text {
font-size: 36rpx;
color: #FFFFFF;
font-weight: 600;
}
}
.agreement-box {
position: absolute;
left: 48rpx;
right: 48rpx;
bottom: 60rpx;
display: flex;
justify-content: center;
align-items: center;
.agreement-label {
display: flex;
align-items: center;
width: 100%;
.agreement-checkbox {
flex-shrink: 0;
transform: scale(0.75);
margin-right: 4rpx;
}
.agreement-text {
flex: 1;
font-size: 24rpx;
color: #666;
line-height: 1.8;
word-break: break-all;
.link {
color: #07c160;
font-weight: 500;
text-decoration: none;
}
}
}
}
}
</style>
+479
View File
@@ -0,0 +1,479 @@
<template>
<view class="my-card-page">
<!-- 会员卡列表 -->
<view class="card-list" v-if="cardList.length > 0">
<view v-for="card in cardList" :key="card.id" style="position: relative;background-color: #f5f5f5;"
@click="viewCardDetail(card)" :style="card.cardType==='COUNT'?'height: 240rpx;':'height: 240rpx;'">
<view
style="height: 120rpx;background-color: #ffffff;z-index: 999;border-radius: 25rpx;padding: 32rpx;position: absolute;top: 0;left: 0;right: 0;">
<!-- 卡片头部标题和日期 -->
<view class="card-header">
<text class="card-name">{{ card.name }}</text>
<view class="card-date">
<text class="date-text" v-if="card.status !== 'expired'">{{ card.endDate }}{{
$t('myCard.expire') }}</text>
<text class="date-text expired" v-else>{{ $t('myCard.expiredOn') }}{{ card.endDate }}</text>
</view>
</view>
<!-- 地区信息 -->
<view class="card-region">
<text
class="region-text">{{ $t('myCard.onlyForRegionBefore') }}{{ card.positionName }}{{ $t('myCard.onlyForRegionAfter') }}</text>
<!-- 状态标签 / 去使用按钮 -->
<view v-if="card.status !== 'expired'" class="status-tag active"
style="display: flex; align-items: center; gap: 4rpx; background-color: #FFE2B8; border-radius: 20rpx; padding: 6rpx 20rpx;"
@click.stop="handleUseCard(card)">
<text class="status-text" style="color: #A16300;">{{ $t('myCard.toUse') }}</text>
<!-- <uv-icon name="scan" size="12" color="#D4A574"></uv-icon> -->
</view>
<view v-else class="status-tag" :class="getStatusClass(card.status)">
<text class="status-text">{{ getStatusText(card.status) }}</text>
</view>
</view>
</view>
<!-- 使用情况和操作按钮 -->
<view style="position: absolute; bottom: -20rpx; left: 0; right: 0; padding: 20rpx;z-index:1;">
<view class="card-footer">
<!-- 次卡信息 -->
<view v-if="card.cardType === 'COUNT'" class="card-usage-info">
<text class="usage-text">{{ $t('myCard.remainingTimes') }}{{ card.remainingCount }}{{
$t('myCard.times') }}</text>
</view>
<!-- 时长卡信息 -->
<view v-else class="card-usage-info">
<text class="usage-text">每日限用次数{{card.dailyLimitCount}}</text>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<!-- 续卡按钮仅次卡显示 -->
<view v-if="card.cardType === 'COUNT'" class="renew-btn" @click.stop="renewCard(card)">
<text class="renew-text">{{ $t('myCard.renew') }}</text>
<uv-icon name="arrow-right" size="14" color="#D4A574"></uv-icon>
</view>
<view v-else class="renew-btn">
<text class="renew-text">单次限时{{card.singleLimitMinutes}}分钟</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<image class="empty-icon" src="/static/empty-card.png" mode="aspectFit"></image>
<text class="empty-text">{{ $t('myCard.noCards') }}</text>
<view class="buy-btn" @click="goToBuy">
<text class="buy-text">{{ $t('myCard.buyNow') }}</text>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted
} from 'vue'
import {
useI18n
} from '@/utils/i18n.js'
import {
getMemberCardsByStatus
} from '@/config/api/member.js'
import {
getQueryString
} from '@/util/index.js'
import {
getDeviceInfo
} from '@/config/api/device.js'
import {
getInUseOrder,
getUnpaidOrder
} from '@/config/api/order.js'
const {
t
} = useI18n()
// 会员卡列表
const cardList = ref([])
// 获取会员卡列表
const getCardList = async () => {
try {
const response = await getMemberCardsByStatus()
// 处理API返回的数据,转换为模板需要的格式
if (response.code === 200 && response.data) {
cardList.value = response.data.map(item => ({
id: item.id,
name: item.cardName,
cardType: item.cardType, // TIME 或 COUNT
// 次卡相关
totalCount: item.totalCount,
remainingCount: item.remainingCount,
singleLimitMinutesForCount: item.singleLimitMinutesForCount,
// 时长卡相关
cycleDays: item.cycleDays,
dailyLimitCount: item.dailyLimitCount,
singleLimitMinutes: item.singleLimitMinutes,
currentCycleStartTime: item.currentCycleStartTime,
currentCycleUsedCount: item.currentCycleUsedCount,
// 通用信息
status: item.status,
startDate: item.startTime?.split(' ')[0] || item.startTime,
endDate: item.endTime?.split(' ')[0] || item.endTime,
positionId: item.positionId,
positionName: item.positionName,
purchasePrice: item.purchasePrice,
remark: item.remark
}))
} else {
cardList.value = []
}
} catch (error) {
console.error('获取会员卡列表失败:', error)
uni.showToast({
title: t('myCard.getListFailed'),
icon: 'none'
})
}
}
// 计算进度条宽度
const getProgressWidth = (used, total) => {
if (!total || total === 0) return '0%'
const percentage = (used / total) * 100
return `${Math.min(percentage, 100)}%`
}
// 获取状态类名
const getStatusClass = (status) => {
const statusMap = {
'unused': 'active',
'expired': 'expired',
'used': 'used',
'active': 'active' // 兼容原始状态
}
return statusMap[status] || 'active' // 默认为active
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
'unused': t('myCard.active'), // unused表示未使用,即活跃状态
'expired': t('myCard.expired'),
'used': t('myCard.used'),
'active': t('myCard.active') // 兼容原始状态
}
return statusMap[status] || t('myCard.active') // 默认为active
}
// 查看卡详情
const viewCardDetail = (card) => {
// TODO: 跳转到卡详情页面
// uni.showToast({
// title: t('common.functionDeveloping'),
// icon: 'none'
// })
}
// 续卡
const renewCard = (card) => {
uni.navigateTo({
url: `/pages/purchase/index?positionId=${card.positionId}`
})
}
// 去使用会员卡
const handleUseCard = async (card) => {
try {
const scanResult = await new Promise((resolve, reject) => {
uni.scanCode({
success: resolve,
fail: reject
})
})
console.log('扫码结果:', scanResult);
let deviceNo;
// 兼容不同平台的扫码结果
if (scanResult.scanType === 'QR_CODE' || scanResult.scanType === 'qrCode') {
deviceNo = getQueryString(scanResult.result, 'deviceNo')
} else if (scanResult.path) {
deviceNo = getQueryString(scanResult.path, 'deviceNo')
} else {
deviceNo = scanResult.result
}
if (!deviceNo) {
uni.showToast({
title: t('home.invalidQRCode'),
icon: 'none'
})
return
}
uni.showLoading({
title: t('common.getting')
})
// 检查是否有使用中的订单
const inUseRes = await getInUseOrder()
if (inUseRes && inUseRes.code === 200 && inUseRes.data) {
uni.hideLoading()
const inUseOrder = inUseRes.data
uni.reLaunch({
url: `/pages/order/detail?orderId=${inUseOrder.orderId}&deviceId=${deviceNo || inUseOrder.deviceNo}`
})
return
}
// 检查是否有待支付订单
const orderRes = await getUnpaidOrder()
if (orderRes && orderRes.code === 200 && orderRes.data) {
uni.hideLoading()
const unpaidOrder = orderRes.data
uni.navigateTo({
url: `/pages/order/payment?orderId=${unpaidOrder.orderId}`
})
return
}
// 获取设备信息并跳转详情
const deviceInfoRes = await getDeviceInfo(deviceNo)
uni.hideLoading()
if (deviceInfoRes.code === 200 && deviceInfoRes.data && deviceInfoRes.data.device) {
const deviceInfo = deviceInfoRes.data.device
let url = `/pages/device/detail?deviceNo=${deviceNo}`
if (deviceInfo.feeConfig) {
url += `&feeConfig=${encodeURIComponent(deviceInfo.feeConfig)}`
}
uni.navigateTo({
url
})
} else {
uni.navigateTo({
url: `/pages/device/detail?deviceNo=${deviceNo}`
})
}
} catch (error) {
console.error('扫码处理失败:', error)
if (error && error.errMsg !== 'scanCode:fail cancel') {
uni.showToast({
title: t('home.scanFailed'),
icon: 'none'
})
}
}
}
// 去购买
const goToBuy = () => {
uni.navigateTo({
url: '/pages/purchase/index'
})
}
onMounted(() => {
uni.setNavigationBarTitle({
title: t('user.myCards')
})
getCardList()
})
</script>
<style lang="scss" scoped>
.my-card-page {
min-height: 100vh;
background-color: #f5f5f5;
padding: 24rpx;
}
.card-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.card-item {
// background-color: #ffffff;
border-radius: 25rpx;
padding: 32rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
position: relative;
z-index: 5;
}
// 卡片头部
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
}
.card-name {
font-size: 36rpx;
font-weight: 600;
color: #333333;
line-height: 50rpx;
}
.card-date {
.date-text {
font-size: 24rpx;
color: #999999;
line-height: 34rpx;
&.expired {
color: #999999;
}
}
}
// 地区信息
.card-region {
margin-bottom: 24rpx;
align-items: center;
display: flex;
justify-content: space-between;
gap: 16rpx;
.region-text {
font-size: 26rpx;
color: #666666;
line-height: 36rpx;
}
}
// 卡片底部
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
padding-top: 50rpx;
position: absolute;
background: rgba(255, 244, 227, 1);
z-index: 1;
right: 0;
bottom: 0;
left: 0;
border-radius: 0 0 25rpx 25rpx;
/* text-align: 30rpx ; */
}
.card-usage-info {
flex: 1;
.usage-text {
font-size: 26rpx;
color: #D4A574;
font-weight: 500;
line-height: 36rpx;
}
}
.card-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
// 续卡按钮
.renew-btn {
display: flex;
align-items: center;
gap: 4rpx;
padding: 8rpx 16rpx;
// background-color: #FFF9F0;
border-radius: 8rpx;
.renew-text {
font-size: 24rpx;
color: #D4A574;
line-height: 34rpx;
}
.arrow {
font-size: 24rpx;
color: #D4A574;
}
}
// 状态标签
.status-tag {
padding: 8rpx 20rpx;
border-radius: 8rpx;
.status-text {
font-size: 24rpx;
line-height: 34rpx;
}
&.active {
// background-color: #FFF9F0;
.status-text {
color: #D4A574;
}
}
&.expired {
// background-color: #F5F5F5;
.status-text {
color: #999999;
}
}
&.used {
// background-color: #F5F5F5;
.status-text {
color: #999999;
}
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
opacity: 0.5;
}
.empty-text {
font-size: 28rpx;
color: #999;
margin-bottom: 40rpx;
}
.buy-btn {
padding: 20rpx 60rpx;
background-color: #B8741A;
border-radius: 48rpx;
.buy-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
}
}
</style>

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