完善生产查询与登录权限配置

This commit is contained in:
2026-09-03 19:56:19 +08:00
parent 793f140a45
commit 02c8fb7021
438 changed files with 1595 additions and 573 deletions
+434
View File
@@ -0,0 +1,434 @@
<template>
<view class="about-page" :class="`theme-${themeMode}`">
<view class="hero-card">
<view class="hero-logo">
<u-icon name="info-circle-fill" color="#ffffff" size="46" />
</view>
<view class="hero-content">
<text class="hero-tag">APP 原型</text>
<text class="hero-title">关于我们</text>
<text class="hero-subtitle">扫描、提报、维修与统计的一体化现场作业助手</text>
</view>
</view>
<view class="section-card">
<text class="section-title">产品信息</text>
<view class="info-list">
<view class="info-row">
<text class="info-label">产品名称</text>
<text class="info-value">ScanApp</text>
</view>
<view class="info-row">
<text class="info-label">适用场景</text>
<text class="info-value">扫码录入 / 异常处理 / 维修协同</text>
</view>
<view class="info-row">
<text class="info-label">当前版本</text>
<text class="info-value">v{{ appVersion }}</text>
</view>
</view>
</view>
<view class="section-card">
<text class="section-title">协议与资质</text>
<view class="nav-list">
<view v-for="item in policyItems" :key="item.title" class="nav-row" :class="{ tappable: item.readable }" @tap="item.readable && handlePolicyTap(item)">
<text class="nav-title">{{ item.title }}</text>
<view class="nav-right">
<text v-if="item.value" class="nav-value">{{ item.value }}</text>
<u-icon v-if="item.readable" class="nav-arrow" name="arrow-right" color="#c1c7d0" size="24" />
</view>
</view>
</view>
</view>
<view class="section-card">
<text class="section-title">联系与支持</text>
<view class="contact-stack">
<view v-for="item in contactItems" :key="item.label" class="contact-block">
<text class="contact-label">{{ item.label }}</text>
<view class="email-row">
<text class="email-value">{{ item.email || '未配置' }}</text>
<view v-if="item.email" class="copy-btn" @tap="copyEmail(item.email)">复制</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { getCurrentAppVersion } from '@/api/app-update.js'
import { getConfigByCode } from '@/api/system-config.js'
export default {
data() {
return {
appVersion: getCurrentAppVersion(),
policyItems: [
{ title: '用户服务协议', value: '', content: '', readable: true },
{ title: '隐私政策', value: '', content: '', readable: true },
{ title: '备案号', value: '', content: '', readable: false }
],
contactItems: [
{ label: '违法和不良信息举报邮箱', email: '' },
{ label: '商务合作', email: '' }
],
}
},
onLoad() {
this.loadAboutConfig()
},
onShow() {
uni.setNavigationBarColor({
frontColor: this.themeMode === 'dark' ? '#ffffff' : '#000000',
backgroundColor: this.themeMode === 'dark' ? '#151923' : '#eef6ff'
})
},
methods: {
async loadAboutConfig() {
try {
const config = await getConfigByCode('app_about_setting')
this.policyItems = [
{ title: '用户服务协议', value: config.user_service_agreement ? '查看' : '', content: config.user_service_agreement || '', readable: true },
{ title: '隐私政策', value: config.privacy_policy ? '查看' : '', content: config.privacy_policy || '', readable: true },
{ title: '备案号', value: config.icp_number || '', content: '', readable: false }
]
this.contactItems = [
{ label: '违法和不良信息举报邮箱', email: config.report_email || '' },
{ label: '商务合作', email: config.business_contact || '' }
]
} catch (error) {
console.error('[scanapp] 获取关于我们配置失败', error)
}
},
handlePolicyTap(item) {
if (!item.content) {
uni.showToast({ title: '暂未配置', icon: 'none' })
return
}
uni.setStorageSync('scanapp_policy_detail', {
title: item.title,
content: item.content
})
uni.navigateTo({ url: '/pages/settings/policy' })
},
copyEmail(email) {
uni.setClipboardData({
data: email,
success: () => {
uni.showToast({ title: '已复制', icon: 'success' })
}
})
}
}
}
</script>
<style lang="scss" scoped>
.about-page {
min-height: 100vh;
padding: 28rpx 24rpx 48rpx;
box-sizing: border-box;
background: linear-gradient(180deg, #eef6ff 0%, #f4f6fb 36%, #f4f6fb 100%);
}
.hero-card,
.section-card {
padding: 34rpx;
border-radius: 28rpx;
border: 1rpx solid rgba(255, 255, 255, 0.75);
background: #ffffff;
box-shadow: 0 16rpx 40rpx rgba(23, 35, 61, 0.06);
}
.section-card {
margin-top: 28rpx;
}
.hero-card {
display: flex;
align-items: center;
}
.hero-content {
min-width: 0;
margin-left: 24rpx;
}
.hero-logo {
display: flex;
align-items: center;
justify-content: center;
width: 96rpx;
height: 96rpx;
border-radius: 28rpx;
background: linear-gradient(135deg, #1677ff 0%, #35bfa3 100%);
flex: none;
}
.hero-tag,
.hero-subtitle,
.info-label,
.feature-desc,
.contact-label,
.contact-value,
.nav-title,
.nav-value,
.email-value {
display: block;
}
.hero-tag {
color: #1677ff;
font-size: 20rpx;
font-weight: 700;
}
.hero-title,
.section-title,
.feature-title {
display: block;
color: #17233d;
font-weight: 800;
}
.hero-title {
margin-top: 10rpx;
font-size: 34rpx;
}
.hero-subtitle {
margin-top: 10rpx;
color: #7d8798;
font-size: 20rpx;
line-height: 1.5;
}
.section-title {
font-size: 26rpx;
margin-bottom: 18rpx;
}
.info-row,
.contact-item {
display: flex;
justify-content: space-between;
padding: 18rpx 0;
border-bottom: 1rpx solid #edf1f6;
}
.info-value,
.contact-value {
min-width: 0;
margin-left: 20rpx;
}
.info-row:last-child,
.contact-item:last-child {
border-bottom: 0;
}
.info-label,
.contact-label {
color: #7d8798;
font-size: 22rpx;
}
.info-value,
.contact-value {
color: #17233d;
font-size: 22rpx;
font-weight: 600;
text-align: right;
}
.feature-grid {
display: grid;
grid-template-columns: 1fr;
gap: 14rpx;
}
.feature-item {
padding: 22rpx;
border-radius: 20rpx;
background: #f8fafc;
}
.feature-title {
font-size: 24rpx;
}
.feature-desc {
margin-top: 8rpx;
color: #7d8798;
font-size: 20rpx;
line-height: 1.55;
}
.nav-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #edf1f6;
}
.nav-title {
min-width: 0;
flex: 1;
}
.nav-row:last-child {
border-bottom: 0;
}
.nav-title {
color: #17233d;
font-size: 28rpx;
font-weight: 600;
line-height: 1.45;
}
.nav-right {
display: flex;
align-items: center;
flex: none;
margin-left: 20rpx;
}
.nav-value {
flex-shrink: 1;
min-width: 0;
}
.nav-arrow {
flex: none;
margin-left: 12rpx;
}
.nav-value {
color: #5f6b7a;
font-size: 22rpx;
line-height: 1.45;
text-align: right;
}
.contact-stack {
display: flex;
flex-direction: column;
gap: 22rpx;
}
.contact-block + .contact-block {
padding-top: 6rpx;
}
.contact-label {
margin-bottom: 14rpx;
color: #17233d;
font-size: 26rpx;
font-weight: 600;
line-height: 1.45;
}
.email-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 22rpx 24rpx;
border-radius: 20rpx;
background: #f8fafc;
}
.email-value {
flex: 1;
min-width: 0;
margin-right: 18rpx;
}
.copy-btn {
flex: none;
}
.email-value {
flex: 1;
min-width: 0;
color: #8f98a8;
font-size: 22rpx;
line-height: 1.45;
word-break: break-all;
}
.copy-btn {
flex: none;
padding: 0 8rpx;
color: #7d8798;
font-size: 24rpx;
font-weight: 600;
line-height: 1.45;
}
.tappable {
cursor: pointer;
}
.theme-dark.about-page {
background: #151923;
}
.theme-dark .policy-fullscreen {
background: #151923;
}
.theme-dark .policy-header {
border-color: #364152;
}
.theme-dark .policy-back,
.theme-dark .policy-header-title,
.theme-dark .policy-title {
color: #f2f5fa;
}
.theme-dark .policy-content {
color: #c4cede;
}
.theme-dark .hero-card,
.theme-dark .section-card {
border-color: #364152;
background: #222936;
box-shadow: none;
}
.theme-dark .feature-item,
.theme-dark .email-row {
background: #1c2431;
}
.theme-dark .hero-title,
.theme-dark .section-title,
.theme-dark .feature-title,
.theme-dark .info-value,
.theme-dark .contact-value,
.theme-dark .nav-title,
.theme-dark .contact-label {
color: #f2f5fa;
}
.theme-dark .hero-subtitle,
.theme-dark .info-label,
.theme-dark .feature-desc,
.theme-dark .nav-value,
.theme-dark .email-value,
.theme-dark .copy-btn {
color: #aeb9ca;
}
.theme-dark .info-row,
.theme-dark .contact-item,
.theme-dark .nav-row {
border-color: #364152;
}
</style>
@@ -0,0 +1,264 @@
<template>
<view class="feedback-detail-page" :class="`theme-${themeMode}`">
<view v-if="loading" class="loading-state">正在加载反馈详情…</view>
<view v-else-if="feedback" class="detail-card">
<view class="detail-header">
<view>
<text class="detail-type">{{ getTypeLabel(feedback.type) }}</text>
<text class="detail-time">{{ feedback.created_date || '时间未知' }}</text>
</view>
<u-icon name="chat" color="#ff8a3d" size="40" />
</view>
<view class="detail-section">
<text class="detail-label">问题描述</text>
<text class="detail-content">{{ feedback.content || '/' }}</text>
</view>
<view class="detail-section">
<text class="detail-label">联系方式</text>
<text class="detail-content">{{ feedback.contact || '未填写' }}</text>
</view>
<view v-if="imageList.length" class="detail-section">
<text class="detail-label">截图附件</text>
<view class="image-list">
<image v-for="image in imageList" :key="image" :src="image" mode="aspectFill" @tap="previewImage(image)" />
</view>
</view>
<view v-if="feedback.reply_content" class="reply-card">
<view class="reply-header">
<text class="reply-title">管理员回复</text>
<text class="reply-time">{{ feedback.reply_date || '' }}</text>
</view>
<text class="reply-content">{{ feedback.reply_content }}</text>
<text v-if="feedback.reply_name" class="reply-admin">回复人:{{ feedback.reply_name }}</text>
</view>
<view v-else class="pending-reply">该反馈暂未收到回复,请耐心等待。</view>
</view>
<u-empty v-else mode="data" text="反馈记录不存在" icon="data" />
</view>
</template>
<script>
import { getMyFeedbackDetail } from '@/api/feedback.js'
export default {
data() {
return {
loading: true,
feedback: null,
feedbackId: ''
}
},
computed: {
imageList() {
return Array.isArray(this.feedback?.images)
? this.feedback.images.filter((image) => typeof image === 'string' && image)
: []
}
},
onLoad(options) {
this.feedbackId = String(options?.id || '')
this.loadFeedbackDetail()
},
methods: {
getTypeLabel(type) {
return {
bug: '功能异常',
suggestion: '体验建议',
correction: '内容纠错'
}[type] || '问题反馈'
},
async loadFeedbackDetail() {
if (!this.feedbackId) {
this.loading = false
return
}
this.loading = true
try {
this.feedback = await getMyFeedbackDetail(this.feedbackId)
} catch (error) {
console.error('[scanapp] 获取反馈详情失败', error)
this.feedback = null
} finally {
this.loading = false
}
},
previewImage(current) {
uni.previewImage({
current,
urls: this.imageList
})
}
}
}
</script>
<style lang="scss" scoped>
.feedback-detail-page {
min-height: 100vh;
padding: 28rpx 24rpx 48rpx;
box-sizing: border-box;
background: #f4f6fb;
}
.loading-state {
padding: 80rpx 0;
color: #7d8798;
font-size: 22rpx;
text-align: center;
}
.detail-card {
padding: 34rpx;
border-radius: 28rpx;
background: #ffffff;
box-shadow: 0 16rpx 40rpx rgba(23, 35, 61, 0.06);
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 28rpx;
border-bottom: 1rpx solid #edf0f5;
}
.detail-type,
.detail-time,
.detail-label,
.detail-content {
display: block;
}
.detail-type {
color: #ff8a3d;
font-size: 28rpx;
font-weight: 800;
}
.detail-time {
margin-top: 10rpx;
color: #7d8798;
font-size: 20rpx;
}
.detail-section {
padding-top: 28rpx;
}
.detail-label {
color: #7d8798;
font-size: 20rpx;
}
.detail-content {
margin-top: 12rpx;
color: #17233d;
font-size: 24rpx;
line-height: 1.6;
word-break: break-all;
}
.image-list {
display: flex;
flex-wrap: wrap;
margin-top: 16rpx;
}
.image-list image {
width: 180rpx;
height: 180rpx;
margin-right: 14rpx;
margin-bottom: 14rpx;
border-radius: 18rpx;
}
.reply-card {
margin-top: 32rpx;
padding: 24rpx;
border-radius: 18rpx;
background: #fff8f2;
}
.reply-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.reply-title {
color: #d86c2d;
font-size: 24rpx;
font-weight: 700;
}
.reply-time,
.reply-admin {
color: #8b735f;
font-size: 20rpx;
}
.reply-content {
display: block;
margin-top: 14rpx;
color: #3e3128;
font-size: 24rpx;
line-height: 1.6;
word-break: break-all;
}
.reply-admin {
display: block;
margin-top: 14rpx;
}
.pending-reply {
margin-top: 32rpx;
padding: 24rpx;
border-radius: 18rpx;
color: #7d8798;
background: #f3f5f8;
font-size: 22rpx;
}
.theme-dark.feedback-detail-page {
background: #151923;
}
.theme-dark .detail-card {
background: #222936;
box-shadow: none;
}
.theme-dark .detail-header {
border-color: #364152;
}
.theme-dark .detail-content {
color: #f2f5fa;
}
.theme-dark .detail-label,
.theme-dark .detail-time,
.theme-dark .loading-state {
color: #aeb9ca;
}
.theme-dark .reply-card {
background: #34291f;
}
.theme-dark .reply-content {
color: #f2e6da;
}
.theme-dark .reply-time,
.theme-dark .reply-admin,
.theme-dark .pending-reply {
color: #c7b8a8;
}
.theme-dark .pending-reply {
background: #1c2431;
}
</style>
+647
View File
@@ -0,0 +1,647 @@
<template>
<view class="feedback-page" :class="`theme-${themeMode}`">
<view class="hero-card">
<view>
<text class="hero-tag">意见收集</text>
<text class="hero-title">问题反馈</text>
<text class="hero-subtitle">提交使用建议、问题描述或异常截图,我们会尽快跟进</text>
</view>
<view class="hero-icon">
<u-icon name="email" color="#ffffff" size="44" />
</view>
</view>
<view class="section-card">
<view class="section-header">
<text class="section-title">反馈内容</text>
</view>
<view class="field-card">
<text class="field-label">反馈类型</text>
<view class="chip-row">
<view
v-for="item in typeOptions"
:key="item.value"
class="chip"
:class="{ 'is-active': form.type === item.value }"
@tap="form.type = item.value"
>
{{ item.label }}
</view>
</view>
</view>
<view class="field-card">
<text class="field-label">问题描述</text>
<view class="textarea-wrap">
<textarea v-model="form.content" placeholder="请描述遇到的问题、操作步骤或建议" placeholder-class="textarea-placeholder" maxlength="500" />
</view>
</view>
<view class="field-card">
<text class="field-label">联系方式(选填)</text>
<u-input v-model="form.contact" placeholder="手机号 / 微信 / 邮箱" :border="false" />
</view>
<view class="field-card">
<text class="field-label">截图附件</text>
<view class="upload-row">
<view v-if="form.images.length < 3" class="upload-box" @tap="pickImage" :class="{ 'is-disabled': uploading || form.images.length >= 3 }">
<u-icon name="plus" color="#ff9d42" size="34" />
<text>{{ uploading ? '上传中' : '添加截图' }}</text>
</view>
<view v-for="(img, index) in form.images" :key="img" class="upload-preview">
<image :src="img" mode="aspectFill" />
<text class="upload-label">图片{{ index + 1 }}</text>
<view class="upload-delete" @tap.stop="removeImage(index)">
<u-icon name="close" color="#ffffff" size="20" />
</view>
</view>
</view>
</view>
</view>
<view class="tips-card">
<text class="tips-title">提交说明</text>
<text class="tips-text">1. 请优先说明页面名称和操作路径;2. 如果是扫码异常,请附上单号或二维码截图;3. 反馈会保存在本机历史记录中。</text>
</view>
<view class="bottom-actions">
<view class="secondary-button" @tap="resetForm">清空</view>
<view class="primary-button" :class="{ 'is-disabled': submitting }" @tap="submitFeedback">{{ submitting ? '提交中' : '提交反馈' }}</view>
</view>
<view class="history-card">
<view class="history-header">
<view>
<text class="section-title">我的反馈</text>
<text class="history-subtitle">仅展示当前账号已提交的问题</text>
</view>
<view class="refresh-button" :class="{ 'is-disabled': historyLoading }" @tap="loadFeedbackList">
<u-icon name="reload" color="#ff8a3d" size="28" />
<text>刷新</text>
</view>
</view>
<view v-if="historyLoading" class="history-loading">正在加载反馈记录…</view>
<view v-else-if="feedbackList.length" class="feedback-list">
<view v-for="item in feedbackList" :key="item.id" class="feedback-record" @tap="goFeedbackDetail(item.id)">
<view class="record-main">
<view class="record-title-row">
<text class="record-type">{{ getTypeLabel(item.type) }}</text>
<text class="record-time">{{ item.created_date || '时间未知' }}</text>
</view>
<text class="record-content">{{ item.content }}</text>
</view>
<u-icon name="arrow-right" color="#9aa5b5" size="28" />
</view>
</view>
<u-empty v-else mode="list" text="暂无已提交的反馈" icon="list" />
</view>
</view>
</template>
<script>
import { getMyFeedbackList, submitFeedback as createFeedback } from '@/api/feedback.js'
import { uploadImage } from '@/api/upload.js'
export default {
data() {
return {
uploading: false,
submitting: false,
historyLoading: false,
feedbackList: [],
form: {
type: 'bug',
content: '',
contact: '',
images: []
},
typeOptions: [
{ label: '功能异常', value: 'bug' },
{ label: '体验建议', value: 'suggestion' },
{ label: '内容纠错', value: 'correction' }
]
}
},
onShow() {
this.loadFeedbackList()
},
methods: {
getTypeLabel(type) {
return this.typeOptions.find((item) => item.value === type)?.label || '问题反馈'
},
async loadFeedbackList() {
if (this.historyLoading) {
return
}
this.historyLoading = true
try {
const result = await getMyFeedbackList({ page: 1, limit: 10 })
this.feedbackList = Array.isArray(result?.items) ? result.items : []
} catch (error) {
console.error('[scanapp] 获取反馈记录失败', error)
this.feedbackList = []
} finally {
this.historyLoading = false
}
},
goFeedbackDetail(id) {
if (!id) {
return
}
uni.navigateTo({
url: `/pages/settings/feedback-detail?id=${encodeURIComponent(id)}`
})
},
pickImage() {
if (this.uploading || this.submitting || this.form.images.length >= 3) {
return
}
uni.chooseImage({
count: 3 - this.form.images.length,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: ({ tempFilePaths = [] }) => {
this.form.images = [...this.form.images, ...tempFilePaths].slice(0, 3)
}
})
},
removeImage(index) {
if (this.uploading || this.submitting) return
this.form.images.splice(index, 1)
},
resetForm() {
if (this.submitting || this.uploading) {
return
}
this.form.content = ''
this.form.contact = ''
this.form.images = []
this.form.type = 'bug'
},
async submitFeedback() {
if (this.submitting || this.uploading) {
return
}
if (!this.form.content.trim()) {
uni.showToast({ title: '请输入问题描述', icon: 'none' })
return
}
this.submitting = true
try {
this.uploading = true
const uploadedImages = []
for (const filePath of this.form.images) {
uploadedImages.push(await uploadImage(filePath, 'feedback'))
}
await createFeedback({
type: this.form.type,
content: this.form.content.trim(),
contact: this.form.contact.trim(),
images: uploadedImages,
source: 'app'
})
uni.showToast({ title: '反馈已提交', icon: 'none' })
this.form.content = ''
this.form.contact = ''
this.form.images = []
this.form.type = 'bug'
await this.loadFeedbackList()
} catch (error) {
console.error('[scanapp] 提交反馈失败', error)
} finally {
this.uploading = false
this.submitting = false
}
}
}
}
</script>
<style lang="scss" scoped>
.feedback-page {
min-height: 100vh;
padding: 28rpx 24rpx 48rpx;
box-sizing: border-box;
background: linear-gradient(180deg, #eef6ff 0%, #f4f6fb 36%, #f4f6fb 100%);
}
.hero-card,
.section-card,
.tips-card {
padding: 34rpx;
border-radius: 28rpx;
border: 1rpx solid rgba(255, 255, 255, 0.75);
background: #ffffff;
box-shadow: 0 16rpx 40rpx rgba(23, 35, 61, 0.06);
}
.section-card,
.tips-card {
margin-top: 28rpx;
}
.hero-card {
display: flex;
align-items: center;
justify-content: space-between;
}
.hero-tag,
.section-subtitle,
.tips-text {
display: block;
}
.hero-tag {
color: #ff8a3d;
font-size: 20rpx;
font-weight: 700;
}
.hero-title,
.section-title {
display: block;
margin-top: 10rpx;
color: #17233d;
font-size: 28rpx;
font-weight: 800;
}
.hero-subtitle,
.section-subtitle {
margin-top: 10rpx;
color: #7d8798;
font-size: 20rpx;
line-height: 1.5;
}
.hero-icon {
display: flex;
align-items: center;
justify-content: center;
width: 88rpx;
height: 88rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, #ff9d42 0%, #ff7048 100%);
}
.section-header {
margin-bottom: 20rpx;
}
.field-card + .field-card {
margin-top: 22rpx;
}
.field-card {
padding: 24rpx;
border-radius: 22rpx;
background: #f8fafc;
}
.field-label {
display: block;
margin-bottom: 16rpx;
color: #17233d;
font-size: 24rpx;
font-weight: 700;
}
.chip-row {
display: flex;
flex-wrap: wrap;
}
.chip-row .chip {
display: flex;
flex: 0 0 auto;
margin-right: 24rpx;
margin-bottom: 16rpx;
}
.chip {
padding: 16rpx 22rpx;
border-radius: 18rpx;
background: #e9edf4;
color: #5f6b7a;
font-size: 22rpx;
font-weight: 600;
}
.chip.is-active {
background: linear-gradient(135deg, #ff9d42 0%, #ff7d47 100%);
color: #ffffff;
}
.textarea-wrap {
padding: 18rpx;
border-radius: 20rpx;
background: #ffffff;
}
textarea {
width: 100%;
min-height: 220rpx;
color: #17233d;
font-size: 24rpx;
line-height: 1.6;
}
.upload-row {
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
overflow-x: auto;
padding-bottom: 4rpx;
}
.upload-row > * {
margin-right: 14rpx;
margin-bottom: 14rpx;
}
.upload-box,
.upload-preview {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 144rpx;
height: 144rpx;
border-radius: 20rpx;
overflow: hidden;
}
.upload-box {
flex-direction: column;
border: 2rpx dashed #ffb47e;
background: #fff8f2;
color: #ff8a3d;
font-size: 20rpx;
}
.upload-box text {
margin-top: 8rpx;
}
.upload-preview image {
width: 100%;
height: 100%;
}
.upload-label {
position: absolute;
right: 8rpx;
bottom: 8rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
color: #ffffff;
background: rgba(0, 0, 0, 0.45);
font-size: 18rpx;
line-height: 1.2;
}
.upload-delete {
position: absolute;
top: 8rpx;
right: 8rpx;
display: flex;
align-items: center;
justify-content: center;
width: 34rpx;
height: 34rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.55);
}
.tips-title {
display: block;
color: #17233d;
font-size: 26rpx;
font-weight: 800;
}
.tips-text {
margin-top: 12rpx;
color: #7d8798;
font-size: 22rpx;
line-height: 1.6;
}
.bottom-actions {
display: flex;
margin-top: 28rpx;
}
.bottom-actions .primary-button {
margin-left: 14rpx;
}
.bottom-actions .secondary-button {
flex-shrink: 0;
}
.secondary-button,
.primary-button {
display: flex;
align-items: center;
justify-content: center;
height: 84rpx;
border-radius: 20rpx;
font-size: 24rpx;
font-weight: 700;
}
.secondary-button {
width: 32%;
background: #f0f3f8;
color: #56647a;
}
.primary-button {
flex: 1;
background: linear-gradient(135deg, #ff9d42 0%, #ff7048 100%);
color: #ffffff;
}
.upload-box.is-disabled,
.primary-button.is-disabled,
.refresh-button.is-disabled {
opacity: 0.6;
}
.history-card {
margin-top: 28rpx;
padding: 34rpx;
border-radius: 28rpx;
background: #ffffff;
box-shadow: 0 16rpx 40rpx rgba(23, 35, 61, 0.06);
}
.history-header,
.feedback-record,
.record-title-row,
.refresh-button {
display: flex;
align-items: center;
}
.history-header {
justify-content: space-between;
}
.history-subtitle {
display: block;
margin-top: 10rpx;
color: #7d8798;
font-size: 20rpx;
}
.refresh-button {
padding: 12rpx 16rpx;
border-radius: 16rpx;
background: #fff8f2;
color: #ff8a3d;
font-size: 20rpx;
}
.refresh-button text {
margin-left: 8rpx;
}
.history-loading {
padding: 40rpx 0;
color: #7d8798;
font-size: 22rpx;
text-align: center;
}
.feedback-list {
margin-top: 20rpx;
}
.feedback-record {
justify-content: space-between;
padding: 22rpx 0;
border-bottom: 1rpx solid #edf0f5;
}
.feedback-record:last-child {
border-bottom: 0;
}
.record-main {
min-width: 0;
flex: 1;
}
.record-title-row {
justify-content: space-between;
}
.record-type {
color: #17233d;
font-size: 24rpx;
font-weight: 700;
}
.record-time {
margin-left: 20rpx;
color: #7d8798;
font-size: 18rpx;
flex-shrink: 0;
}
.record-content {
display: block;
margin-top: 10rpx;
margin-right: 20rpx;
color: #5f6b7a;
font-size: 22rpx;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.theme-dark.feedback-page {
background: #151923;
}
.theme-dark .hero-card,
.theme-dark .section-card,
.theme-dark .tips-card,
.theme-dark .history-card {
border-color: #364152;
background: #222936;
box-shadow: none;
}
.theme-dark .field-card,
.theme-dark .textarea-wrap,
.theme-dark .upload-box {
border-color: #364152;
background: #1c2431;
}
.theme-dark .hero-title,
.theme-dark .section-title,
.theme-dark .field-label,
.theme-dark .tips-title,
.theme-dark textarea {
color: #f2f5fa;
}
.theme-dark .hero-subtitle,
.theme-dark .section-subtitle,
.theme-dark .tips-text,
.theme-dark .history-subtitle,
.theme-dark .history-loading,
.theme-dark .record-time,
.theme-dark .record-content,
.theme-dark .chip,
.theme-dark .upload-box {
color: #aeb9ca;
}
.theme-dark .record-type {
color: #f2f5fa;
}
.theme-dark .feedback-record {
border-color: #364152;
}
.theme-dark .refresh-button {
background: #34291f;
}
.theme-dark .chip {
background: #1a2230;
}
.theme-dark .chip.is-active {
background: linear-gradient(135deg, #a85d2f 0%, #8b4d35 100%);
color: #ffffff;
}
.theme-dark textarea::placeholder,
.theme-dark :deep(.u-input__input)::placeholder {
color: #8996aa;
}
.theme-dark .secondary-button {
background: #293344;
color: #d7deea;
}
.theme-dark .primary-button {
background: linear-gradient(135deg, #ff9d42 0%, #ff7048 100%);
}
</style>
+708
View File
@@ -0,0 +1,708 @@
<template>
<view class="settings-page" :class="`theme-${themeMode}`">
<view class="page-accent"></view>
<view class="settings-card">
<view class="card-header">
<text class="card-title">基础配置</text>
<text class="card-subtitle">扫码与反馈</text>
</view>
<view class="setting-row tappable" @tap="selectScanInterval">
<view class="setting-main">
<text class="setting-label">连续扫码时间间隔</text>
</view>
<view class="setting-right interval-right">
<view class="value-line">
<text class="setting-value">{{ settings.scanInterval }}s</text>
<u-icon name="arrow-right" color="#d0d6e2" size="24" />
</view>
</view>
</view>
<view class="setting-row tappable" @tap="selectScanRegionPreset">
<view class="setting-main">
<text class="setting-label">有效识别区域</text>
<text class="setting-desc">{{ scanRegionSummary }}</text>
</view>
<view class="setting-right">
<text class="setting-value">{{ scanRegionPresetLabel }}</text>
<u-icon name="arrow-right" color="#d0d6e2" size="24" />
</view>
</view>
<view v-if="settings.scanRegionPreset === 'barcode'" class="setting-row tappable" @tap="selectScanRegionPosition">
<view class="setting-main">
<text class="setting-label">横向条码框位置</text>
</view>
<view class="setting-right">
<text class="setting-value">{{ scanRegionPositionLabel }}</text>
<u-icon name="arrow-right" color="#d0d6e2" size="24" />
</view>
</view>
<view v-if="settings.scanRegionPreset === 'barcode'" class="setting-row tappable" @tap="selectScanRegionHeight">
<view class="setting-main">
<text class="setting-label">扫描框高度</text>
</view>
<view class="setting-right">
<text class="setting-value">{{ scanRegionHeightLabel }}</text>
<u-icon name="arrow-right" color="#d0d6e2" size="24" />
</view>
</view>
<view v-if="settings.scanRegionPreset === 'barcode' && settings.scanRegionPosition === 'custom'" class="setting-row tappable" @tap="adjustCustomScanRegion">
<view class="setting-main">
<text class="setting-label">自定义条码框位置</text>
</view>
<view class="setting-right">
<text class="setting-value">{{ regionPercent(settings.scanRegion.centerY) }}%</text>
<u-icon name="arrow-right" color="#d0d6e2" size="24" />
</view>
</view>
<view class="setting-row">
<view class="setting-main">
<text class="setting-label">成功提示音</text>
</view>
<u-switch v-model="settings.successSound" active-color="#2f6cf6" />
</view>
<view class="setting-row">
<view class="setting-main">
<text class="setting-label">失败提示音</text>
</view>
<u-switch v-model="settings.failSound" active-color="#ef4f4f" />
</view>
<view class="setting-row">
<view class="setting-main">
<text class="setting-label">震动反馈</text>
</view>
<u-switch v-model="settings.vibration" active-color="#35bfa3" />
</view>
<view class="setting-row">
<view class="setting-main">
<text class="setting-label">PDA 模式</text>
</view>
<u-switch v-model="settings.pdaMode" active-color="#1677ff" />
</view>
</view>
<view class="settings-card">
<view class="card-header">
<text class="card-title">识别处理</text>
<text class="card-subtitle">扫码识别与界面偏好</text>
</view>
<view class="setting-row tappable" @tap="selectTheme">
<view class="setting-main">
<text class="setting-label">主题模式</text>
</view>
<view class="setting-right">
<text class="setting-value">{{ settings.theme === 'light' ? '浅色' : '深色' }}</text>
<u-icon name="arrow-right" color="#d0d6e2" size="24" />
</view>
</view>
<view class="setting-row tappable" @tap="selectLanguage">
<view class="setting-main">
<text class="setting-label">语言</text>
</view>
<view class="setting-right">
<text class="setting-value">{{ settings.language === 'zh' ? '中文' : 'EN' }}</text>
<u-icon name="arrow-right" color="#d0d6e2" size="24" />
</view>
</view>
<view class="setting-row">
<view class="setting-main">
<text class="setting-label">自动保存草稿</text>
</view>
<u-switch v-model="settings.autoSave" active-color="#2f6cf6" />
</view>
<view class="setting-row tappable" @tap="selectUploadMode">
<view class="setting-main">
<text class="setting-label">图片上传方式</text>
</view>
<view class="setting-right">
<text class="setting-value">{{ settings.uploadMode === 'wifi' ? '仅 Wi-Fi' : '任意网络' }}</text>
<u-icon name="arrow-right" color="#d0d6e2" size="24" />
</view>
</view>
</view>
<view class="settings-card">
<view class="card-header">
<text class="card-title">软件维护</text>
<text class="card-subtitle">本地与维护</text>
</view>
<view class="setting-row tappable" @tap="selectCacheDays">
<view class="setting-main">
<text class="setting-label">历史缓存保留</text>
</view>
<view class="setting-right">
<text class="setting-value">{{ settings.cacheDays }} 天</text>
<u-icon name="arrow-right" color="#d0d6e2" size="24" />
</view>
</view>
<view class="setting-row tappable" @tap="checkForUpdate">
<view class="setting-main">
<text class="setting-label">检查更新</text>
</view>
<view class="setting-right">
<text class="setting-value">v{{ currentVersion }}</text>
<u-icon name="arrow-right" color="#c1c7d0" size="24" />
</view>
</view>
<view class="setting-row tappable" @tap="clearBusinessCache">
<view class="setting-main">
<text class="setting-label">清理缓存</text>
</view>
<u-icon name="arrow-right" color="#c1c7d0" size="24" />
</view>
<view class="setting-row tappable" @tap="resetSettings">
<view class="setting-main">
<text class="setting-label">恢复默认设置</text>
</view>
<u-icon name="arrow-right" color="#c1c7d0" size="24" />
</view>
</view>
<view class="settings-card">
<view class="card-header">
<text class="card-title">帮助与支持</text>
<text class="card-subtitle">问题反馈与应用信息</text>
</view>
<view class="setting-row tappable" @tap="goFeedback">
<view class="setting-main">
<text class="setting-label">问题反馈</text>
</view>
<u-icon name="arrow-right" color="#d0d6e2" size="24" />
</view>
<view class="setting-row tappable" @tap="goAbout">
<view class="setting-main">
<text class="setting-label">关于我们</text>
</view>
<u-icon name="arrow-right" color="#d0d6e2" size="24" />
</view>
</view>
<view class="footer-tip">设置会自动保存在本机,切换账号后设置相互独立</view>
</view>
</template>
<script>
import {
compareVersions,
getCurrentAppVersion,
getLatestRelease,
getReleaseUpdateInfo
} from '@/api/app-update.js'
import { applyAppTheme } from '@/utils/theme.js'
import { getScanappSettings, getScanappSettingsStorageKey } from '@/utils/scan-settings.js'
export default {
data() {
const defaultSettings = {
scanInterval: 1,
successSound: true,
failSound: true,
vibration: true,
pdaMode: false,
theme: 'light',
language: 'zh',
autoSave: true,
cacheDays: 30,
uploadMode: 'wifi',
scanRegionPreset: 'fullscreen',
scanRegionPosition: 'middle',
scanRegionHeight: 'medium',
scanRegion: { fullScreen: true }
}
return {
settingsStorageKey: getScanappSettingsStorageKey(),
isLoadingSettings: true,
intervalOptions: [0.3, 0.5, 1, 1.5, 2, 3, 5],
cacheDayOptions: [7, 15, 30, 60, 90],
scanRegionPresets: [
{ key: 'fullscreen', label: '全屏识别', description: '整画面均可识别,不显示扫描框' },
{ key: 'qrcode', label: '二维码正方形框', description: '适合二维码与方形标签' },
{ key: 'barcode', label: '横向条码框', description: '适合商品条码,可调整位置和高度' }
],
scanRegionPositions: [
{ key: 'top', label: '上方', centerY: 0.28 },
{ key: 'middle', label: '居中', centerY: 0.5 },
{ key: 'bottom', label: '下方', centerY: 0.72 },
{ key: 'custom', label: '自定义' }
],
scanRegionHeights: [
{ key: 'medium', label: '中(24%)', height: 0.24 },
{ key: 'large', label: '大(32%)', height: 0.32 },
{ key: 'xlarge', label: '特大(40%)', height: 0.4 }
],
defaultSettings,
settings: { ...defaultSettings, scanRegion: { ...defaultSettings.scanRegion } },
checkingUpdate: false,
currentVersion: getCurrentAppVersion()
}
},
onLoad() {
this.settingsStorageKey = getScanappSettingsStorageKey()
this.loadSettings()
this.isLoadingSettings = false
},
watch: {
settings: {
deep: true,
handler() {
if (this.isLoadingSettings) return
this.saveSettings()
}
}
},
methods: {
loadSettings() {
const savedSettings = getScanappSettings()
if (savedSettings && typeof savedSettings === 'object') {
const preset = savedSettings.scanRegionPreset || (savedSettings.scanRegion?.fullScreen ? 'fullscreen' : 'barcode')
const savedRegion = savedSettings.scanRegion || {}
let scanRegion = { ...this.defaultSettings.scanRegion }
if (preset === 'fullscreen') {
scanRegion = { fullScreen: true }
} else if (preset === 'qrcode') {
scanRegion = { width: 0.74, height: 0.74, centerX: 0.5, centerY: 0.5 }
} else {
scanRegion = {
width: Number(savedRegion.width) || 0.96,
height: Number(savedRegion.height) || 0.24,
centerX: Number.isFinite(Number(savedRegion.centerX)) ? Number(savedRegion.centerX) : 0.5,
centerY: Number.isFinite(Number(savedRegion.centerY)) ? Number(savedRegion.centerY) : 0.5
}
}
this.settings = {
...this.defaultSettings,
...savedSettings,
scanRegionPreset: preset,
scanRegion
}
}
applyAppTheme(this.settings.theme)
},
saveSettings() {
uni.setStorageSync(this.settingsStorageKey, { ...this.settings })
},
checkForUpdate() {
if (this.checkingUpdate) return
this.checkingUpdate = true
uni.showLoading({ title: '正在检查更新', mask: true })
getLatestRelease()
.then((release) => {
const currentVersion = getCurrentAppVersion()
const updateInfo = getReleaseUpdateInfo(release)
if (compareVersions(updateInfo.latestVersion, currentVersion) <= 0) {
uni.showModal({
title: '检查更新',
content: `当前版本 v${currentVersion},已是最新版本`,
showCancel: false,
confirmText: '知道了'
})
return
}
const content = `发现新版本 v${updateInfo.latestVersion}\n\n${updateInfo.updateNote}${updateInfo.apkUrl ? '\n\n是否立即下载并安装?' : '\n\n更新包暂未发布,请稍后再试。'}`
uni.showModal({
title: '发现新版本',
content,
confirmText: updateInfo.apkUrl ? '立即更新' : '知道了',
showCancel: Boolean(updateInfo.apkUrl),
cancelText: '暂不更新',
success: (modalResult) => {
if (modalResult.confirm && updateInfo.apkUrl) {
this.downloadAndInstallApk(updateInfo.apkUrl)
}
}
})
})
.catch((error) => {
uni.showModal({
title: '检查更新失败',
content: error.message || '无法获取最新版本信息,请稍后重试',
showCancel: false,
confirmText: '知道了'
})
})
.finally(() => {
this.checkingUpdate = false
uni.hideLoading()
})
},
downloadAndInstallApk(downloadUrl) {
// #ifdef APP-PLUS
if (!downloadUrl) return
uni.showLoading({ title: '正在下载更新包', mask: true })
uni.downloadFile({
url: downloadUrl,
success: (result) => {
if (result.statusCode !== 200) {
uni.showToast({ title: '更新包下载失败', icon: 'none' })
return
}
plus.runtime.install(result.tempFilePath, {}, () => {}, (error) => {
uni.showModal({ title: '安装失败', content: error.message || '请允许安装未知来源应用后重试', showCancel: false })
})
},
fail: () => uni.showToast({ title: '更新包下载失败,请检查网络', icon: 'none' }),
complete: () => uni.hideLoading()
})
// #endif
// #ifndef APP-PLUS
this.openReleasePage(downloadUrl)
// #endif
},
openReleasePage(url) {
const releaseUrl = url || 'https://gitee.com/li-hua-88/scanapp/releases/latest'
// #ifdef APP-PLUS
plus.runtime.openURL(releaseUrl)
// #endif
// #ifndef APP-PLUS
window.location.href = releaseUrl
// #endif
},
clearBusinessCache() {
uni.showModal({
title: '清理缓存',
content: '将清理扫码业务缓存,保留登录状态和当前设置,是否继续?',
confirmText: '清理',
confirmColor: '#1677ff',
success: (res) => {
if (!res.confirm) return
const protectedKeys = new Set(['scanapp_token', 'scanapp_refresh_token', 'scanapp_user'])
const storageInfo = uni.getStorageInfoSync()
;(storageInfo.keys || []).forEach((key) => {
const isSettingsKey = key === 'scanapp_settings' || key.indexOf('scanapp_settings_') === 0
if (key.indexOf('scanapp_') === 0 && !protectedKeys.has(key) && !isSettingsKey) {
uni.removeStorageSync(key)
}
})
uni.showToast({ title: '缓存已清理', icon: 'success' })
}
})
},
resetSettings() {
uni.showModal({
title: '恢复默认设置',
content: '所有设置项将恢复为默认值,是否继续?',
confirmText: '恢复',
confirmColor: '#1677ff',
success: (res) => {
if (!res.confirm) return
this.settings = { ...this.defaultSettings, scanRegion: { ...this.defaultSettings.scanRegion } }
this.saveSettings()
applyAppTheme(this.settings.theme)
uni.showToast({ title: '已恢复默认设置', icon: 'success' })
}
})
},
selectScanInterval() {
this.showSettingOptions('连续扫码时间间隔', this.intervalOptions.map((item) => `${item}S`), (index) => {
this.settings.scanInterval = this.intervalOptions[index]
})
},
selectScanRegionPreset() {
this.showSettingOptions('有效识别区域', this.scanRegionPresets.map((item) => item.label), (index) => {
const preset = this.scanRegionPresets[index]
if (!preset) return
this.settings.scanRegionPreset = preset.key
if (preset.key === 'fullscreen') {
this.settings.scanRegion = { fullScreen: true }
return
}
if (preset.key === 'qrcode') {
this.settings.scanRegion = { width: 0.74, height: 0.74, centerX: 0.5, centerY: 0.5 }
return
}
const position = this.scanRegionPositions.find((item) => item.key === this.settings.scanRegionPosition) || this.scanRegionPositions[1]
const height = this.scanRegionHeights.find((item) => item.key === this.settings.scanRegionHeight) || this.scanRegionHeights[0]
this.settings.scanRegion = { width: 0.96, height: height.height, centerX: 0.5, centerY: position.centerY || 0.5 }
})
},
selectScanRegionPosition() {
this.showSettingOptions('横向条码框位置', this.scanRegionPositions.map((item) => item.label), (index) => {
const position = this.scanRegionPositions[index]
if (!position) return
this.settings.scanRegionPosition = position.key
if (position.key !== 'custom') this.settings.scanRegion.centerY = position.centerY
})
},
selectScanRegionHeight() {
this.showSettingOptions('扫描框高度', this.scanRegionHeights.map((item) => item.label), (index) => {
const height = this.scanRegionHeights[index]
if (!height) return
this.settings.scanRegionHeight = height.key
this.settings.scanRegion.height = height.height
const minCenterY = height.height / 2
this.settings.scanRegion.centerY = Math.max(minCenterY, Math.min(1 - minCenterY, Number(this.settings.scanRegion.centerY || 0.5)))
})
},
adjustCustomScanRegion() {
this.showSettingOptions('调整自定义位置', ['上移 5%', '下移 5%'], (index) => {
const delta = index === 0 ? -0.05 : 0.05
const height = Number(this.settings.scanRegion.height || 0.24)
const minCenterY = height / 2
const maxCenterY = 1 - minCenterY
this.settings.scanRegion.centerY = Math.max(minCenterY, Math.min(maxCenterY, Number((Number(this.settings.scanRegion.centerY || 0.5) + delta).toFixed(2))))
})
},
regionPercent(value) {
return Math.round(Number(value || 0) * 100)
},
selectTheme() {
this.showSettingOptions('主题模式', ['浅色模式', '深色模式'], (index) => {
this.settings.theme = index === 0 ? 'light' : 'dark'
applyAppTheme(this.settings.theme)
})
},
selectLanguage() {
this.showSettingOptions('语言', ['简体中文', 'English'], (index) => {
this.settings.language = index === 0 ? 'zh' : 'en'
})
},
selectUploadMode() {
this.showSettingOptions('图片上传方式', ['仅 Wi-Fi', '任意网络'], (index) => {
this.settings.uploadMode = index === 0 ? 'wifi' : 'all'
})
},
selectCacheDays() {
this.showSettingOptions('历史缓存保留', this.cacheDayOptions.map((item) => `${item} 天`), (index) => {
this.settings.cacheDays = this.cacheDayOptions[index]
})
},
showSettingOptions(title, itemList, onSelect) {
uni.showActionSheet({
title,
itemList,
success: (res) => onSelect(res.tapIndex)
})
},
goFeedback() {
uni.navigateTo({
url: '/pages/settings/feedback'
})
},
goAbout() {
uni.navigateTo({
url: '/pages/settings/about'
})
},
toggleAutoSave() {
this.settings.autoSave = !this.settings.autoSave
},
showMockToast(title) {
uni.showToast({ title, icon: 'none' })
}
},
computed: {
scanRegionPresetLabel() {
return this.scanRegionPresets.find((item) => item.key === this.settings.scanRegionPreset)?.label || '横向条码框'
},
scanRegionPositionLabel() {
return this.scanRegionPositions.find((item) => item.key === this.settings.scanRegionPosition)?.label || '居中'
},
scanRegionHeightLabel() {
return this.scanRegionHeights.find((item) => item.key === this.settings.scanRegionHeight)?.label || '中(24%)'
},
scanRegionSummary() {
if (this.settings.scanRegionPreset === 'fullscreen') return '整画面识别,不显示扫描框'
const region = this.settings.scanRegion || {}
return `宽 ${this.regionPercent(region.width)}% · 高 ${this.regionPercent(region.height)}% · 中心 ${this.regionPercent(region.centerX)}%, ${this.regionPercent(region.centerY)}%`
}
}
}
</script>
<style lang="scss" scoped>
.settings-page {
min-height: 100vh;
padding: 0 22rpx 40rpx;
box-sizing: border-box;
background: linear-gradient(180deg, #eaf2ff 0%, #f6f8fd 18%, #f4f7fb 100%);
}
.page-accent {
height: 10rpx;
margin: 0 -22rpx 18rpx;
background: linear-gradient(90deg, #2f6cf6 0%, #35bfa3 100%);
}
.settings-card {
margin-top: 18rpx;
padding: 30rpx 26rpx;
border-radius: 30rpx;
background: #ffffff;
box-shadow: 0 14rpx 32rpx rgba(24, 37, 68, 0.06);
}
.card-header {
margin-bottom: 14rpx;
}
.card-title {
display: block;
color: #8a94a6;
font-size: 24rpx;
font-weight: 700;
line-height: 1.35;
}
.card-subtitle {
display: block;
margin-top: 6rpx;
color: #c1c7d0;
font-size: 18rpx;
line-height: 1.35;
}
.setting-row,
.action-list .setting-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18rpx;
padding: 20rpx 0;
border-bottom: 1rpx solid #eef1f6;
}
.setting-row:last-child,
.action-list .setting-row:last-child {
padding-bottom: 0;
border-bottom: 0;
}
.setting-main {
min-width: 0;
flex: 1;
}
.setting-label {
display: block;
color: #17233d;
font-size: 26rpx;
font-weight: 700;
line-height: 1.35;
}
.setting-desc {
display: block;
margin-top: 10rpx;
color: #8f98a8;
font-size: 18rpx;
line-height: 1.45;
}
.setting-right {
display: flex;
align-items: center;
gap: 10rpx;
flex: none;
}
.setting-row :deep(.u-switch) {
transform: scale(0.82);
transform-origin: right center;
}
.value-line {
display: flex;
align-items: center;
gap: 8rpx;
}
.setting-value {
color: #5f6b7a;
font-size: 22rpx;
font-weight: 700;
line-height: 1.35;
}
.tappable {
cursor: pointer;
}
.footer-tip {
margin-top: 18rpx;
padding: 0 8rpx;
color: #9aa4b4;
font-size: 18rpx;
line-height: 1.45;
text-align: center;
}
.theme-dark {
background: #151923;
}
.theme-dark .settings-card {
background: #222936;
box-shadow: none;
}
.theme-dark .card-title,
.theme-dark .setting-desc,
.theme-dark .footer-tip {
color: #aeb9ca;
}
.theme-dark .card-subtitle {
color: #718096;
}
.theme-dark .setting-label {
color: #f2f5fa;
}
.theme-dark .setting-value {
color: #c6d0df;
}
.theme-dark .setting-row,
.theme-dark .action-list .setting-row {
border-bottom-color: #343e4d;
}
.theme-dark.settings-page {
background: #151923;
}
.theme-dark .settings-card {
border-color: #364152;
background: #222936;
box-shadow: none;
}
.theme-dark .card-title,
.theme-dark .setting-label {
color: #f2f5fa;
}
.theme-dark .card-subtitle,
.theme-dark .setting-desc,
.theme-dark .footer-tip {
color: #aeb9ca;
}
.theme-dark .setting-value {
color: #d7deea;
}
.theme-dark .setting-row,
.theme-dark .action-list .setting-row {
border-color: #364152;
}
</style>
+80
View File
@@ -0,0 +1,80 @@
<template>
<view class="policy-page" :class="`theme-${themeMode}`">
<scroll-view scroll-y class="policy-scroll">
<view class="policy-paper">
<text class="policy-title">{{ title }}</text>
<text class="policy-content">{{ content }}</text>
</view>
</scroll-view>
</view>
</template>
<script>
const POLICY_STORAGE_KEY = 'scanapp_policy_detail'
export default {
data() {
return {
title: '',
content: ''
}
},
onLoad() {
const policy = uni.getStorageSync(POLICY_STORAGE_KEY) || {}
this.title = policy.title || '协议详情'
this.content = policy.content || ''
uni.setNavigationBarTitle({ title: this.title })
uni.setNavigationBarColor({
frontColor: this.themeMode === 'dark' ? '#ffffff' : '#000000',
backgroundColor: this.themeMode === 'dark' ? '#151923' : '#ffffff'
})
}
}
</script>
<style lang="scss" scoped>
.policy-page {
min-height: 100vh;
background: #ffffff;
}
.policy-scroll {
height: 100vh;
box-sizing: border-box;
}
.policy-paper {
padding: 44rpx 36rpx 80rpx;
box-sizing: border-box;
}
.policy-title {
display: block;
margin-bottom: 34rpx;
color: #17233d;
font-size: 38rpx;
font-weight: 800;
line-height: 1.4;
}
.policy-content {
display: block;
color: #3f4b5d;
font-size: 28rpx;
line-height: 1.9;
white-space: pre-wrap;
word-break: break-word;
}
.theme-dark.policy-page {
background: #151923;
}
.theme-dark .policy-title {
color: #f2f5fa;
}
.theme-dark .policy-content {
color: #c4cede;
}
</style>