Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 120eba6c6a | |||
| 555035388a | |||
| 2f618ef6ec | |||
| 09be26e2aa | |||
| 9ca377907b | |||
| 337a92d8c1 | |||
| 802aee59cb | |||
| a79cf10bd4 | |||
| b3836b8bf2 | |||
| 069677957e | |||
| 3462a24d1e | |||
| 99872dd6df | |||
| 7fd9c25ea8 | |||
| bb5a6dd100 | |||
| f476cee76d | |||
| 5a13803743 | |||
| 9f66ee9658 | |||
| 6a1dff4b94 | |||
| b0daa7b59b | |||
| dbf7fa0c95 | |||
| be01fb211e | |||
| 9d4e229312 |
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version" : "1.0",
|
||||||
|
"configurations" : [
|
||||||
|
{
|
||||||
|
"playground" : "standard",
|
||||||
|
"type" : "uni-app:app-android"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"playground" : "standard",
|
||||||
|
"type" : "uni-app:app-ios"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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')
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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 },
|
||||||
|
|||||||
@@ -1,41 +1,91 @@
|
|||||||
<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" :longitude="mapCenter.longitude" :latitude="mapCenter.latitude"
|
<map
|
||||||
:markers="mapMarkers" :scale="mapZoom" :show-location="false" @regionchange="onMapRegionChange"
|
id="map"
|
||||||
@markertap="onMapMarkerTap" @callouttap="onCalloutTap" @updated="onMapUpdated" @error="onMapError">
|
class="native-map"
|
||||||
<!-- 覆盖在地图上的广告轮播(使用 cover-view 以兼容小程序原生组件层级) -->
|
:longitude="mapCenter.longitude"
|
||||||
<cover-view class="index-swiper" v-if="!props.hideControls && !props.hideMapOverlays && currentBannerImage">
|
:latitude="mapCenter.latitude"
|
||||||
<cover-image :src="currentBannerImage" class="index-swiper-img" mode="aspectFill" @tap="handleBannerTap"></cover-image>
|
:markers="mapMarkers"
|
||||||
<!-- 轮播指示器 -->
|
:scale="mapZoom"
|
||||||
<cover-view class="banner-indicators" v-if="props.bannerImages.length > 1">
|
:show-location="false"
|
||||||
<cover-view
|
@regionchange="onMapRegionChange"
|
||||||
v-for="(img, idx) in props.bannerImages"
|
@markertap="onMapMarkerTap"
|
||||||
:key="idx"
|
@callouttap="onCalloutTap"
|
||||||
class="indicator-dot"
|
@updated="onMapUpdated"
|
||||||
:class="{ active: idx === currentBannerIndex }">
|
@error="onMapError"
|
||||||
|
>
|
||||||
|
<!-- 覆盖在地图上的广告轮播(使用 cover-view 以兼容小程序原生组件层级) -->
|
||||||
|
<cover-view
|
||||||
|
class="index-swiper"
|
||||||
|
v-if="!props.hideControls && !props.hideMapOverlays && currentBannerImage"
|
||||||
|
>
|
||||||
|
<cover-image
|
||||||
|
:src="currentBannerImage"
|
||||||
|
class="index-swiper-img"
|
||||||
|
mode="aspectFill"
|
||||||
|
@tap="handleBannerTap"
|
||||||
|
></cover-image>
|
||||||
|
<!-- 轮播指示器 -->
|
||||||
|
<cover-view
|
||||||
|
class="banner-indicators"
|
||||||
|
v-if="props.bannerImages.length > 1"
|
||||||
|
>
|
||||||
|
<cover-view
|
||||||
|
v-for="(img, idx) in props.bannerImages"
|
||||||
|
:key="idx"
|
||||||
|
class="indicator-dot"
|
||||||
|
:class="{ active: idx === currentBannerIndex }"
|
||||||
|
>
|
||||||
|
</cover-view>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
</cover-view>
|
|
||||||
|
|
||||||
<!-- 地图中心固定定位图标 -->
|
<!-- 地图中心固定定位图标 -->
|
||||||
<cover-view class="center-location-marker" v-if="!props.hideMapOverlays">
|
<cover-view
|
||||||
<cover-image src="/static/location-icon.png" class="center-marker-icon"></cover-image>
|
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>
|
||||||
|
|
||||||
|
|
||||||
<cover-view class="map-side-controls" v-if="!props.hideControls && !props.hideMapOverlays">
|
<!-- 侧边控制按钮 -->
|
||||||
<cover-view class="side-btn locate" @tap="handleRelocate">
|
<cover-view
|
||||||
<cover-image class="side-icon" src="/static/location.png"></cover-image>
|
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>
|
||||||
<cover-view class="side-btn service" @tap="handleService">
|
<cover-view class="side-btn locate" @tap="handleRelocate">
|
||||||
<cover-image class="side-icon" src="/static/customer-service.png"></cover-image>
|
<cover-image
|
||||||
|
class="side-icon"
|
||||||
|
src="/static/location.png"
|
||||||
|
></cover-image>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
<cover-view class="side-btn search" @tap="handleSearch">
|
<cover-view class="side-btn search" @tap="handleSearch">
|
||||||
<cover-image class="side-icon" src="/static/other_device.png"></cover-image>
|
<cover-image
|
||||||
|
class="side-icon"
|
||||||
|
src="/static/other_device.png"
|
||||||
|
></cover-image>
|
||||||
|
</cover-view>
|
||||||
|
<cover-view class="side-btn service" @tap="handleService">
|
||||||
|
<cover-image
|
||||||
|
class="side-icon"
|
||||||
|
src="/static/customer-service.png"
|
||||||
|
></cover-image>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
|
|
||||||
</cover-view>
|
</cover-view>
|
||||||
</map>
|
</map>
|
||||||
|
|
||||||
@@ -68,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)
|
||||||
@@ -128,7 +178,8 @@
|
|||||||
'showList',
|
'showList',
|
||||||
'markerTap',
|
'markerTap',
|
||||||
'mapCenterChange',
|
'mapCenterChange',
|
||||||
'bannerClick'
|
'bannerClick',
|
||||||
|
'guide'
|
||||||
])
|
])
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
@@ -247,14 +298,12 @@
|
|||||||
|
|
||||||
// 监听广告图片变化,启动或停止轮播
|
// 监听广告图片变化,启动或停止轮播
|
||||||
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()
|
||||||
@@ -274,13 +323,19 @@
|
|||||||
const onMapRegionChange = (e) => {
|
const onMapRegionChange = (e) => {
|
||||||
|
|
||||||
// 只处理结束事件
|
// 只处理结束事件
|
||||||
if (!e || e.type !== 'end') {
|
if (!e || (e.type !== 'end' && e.type !== 'regionchange')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const causedBy = e.causedBy || e.detail?.causedBy
|
const causedBy = e.causedBy || e.detail?.causedBy
|
||||||
|
|
||||||
if (causedBy === 'gesture' || causedBy === 'scale' || causedBy === 'drag'||causedBy==='update') {
|
// H5 环境下可能没有 causedBy,只要是 end 事件就处理
|
||||||
|
const isH5 = false;
|
||||||
|
// #ifdef H5
|
||||||
|
const h5Status = true;
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
if (causedBy === 'gesture' || causedBy === 'scale' || causedBy === 'drag' || causedBy === 'update' || (typeof h5Status !== 'undefined' && e.type === 'end')) {
|
||||||
// 清除之前的定时器
|
// 清除之前的定时器
|
||||||
if (regionChangeTimer) {
|
if (regionChangeTimer) {
|
||||||
clearTimeout(regionChangeTimer)
|
clearTimeout(regionChangeTimer)
|
||||||
@@ -372,41 +427,39 @@ 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 = () => {
|
const startBannerRotation = () => {
|
||||||
// 如果只有一张或没有图片,不需要轮播
|
// 如果只有一张或没有图片,不需要轮播
|
||||||
if (!props.bannerImages || props.bannerImages.length <= 1) {
|
if (!props.bannerImages || props.bannerImages.length <= 1) {
|
||||||
console.log('图片数量不足,不启动轮播')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除旧的定时器
|
// 清除旧的定时器
|
||||||
stopBannerRotation()
|
stopBannerRotation()
|
||||||
|
|
||||||
console.log('开始广告轮播定时器')
|
|
||||||
// 每3秒切换一次
|
// 每3秒切换一次
|
||||||
bannerTimer = setInterval(() => {
|
bannerTimer = setInterval(() => {
|
||||||
const nextIndex = (currentBannerIndex.value + 1) % props.bannerImages.length
|
const nextIndex = (currentBannerIndex.value + 1) % props.bannerImages.length
|
||||||
console.log('轮播切换:', currentBannerIndex.value, '->', nextIndex)
|
|
||||||
currentBannerIndex.value = nextIndex
|
currentBannerIndex.value = nextIndex
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
@@ -414,7 +467,6 @@ const handleSearch = () => {
|
|||||||
// 停止广告轮播
|
// 停止广告轮播
|
||||||
const stopBannerRotation = () => {
|
const stopBannerRotation = () => {
|
||||||
if (bannerTimer) {
|
if (bannerTimer) {
|
||||||
console.log('停止广告轮播')
|
|
||||||
clearInterval(bannerTimer)
|
clearInterval(bannerTimer)
|
||||||
bannerTimer = null
|
bannerTimer = null
|
||||||
}
|
}
|
||||||
@@ -450,7 +502,6 @@ const handleSearch = () => {
|
|||||||
|
|
||||||
// 初始化广告轮播
|
// 初始化广告轮播
|
||||||
if (props.bannerImages && props.bannerImages.length > 1) {
|
if (props.bannerImages && props.bannerImages.length > 1) {
|
||||||
console.log('onMounted: 初始化广告轮播')
|
|
||||||
startBannerRotation()
|
startBannerRotation()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -491,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 {
|
||||||
@@ -596,13 +660,13 @@ const handleSearch = () => {
|
|||||||
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 {
|
||||||
@@ -634,8 +698,8 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.side-icon {
|
.side-icon {
|
||||||
width: 44rpx;
|
width: 40rpx;
|
||||||
height: 44rpx;
|
height: 40rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.index-swiper {
|
.index-swiper {
|
||||||
@@ -667,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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<view class="header-right">
|
<view class="header-right">
|
||||||
<!-- 支付方式标识(移到头部右侧) -->
|
<!-- 支付方式标识(移到头部右侧) -->
|
||||||
<view class="payment-badge wx-score" v-if="order.payWay == 'wx_score_pay'">
|
<view class="payment-badge wx-score" v-if="order.payWay == 'wx_score_pay'">
|
||||||
<image src="/static/images/wxpayflag.png" mode="aspectFit" class="badge-icon"></image>
|
<image src="/static/images/wxpayflag.png" mode="aspectFit" class="badge-icon" lazy-load="true"></image>
|
||||||
<view class="badge-text">
|
<view class="badge-text">
|
||||||
<text>{{ $t('order.wxPayScore') }}</text>
|
<text>{{ $t('order.wxPayScore') }}</text>
|
||||||
<text class="divider">|</text>
|
<text class="divider">|</text>
|
||||||
@@ -22,6 +22,12 @@
|
|||||||
<view class="payment-badge member" v-else-if="order.payWay == 'wx_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.wxPay') }}</text>
|
<text class="badge-text">{{ $t('order.wxPay') }}</text>
|
||||||
<text class="divider">|</text>
|
<text class="divider">|</text>
|
||||||
@@ -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 },
|
||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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,9 +93,130 @@ export const getOrderByOrderNo = (orderNo) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 充电宝未弹出反馈(快捷反馈)
|
||||||
|
export const reportDeviceNoEject = (data) => {
|
||||||
|
return request({
|
||||||
|
url: '/app/order/report-no-eject',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 充电宝转为自用
|
||||||
|
export const convertToOwned = (orderId) => {
|
||||||
|
return request({
|
||||||
|
url: `/app/order/convert-to-owned/${orderId}`,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不想还了转为自用(按最高费用)
|
||||||
|
export const closeWithMaxFee = (orderNo) => {
|
||||||
|
return request({
|
||||||
|
url: `/app/order/closeWithMaxFee/${orderNo}`,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建微信支付订单
|
||||||
|
export const createWxPayment = (orderNo) => {
|
||||||
|
return request({
|
||||||
|
url: `/app/wx-payment/create/${orderNo}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建支付宝支付订单(租借押金 H5 支付)
|
||||||
|
// 对应文档《支付宝接口文档》:GET /app/ali-payment/create/{orderNo}
|
||||||
|
export const createAliPayment = (orderNo) => {
|
||||||
|
return request({
|
||||||
|
url: `/app/ali-payment/create/${orderNo}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取正在使用中的订单(可传 hideLoading: true 由业务自行控制 loading)
|
||||||
|
export const getInUseOrder = (opts = {}) => {
|
||||||
|
return request({
|
||||||
|
url: '/app/order/inUse',
|
||||||
|
method: 'get',
|
||||||
|
...opts
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询订单是否可申请暂停计费
|
||||||
|
export const getPauseBillingEligible = (orderId) => {
|
||||||
|
return request({
|
||||||
|
url: `/app/order/pauseBilling/eligible/${orderId}`,
|
||||||
|
method: 'get',
|
||||||
|
hideLoading: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对订单执行暂停计费
|
||||||
|
export const requestPauseBilling = (orderId) => {
|
||||||
|
return request({
|
||||||
|
url: `/app/order/pauseBilling/${orderId}`,
|
||||||
|
method: 'post',
|
||||||
|
hideLoading: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取待支付订单
|
||||||
|
export const getUnpaidOrder = () => {
|
||||||
|
return request({
|
||||||
|
url: '/app/order/unpaid',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询微信支付状态
|
||||||
|
export const getWxPaymentStatus = (orderNo) => {
|
||||||
|
return request({
|
||||||
|
url: `/app/wx-payment/status/${orderNo}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询支付宝支付状态
|
||||||
|
// 对应文档:GET /app/ali-payment/status/{orderNo}
|
||||||
|
export const getAliPaymentStatus = (orderNo) => {
|
||||||
|
return request({
|
||||||
|
url: `/app/ali-payment/status/${orderNo}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Antom 支付相关接口 ====================
|
||||||
|
|
||||||
|
// 创建 Antom H5 支付订单
|
||||||
|
export const createAntomPayment = (orderNo, paymentType, osType) => {
|
||||||
|
return request({
|
||||||
|
url: `/app/antom-payment/create/${orderNo}?paymentType=${paymentType}&osType=${osType}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 Antom 可用支付方式列表
|
||||||
|
export const getAntomPaymentMethods = (orderNo, osType) => {
|
||||||
|
return request({
|
||||||
|
url: `/app/antom-payment/consult/${orderNo}?osType=${osType}`,
|
||||||
|
method: 'get',
|
||||||
|
hideLoading: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Antom 支付结果查询
|
||||||
|
export const getAntomPaymentStatus = (orderNo, osType) => {
|
||||||
|
return request({
|
||||||
|
url: `/app/antom-payment/inquiry/${orderNo}?osType=${osType}`,
|
||||||
|
method: 'get',
|
||||||
|
hideLoading: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 通过订单号获取支付分订单信息
|
// 通过订单号获取支付分订单信息
|
||||||
export const getOrderByOrderNoScore = (orderNo) => {
|
export const getOrderByOrderNoScore = (orderNo) => {
|
||||||
console.log('通过订单号获取支付分订单信息', orderNo);
|
|
||||||
return request({
|
return request({
|
||||||
url: `/app/wx-payment/score/create/${orderNo}`,
|
url: `/app/wx-payment/score/create/${orderNo}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@@ -88,7 +226,6 @@ export const getOrderByOrderNoScore = (orderNo) => {
|
|||||||
|
|
||||||
// 通过订单号获取支付分订单状态
|
// 通过订单号获取支付分订单状态
|
||||||
export const getOrderByOrderNoScorePayStatus = (orderNo) => {
|
export const getOrderByOrderNoScorePayStatus = (orderNo) => {
|
||||||
console.log('通过订单号获取支付分订单状态', orderNo);
|
|
||||||
return request({
|
return request({
|
||||||
url: `/app/wx-payment/score/status/${orderNo}`,
|
url: `/app/wx-payment/score/status/${orderNo}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
// 兼容保留:统一走 quickLogin(SMS)
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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 {
|
||||||
@@ -23,35 +30,40 @@ const request = (option) => {
|
|||||||
mask: true
|
mask: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
uni.request({
|
uni.request({
|
||||||
url: URL + option.url,
|
url: URL + option.url,
|
||||||
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'),
|
||||||
|
...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) {
|
||||||
|
|
||||||
|
|
||||||
// 检查响应状态码
|
// 检查响应状态码
|
||||||
if (res.statusCode !== 200) {
|
if (res.statusCode !== 200) {
|
||||||
console.error(`HTTP状态码错误: ${res.statusCode}`, res.data)
|
console.error(`HTTP状态码错误: ${res.statusCode}`, res.data)
|
||||||
|
|
||||||
// 为了适应某些服务器的异常响应,我们仍然返回数据
|
// 为了适应某些服务器的异常响应,我们仍然返回数据
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
resolve(res.data)
|
resolve(res.data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reject({msg: `请求失败,状态码:${res.statusCode}`})
|
reject({
|
||||||
|
msg: `请求失败,状态码:${res.statusCode}`
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未登录或 token 失效处理(支持多种后端返回)
|
// 未登录或 token 失效处理(支持多种后端返回)
|
||||||
if (res.statusCode === 401 || res.data?.code === 401 || res.data?.code === 40101) {
|
if (res.statusCode === 401 || res.data?.code === 401 || res.data?.code === 40101) {
|
||||||
try {
|
try {
|
||||||
@@ -60,22 +72,32 @@ 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"
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查业务状态码
|
// 检查业务状态码
|
||||||
if (res.data && res.data.code !== 200) {
|
if (res.data && res.data.code !== 200) {
|
||||||
console.warn(`业务状态码错误: ${res.data.code}`, res.data)
|
console.warn(`业务状态码错误: ${res.data.code}`, res.data)
|
||||||
|
|
||||||
// 判断是否需要忽略数据为空的错误
|
// 判断是否需要忽略数据为空的错误
|
||||||
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,
|
||||||
@@ -84,12 +106,12 @@ const request = (option) => {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仍然返回数据,由业务逻辑处理
|
// 仍然返回数据,由业务逻辑处理
|
||||||
resolve(res.data)
|
resolve(res.data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(res.data)
|
resolve(res.data)
|
||||||
},
|
},
|
||||||
fail(err) {
|
fail(err) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// 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 = "wx2165f0be356ae7a9" //微信小程序appid
|
export const appid = "wx2165f0be356ae7a9" //微信小程序appid
|
||||||
|
|||||||
@@ -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...'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,11 +71,7 @@ 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) {
|
||||||
console.log('=== 检测到语言变化,强制更新 ===')
|
console.log('=== 检测到语言变化,强制更新 ===')
|
||||||
@@ -58,46 +80,32 @@ 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)
|
||||||
|
|
||||||
// 使用 uView
|
// 使用 uView
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,9 @@
|
|||||||
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
|
"requiredPrivateInfos" : [ "getLocation", "chooseLocation" ]
|
||||||
},
|
},
|
||||||
"mp-alipay" : {
|
"mp-alipay" : {
|
||||||
|
"component2" : true,
|
||||||
|
"transpile" : [ "uview-ui", "vue-i18n" ],
|
||||||
|
"skia" : true,
|
||||||
"usingComponents" : true,
|
"usingComponents" : true,
|
||||||
"appid" : "2021006117693332",
|
"appid" : "2021006117693332",
|
||||||
"unipush" : {
|
"unipush" : {
|
||||||
@@ -81,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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -11,4 +12,4 @@
|
|||||||
"sass": "^1.57.1",
|
"sass": "^1.57.1",
|
||||||
"sass-loader": "^13.2.0"
|
"sass-loader": "^13.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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,14 +78,25 @@
|
|||||||
</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('wx-score-pay')">
|
@click="handleRent">
|
||||||
<text>{{ hasActiveOrder ? $t('order.returnDevice') : $t('device.rentDepositFree') }}</text>
|
<text>{{ hasActiveOrder ? $t('order.returnDevice') : getRentButtonText() }}</text>
|
||||||
</button>
|
</view>
|
||||||
<view class="wechat-credit">
|
<!-- 微信支付分标识仅在微信小程序环境显示 -->
|
||||||
<image src="/static/images/wxpayflag.png" mode="aspectFit" class="wx-icon"></image>
|
<view class="wechat-credit" v-if="isWechatMiniProgram">
|
||||||
|
<image src="/static/images/wxpayflag.png" mode="aspectFit" class="wx-icon" lazy-load="true"></image>
|
||||||
<text class="credit-text">{{ $t('device.wxPayScoreDesc') }}</text>
|
<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,7 +142,9 @@
|
|||||||
import {
|
import {
|
||||||
getOrderByOrderNoScore,
|
getOrderByOrderNoScore,
|
||||||
getOrderByOrderNo,
|
getOrderByOrderNo,
|
||||||
cancelOrder
|
cancelOrder,
|
||||||
|
getInUseOrder,
|
||||||
|
getUnpaidOrder
|
||||||
} from '@/config/api/order.js'
|
} from '@/config/api/order.js'
|
||||||
import {
|
import {
|
||||||
initiateWeChatScorePayment,
|
initiateWeChatScorePayment,
|
||||||
@@ -130,12 +154,14 @@
|
|||||||
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,95 +358,117 @@
|
|||||||
|
|
||||||
// 检查登录状态和订单
|
// 检查登录状态和订单
|
||||||
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 {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示登录提示
|
// 显示登录提示
|
||||||
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
|
||||||
@@ -377,9 +479,22 @@
|
|||||||
showPhoneAuthPopup.value = true
|
showPhoneAuthPopup.value = true
|
||||||
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')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算计费单位时间(分钟)
|
// 计算计费单位时间(分钟)
|
||||||
@@ -412,12 +527,12 @@
|
|||||||
const getBillingUnitPrice = () => {
|
const getBillingUnitPrice = () => {
|
||||||
if (!deviceFeeConfig.value || !deviceFeeConfig.value.maxHourPrice) return '5'
|
if (!deviceFeeConfig.value || !deviceFeeConfig.value.maxHourPrice) return '5'
|
||||||
const maxHourPrice = parseFloat(deviceFeeConfig.value.maxHourPrice)
|
const maxHourPrice = parseFloat(deviceFeeConfig.value.maxHourPrice)
|
||||||
|
|
||||||
// 按分钟计费时,直接返回每分钟价格
|
// 按分钟计费时,直接返回每分钟价格
|
||||||
if (deviceInfo.value && deviceInfo.value.feeType === 'minute') {
|
if (deviceInfo.value && deviceInfo.value.feeType === 'minute') {
|
||||||
return maxHourPrice.toFixed(2)
|
return maxHourPrice.toFixed(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按小时计费
|
// 按小时计费
|
||||||
const hourPrice = parseFloat(deviceFeeConfig.value.hourPrice || 1)
|
const hourPrice = parseFloat(deviceFeeConfig.value.hourPrice || 1)
|
||||||
const unitPrice = maxHourPrice
|
const unitPrice = maxHourPrice
|
||||||
@@ -460,11 +575,20 @@
|
|||||||
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') {
|
if (payWay === 'wx-score-pay') {
|
||||||
@@ -495,28 +619,35 @@
|
|||||||
|
|
||||||
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 == 'wx-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);
|
||||||
console.log(res);
|
console.log(res);
|
||||||
|
|
||||||
const deposit = parseFloat(order.depositAmount);
|
const deposit = parseFloat(order.depositAmount);
|
||||||
const packagePrice = parseFloat(order.unitPrice);
|
const packagePrice = parseFloat(order.unitPrice);
|
||||||
const totalAmount = deposit.toFixed(2);
|
const totalAmount = deposit.toFixed(2);
|
||||||
|
|
||||||
// 跳转到订单支付页面
|
// 跳转到订单支付页面
|
||||||
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 == 'wx-score-pay') {
|
} else if (payWay == 'wx-score-pay') {
|
||||||
@@ -545,7 +676,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
|
||||||
@@ -554,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
|
||||||
});
|
});
|
||||||
@@ -569,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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -580,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
|
||||||
@@ -593,7 +724,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: $t('device.payScoreFailedCancelled'),
|
title: t('device.payScoreFailedCancelled'),
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -605,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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -613,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'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -664,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;
|
||||||
}
|
}
|
||||||
@@ -681,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;
|
||||||
@@ -708,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,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;
|
||||||
@@ -738,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;
|
||||||
}
|
}
|
||||||
@@ -760,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;
|
||||||
}
|
}
|
||||||
@@ -836,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;
|
||||||
@@ -888,10 +1065,10 @@
|
|||||||
margin-right: 8rpx;
|
margin-right: 8rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.credit-text {
|
.credit-text {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -1,587 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view class="payment-container">
|
|
||||||
<!-- 订单状态 -->
|
|
||||||
<view class="status-card">
|
|
||||||
<view class="status-icon" :class="orderStatus.class"></view>
|
|
||||||
<view class="status-text">{{ orderStatus.text }}</view>
|
|
||||||
<view class="status-desc">{{ orderStatus.desc }}</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 订单信息 -->
|
|
||||||
<view class="order-card">
|
|
||||||
<view class="card-title">{{ $t('payment.orderInfo') }}</view>
|
|
||||||
<view class="info-item">
|
|
||||||
<text class="label">{{ $t('order.orderNo') }}</text>
|
|
||||||
<text class="value">{{ orderInfo.orderNo || '-' }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="info-item">
|
|
||||||
<text class="label">{{ $t('order.deviceNo') }}</text>
|
|
||||||
<text class="value">{{ orderInfo.deviceNo || '-' }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="info-item">
|
|
||||||
<text class="label">{{ $t('payment.createTime') }}</text>
|
|
||||||
<text class="value">{{ orderInfo.createTime || '-' }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="info-item">
|
|
||||||
<text class="label">{{ $t('payment.contactPhone') }}</text>
|
|
||||||
<text class="value">{{ orderInfo.phone || '-' }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 费用信息 -->
|
|
||||||
<view class="price-card">
|
|
||||||
<view class="card-title">{{ $t('payment.feeInfo') }}</view>
|
|
||||||
<view class="price-item">
|
|
||||||
<text class="label">{{ $t('payment.deposit') }}</text>
|
|
||||||
<text class="value">¥{{ orderInfo.deposit || '99.00' }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="price-item">
|
|
||||||
<text class="label">{{ $t('payment.package') }}</text>
|
|
||||||
<text class="value">{{ packageInfo.price }}{{ $t('unit.yuan') }}/{{ packageInfo.time }}{{ $t('time.hour') }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="price-item total">
|
|
||||||
<text class="label">{{ $t('payment.total') }}</text>
|
|
||||||
<text class="value">¥{{ totalAmount }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 底部操作栏 -->
|
|
||||||
<view class="bottom-bar">
|
|
||||||
<view class="total-amount">
|
|
||||||
<text>{{ $t('payment.total') }}:</text>
|
|
||||||
<text class="amount">¥{{ totalAmount }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="pay-btn" @click="handlePayment">{{ $t('payment.payNow') }}</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { queryById, updateOrderPackage } from '@/config/api/order.js'
|
|
||||||
import { getDeviceInfo } from '@/config/api/device.js'
|
|
||||||
import { updateUserBalance } from '@/config/api/user.js'
|
|
||||||
import {
|
|
||||||
URL
|
|
||||||
}from"@/config/url.js"
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
orderId: null,
|
|
||||||
deviceNo: null,
|
|
||||||
orderInfo: {},
|
|
||||||
packageInfo: {
|
|
||||||
time: '',
|
|
||||||
price: '0.00'
|
|
||||||
},
|
|
||||||
deviceInfo: null,
|
|
||||||
passedTotalAmount: null,
|
|
||||||
passedDepositAmount: null,
|
|
||||||
orderStatus: {
|
|
||||||
get text() { return this.$t('payment.waitingForPayment') },
|
|
||||||
get desc() { return this.$t('payment.pleasePayIn15Min') },
|
|
||||||
class: 'waiting'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
totalAmount() {
|
|
||||||
if (this.passedTotalAmount !== null) {
|
|
||||||
return parseFloat(this.passedTotalAmount).toFixed(2);
|
|
||||||
}
|
|
||||||
const deposit = parseFloat(this.orderInfo.deposit || this.passedDepositAmount || 99)
|
|
||||||
const packagePrice = parseFloat(this.packageInfo.price || 0)
|
|
||||||
return (deposit + packagePrice).toFixed(2)
|
|
||||||
},
|
|
||||||
// 计算每小时的价格
|
|
||||||
hourlyRate() {
|
|
||||||
const price = parseFloat(this.packageInfo.price || 0);
|
|
||||||
let time = parseFloat(this.packageInfo.time || 1);
|
|
||||||
|
|
||||||
// 如果时间单位不是小时(例如分钟),需要转换
|
|
||||||
if (this.packageInfo.time && this.packageInfo.time.includes('分钟')) {
|
|
||||||
time = time / 60; // 将分钟转换为小时
|
|
||||||
} else if (this.packageInfo.time && this.packageInfo.time.includes('次')) {
|
|
||||||
// 按次计费时,暂时显示单次价格
|
|
||||||
return price.toFixed(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 避免除以零
|
|
||||||
if (time <= 0) time = 1;
|
|
||||||
|
|
||||||
// 计算每小时价格
|
|
||||||
return (price / time).toFixed(2);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLoad(options) {
|
|
||||||
// 设置页面标题
|
|
||||||
uni.setNavigationBarTitle({
|
|
||||||
title: this.$t('payment.orderPayment')
|
|
||||||
})
|
|
||||||
|
|
||||||
if (options && options.orderId) {
|
|
||||||
this.orderId = options.orderId
|
|
||||||
|
|
||||||
if (options.totalAmount) {
|
|
||||||
this.passedTotalAmount = options.depositAmount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.depositAmount) {
|
|
||||||
this.passedDepositAmount = options.depositAmount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果URL中包含了feeConfig参数,保存它
|
|
||||||
if (options.feeConfig) {
|
|
||||||
try {
|
|
||||||
console.log('从URL获取到feeConfig:', options.feeConfig)
|
|
||||||
const feeConfigStr = decodeURIComponent(options.feeConfig)
|
|
||||||
// 创建一个临时的deviceInfo对象保存feeConfig
|
|
||||||
this.deviceInfo = { feeConfig: feeConfigStr }
|
|
||||||
} catch (e) {
|
|
||||||
console.error('解析URL中的feeConfig失败:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadOrderInfo()
|
|
||||||
} else {
|
|
||||||
uni.showToast({
|
|
||||||
title: this.$t('order.orderNotExist'),
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
uni.redirectTo({
|
|
||||||
url: '/pages/index/index'
|
|
||||||
})
|
|
||||||
}, 1500)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// 加载订单信息
|
|
||||||
async loadOrderInfo() {
|
|
||||||
try {
|
|
||||||
uni.showLoading({
|
|
||||||
title: this.$t('common.loading')
|
|
||||||
})
|
|
||||||
|
|
||||||
const res = await queryById(this.orderId)
|
|
||||||
if (res.code === 200 && res.data) {
|
|
||||||
const orderData = res.data
|
|
||||||
|
|
||||||
// 处理创建时间,确保显示的是格式化后的时间
|
|
||||||
let formattedTime;
|
|
||||||
try {
|
|
||||||
// 如果orderData.createTime存在并且是有效的日期字符串/时间戳,则格式化它
|
|
||||||
if (orderData.createTime) {
|
|
||||||
formattedTime = this.formatTime(new Date(orderData.createTime));
|
|
||||||
} else {
|
|
||||||
// 如果createTime不存在,使用当前时间作为创建时间
|
|
||||||
formattedTime = this.formatTime(new Date());
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('时间格式化错误:', e);
|
|
||||||
formattedTime = this.formatTime(new Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.orderInfo = {
|
|
||||||
orderNo: orderData.orderNo || orderData.orderId,
|
|
||||||
deviceNo: orderData.deviceNo,
|
|
||||||
createTime: formattedTime,
|
|
||||||
phone: orderData.phone,
|
|
||||||
deposit: this.passedDepositAmount || orderData.depositAmount || '99.00',
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接从订单数据中获取套餐信息
|
|
||||||
if (orderData.packageTime && orderData.packagePrice) {
|
|
||||||
// 将分钟转换为小时
|
|
||||||
const timeInHours = (parseFloat(orderData.packageTime) / 60).toFixed(1);
|
|
||||||
this.packageInfo = {
|
|
||||||
time: timeInHours.toString(),
|
|
||||||
price: orderData.packagePrice.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取设备信息(但不再用于设置套餐信息)
|
|
||||||
this.deviceNo = orderData.deviceNo;
|
|
||||||
await this.loadDeviceInfo();
|
|
||||||
} else {
|
|
||||||
throw new Error('获取订单信息失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
uni.hideLoading()
|
|
||||||
} catch (error) {
|
|
||||||
uni.hideLoading()
|
|
||||||
uni.showToast({
|
|
||||||
title: error.message || '获取订单信息失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 加载设备信息
|
|
||||||
async loadDeviceInfo() {
|
|
||||||
if (!this.deviceNo) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await getDeviceInfo(this.deviceNo);
|
|
||||||
if (res.code === 200 && res.data) {
|
|
||||||
this.deviceInfo = res.data.device;
|
|
||||||
|
|
||||||
// 设置存款金额
|
|
||||||
if (this.deviceInfo && this.deviceInfo.depositAmount) {
|
|
||||||
this.orderInfo.deposit = this.deviceInfo.depositAmount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取设备信息失败:', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 处理支付
|
|
||||||
async handlePayment() {
|
|
||||||
try {
|
|
||||||
uni.showLoading({
|
|
||||||
title: this.$t('common.processing')
|
|
||||||
})
|
|
||||||
|
|
||||||
// 调用后端创建微信支付订单接口
|
|
||||||
const res = await uni.request({
|
|
||||||
url: `${URL || 'http://127.0.0.1:8080'}/app/wx-payment/create/${this.orderInfo.orderNo}`,
|
|
||||||
method: 'GET',
|
|
||||||
header: {
|
|
||||||
'Authorization': "Bearer " + uni.getStorageSync('token'),
|
|
||||||
'Clientid': uni.getStorageSync('client_id')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.statusCode === 200 && res.data.code === 200) {
|
|
||||||
const payParams = res.data.data
|
|
||||||
|
|
||||||
// 调用微信支付
|
|
||||||
await uni.requestPayment({
|
|
||||||
...payParams,
|
|
||||||
success: async () => {
|
|
||||||
uni.showToast({
|
|
||||||
title: this.$t('payment.paymentSuccess'),
|
|
||||||
icon: 'success'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新用户余额
|
|
||||||
try {
|
|
||||||
await updateUserBalance(this.orderId);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('更新用户余额失败:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 支付成功后直接跳转到订单页面,不再轮询
|
|
||||||
setTimeout(() => {
|
|
||||||
uni.redirectTo({
|
|
||||||
url: `/pages/order/index?orderId=${this.orderId}`
|
|
||||||
});
|
|
||||||
}, 1500);
|
|
||||||
},
|
|
||||||
fail: (err) => {
|
|
||||||
console.error('支付失败:', err)
|
|
||||||
throw new Error(this.$t('payment.paymentFailedRetry'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
throw new Error(res.data.msg || '创建支付订单失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
uni.hideLoading()
|
|
||||||
uni.showToast({
|
|
||||||
title: error.message || '支付失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 发送租借指令
|
|
||||||
async sendRentCommand() {
|
|
||||||
try {
|
|
||||||
uni.showLoading({
|
|
||||||
title: this.$t('common.processing')
|
|
||||||
})
|
|
||||||
|
|
||||||
// 调用发送租借指令的接口
|
|
||||||
const res = await this.sendRentRequest()
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
|
||||||
uni.hideLoading()
|
|
||||||
uni.showToast({
|
|
||||||
title: this.$t('device.rentSuccess'),
|
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 跳转到订单列表页面
|
|
||||||
setTimeout(() => {
|
|
||||||
uni.redirectTo({
|
|
||||||
url: `/pages/order/index?orderId=${this.orderId}`
|
|
||||||
})
|
|
||||||
}, 1500)
|
|
||||||
} else {
|
|
||||||
throw new Error(res.msg || this.$t('device.rentFailed'))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
uni.hideLoading()
|
|
||||||
uni.showToast({
|
|
||||||
title: error.message || this.$t('device.rentFailed'),
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 发送租借请求
|
|
||||||
sendRentRequest() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
uni.request({
|
|
||||||
url: `${URL || 'http://127.0.0.1:8080'}/app/device/sendRentCommand`,
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
orderId: this.orderId
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
'Authorization': "Bearer " + uni.getStorageSync('token'),
|
|
||||||
'Clientid': uni.getStorageSync('client_id')
|
|
||||||
},
|
|
||||||
success(res) {
|
|
||||||
if (res.statusCode === 200) {
|
|
||||||
resolve(res.data)
|
|
||||||
} else {
|
|
||||||
reject(new Error('请求失败'))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail(err) {
|
|
||||||
reject(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
// 格式化时间
|
|
||||||
formatTime(date) {
|
|
||||||
const year = date.getFullYear()
|
|
||||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
||||||
const day = date.getDate().toString().padStart(2, '0')
|
|
||||||
const hour = date.getHours().toString().padStart(2, '0')
|
|
||||||
const minute = date.getMinutes().toString().padStart(2, '0')
|
|
||||||
|
|
||||||
return `${year}-${month}-${day} ${hour}:${minute}`
|
|
||||||
},
|
|
||||||
|
|
||||||
// 检查订单状态(单次查询,不轮询)
|
|
||||||
async checkOrderStatus() {
|
|
||||||
try {
|
|
||||||
const res = await uni.request({
|
|
||||||
url: `${URL || 'http://127.0.0.1:8080'}/app/wx-payment/status/${this.orderInfo.orderNo}`,
|
|
||||||
method: 'GET',
|
|
||||||
header: {
|
|
||||||
'Authorization': "Bearer " + uni.getStorageSync('token'),
|
|
||||||
'Clientid': uni.getStorageSync('client_id')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.statusCode === 200 && res.data.code === 200) {
|
|
||||||
return res.data.data;
|
|
||||||
} else {
|
|
||||||
throw new Error('查询订单状态失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('查询订单状态错误:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.payment-container {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #f8f8f8;
|
|
||||||
padding: 30rpx;
|
|
||||||
padding-bottom: 180rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
.status-card {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
padding: 40rpx 30rpx;
|
|
||||||
margin-bottom: 30rpx;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
|
||||||
|
|
||||||
.status-icon {
|
|
||||||
width: 120rpx;
|
|
||||||
height: 120rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #f5f5f5;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
|
|
||||||
&.waiting {
|
|
||||||
background: #FFF9C4;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.success {
|
|
||||||
background: #E8F5E9;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.failed {
|
|
||||||
background: #FFEBEE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-desc {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-card, .price-card {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
padding: 30rpx;
|
|
||||||
margin-bottom: 30rpx;
|
|
||||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
position: relative;
|
|
||||||
padding-left: 20rpx;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
width: 8rpx;
|
|
||||||
height: 32rpx;
|
|
||||||
background: #1976D2;
|
|
||||||
border-radius: 4rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item, .price-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20rpx 0;
|
|
||||||
border-bottom: 1px solid #f5f5f5;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.total {
|
|
||||||
margin-top: 10rpx;
|
|
||||||
padding-top: 30rpx;
|
|
||||||
border-top: 1px solid #f5f5f5;
|
|
||||||
|
|
||||||
.label, .value {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
color: #FF5722;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wechat-tip {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
padding: 30rpx;
|
|
||||||
margin-bottom: 30rpx;
|
|
||||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.method-icon {
|
|
||||||
width: 48rpx;
|
|
||||||
height: 48rpx;
|
|
||||||
margin-right: 20rpx;
|
|
||||||
|
|
||||||
&.wechat {
|
|
||||||
background: url('../../static/images/wechat.svg') no-repeat center/contain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.method-text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-bar {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: #fff;
|
|
||||||
padding: 20rpx 30rpx;
|
|
||||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
|
|
||||||
|
|
||||||
.total-amount {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #666;
|
|
||||||
|
|
||||||
.amount {
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #FF5722;
|
|
||||||
margin-left: 10rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pay-btn {
|
|
||||||
background: #1976D2;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 20rpx 60rpx;
|
|
||||||
border-radius: 100rpx;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -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="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="section-divider"></view>
|
||||||
<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="order-section">
|
||||||
<button class="primary-btn" @click="goToHome">{{ $t('success.backToHome') }}</button>
|
<view class="card-title">{{ $t('success.orderInfo') }}</view>
|
||||||
<button class="secondary-btn" @click="goToOrderList">{{ $t('success.viewOrder') }}</button>
|
<view class="info-item">
|
||||||
</view>
|
<text class="label">{{ $t('order.orderNo') }}</text>
|
||||||
</view>
|
<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>
|
</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: '',
|
const orderId = ref('')
|
||||||
hasTriggeredDevice: false
|
const orderInfo = ref({})
|
||||||
}
|
const isLoading = ref(true)
|
||||||
},
|
const deviceMessage = ref('')
|
||||||
onLoad(options) {
|
const hasTriggeredDevice = ref(false)
|
||||||
// 设置页面标题
|
|
||||||
uni.setNavigationBarTitle({
|
// 页面加载
|
||||||
title: this.$t('success.paymentSuccess')
|
onLoad((options) => {
|
||||||
})
|
// 设置页面标题
|
||||||
|
uni.setNavigationBarTitle({
|
||||||
this.deviceMessage = this.$t('success.preparingDevice')
|
title: proxy.$t('success.paymentSuccess')
|
||||||
|
})
|
||||||
if (options && options.orderId) {
|
|
||||||
this.orderId = options.orderId
|
deviceMessage.value = proxy.$t('success.preparingDevice')
|
||||||
this.loadOrderInfo()
|
|
||||||
|
// #ifdef H5
|
||||||
// 添加页面显示监听,防止页面切换后重复触发弹出
|
if (uni.getStorageSync('pendingPaymentNo')) {
|
||||||
uni.$once('orderSuccess:' + this.orderId, () => {
|
orderId.value = options.orderId
|
||||||
console.log('已经触发过弹出逻辑,不再重复触发')
|
loadOrderInfo()
|
||||||
this.hasTriggeredDevice = true
|
|
||||||
})
|
// 添加页面显示监听,防止页面切换后重复触发弹出
|
||||||
} else {
|
uni.$once('orderSuccess:' + orderId.value, () => {
|
||||||
uni.showToast({
|
console.log('已经触发过弹出逻辑,不再重复触发')
|
||||||
title: this.$t('order.orderNotExist'),
|
hasTriggeredDevice.value = true
|
||||||
icon: 'none'
|
})
|
||||||
})
|
} else {
|
||||||
setTimeout(() => {
|
uni.showToast({
|
||||||
this.goToHome()
|
title: proxy.$t('order.orderNotExist'),
|
||||||
}, 1500)
|
icon: 'none'
|
||||||
}
|
})
|
||||||
},
|
setTimeout(() => {
|
||||||
methods: {
|
goToHome()
|
||||||
async loadOrderInfo() {
|
}, 1500)
|
||||||
try {
|
}
|
||||||
uni.showLoading({
|
// #endif
|
||||||
title: this.$t('common.loading')
|
// #ifndef H5
|
||||||
})
|
if (options && options.orderId) {
|
||||||
|
orderId.value = options.orderId
|
||||||
const res = await queryById(this.orderId)
|
loadOrderInfo()
|
||||||
if (res.code === 200 && res.data) {
|
|
||||||
const orderData = res.data
|
// 添加页面显示监听,防止页面切换后重复触发弹出
|
||||||
this.orderInfo = {
|
uni.$once('orderSuccess:' + orderId.value, () => {
|
||||||
orderNo: orderData.orderNo || orderData.orderId,
|
console.log('已经触发过弹出逻辑,不再重复触发')
|
||||||
deviceNo: orderData.deviceNo,
|
hasTriggeredDevice.value = true
|
||||||
amount: orderData.payAmount || orderData.amount,
|
})
|
||||||
payTime: orderData.payTime || this.formatTime(new Date())
|
} else {
|
||||||
}
|
uni.showToast({
|
||||||
|
title: proxy.$t('order.orderNotExist'),
|
||||||
// 检查订单状态
|
icon: 'none'
|
||||||
if (orderData.orderStatus === 'IN_USED') {
|
})
|
||||||
// 如果已经是使用中状态,可能说明开锁已经完成
|
setTimeout(() => {
|
||||||
this.deviceMessage = '设备已弹出,请取走您的风扇'
|
goToHome()
|
||||||
this.isLoading = false
|
}, 1500)
|
||||||
|
}
|
||||||
// 如果是第一次加载页面且设备已弹出,记录状态,避免重复弹出
|
// #endif
|
||||||
if (!this.hasTriggeredDevice) {
|
})
|
||||||
uni.$emit('orderSuccess:' + this.orderId)
|
|
||||||
this.hasTriggeredDevice = true
|
// 加载订单信息
|
||||||
}
|
const loadOrderInfo = async () => {
|
||||||
} else {
|
try {
|
||||||
// 正常触发弹出逻辑
|
uni.showLoading({
|
||||||
this.triggerDeviceEject()
|
title: proxy.$t('common.loading')
|
||||||
}
|
})
|
||||||
} else {
|
|
||||||
throw new Error('获取订单信息失败')
|
const res = await queryById(orderId.value)
|
||||||
}
|
if (res.code === 200 && res.data) {
|
||||||
|
const orderData = res.data
|
||||||
uni.hideLoading()
|
orderInfo.value = {
|
||||||
} catch (error) {
|
orderNo: orderData.orderNo || orderData.orderId,
|
||||||
uni.hideLoading()
|
deviceNo: orderData.deviceNo,
|
||||||
uni.showToast({
|
amount: orderData.payAmount || orderData.amount,
|
||||||
title: error.message || this.$t('order.getOrderFailed'),
|
payTime: orderData.payTime || formatTime(new Date())
|
||||||
icon: 'none'
|
}
|
||||||
})
|
|
||||||
}
|
// 检查订单状态
|
||||||
},
|
if (orderData.orderStatus === 'IN_USED') {
|
||||||
|
// 如果已经是使用中状态,可能说明开锁已经完成
|
||||||
// 触发弹出风扇
|
deviceMessage.value = '设备已弹出,请取走您的风扇'
|
||||||
async triggerDeviceEject() {
|
isLoading.value = false
|
||||||
if (this.hasTriggeredDevice) {
|
|
||||||
console.log('已经触发过弹出风扇,不重复触发')
|
// 如果是第一次加载页面且设备已弹出,记录状态,避免重复弹出
|
||||||
return
|
if (!hasTriggeredDevice.value) {
|
||||||
}
|
uni.$emit('orderSuccess:' + orderId.value)
|
||||||
|
hasTriggeredDevice.value = true
|
||||||
this.hasTriggeredDevice = true
|
}
|
||||||
uni.$emit('orderSuccess:' + this.orderId)
|
} else {
|
||||||
this.isLoading = true
|
// 在此页面不再触发设备弹出操作,仅更新展示文案和加载状态
|
||||||
this.deviceMessage = this.$t('success.preparingDevice')
|
deviceMessage.value = proxy.$t('success.paymentSuccessDesc')
|
||||||
|
isLoading.value = false
|
||||||
try {
|
}
|
||||||
console.log(`准备触发弹出风扇,orderId: ${this.orderId}`)
|
} else {
|
||||||
|
throw new Error('获取订单信息失败')
|
||||||
// 调用确认支付并弹出的方法
|
}
|
||||||
const result = await confirmPaymentAndRent(this.orderId)
|
|
||||||
console.log('确认支付并弹出风扇结果:', JSON.stringify(result))
|
uni.hideLoading()
|
||||||
|
} catch (error) {
|
||||||
if (result && result.code === 200) {
|
uni.hideLoading()
|
||||||
this.deviceMessage = this.$t('success.deviceReady')
|
uni.showToast({
|
||||||
uni.showToast({
|
title: error.message || proxy.$t('order.getOrderFailed'),
|
||||||
title: this.$t('success.deviceReady'),
|
icon: 'none'
|
||||||
icon: 'success'
|
})
|
||||||
})
|
}
|
||||||
} else {
|
}
|
||||||
throw new Error((result && result.msg) || this.$t('success.deviceFailed'))
|
|
||||||
}
|
// 格式化时间
|
||||||
} catch (error) {
|
const formatTime = (date) => {
|
||||||
console.error('弹出风扇错误:', error)
|
const year = date.getFullYear()
|
||||||
this.deviceMessage = this.$t('success.deviceFailed')
|
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||||
uni.showToast({
|
const day = date.getDate().toString().padStart(2, '0')
|
||||||
title: error.message || this.$t('success.deviceFailed'),
|
const hour = date.getHours().toString().padStart(2, '0')
|
||||||
icon: 'none'
|
const minute = date.getMinutes().toString().padStart(2, '0')
|
||||||
})
|
const second = date.getSeconds().toString().padStart(2, '0')
|
||||||
} finally {
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
||||||
this.isLoading = false
|
}
|
||||||
}
|
|
||||||
},
|
// 返回首页
|
||||||
|
const goToHome = () => {
|
||||||
formatTime(date) {
|
uni.reLaunch({
|
||||||
const year = date.getFullYear()
|
url: '/pages/index/index'
|
||||||
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')
|
const goToOrderList = () => {
|
||||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
uni.redirectTo({
|
||||||
},
|
url: '/pages/order/index'
|
||||||
goToHome() {
|
})
|
||||||
uni.switchTab({
|
}
|
||||||
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 {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-card {
|
.status-section {
|
||||||
background-color: #fff;
|
padding: 30px;
|
||||||
border-radius: 12px;
|
text-align: center;
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
.label {
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
color: #333;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-status {
|
.status-icon {
|
||||||
background-color: #fff;
|
width: 60px;
|
||||||
border-radius: 12px;
|
height: 60px;
|
||||||
padding: 20px;
|
margin: 0 auto 16px;
|
||||||
margin-bottom: 20px;
|
background-color: #07c160;
|
||||||
text-align: center;
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
.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 {
|
&::after {
|
||||||
margin-top: 30px;
|
content: '';
|
||||||
display: flex;
|
position: absolute;
|
||||||
// flex-direction: column;
|
left: 50%;
|
||||||
gap: 16px;
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
.primary-btn {
|
width: 30px;
|
||||||
background-color: #07c160;
|
height: 20px;
|
||||||
color: #fff;
|
border: 3px solid #fff;
|
||||||
border: none;
|
border-top: none;
|
||||||
border-radius: 24px;
|
border-right: none;
|
||||||
padding: 12px;
|
transform-origin: center;
|
||||||
font-size: 16px;
|
transform: translate(-50%, -70%) rotate(-45deg);
|
||||||
|
}
|
||||||
&:active {
|
}
|
||||||
opacity: 0.8;
|
|
||||||
}
|
.status-text {
|
||||||
}
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
.secondary-btn {
|
color: #07c160;
|
||||||
background-color: #fff;
|
margin-bottom: 8px;
|
||||||
color: #07c160;
|
}
|
||||||
border: 1px solid #07c160;
|
|
||||||
border-radius: 24px;
|
.status-desc {
|
||||||
padding: 12px;
|
font-size: 14px;
|
||||||
font-size: 16px;
|
color: #666;
|
||||||
|
}
|
||||||
&:active {
|
}
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
.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>
|
</style>
|
||||||
@@ -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>
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -7,15 +7,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
wxLogin,
|
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 {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -1,413 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view class="profile-page">
|
|
||||||
<view class="avatar-section">
|
|
||||||
<view class="avatar-container">
|
|
||||||
<image class="avatar" v-if="userInfo.avatar" :src="userInfo.avatar" mode="aspectFill"></image>
|
|
||||||
<image v-else class="avatar" src="@/static/head.png" mode="aspectFill"></image>
|
|
||||||
<!-- 覆盖在头像上的微信选择头像授权按钮,仅小程序生效 -->
|
|
||||||
<!-- #ifdef MP-WEIXIN -->
|
|
||||||
<button class="avatar-choose-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar"></button>
|
|
||||||
<!-- #endif -->
|
|
||||||
</view>
|
|
||||||
<view class="avatar-tip">{{ $t('userProfile.clickToChange') }}</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-section">
|
|
||||||
<!-- 昵称编辑区域 -->
|
|
||||||
<view class="form-item nickname-item" :class="{ editing: isEditingNickname }">
|
|
||||||
<view class="label">{{ $t('userProfile.nickname') }}</view>
|
|
||||||
<view class="value" v-if="!isEditingNickname" @click="startEditNickname">
|
|
||||||
<text class="value-text">{{ userInfo.nickName || $t('userProfile.notSet') }}</text>
|
|
||||||
<uv-icon name="edit-pen" size="16" color="#999999"></uv-icon>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 昵称编辑输入框(展开状态) -->
|
|
||||||
<view class="nickname-edit-area" v-if="isEditingNickname">
|
|
||||||
<input
|
|
||||||
class="nickname-input"
|
|
||||||
v-model="newNickname"
|
|
||||||
:placeholder="$t('userProfile.enterNickname')"
|
|
||||||
maxlength="20"
|
|
||||||
:focus="true"
|
|
||||||
/>
|
|
||||||
<view class="edit-buttons">
|
|
||||||
<button class="cancel-btn" @click="cancelEditNickname">{{ $t('common.cancel') }}</button>
|
|
||||||
<button class="save-btn" @click="saveNickname">{{ $t('common.save') }}</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item">
|
|
||||||
<view class="label">{{ $t('userProfile.phone') }}</view>
|
|
||||||
<view class="value">
|
|
||||||
<text class="value-text">{{ userInfo.phone ? maskPhone(userInfo.phone) : $t('userProfile.notBound') }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- <view class="form-item" v-if="userInfo.balanceAmount !== undefined">
|
|
||||||
<view class="label">{{ $t('userProfile.balance') }}</view>
|
|
||||||
<view class="value">
|
|
||||||
<text class="value-text amount">¥{{ userInfo.balanceAmount || '0.00' }}</text>
|
|
||||||
</view>
|
|
||||||
</view> -->
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, onMounted } from 'vue';
|
|
||||||
import { getMyIndexInfo, uploadUserAvatar, updateUserInfo } from '../../config/api/user.js';
|
|
||||||
import { useI18n } from '@/utils/i18n.js'
|
|
||||||
|
|
||||||
const { t: $t } = useI18n()
|
|
||||||
|
|
||||||
// 响应式状态
|
|
||||||
const userInfo = ref({
|
|
||||||
nickName: '',
|
|
||||||
phone: '',
|
|
||||||
avatar: '',
|
|
||||||
balanceAmount: '0.00'
|
|
||||||
});
|
|
||||||
|
|
||||||
const newNickname = ref('');
|
|
||||||
const isEditingNickname = ref(false);
|
|
||||||
|
|
||||||
// 页面加载时初始化
|
|
||||||
onMounted(() => {
|
|
||||||
uni.setNavigationBarTitle({
|
|
||||||
title: $t('userProfile.title')
|
|
||||||
})
|
|
||||||
loadUserInfo();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取用户信息
|
|
||||||
const loadUserInfo = async () => {
|
|
||||||
try {
|
|
||||||
const res = await getMyIndexInfo();
|
|
||||||
console.log('User info response:', res);
|
|
||||||
|
|
||||||
if (res.code == 401 || res.code == 40101) {
|
|
||||||
redirectToLogin();
|
|
||||||
return;
|
|
||||||
} else if (res.code == 200) {
|
|
||||||
userInfo.value = {
|
|
||||||
nickName: res.data.nickname,
|
|
||||||
phone: res.data.phone,
|
|
||||||
avatar: res.data.iconUrl,
|
|
||||||
balanceAmount: res.data.balanceAmount || '0.00',
|
|
||||||
isAdmin: res.data.isAdmin
|
|
||||||
};
|
|
||||||
uni.setStorageSync('userInfo', userInfo.value);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取用户信息失败:', error);
|
|
||||||
uni.showToast({
|
|
||||||
title: $t('user.getUserInfoFailed'),
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 跳转登录
|
|
||||||
const redirectToLogin = () => {
|
|
||||||
try {
|
|
||||||
const pages = getCurrentPages();
|
|
||||||
const current = pages && pages.length ? pages[pages.length - 1] : null;
|
|
||||||
const route = current && current.route ? ('/' + current.route) : '/pages/index/index';
|
|
||||||
const query = current && current.options ? Object.keys(current.options).map(k =>
|
|
||||||
`${k}=${encodeURIComponent(current.options[k])}`).join('&') : '';
|
|
||||||
const redirect = encodeURIComponent(query ? `${route}?${query}` : route);
|
|
||||||
uni.reLaunch({
|
|
||||||
url: `/pages/login/index?redirect=${redirect}`
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
uni.reLaunch({
|
|
||||||
url: '/pages/login/index'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 小程序原生选择头像回调
|
|
||||||
const onChooseAvatar = async (e) => {
|
|
||||||
try {
|
|
||||||
const token = uni.getStorageSync('token');
|
|
||||||
if (!token) {
|
|
||||||
redirectToLogin();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const avatarLocalPath = e?.detail?.avatarUrl;
|
|
||||||
if (!avatarLocalPath) {
|
|
||||||
uni.showToast({
|
|
||||||
title: $t('user.noAvatar'),
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uni.showLoading({
|
|
||||||
title: $t('userProfile.uploading'),
|
|
||||||
mask: true
|
|
||||||
});
|
|
||||||
const uploadRes = await uploadUserAvatar(avatarLocalPath);
|
|
||||||
const serverAvatar = uploadRes?.data?.url || uploadRes?.url || uploadRes?.data || '';
|
|
||||||
if (serverAvatar) {
|
|
||||||
userInfo.value = {
|
|
||||||
...userInfo.value,
|
|
||||||
avatar: serverAvatar
|
|
||||||
};
|
|
||||||
uni.setStorageSync('userInfo', userInfo.value);
|
|
||||||
}
|
|
||||||
uni.showToast({
|
|
||||||
title: $t('user.avatarUpdated'),
|
|
||||||
icon: 'success'
|
|
||||||
});
|
|
||||||
await loadUserInfo();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('选择/上传头像失败:', err);
|
|
||||||
uni.showToast({
|
|
||||||
title: $t('user.avatarUploadFailed'),
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
uni.hideLoading();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 开始编辑昵称
|
|
||||||
const startEditNickname = () => {
|
|
||||||
newNickname.value = userInfo.value.nickName || '';
|
|
||||||
isEditingNickname.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 取消编辑昵称
|
|
||||||
const cancelEditNickname = () => {
|
|
||||||
isEditingNickname.value = false;
|
|
||||||
newNickname.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
// 保存昵称
|
|
||||||
const saveNickname = async () => {
|
|
||||||
if (!newNickname.value || !newNickname.value.trim()) {
|
|
||||||
uni.showToast({
|
|
||||||
title: $t('userProfile.nicknameRequired'),
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
uni.showLoading({
|
|
||||||
title: $t('userProfile.saving'),
|
|
||||||
mask: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// 先获取最新的用户信息,确保数据是最新的
|
|
||||||
const latestUserInfo = await getMyIndexInfo();
|
|
||||||
|
|
||||||
if (latestUserInfo.code !== 200) {
|
|
||||||
throw new Error('获取用户信息失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用最新的服务器数据,只修改昵称字段
|
|
||||||
const updateData = {
|
|
||||||
nickname: newNickname.value.trim(),
|
|
||||||
phone: latestUserInfo.data.phone,
|
|
||||||
iconUrl: latestUserInfo.data.iconUrl,
|
|
||||||
// 保留其他可能的字段
|
|
||||||
...latestUserInfo.data
|
|
||||||
};
|
|
||||||
|
|
||||||
// 确保昵称使用新值
|
|
||||||
updateData.nickname = newNickname.value.trim();
|
|
||||||
|
|
||||||
// 调用后端接口更新用户信息
|
|
||||||
const res = await updateUserInfo(updateData);
|
|
||||||
|
|
||||||
if (res.code === 200) {
|
|
||||||
// 更新成功后重新获取用户信息,确保数据同步
|
|
||||||
await loadUserInfo();
|
|
||||||
|
|
||||||
uni.hideLoading();
|
|
||||||
uni.showToast({
|
|
||||||
title: $t('userProfile.nicknameUpdated'),
|
|
||||||
icon: 'success'
|
|
||||||
});
|
|
||||||
isEditingNickname.value = false;
|
|
||||||
} else {
|
|
||||||
throw new Error(res.message || $t('userProfile.updateFailed'));
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('修改昵称失败:', error);
|
|
||||||
uni.hideLoading();
|
|
||||||
uni.showToast({
|
|
||||||
title: error.message || $t('userProfile.updateFailed'),
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 手机号掩码函数
|
|
||||||
function maskPhone(phone) {
|
|
||||||
if (!phone) return '';
|
|
||||||
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.profile-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-section {
|
|
||||||
background: linear-gradient(180deg, #D1FFE1 0%, #ffffff 100%);
|
|
||||||
padding: 60rpx 0 40rpx;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-container {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 160rpx;
|
|
||||||
height: 160rpx;
|
|
||||||
border-radius: 80rpx;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 仅小程序端存在,此按钮覆盖在头像上捕获点击以触发选择头像 */
|
|
||||||
/* #ifdef MP-WEIXIN */
|
|
||||||
.avatar-choose-btn {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 160rpx;
|
|
||||||
height: 160rpx;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
opacity: 0;
|
|
||||||
border-radius: 80rpx;
|
|
||||||
}
|
|
||||||
/* #endif */
|
|
||||||
|
|
||||||
.avatar-tip {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #999999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section {
|
|
||||||
margin: 20rpx 30rpx;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 32rpx 30rpx;
|
|
||||||
border-bottom: 1rpx solid #f5f5f5;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-item.nickname-item.editing {
|
|
||||||
border-bottom: none;
|
|
||||||
padding-bottom: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
font-size: 30rpx;
|
|
||||||
color: #333333;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value-text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #666666;
|
|
||||||
margin-right: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value-text.amount {
|
|
||||||
color: #e2231a;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 32rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 昵称编辑区域样式 */
|
|
||||||
.nickname-edit-area {
|
|
||||||
padding: 0 30rpx 30rpx 30rpx;
|
|
||||||
border-bottom: 1rpx solid #f5f5f5;
|
|
||||||
animation: slideDown 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideDown {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-20rpx);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nickname-input {
|
|
||||||
width: 100%;
|
|
||||||
height: 80rpx;
|
|
||||||
border: 1rpx solid #e0e0e0;
|
|
||||||
border-radius: 10rpx;
|
|
||||||
padding: 0 20rpx;
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #333333;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
background-color: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn,
|
|
||||||
.save-btn {
|
|
||||||
padding: 0 40rpx;
|
|
||||||
height: 64rpx;
|
|
||||||
line-height: 64rpx;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 32rpx;
|
|
||||||
font-size: 28rpx;
|
|
||||||
border: none;
|
|
||||||
min-width: 120rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cancel-btn {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
color: #666666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-btn {
|
|
||||||
background: linear-gradient(135deg, #42d392 0%, #28c76f 100%);
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
|
After Width: | Height: | Size: 847 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 348 B |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 676 KiB After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 868 B |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -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>
|
||||||
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
@@ -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,28 +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/wx-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({
|
||||||
...payParams,
|
...payParams,
|
||||||
success: async () => {
|
success: async () => {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: $t('payment.paymentSuccess'),
|
title: t('payment.paymentSuccess'),
|
||||||
icon: 'success'
|
icon: 'success'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -305,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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -326,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({
|
||||||
@@ -341,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'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
@@ -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>
|
||||||
@@ -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)
|
||||||
})
|
})
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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>
|
||||||
@@ -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 || '-';
|
||||||
};
|
};
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
@@ -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="getPhoneNumber" @getphonenumber="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="onWeChatLogin">仅微信登录</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 { wxLogin, 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,36 +130,51 @@
|
|||||||
uni.reLaunch({ url: target })
|
uni.reLaunch({ url: target })
|
||||||
}
|
}
|
||||||
|
|
||||||
const onWeChatLogin = async () => {
|
// 微信快捷登录入口(备用,目前主流程使用一键手机号登录)
|
||||||
|
// const onWeChatLogin = async () => {
|
||||||
|
// try {
|
||||||
|
// await checkAgreement()
|
||||||
|
// await wxLogin()
|
||||||
|
// uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
|
||||||
|
// await navigateAfterLogin()
|
||||||
|
// } catch (error) {
|
||||||
|
// if (error.message !== t('auth.pleaseAgreeToTerms')) {
|
||||||
|
// uni.showToast({ title: error.message || t('auth.loginFailed'), icon: 'none' })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 支付宝快捷登录入口(支付宝小程序)
|
||||||
|
const onAlipayLogin = async () => {
|
||||||
try {
|
try {
|
||||||
// 先检查是否同意协议
|
|
||||||
await checkAgreement()
|
await checkAgreement()
|
||||||
|
await alipayLogin()
|
||||||
await wxLogin()
|
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.errMsg !== 'getPhoneNumber:ok') {
|
||||||
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 wxLogin()
|
await wxLogin(e.detail.code)
|
||||||
// 再用微信返回的临时 code 换取手机号
|
// 再用微信返回的临时 code 换取手机号(后端按文档更新 phone 字段)
|
||||||
await getUserPhoneNumber(e.detail.code)
|
// await getUserPhoneNumber(e.detail.code)
|
||||||
uni.showToast({ title: $t('auth.loginSuccess'), icon: 'success' })
|
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' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,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;
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||