Files
MrleeAdmin/frontend/app/pages/settings/index.vue
T

709 lines
21 KiB
Vue
Raw Normal View History

2026-08-31 11:33:59 +08:00
<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>