初始化本地仓库
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<view class="auth-page" :class="`theme-${themeMode}`">
|
||||
<view class="bg-circle bg-circle-primary"></view>
|
||||
<view class="bg-circle bg-circle-light"></view>
|
||||
|
||||
<view class="brand-area">
|
||||
<view class="brand-mark">
|
||||
<u-icon name="scan" color="#ffffff" size="52" />
|
||||
</view>
|
||||
<text class="brand-title">扫码生产助手</text>
|
||||
<text class="brand-subtitle">登录后开始扫码录入生产数据</text>
|
||||
</view>
|
||||
|
||||
<view class="auth-panel">
|
||||
<view class="panel-header">
|
||||
<text class="panel-title">账号登录</text>
|
||||
<text class="panel-desc">请输入账号和密码</text>
|
||||
</view>
|
||||
|
||||
<u-form :model="form" :border-bottom="false" label-position="top" label-width="120">
|
||||
<u-form-item prop="account" :border-bottom="false">
|
||||
<view class="field-card">
|
||||
<view class="field-icon">
|
||||
<u-icon name="account-fill" color="#1677ff" size="36" />
|
||||
</view>
|
||||
<u-input v-model="form.account" clearable placeholder="请输入账号/手机号" placeholder-style="color: #a0a7b5;" />
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item prop="password" :border-bottom="false">
|
||||
<view class="field-card">
|
||||
<view class="field-icon">
|
||||
<u-icon name="lock-fill" color="#1677ff" size="36" />
|
||||
</view>
|
||||
<u-input v-model="form.password" type="password" clearable password-icon placeholder="请输入密码" placeholder-style="color: #a0a7b5;" />
|
||||
</view>
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
|
||||
<view class="actions">
|
||||
<u-button type="primary" shape="circle" size="large" :loading="loading" @click="handleLogin">登录</u-button>
|
||||
</view>
|
||||
|
||||
<view class="footer-link">
|
||||
<text>还没有账号?</text>
|
||||
<text class="link" @tap="goRegister">立即注册</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { login } from '@/api/auth.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
redirectUrl: '/pages/index/index',
|
||||
form: {
|
||||
account: '',
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad(options = {}) {
|
||||
if (options.redirect) {
|
||||
this.redirectUrl = decodeURIComponent(options.redirect)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleLogin() {
|
||||
if (!this.form.account || !this.form.password) {
|
||||
uni.showToast({
|
||||
title: '请输入账号和密码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const result = await login({
|
||||
username: this.form.account,
|
||||
password: this.form.password
|
||||
})
|
||||
const accessToken = result?.access_token || result?.token?.access_token
|
||||
if (!accessToken) {
|
||||
throw new Error('登录响应缺少访问令牌')
|
||||
}
|
||||
|
||||
uni.setStorageSync('scanapp_token', accessToken)
|
||||
if (result?.refresh_token || result?.token?.refresh_token) {
|
||||
uni.setStorageSync('scanapp_refresh_token', result.refresh_token || result.token.refresh_token)
|
||||
}
|
||||
if (result?.user_info) {
|
||||
uni.setStorageSync('scanapp_user', result.user_info)
|
||||
}
|
||||
|
||||
uni.showToast({ title: '登录成功', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({ url: this.redirectUrl })
|
||||
}, 300)
|
||||
} catch (error) {
|
||||
console.error('[scanapp] 登录失败', error)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
goRegister() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/auth/register'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.auth-page {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
padding: 92rpx 40rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #eaf4ff 0%, #f6f8fc 50%, #ffffff 100%);
|
||||
}
|
||||
|
||||
.bg-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bg-circle-primary {
|
||||
top: -120rpx;
|
||||
right: -90rpx;
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
background: rgba(22, 119, 255, 0.12);
|
||||
}
|
||||
|
||||
.bg-circle-light {
|
||||
top: 160rpx;
|
||||
left: -140rpx;
|
||||
width: 260rpx;
|
||||
height: 260rpx;
|
||||
background: rgba(53, 191, 163, 0.12);
|
||||
}
|
||||
|
||||
.brand-area {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 64rpx;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border-radius: 36rpx;
|
||||
background: linear-gradient(135deg, #1677ff 0%, #35bfa3 100%);
|
||||
box-shadow: 0 20rpx 46rpx rgba(22, 119, 255, 0.26);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
color: #17233d;
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
margin-top: 12rpx;
|
||||
color: #6b778c;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.auth-panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 44rpx 32rpx 38rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.8);
|
||||
border-radius: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 22rpx 60rpx rgba(23, 35, 61, 0.1);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: block;
|
||||
color: #17233d;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.panel-desc {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 36rpx;
|
||||
}
|
||||
|
||||
.field-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
border: 1rpx solid #edf1f7;
|
||||
border-radius: 22rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.field-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
margin-right: 14rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(22, 119, 255, 0.1);
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 32rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.link {
|
||||
margin-left: 8rpx;
|
||||
color: #1677ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.theme-dark.auth-page {
|
||||
background: linear-gradient(180deg, #111827 0%, #151923 50%, #151923 100%);
|
||||
}
|
||||
|
||||
.theme-dark .auth-panel {
|
||||
border-color: #364152;
|
||||
background: #222936;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-dark .field-card {
|
||||
border-color: #364152;
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .panel-title {
|
||||
color: #f2f5fa;
|
||||
}
|
||||
|
||||
.theme-dark .panel-desc,
|
||||
.theme-dark .footer-link {
|
||||
color: #aeb9ca;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.u-input__input) {
|
||||
color: #f2f5fa !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<view class="auth-page" :class="`theme-${themeMode}`">
|
||||
<view class="bg-circle bg-circle-primary"></view>
|
||||
<view class="bg-circle bg-circle-light"></view>
|
||||
|
||||
<view class="brand-area">
|
||||
<view class="brand-mark">
|
||||
<u-icon name="edit-pen-fill" color="#ffffff" size="52" />
|
||||
</view>
|
||||
<text class="brand-title">创建新账号</text>
|
||||
<text class="brand-subtitle">注册后可进行扫码、录入和查看统计</text>
|
||||
</view>
|
||||
|
||||
<view class="auth-panel">
|
||||
<view class="panel-header">
|
||||
<text class="panel-title">账号注册</text>
|
||||
<text class="panel-desc">请填写以下信息完成注册</text>
|
||||
</view>
|
||||
|
||||
<u-form :model="form" :border-bottom="false" label-position="top" label-width="120">
|
||||
<u-form-item prop="username" :border-bottom="false">
|
||||
<view class="field-card">
|
||||
<view class="field-icon">
|
||||
<u-icon name="account-fill" color="#19be6b" size="36" />
|
||||
</view>
|
||||
<u-input v-model="form.username" clearable placeholder="请输入用户名" placeholder-style="color: #a0a7b5;" />
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item prop="email" :border-bottom="false">
|
||||
<view class="field-card">
|
||||
<view class="field-icon">
|
||||
<u-icon name="email-fill" color="#19be6b" size="36" />
|
||||
</view>
|
||||
<u-input v-model="form.email" type="text" clearable placeholder="请输入邮箱" placeholder-style="color: #a0a7b5;" />
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item prop="password" :border-bottom="false">
|
||||
<view class="field-card">
|
||||
<view class="field-icon">
|
||||
<u-icon name="lock-fill" color="#19be6b" size="36" />
|
||||
</view>
|
||||
<u-input v-model="form.password" type="password" clearable password-icon placeholder="请输入密码" placeholder-style="color: #a0a7b5;" />
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item prop="confirm_password" :border-bottom="false">
|
||||
<view class="field-card">
|
||||
<view class="field-icon">
|
||||
<u-icon name="lock-fill" color="#19be6b" size="36" />
|
||||
</view>
|
||||
<u-input v-model="form.confirm_password" type="password" clearable password-icon placeholder="请再次输入密码" placeholder-style="color: #a0a7b5;" />
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item prop="captcha_code" :border-bottom="false">
|
||||
<view class="code-field">
|
||||
<view class="field-card code-input-card">
|
||||
<view class="field-icon">
|
||||
<u-icon name="coupon-fill" color="#19be6b" size="36" />
|
||||
</view>
|
||||
<u-input v-model="form.captcha_code" clearable placeholder="请输入图形验证码" placeholder-style="color: #a0a7b5;" />
|
||||
</view>
|
||||
<view class="captcha-box" @tap="refreshCaptcha">{{ captchaText }}</view>
|
||||
</view>
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
|
||||
<view class="actions">
|
||||
<u-button type="primary" shape="circle" size="large" :loading="loading" @click="handleRegister">注册</u-button>
|
||||
</view>
|
||||
|
||||
<view class="footer-link">
|
||||
<text>已有账号?</text>
|
||||
<text class="link" @tap="goLogin">返回登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
captchaText: '8K4P',
|
||||
form: {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
captcha_key: '',
|
||||
captcha_code: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleRegister() {
|
||||
if (!this.form.username || !this.form.email || !this.form.password || !this.form.confirm_password || !this.form.captcha_code) {
|
||||
uni.showToast({
|
||||
title: '请完整填写注册信息',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (this.form.password !== this.form.confirm_password) {
|
||||
uni.showToast({
|
||||
title: '两次密码不一致',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
setTimeout(() => {
|
||||
this.loading = false
|
||||
uni.showToast({
|
||||
title: '注册接口待接入',
|
||||
icon: 'none'
|
||||
})
|
||||
}, 500)
|
||||
},
|
||||
refreshCaptcha() {
|
||||
this.captchaText = Math.random().toString(36).slice(2, 6).toUpperCase()
|
||||
},
|
||||
goLogin() {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.auth-page {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
padding: 56rpx 40rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #ebfff7 0%, #f6f8fc 48%, #ffffff 100%);
|
||||
}
|
||||
|
||||
.bg-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bg-circle-primary {
|
||||
top: -130rpx;
|
||||
right: -100rpx;
|
||||
width: 310rpx;
|
||||
height: 310rpx;
|
||||
background: rgba(25, 190, 107, 0.14);
|
||||
}
|
||||
|
||||
.bg-circle-light {
|
||||
top: 190rpx;
|
||||
left: -150rpx;
|
||||
width: 280rpx;
|
||||
height: 280rpx;
|
||||
background: rgba(22, 119, 255, 0.1);
|
||||
}
|
||||
|
||||
.brand-area {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 42rpx;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
margin-bottom: 24rpx;
|
||||
border-radius: 34rpx;
|
||||
background: linear-gradient(135deg, #19be6b 0%, #1677ff 100%);
|
||||
box-shadow: 0 20rpx 46rpx rgba(25, 190, 107, 0.24);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
color: #17233d;
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
margin-top: 12rpx;
|
||||
color: #6b778c;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.auth-panel {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 40rpx 32rpx 36rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.8);
|
||||
border-radius: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 22rpx 60rpx rgba(23, 35, 61, 0.1);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: block;
|
||||
color: #17233d;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.panel-desc {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 36rpx;
|
||||
}
|
||||
|
||||
.field-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
padding: 0 22rpx;
|
||||
box-sizing: border-box;
|
||||
border: 1rpx solid #edf1f7;
|
||||
border-radius: 22rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.field-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
margin-right: 12rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(25, 190, 107, 0.1);
|
||||
}
|
||||
|
||||
.code-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.code-input-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.captcha-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 170rpx;
|
||||
margin-left: 20rpx;
|
||||
height: 92rpx;
|
||||
border: 1rpx solid rgba(25, 190, 107, 0.16);
|
||||
border-radius: 22rpx;
|
||||
background: linear-gradient(135deg, #effff7 0%, #eef6ff 100%);
|
||||
color: #19be6b;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 32rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.link {
|
||||
margin-left: 8rpx;
|
||||
color: #19be6b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.theme-dark.auth-page {
|
||||
background: linear-gradient(180deg, #111827 0%, #151923 50%, #151923 100%);
|
||||
}
|
||||
|
||||
.theme-dark .auth-panel {
|
||||
border-color: #364152;
|
||||
background: #222936;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-dark .field-card {
|
||||
border-color: #364152;
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .captcha-box {
|
||||
border-color: #36534c;
|
||||
background: #1c2d2b;
|
||||
}
|
||||
|
||||
.theme-dark .panel-title {
|
||||
color: #f2f5fa;
|
||||
}
|
||||
|
||||
.theme-dark .panel-desc,
|
||||
.theme-dark .footer-link {
|
||||
color: #aeb9ca;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.u-input__input) {
|
||||
color: #f2f5fa !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,856 @@
|
||||
<template>
|
||||
<view class="entry-page machine-page" :class="`theme-${themeMode}`">
|
||||
<view class="hero-card">
|
||||
<view>
|
||||
<text class="hero-title">整机模式</text>
|
||||
<text class="hero-subtitle">扫描加工单和商品编码,自动使用当前登录用户编码完成录入</text>
|
||||
</view>
|
||||
<view class="hero-icon">
|
||||
<u-icon name="scan" color="#ffffff" size="46" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="config-card">
|
||||
<view class="card-title">当前配置</view>
|
||||
<view class="chip-row">
|
||||
<view class="select-chip" @tap="showPicker('process')">
|
||||
<text class="chip-label">工序</text>
|
||||
<text class="chip-value">{{ processLabel }}</text>
|
||||
</view>
|
||||
<view class="select-chip" @tap="showPicker('step')">
|
||||
<text class="chip-label">步骤</text>
|
||||
<text class="chip-value">{{ stepLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="card-title">整机录入</view>
|
||||
<u-form :model="form" :border-bottom="false" label-position="top">
|
||||
<u-form-item :border-bottom="false" prop="processOrderNo">
|
||||
<view class="field-card primary-field">
|
||||
<view class="field-main">
|
||||
<text class="field-label">加工单号</text>
|
||||
<u-input
|
||||
v-model="form.processOrderNo"
|
||||
:input-mode="pdaMode ? 'none' : 'text'"
|
||||
clearable
|
||||
placeholder="请扫描或输入加工单号"
|
||||
placeholder-style="color: #a0a7b5;"
|
||||
@focus="handleMachineInputFocus"
|
||||
/>
|
||||
</view>
|
||||
<view class="scan-button" @tap="scanToField('processOrderNo')">
|
||||
<u-icon name="scan" color="#ffffff" size="36" />
|
||||
</view>
|
||||
</view>
|
||||
</u-form-item>
|
||||
|
||||
<u-form-item :border-bottom="false" prop="productCode">
|
||||
<view class="field-card">
|
||||
<view class="field-main">
|
||||
<text class="field-label">商品编码</text>
|
||||
<u-input
|
||||
v-model="form.productCode"
|
||||
:input-mode="pdaMode ? 'none' : 'text'"
|
||||
clearable
|
||||
placeholder="请扫描或输入商品编码"
|
||||
placeholder-style="color: #a0a7b5;"
|
||||
@focus="handleMachineInputFocus"
|
||||
/>
|
||||
</view>
|
||||
<view class="scan-button" @tap="scanToField('productCode')">
|
||||
<u-icon name="scan" color="#ffffff" size="36" />
|
||||
</view>
|
||||
</view>
|
||||
</u-form-item>
|
||||
|
||||
<u-form-item :border-bottom="false" prop="quantity">
|
||||
<view class="quantity-card">
|
||||
<view>
|
||||
<text class="field-label">数量</text>
|
||||
<text class="quantity-tip">默认 1,可手动调整</text>
|
||||
</view>
|
||||
<view class="quantity-control">
|
||||
<view class="qty-button" @tap="decreaseQuantity">-</view>
|
||||
<input v-model="form.quantity" class="qty-input" type="number" />
|
||||
<view class="qty-button" @tap="increaseQuantity">+</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
|
||||
<view class="action-row">
|
||||
<view class="submit-secondary" @tap="resetForm">重置</view>
|
||||
<view class="submit-primary" :class="{ 'is-loading': submitting }" @tap="submitEntry">{{ submitting ? '提交中...' : '提交录入' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-card">
|
||||
<view class="stats-header">
|
||||
<view>
|
||||
<text class="card-title">今日统计</text>
|
||||
<text class="card-subtitle">当前工序/步骤整机扫码量</text>
|
||||
</view>
|
||||
<view class="total-badge">{{ hideTotal ? '***' : statistics.todayTotal }}</view>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value success">{{ statistics.successTotal }}</text>
|
||||
<text class="stat-label">成功</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value danger">{{ statistics.failedTotal }}</text>
|
||||
<text class="stat-label">失败</text>
|
||||
</view>
|
||||
<view class="stat-item" @tap="hideTotal = !hideTotal">
|
||||
<text class="stat-value">{{ hideTotal ? '***' : statistics.todayTotal }}</text>
|
||||
<text class="stat-label">总数</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="record-card">
|
||||
<view class="card-title">最近记录</view>
|
||||
<view v-for="item in recentRecords" :key="item.id" class="record-item">
|
||||
<view>
|
||||
<text class="record-main">{{ item.processOrderNo }}</text>
|
||||
<text class="record-sub">商品:{{ item.productCode }} / 数量:{{ item.quantity }}</text>
|
||||
<text class="record-sub">用户编码:{{ item.userCode }}</text>
|
||||
</view>
|
||||
<view class="record-status">成功</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// UTS 原生扫码插件仅在 App-Plus 真机/自定义基座中解析,Web 环境不加载。
|
||||
// #ifdef APP-PLUS
|
||||
import { openCodeScan } from '@/uni_modules/mas-code-scan'
|
||||
// #endif
|
||||
|
||||
import { checkMemberUser, getDataEntryConfig, getProcessOptions, getStepOptions, getProductRecentRecords, getProductStatistics, submitProductRecord } from '@/api/produce/data-entry.js'
|
||||
import { getScanRegionSetting } from '@/utils/scan-settings.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
submitting: false,
|
||||
scanning: false,
|
||||
pdaMode: false,
|
||||
hideTotal: false,
|
||||
currentUser: {
|
||||
userCode: 'U001'
|
||||
},
|
||||
basicSettings: {},
|
||||
specificRules: [],
|
||||
processOptions: [],
|
||||
stepOptions: [],
|
||||
selectedProcessIndex: 0,
|
||||
selectedStepIndex: 0,
|
||||
form: {
|
||||
processOrderNo: '',
|
||||
productCode: '',
|
||||
quantity: 1
|
||||
},
|
||||
statistics: {
|
||||
todayTotal: 0,
|
||||
successTotal: 0,
|
||||
failedTotal: 0
|
||||
},
|
||||
recentRecords: []
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadPdaMode()
|
||||
this.loadPageData()
|
||||
},
|
||||
onShow() {
|
||||
this.loadPdaMode()
|
||||
},
|
||||
|
||||
computed: {
|
||||
processLabel() {
|
||||
const item = this.processOptions[this.selectedProcessIndex]
|
||||
return item ? item.label : '请选择'
|
||||
},
|
||||
stepLabel() {
|
||||
const item = this.stepOptions[this.selectedStepIndex]
|
||||
return item ? item.label : '请选择'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCurrentUserCode() {
|
||||
const user = uni.getStorageSync('scanapp_user') || {}
|
||||
return user.username || user.nickname || 'U001'
|
||||
},
|
||||
loadPdaMode() {
|
||||
const userCode = this.getCurrentUserCode()
|
||||
const settings = uni.getStorageSync(`scanapp_settings_${userCode}`)
|
||||
this.pdaMode = settings?.pdaMode === true
|
||||
},
|
||||
handleMachineInputFocus() {
|
||||
// #ifdef APP-PLUS
|
||||
try {
|
||||
const activity = plus.android.runtimeMainActivity()
|
||||
const focusedView = activity.getWindow().getDecorView().findFocus()
|
||||
if (focusedView) {
|
||||
plus.android.invoke(focusedView, 'setShowSoftInputOnFocus', !this.pdaMode)
|
||||
}
|
||||
if (this.pdaMode) {
|
||||
uni.hideKeyboard()
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.pdaMode) uni.hideKeyboard()
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
getSelectionStorageKey() {
|
||||
return `scanapp_entry_machine_selection_${this.currentUser.userCode}`
|
||||
},
|
||||
getSavedSelection() {
|
||||
const selection = uni.getStorageSync(this.getSelectionStorageKey())
|
||||
return selection && typeof selection === 'object' ? selection : {}
|
||||
},
|
||||
findOptionIndex(options, value) {
|
||||
const index = options.findIndex((item) => String(item.value) === String(value))
|
||||
return index >= 0 ? index : 0
|
||||
},
|
||||
saveSelection() {
|
||||
const processItem = this.processOptions[this.selectedProcessIndex]
|
||||
const stepItem = this.stepOptions[this.selectedStepIndex]
|
||||
uni.setStorageSync(this.getSelectionStorageKey(), {
|
||||
processId: processItem?.value || '',
|
||||
stepId: stepItem?.value || ''
|
||||
})
|
||||
},
|
||||
async loadPageData() {
|
||||
this.currentUser.userCode = this.getCurrentUserCode()
|
||||
const savedSelection = this.getSavedSelection()
|
||||
try {
|
||||
const [processOptions, statistics, recent] = await Promise.all([
|
||||
getProcessOptions(),
|
||||
getProductStatistics({ user_code: this.currentUser.userCode }),
|
||||
getProductRecentRecords({ user_code: this.currentUser.userCode, limit: 5 })
|
||||
])
|
||||
this.processOptions = Array.isArray(processOptions) ? processOptions : []
|
||||
this.selectedProcessIndex = this.findOptionIndex(this.processOptions, savedSelection.processId)
|
||||
this.statistics = {
|
||||
todayTotal: Number(statistics?.today_total || 0),
|
||||
successTotal: Number(statistics?.success_total || 0),
|
||||
failedTotal: Number(statistics?.failed_total || 0)
|
||||
}
|
||||
this.recentRecords = Array.isArray(recent?.items) ? recent.items.map((item, index) => ({
|
||||
id: item.id || index,
|
||||
processOrderNo: item.process_order_no || '-',
|
||||
productCode: item.product_code || '-',
|
||||
quantity: Number(item.quantity || 1),
|
||||
userCode: item.user_code || this.currentUser.userCode
|
||||
})) : []
|
||||
if (this.processOptions.length > 0) {
|
||||
await this.loadStepOptions(this.processOptions[this.selectedProcessIndex].value, savedSelection.stepId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[scanapp] 整机页数据加载失败', error)
|
||||
}
|
||||
},
|
||||
async loadStepOptions(processId, stepId = '') {
|
||||
try {
|
||||
const options = await getStepOptions(processId)
|
||||
this.stepOptions = Array.isArray(options) ? options : []
|
||||
this.selectedStepIndex = this.findOptionIndex(this.stepOptions, stepId)
|
||||
await this.loadDataEntryConfig()
|
||||
this.saveSelection()
|
||||
} catch (error) {
|
||||
this.stepOptions = []
|
||||
this.selectedStepIndex = 0
|
||||
this.basicSettings = {}
|
||||
this.specificRules = []
|
||||
console.error('[scanapp] 整机步骤加载失败', error)
|
||||
}
|
||||
},
|
||||
async loadDataEntryConfig() {
|
||||
const processItem = this.processOptions[this.selectedProcessIndex]
|
||||
const stepItem = this.stepOptions[this.selectedStepIndex]
|
||||
if (!processItem || !stepItem) {
|
||||
this.basicSettings = {}
|
||||
this.specificRules = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const config = await getDataEntryConfig({
|
||||
process_id: processItem.value,
|
||||
step_id: stepItem.value,
|
||||
apply_mode: 'machine',
|
||||
process_order_no: this.form.processOrderNo.trim(),
|
||||
product_code: this.form.productCode.trim(),
|
||||
user_code: this.currentUser.userCode.trim(),
|
||||
order_no: this.form.processOrderNo.trim()
|
||||
})
|
||||
this.basicSettings = config?.basic_setting || {}
|
||||
this.specificRules = Array.isArray(config?.specific_rules) ? config.specific_rules : []
|
||||
} catch (error) {
|
||||
this.basicSettings = {}
|
||||
this.specificRules = []
|
||||
console.error('[scanapp] 整机规则加载失败', error)
|
||||
}
|
||||
},
|
||||
matchSpecificRule(rule, valueMap) {
|
||||
const matchField = rule.match_field || 'process_order_no'
|
||||
const value = String(valueMap[matchField] || '').trim()
|
||||
const pattern = String(rule.rule_pattern || '').trim()
|
||||
if (!value || !pattern) return false
|
||||
try {
|
||||
return new RegExp(pattern).test(value)
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
validateSpecificRules() {
|
||||
const context = {
|
||||
order_no: this.form.processOrderNo.trim(),
|
||||
process_order_no: this.form.processOrderNo.trim(),
|
||||
product_code: this.form.productCode.trim(),
|
||||
user_code: this.currentUser.userCode.trim()
|
||||
}
|
||||
const blockedRule = this.specificRules.find((rule) => {
|
||||
if (rule.apply_mode && rule.apply_mode !== 'machine' && rule.apply_mode !== 'both') return false
|
||||
return Number(rule.rule_type) === 2 && this.matchSpecificRule(rule, context)
|
||||
})
|
||||
if (blockedRule) {
|
||||
uni.showToast({ title: blockedRule.rule_name || '当前整机数据命中特定规则,禁止提交', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
validatePattern(value, settingKey, label) {
|
||||
const pattern = String(this.basicSettings[settingKey] || '').trim()
|
||||
if (!pattern) return true
|
||||
try {
|
||||
if (!new RegExp(pattern).test(value)) {
|
||||
uni.showToast({ title: `${label}格式不符合规则`, icon: 'none' })
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[scanapp] ${label}规则格式错误`, error)
|
||||
}
|
||||
return true
|
||||
},
|
||||
showPicker(type) {
|
||||
const list = type === 'process' ? this.processOptions : this.stepOptions
|
||||
if (!list.length) {
|
||||
uni.showToast({ title: '当前没有可选项', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showActionSheet({
|
||||
itemList: list.map((item) => item.label),
|
||||
success: async (res) => {
|
||||
if (type === 'process') {
|
||||
this.selectedProcessIndex = res.tapIndex
|
||||
await this.loadStepOptions(this.processOptions[res.tapIndex]?.value)
|
||||
} else {
|
||||
this.selectedStepIndex = res.tapIndex
|
||||
await this.loadDataEntryConfig()
|
||||
this.saveSelection()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
scanToField(field) {
|
||||
if (this.scanning || this.submitting) return
|
||||
// #ifdef APP-PLUS
|
||||
this.scanning = true
|
||||
openCodeScan({
|
||||
smartZoom: true,
|
||||
vibrate: true,
|
||||
scanRegion: getScanRegionSetting(),
|
||||
success: async (result) => {
|
||||
const value = String(result?.content || '').trim()
|
||||
if (!value) return
|
||||
this.form[field] = value
|
||||
await this.loadDataEntryConfig()
|
||||
},
|
||||
fail: (error) => {
|
||||
if (Number(error?.errCode) === 9021003) return
|
||||
uni.showToast({
|
||||
title: Number(error?.errCode) === 9021002 ? '未获得扫码权限' : '未识别到有效条码',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
complete: () => {
|
||||
this.scanning = false
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
uni.showToast({ title: '请在 Android App 中使用扫码功能', icon: 'none' })
|
||||
// #endif
|
||||
},
|
||||
increaseQuantity() {
|
||||
this.form.quantity = Number(this.form.quantity || 1) + 1
|
||||
},
|
||||
decreaseQuantity() {
|
||||
this.form.quantity = Math.max(1, Number(this.form.quantity || 1) - 1)
|
||||
},
|
||||
validateForm() {
|
||||
if (!this.form.processOrderNo.trim()) {
|
||||
uni.showToast({ title: '请输入加工单号', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!this.form.productCode.trim()) {
|
||||
uni.showToast({ title: '请输入商品编码', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!Number(this.form.quantity) || Number(this.form.quantity) < 1) {
|
||||
uni.showToast({ title: '数量必须大于 0', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!this.processOptions[this.selectedProcessIndex]) {
|
||||
uni.showToast({ title: '请选择工序', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!this.stepOptions[this.selectedStepIndex]) {
|
||||
uni.showToast({ title: '请选择步骤', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!this.validatePattern(this.form.processOrderNo.trim(), 'process_order_no_regex', '加工单号')) return false
|
||||
const minLength = Number(this.basicSettings.product_code_min_length || 0)
|
||||
if (minLength > 0 && this.form.productCode.trim().length < minLength) {
|
||||
uni.showToast({ title: `商品编码长度不能少于 ${minLength} 位`, icon: 'none' })
|
||||
return false
|
||||
}
|
||||
const maxLength = Number(this.basicSettings.product_code_max_length || 0)
|
||||
if (maxLength > 0 && this.form.productCode.trim().length > maxLength) {
|
||||
uni.showToast({ title: `商品编码长度不能超过 ${maxLength} 位`, icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!this.validatePattern(this.form.productCode.trim(), 'product_code_regex', '商品编码')) return false
|
||||
if (!this.validateSpecificRules()) return false
|
||||
return true
|
||||
},
|
||||
async submitEntry() {
|
||||
await this.loadDataEntryConfig()
|
||||
if (!this.validateForm()) return
|
||||
this.submitting = true
|
||||
try {
|
||||
const processItem = this.processOptions[this.selectedProcessIndex]
|
||||
const stepItem = this.stepOptions[this.selectedStepIndex]
|
||||
await submitProductRecord({
|
||||
process_order_no: this.form.processOrderNo.trim(),
|
||||
product_code: this.form.productCode.trim(),
|
||||
quantity: Number(this.form.quantity) || 1,
|
||||
user_code: this.currentUser.userCode,
|
||||
process_id: processItem?.value,
|
||||
step_id: stepItem?.value,
|
||||
scan_result: 1
|
||||
})
|
||||
uni.showToast({ title: '整机录入成功', icon: 'success' })
|
||||
this.resetForm()
|
||||
await this.loadPageData()
|
||||
} catch (error) {
|
||||
console.error('[scanapp] 整机录入失败', error)
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
this.form.processOrderNo = ''
|
||||
this.form.productCode = ''
|
||||
this.form.quantity = 1
|
||||
this.loadDataEntryConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.entry-page {
|
||||
min-height: 100vh;
|
||||
padding: 28rpx 24rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #ebfff7 0%, #f4f6fb 38%, #f4f6fb 100%);
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.config-card,
|
||||
.form-card,
|
||||
.stats-card,
|
||||
.record-card {
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 28rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 16rpx 42rpx rgba(23, 35, 61, 0.06);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 34rpx 30rpx;
|
||||
background: linear-gradient(135deg, #35bfa3 0%, #1677ff 100%);
|
||||
}
|
||||
|
||||
.hero-title,
|
||||
.hero-subtitle,
|
||||
.card-title,
|
||||
.card-subtitle,
|
||||
.field-label,
|
||||
.quantity-tip,
|
||||
.chip-label,
|
||||
.chip-value,
|
||||
.stat-value,
|
||||
.stat-label,
|
||||
.record-main,
|
||||
.record-sub {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
color: #ffffff;
|
||||
font-size: 38rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
margin-top: 10rpx;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 22rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 86rpx;
|
||||
height: 86rpx;
|
||||
border-radius: 28rpx;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.config-card,
|
||||
.form-card,
|
||||
.stats-card,
|
||||
.record-card {
|
||||
margin-top: 24rpx;
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #17233d;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
margin-top: 6rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chip-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 18rpx;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.select-chip {
|
||||
padding: 20rpx;
|
||||
border-radius: 22rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.chip-label {
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.chip-value {
|
||||
margin-top: 8rpx;
|
||||
color: #17233d;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-card,
|
||||
.quantity-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 22rpx;
|
||||
box-sizing: border-box;
|
||||
border: 1rpx solid #edf1f7;
|
||||
border-radius: 24rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.primary-field {
|
||||
border-color: rgba(53, 191, 163, 0.18);
|
||||
background: #f5fffb;
|
||||
}
|
||||
|
||||
.field-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.field-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
margin-bottom: 6rpx;
|
||||
color: #5f6b7a;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.quantity-tip {
|
||||
color: #a0a7b5;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.lock-pill {
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #eef2f7;
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.lock-pill.is-active {
|
||||
background: rgba(25, 190, 107, 0.12);
|
||||
color: #19be6b;
|
||||
}
|
||||
|
||||
.scan-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
margin-left: 18rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, #35bfa3 0%, #1677ff 100%);
|
||||
box-shadow: 0 12rpx 26rpx rgba(53, 191, 163, 0.2);
|
||||
}
|
||||
|
||||
.quantity-card {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.quantity-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: 18rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.qty-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 58rpx;
|
||||
height: 58rpx;
|
||||
color: #1677ff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.qty-input {
|
||||
width: 80rpx;
|
||||
height: 58rpx;
|
||||
color: #17233d;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.submit-secondary,
|
||||
.submit-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 86rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.submit-secondary {
|
||||
width: 176rpx;
|
||||
background: #f2f4f8;
|
||||
color: #5f6b7a;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.submit-primary {
|
||||
margin-left: 14rpx;
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, #35bfa3 0%, #1677ff 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 14rpx 24rpx rgba(53, 191, 163, 0.18);
|
||||
}
|
||||
|
||||
.submit-primary.is-loading {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 22rpx;
|
||||
}
|
||||
|
||||
.total-badge {
|
||||
padding: 10rpx 22rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(53, 191, 163, 0.1);
|
||||
color: #19be6b;
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 112rpx;
|
||||
border-radius: 22rpx;
|
||||
background: #f8fbff;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #17233d;
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.stat-value.success {
|
||||
color: #19be6b;
|
||||
}
|
||||
|
||||
.stat-value.danger {
|
||||
color: #e91f3f;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
margin-top: 8rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 22rpx 0;
|
||||
border-bottom: 1rpx solid #edf1f7;
|
||||
}
|
||||
|
||||
.record-item:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.record-main {
|
||||
color: #17233d;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.record-sub {
|
||||
margin-top: 6rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.record-status {
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(25, 190, 107, 0.12);
|
||||
color: #19be6b;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.theme-dark.entry-page {
|
||||
background: #151923;
|
||||
}
|
||||
|
||||
.theme-dark .config-card,
|
||||
.theme-dark .form-card,
|
||||
.theme-dark .stats-card,
|
||||
.theme-dark .record-card {
|
||||
border-color: #364152;
|
||||
background: #222936;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-dark .select-chip,
|
||||
.theme-dark .field-card,
|
||||
.theme-dark .quantity-card,
|
||||
.theme-dark .stat-item {
|
||||
border-color: #364152;
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .quantity-control,
|
||||
.theme-dark .qty-input {
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .qty-button,
|
||||
.theme-dark .submit-secondary,
|
||||
.theme-dark .lock-pill {
|
||||
background: #293344;
|
||||
}
|
||||
|
||||
.theme-dark .card-title,
|
||||
.theme-dark .chip-value,
|
||||
.theme-dark .field-label,
|
||||
.theme-dark .record-main,
|
||||
.theme-dark .stat-value,
|
||||
.theme-dark .qty-input {
|
||||
color: #f2f5fa;
|
||||
}
|
||||
|
||||
.theme-dark .card-subtitle,
|
||||
.theme-dark .chip-label,
|
||||
.theme-dark .quantity-tip,
|
||||
.theme-dark .record-sub,
|
||||
.theme-dark .stat-label {
|
||||
color: #aeb9ca;
|
||||
}
|
||||
|
||||
.theme-dark .record-item {
|
||||
border-color: #364152;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.u-input__input) {
|
||||
color: #f2f5fa !important;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,791 @@
|
||||
<template>
|
||||
<view class="entry-page" :class="`theme-${themeMode}`">
|
||||
<view class="hero-card">
|
||||
<view>
|
||||
<text class="hero-title">订单模式</text>
|
||||
<text class="hero-subtitle">扫描订单号,自动使用当前登录用户编码完成录入</text>
|
||||
</view>
|
||||
<view class="hero-icon">
|
||||
<u-icon name="order" color="#ffffff" size="46" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="config-card">
|
||||
<view class="card-title">当前配置</view>
|
||||
<view class="chip-row">
|
||||
<view class="select-chip" @tap="showPicker('process')">
|
||||
<text class="chip-label">工序</text>
|
||||
<text class="chip-value">{{ processLabel }}</text>
|
||||
</view>
|
||||
<view class="select-chip" @tap="showPicker('step')">
|
||||
<text class="chip-label">步骤</text>
|
||||
<text class="chip-value">{{ stepLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-card">
|
||||
<view class="form-title-row">
|
||||
<view class="card-title">扫码录入</view>
|
||||
<view class="scan-mode-inline">
|
||||
<text class="scan-mode-title">连续扫码</text>
|
||||
<u-switch v-model="continuousScan" size="34" active-color="#1677ff" inactive-color="#d6dbe5" />
|
||||
</view>
|
||||
</view>
|
||||
<u-form :model="form" :border-bottom="false" label-position="top">
|
||||
<u-form-item :border-bottom="false" prop="orderNo">
|
||||
<view class="field-card primary-field">
|
||||
<view class="field-main">
|
||||
<text class="field-label">订单号</text>
|
||||
<u-input
|
||||
v-model="form.orderNo"
|
||||
:input-mode="pdaMode ? 'none' : 'text'"
|
||||
clearable
|
||||
placeholder="请扫描或输入订单号"
|
||||
placeholder-style="color: #a0a7b5;"
|
||||
@focus="handleOrderInputFocus"
|
||||
/>
|
||||
</view>
|
||||
<view class="scan-button" @tap="scanToField('orderNo')">
|
||||
<u-icon name="scan" color="#ffffff" size="36" />
|
||||
</view>
|
||||
</view>
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
|
||||
<view class="action-row">
|
||||
<view class="submit-secondary" @tap="resetForm">重置</view>
|
||||
<view class="submit-primary" :class="{ 'is-loading': submitting }" @tap="submitEntry">{{ submitting ? '提交中...' : '提交录入' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-card">
|
||||
<view class="stats-header">
|
||||
<view>
|
||||
<text class="card-title">今日统计</text>
|
||||
<text class="card-subtitle">当前工序/步骤扫码量</text>
|
||||
</view>
|
||||
<view class="total-badge">{{ hideTotal ? '***' : statistics.todayTotal }}</view>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value success">{{ statistics.successTotal }}</text>
|
||||
<text class="stat-label">成功</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value danger">{{ statistics.failedTotal }}</text>
|
||||
<text class="stat-label">失败</text>
|
||||
</view>
|
||||
<view class="stat-item" @tap="hideTotal = !hideTotal">
|
||||
<text class="stat-value">{{ hideTotal ? '***' : statistics.todayTotal }}</text>
|
||||
<text class="stat-label">总数</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="record-card">
|
||||
<view class="card-title">最近记录</view>
|
||||
<view v-for="item in recentRecords" :key="item.id" class="record-item">
|
||||
<view>
|
||||
<text class="record-main">{{ item.orderNo }}</text>
|
||||
<text class="record-sub">用户编码:{{ item.userCode }}</text>
|
||||
</view>
|
||||
<view class="record-status">成功</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// UTS 原生扫码插件仅在 App-Plus 真机/自定义基座中解析,Web 环境不加载。
|
||||
// #ifdef APP-PLUS
|
||||
import { openCodeScan } from '@/uni_modules/mas-code-scan'
|
||||
// #endif
|
||||
|
||||
import { getDataEntryConfig, getProcessOptions, getStepOptions, getOrderRecentRecords, getOrderStatistics, submitOrderRecord } from '@/api/produce/data-entry.js'
|
||||
import { getScanRegionSetting } from '@/utils/scan-settings.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
submitting: false,
|
||||
scanning: false,
|
||||
pdaMode: false,
|
||||
hideTotal: false,
|
||||
continuousScan: false,
|
||||
currentUser: {
|
||||
userCode: 'U001'
|
||||
},
|
||||
basicSettings: {},
|
||||
specificRules: [],
|
||||
processOptions: [],
|
||||
stepOptions: [],
|
||||
selectedProcessIndex: 0,
|
||||
selectedStepIndex: 0,
|
||||
form: {
|
||||
orderNo: ''
|
||||
},
|
||||
statistics: {
|
||||
todayTotal: 0,
|
||||
successTotal: 0,
|
||||
failedTotal: 0
|
||||
},
|
||||
recentRecords: []
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadPdaMode()
|
||||
this.loadPageData()
|
||||
},
|
||||
onShow() {
|
||||
this.loadPdaMode()
|
||||
},
|
||||
computed: {
|
||||
processLabel() {
|
||||
const item = this.processOptions[this.selectedProcessIndex]
|
||||
return item ? item.label : '请选择'
|
||||
},
|
||||
stepLabel() {
|
||||
const item = this.stepOptions[this.selectedStepIndex]
|
||||
return item ? item.label : '请选择'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCurrentUserCode() {
|
||||
const user = uni.getStorageSync('scanapp_user') || {}
|
||||
return user.username || user.nickname || 'U001'
|
||||
},
|
||||
loadPdaMode() {
|
||||
const userCode = this.getCurrentUserCode()
|
||||
const settings = uni.getStorageSync(`scanapp_settings_${userCode}`)
|
||||
this.pdaMode = settings?.pdaMode === true
|
||||
},
|
||||
handleOrderInputFocus() {
|
||||
// #ifdef APP-PLUS
|
||||
try {
|
||||
const activity = plus.android.runtimeMainActivity()
|
||||
const focusedView = activity.getWindow().getDecorView().findFocus()
|
||||
if (focusedView) {
|
||||
plus.android.invoke(focusedView, 'setShowSoftInputOnFocus', !this.pdaMode)
|
||||
}
|
||||
if (this.pdaMode) {
|
||||
uni.hideKeyboard()
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.pdaMode) uni.hideKeyboard()
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
getSelectionStorageKey() {
|
||||
return `scanapp_entry_order_selection_${this.currentUser.userCode}`
|
||||
},
|
||||
getSavedSelection() {
|
||||
const selection = uni.getStorageSync(this.getSelectionStorageKey())
|
||||
return selection && typeof selection === 'object' ? selection : {}
|
||||
},
|
||||
findOptionIndex(options, value) {
|
||||
const index = options.findIndex((item) => String(item.value) === String(value))
|
||||
return index >= 0 ? index : 0
|
||||
},
|
||||
saveSelection() {
|
||||
const processItem = this.processOptions[this.selectedProcessIndex]
|
||||
const stepItem = this.stepOptions[this.selectedStepIndex]
|
||||
uni.setStorageSync(this.getSelectionStorageKey(), {
|
||||
processId: processItem?.value || '',
|
||||
stepId: stepItem?.value || ''
|
||||
})
|
||||
},
|
||||
async loadPageData() {
|
||||
this.currentUser.userCode = this.getCurrentUserCode()
|
||||
const savedSelection = this.getSavedSelection()
|
||||
try {
|
||||
const [processOptions, statistics, recent] = await Promise.all([
|
||||
getProcessOptions(),
|
||||
getOrderStatistics({ user_code: this.currentUser.userCode }),
|
||||
getOrderRecentRecords({ user_code: this.currentUser.userCode, limit: 5 })
|
||||
])
|
||||
this.processOptions = Array.isArray(processOptions) ? processOptions : []
|
||||
this.selectedProcessIndex = this.findOptionIndex(this.processOptions, savedSelection.processId)
|
||||
this.statistics = {
|
||||
todayTotal: Number(statistics?.today_total || 0),
|
||||
successTotal: Number(statistics?.success_total || 0),
|
||||
failedTotal: Number(statistics?.failed_total || 0)
|
||||
}
|
||||
this.recentRecords = Array.isArray(recent?.items) ? recent.items.map((item, index) => ({
|
||||
id: item.id || index,
|
||||
orderNo: item.order_no || '-',
|
||||
userCode: item.user_code || this.currentUser.userCode
|
||||
})) : []
|
||||
if (this.processOptions.length > 0) {
|
||||
await this.loadStepOptions(this.processOptions[this.selectedProcessIndex].value, savedSelection.stepId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[scanapp] 订单页数据加载失败', error)
|
||||
}
|
||||
},
|
||||
async loadStepOptions(processId, stepId = '') {
|
||||
try {
|
||||
const options = await getStepOptions(processId)
|
||||
this.stepOptions = Array.isArray(options) ? options : []
|
||||
this.selectedStepIndex = this.findOptionIndex(this.stepOptions, stepId)
|
||||
await this.loadDataEntryConfig()
|
||||
this.saveSelection()
|
||||
} catch (error) {
|
||||
this.stepOptions = []
|
||||
this.selectedStepIndex = 0
|
||||
this.basicSettings = {}
|
||||
this.specificRules = []
|
||||
console.error('[scanapp] 步骤加载失败', error)
|
||||
}
|
||||
},
|
||||
async loadDataEntryConfig() {
|
||||
const processItem = this.processOptions[this.selectedProcessIndex]
|
||||
const stepItem = this.stepOptions[this.selectedStepIndex]
|
||||
if (!processItem || !stepItem) {
|
||||
this.basicSettings = {}
|
||||
this.specificRules = []
|
||||
return
|
||||
}
|
||||
try {
|
||||
const config = await getDataEntryConfig({
|
||||
process_id: processItem.value,
|
||||
step_id: stepItem.value,
|
||||
apply_mode: 'order',
|
||||
order_no: this.form.orderNo.trim(),
|
||||
user_code: this.currentUser.userCode.trim()
|
||||
})
|
||||
this.basicSettings = config?.basic_setting || {}
|
||||
this.specificRules = Array.isArray(config?.specific_rules) ? config.specific_rules : []
|
||||
} catch (error) {
|
||||
this.basicSettings = {}
|
||||
this.specificRules = []
|
||||
console.error('[scanapp] 订单规则加载失败', error)
|
||||
}
|
||||
},
|
||||
matchSpecificRule(rule, valueMap) {
|
||||
const matchField = rule.match_field || 'order_no'
|
||||
const value = String(valueMap[matchField] || '').trim()
|
||||
const pattern = String(rule.rule_pattern || '').trim()
|
||||
if (!value || !pattern) return false
|
||||
try {
|
||||
return new RegExp(pattern).test(value)
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
validateSpecificRules() {
|
||||
const context = {
|
||||
order_no: this.form.orderNo.trim(),
|
||||
user_code: this.currentUser.userCode.trim()
|
||||
}
|
||||
const blockedRule = this.specificRules.find((rule) => {
|
||||
if (rule.apply_mode && rule.apply_mode !== 'order' && rule.apply_mode !== 'both') return false
|
||||
if (Array.isArray(rule.step_ids) && rule.step_ids.length && !rule.step_ids.map(String).includes(String(this.stepOptions[this.selectedStepIndex]?.value))) return false
|
||||
return Number(rule.rule_type) === 2 && this.matchSpecificRule(rule, context)
|
||||
})
|
||||
if (blockedRule) {
|
||||
uni.showToast({ title: blockedRule.rule_name || '当前订单命中特定规则,禁止提交', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
validatePattern(value, settingKey, label) {
|
||||
const pattern = String(this.basicSettings[settingKey] || '').trim()
|
||||
if (!pattern) return true
|
||||
try {
|
||||
if (!new RegExp(pattern).test(value)) {
|
||||
uni.showToast({ title: `${label}格式不符合规则`, icon: 'none' })
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[scanapp] ${label}规则格式错误`, error)
|
||||
}
|
||||
return true
|
||||
},
|
||||
showPicker(type) {
|
||||
const list = type === 'process' ? this.processOptions : this.stepOptions
|
||||
if (!list.length) {
|
||||
uni.showToast({ title: '当前没有可选项', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showActionSheet({
|
||||
itemList: list.map((item) => item.label),
|
||||
success: async (res) => {
|
||||
if (type === 'process') {
|
||||
this.selectedProcessIndex = res.tapIndex
|
||||
await this.loadStepOptions(this.processOptions[res.tapIndex]?.value)
|
||||
} else {
|
||||
this.selectedStepIndex = res.tapIndex
|
||||
await this.loadDataEntryConfig()
|
||||
this.saveSelection()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
startOrderScan() {
|
||||
this.scanToField('orderNo')
|
||||
},
|
||||
scanToField(field) {
|
||||
if (this.scanning || this.submitting) return
|
||||
// #ifdef APP-PLUS
|
||||
this.scanning = true
|
||||
openCodeScan({
|
||||
smartZoom: true,
|
||||
vibrate: true,
|
||||
scanRegion: getScanRegionSetting(),
|
||||
success: async (result) => {
|
||||
const value = String(result?.content || '').trim()
|
||||
if (!value) return
|
||||
this.form[field] = value
|
||||
await this.submitEntry()
|
||||
},
|
||||
fail: (error) => {
|
||||
if (Number(error?.errCode) === 9021003) return
|
||||
uni.showToast({
|
||||
title: Number(error?.errCode) === 9021002 ? '未获得扫码权限' : '未识别到有效条码',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
complete: () => {
|
||||
this.scanning = false
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
uni.showToast({ title: '请在 Android App 中使用扫码功能', icon: 'none' })
|
||||
// #endif
|
||||
},
|
||||
validateForm() {
|
||||
if (!this.form.orderNo.trim()) {
|
||||
uni.showToast({ title: '请输入订单号', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!this.processOptions[this.selectedProcessIndex]) {
|
||||
uni.showToast({ title: '请选择工序', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!this.stepOptions[this.selectedStepIndex]) {
|
||||
uni.showToast({ title: '请选择步骤', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
const minLength = Number(this.basicSettings.order_no_min_length || 0)
|
||||
if (minLength > 0 && this.form.orderNo.trim().length < minLength) {
|
||||
uni.showToast({ title: `订单号长度不能少于 ${minLength} 位`, icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!this.validatePattern(this.form.orderNo.trim(), 'order_no_regex', '订单号')) return false
|
||||
if (!this.validateSpecificRules()) return false
|
||||
return true
|
||||
},
|
||||
async submitEntry() {
|
||||
if (this.submitting) return
|
||||
this.submitting = true
|
||||
try {
|
||||
await this.loadDataEntryConfig()
|
||||
if (!this.validateForm()) return
|
||||
const processItem = this.processOptions[this.selectedProcessIndex]
|
||||
const stepItem = this.stepOptions[this.selectedStepIndex]
|
||||
await submitOrderRecord({
|
||||
order_no: this.form.orderNo.trim(),
|
||||
quantity: 1,
|
||||
user_code: this.currentUser.userCode,
|
||||
process_id: processItem?.value,
|
||||
step_id: stepItem?.value,
|
||||
scan_result: 1
|
||||
})
|
||||
uni.showToast({ title: '订单录入成功', icon: 'success' })
|
||||
this.form.orderNo = ''
|
||||
await this.loadPageData()
|
||||
if (this.continuousScan) {
|
||||
setTimeout(() => {
|
||||
this.startOrderScan()
|
||||
}, 350)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[scanapp] 订单录入失败', error)
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
this.form.orderNo = ''
|
||||
this.loadDataEntryConfig()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.entry-page {
|
||||
min-height: 100vh;
|
||||
padding: 28rpx 24rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #eaf4ff 0%, #f4f6fb 38%, #f4f6fb 100%);
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.config-card,
|
||||
.form-card,
|
||||
.stats-card,
|
||||
.record-card {
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 28rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 16rpx 42rpx rgba(23, 35, 61, 0.06);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 34rpx 30rpx;
|
||||
background: linear-gradient(135deg, #1677ff 0%, #35bfa3 100%);
|
||||
}
|
||||
|
||||
.hero-title,
|
||||
.hero-subtitle,
|
||||
.card-title,
|
||||
.card-subtitle,
|
||||
.field-label,
|
||||
.chip-label,
|
||||
.chip-value,
|
||||
.stat-value,
|
||||
.stat-label,
|
||||
.record-main,
|
||||
.record-sub {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
color: #ffffff;
|
||||
font-size: 38rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
margin-top: 10rpx;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 22rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 86rpx;
|
||||
height: 86rpx;
|
||||
border-radius: 28rpx;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.config-card,
|
||||
.form-card,
|
||||
.stats-card,
|
||||
.record-card {
|
||||
margin-top: 24rpx;
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #17233d;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.form-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.scan-mode-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.scan-mode-title {
|
||||
display: block;
|
||||
margin-right: 22rpx;
|
||||
color: #6b778c;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
margin-top: 6rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chip-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 18rpx;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.select-chip {
|
||||
padding: 20rpx;
|
||||
border-radius: 22rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.chip-label {
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.chip-value {
|
||||
margin-top: 8rpx;
|
||||
color: #17233d;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 22rpx;
|
||||
box-sizing: border-box;
|
||||
border: 1rpx solid #edf1f7;
|
||||
border-radius: 24rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.primary-field {
|
||||
border-color: rgba(22, 119, 255, 0.18);
|
||||
background: #f5f9ff;
|
||||
}
|
||||
|
||||
.field-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.field-label-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
margin-bottom: 6rpx;
|
||||
color: #5f6b7a;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lock-pill {
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #eef2f7;
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.lock-pill.is-active {
|
||||
background: rgba(25, 190, 107, 0.12);
|
||||
color: #19be6b;
|
||||
}
|
||||
|
||||
.scan-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
margin-left: 18rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, #1677ff 0%, #35bfa3 100%);
|
||||
box-shadow: 0 12rpx 26rpx rgba(22, 119, 255, 0.2);
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.submit-secondary,
|
||||
.submit-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 86rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.submit-secondary {
|
||||
width: 176rpx;
|
||||
background: #f2f4f8;
|
||||
color: #5f6b7a;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.submit-primary {
|
||||
margin-left: 14rpx;
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, #1677ff 0%, #35bfa3 100%);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 14rpx 24rpx rgba(22, 119, 255, 0.18);
|
||||
}
|
||||
|
||||
.submit-primary.is-loading {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 22rpx;
|
||||
}
|
||||
|
||||
.total-badge {
|
||||
padding: 10rpx 22rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(22, 119, 255, 0.1);
|
||||
color: #1677ff;
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 112rpx;
|
||||
border-radius: 22rpx;
|
||||
background: #f8fbff;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #17233d;
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.stat-value.success {
|
||||
color: #19be6b;
|
||||
}
|
||||
|
||||
.stat-value.danger {
|
||||
color: #e91f3f;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
margin-top: 8rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 22rpx 0;
|
||||
border-bottom: 1rpx solid #edf1f7;
|
||||
}
|
||||
|
||||
.record-item:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.record-main {
|
||||
color: #17233d;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.record-sub {
|
||||
margin-top: 6rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.record-status {
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(25, 190, 107, 0.12);
|
||||
color: #19be6b;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.theme-dark.entry-page {
|
||||
background: #151923;
|
||||
}
|
||||
|
||||
.theme-dark .config-card,
|
||||
.theme-dark .form-card,
|
||||
.theme-dark .stats-card,
|
||||
.theme-dark .record-card {
|
||||
border-color: #364152;
|
||||
background: #222936;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-dark .select-chip,
|
||||
.theme-dark .field-card,
|
||||
.theme-dark .stat-item {
|
||||
border-color: #364152;
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .quantity-control,
|
||||
.theme-dark .qty-input {
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .qty-button,
|
||||
.theme-dark .submit-secondary,
|
||||
.theme-dark .lock-pill {
|
||||
background: #293344;
|
||||
}
|
||||
|
||||
.theme-dark .card-title,
|
||||
.theme-dark .chip-value,
|
||||
.theme-dark .field-label,
|
||||
.theme-dark .record-main,
|
||||
.theme-dark .stat-value {
|
||||
color: #f2f5fa;
|
||||
}
|
||||
|
||||
.theme-dark .card-subtitle,
|
||||
.theme-dark .chip-label,
|
||||
.theme-dark .scan-mode-title,
|
||||
.theme-dark .record-sub,
|
||||
.theme-dark .stat-label,
|
||||
.theme-dark .field-label,
|
||||
.theme-dark .quantity-tip {
|
||||
color: #aeb9ca;
|
||||
}
|
||||
|
||||
.theme-dark .record-item {
|
||||
border-color: #364152;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.u-input__input) {
|
||||
color: #f2f5fa !important;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,641 @@
|
||||
<template>
|
||||
<view class="home-page" :class="`theme-${themeMode}`">
|
||||
<view class="home-hero">
|
||||
<view>
|
||||
<text class="hero-title">扫码工作台</text>
|
||||
<text class="hero-subtitle">高效完成生产扫码与库存管理</text>
|
||||
</view>
|
||||
<view class="hero-action">
|
||||
<u-icon name="bell-fill" color="#1677ff" size="38" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="user-card" @tap="openProfile">
|
||||
<view class="avatar">
|
||||
<u-icon name="account-fill" color="#ffffff" size="54" />
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<text class="user-account">{{ member.displayName }}</text>
|
||||
<text class="user-name">用户名:{{ member.username }}</text>
|
||||
</view>
|
||||
<view class="user-plan-pill" :class="{ disabled: !member.enabled }">{{ member.levelName }}</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-panel">
|
||||
<view class="workspace-header">
|
||||
<text class="workspace-title">工作区</text>
|
||||
</view>
|
||||
<view v-for="(section, sectionIndex) in sections" :key="section.title" class="menu-group" :class="`menu-group-${sectionIndex}`">
|
||||
<view class="menu-grid">
|
||||
<view v-for="item in section.items" :key="item.title" class="menu-item" hover-class="menu-item--active" @tap="handleMenuTap(item)">
|
||||
<view class="icon-wrap" :style="{ backgroundColor: item.bgColor }">
|
||||
<u-icon :name="item.icon" color="#ffffff" size="42" />
|
||||
</view>
|
||||
<text class="menu-title">{{ item.title }}</text>
|
||||
<text v-if="item.desc" class="menu-desc">{{ item.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="chart-panel">
|
||||
<view class="chart-header">
|
||||
<view>
|
||||
<text class="chart-title">今日数据</text>
|
||||
<text class="chart-subtitle">扫码与异常概览</text>
|
||||
</view>
|
||||
<view class="chart-badge">实时</view>
|
||||
</view>
|
||||
|
||||
<view class="summary-grid">
|
||||
<view v-for="item in summaries" :key="item.label" class="summary-card">
|
||||
<text class="summary-value">{{ item.value }}</text>
|
||||
<text class="summary-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="trend-card">
|
||||
<view class="chart-section-title">扫码趋势</view>
|
||||
<view class="bar-chart">
|
||||
<view v-for="item in trendData" :key="item.label" class="bar-item">
|
||||
<view class="bar-track">
|
||||
<view class="bar-fill" :style="{ height: item.percent + '%', backgroundColor: item.color }"></view>
|
||||
</view>
|
||||
<text class="bar-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="progress-card">
|
||||
<view class="chart-section-title">异常处理</view>
|
||||
<view v-for="item in exceptionData" :key="item.label" class="progress-row">
|
||||
<view class="progress-info">
|
||||
<text class="progress-label">{{ item.label }}</text>
|
||||
<text class="progress-value">{{ item.value }}%</text>
|
||||
</view>
|
||||
<view class="progress-track">
|
||||
<view class="progress-fill" :style="{ width: item.value + '%', backgroundColor: item.color }"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUserInfo } from '@/api/member.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
member: {
|
||||
displayName: '未登录',
|
||||
username: '-',
|
||||
levelName: '请先登录',
|
||||
enabled: true
|
||||
},
|
||||
summaries: [
|
||||
{ label: '订单扫码', value: '128' },
|
||||
{ label: '整机扫码', value: '86' },
|
||||
{ label: '异常待处理', value: '12' }
|
||||
],
|
||||
trendData: [
|
||||
{ label: '09', percent: 42, color: '#1677ff' },
|
||||
{ label: '10', percent: 68, color: '#35bfa3' },
|
||||
{ label: '11', percent: 52, color: '#1677ff' },
|
||||
{ label: '12', percent: 76, color: '#35bfa3' },
|
||||
{ label: '13', percent: 45, color: '#1677ff' },
|
||||
{ label: '14', percent: 88, color: '#35bfa3' }
|
||||
],
|
||||
exceptionData: [
|
||||
{ label: '已处理', value: 72, color: '#19be6b' },
|
||||
{ label: '待复核', value: 46, color: '#f7b733' },
|
||||
{ label: '待处理', value: 28, color: '#e91f3f' }
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: '扫码',
|
||||
items: [
|
||||
{ title: '订单模式', icon: 'order', bgColor: '#e91f3f', url: '/pages/entry/order' },
|
||||
{ title: '一单多机', icon: 'grid-fill', bgColor: '#6b70e8', url: '/pages/entry/multi-machine' },
|
||||
{ title: '整机模式', icon: 'scan', bgColor: '#1e9df2', url: '/pages/entry/machine' },
|
||||
{ title: 'UTS 扫码原型', icon: 'scan', bgColor: '#1677ff', url: '/pages/scan/prototype' },
|
||||
{ title: '异常处理', icon: 'warning-fill', bgColor: '#f7b733', url: '/pages/exception/index' },
|
||||
{ title: '维修处理', icon: 'setting-fill', bgColor: '#35bfa3', url: '/pages/repair/index' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '查询',
|
||||
items: [
|
||||
{ title: '查询', icon: 'search', bgColor: '#ef6a62', url: '/pages/query/index' },
|
||||
{ title: '统计', icon: 'grid-fill', bgColor: '#1eb2e8', url: '/pages/statistics/index' },
|
||||
{ title: '设置', icon: 'setting-fill', bgColor: '#9f86e8', url: '/pages/settings/index' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadMemberInfo()
|
||||
},
|
||||
methods: {
|
||||
async loadMemberInfo() {
|
||||
if (!this.isLoggedIn()) {
|
||||
this.member = {
|
||||
displayName: '未登录',
|
||||
username: '-',
|
||||
levelName: '请先登录',
|
||||
enabled: true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await getUserInfo()
|
||||
uni.setStorageSync('scanapp_user', profile)
|
||||
this.member = {
|
||||
displayName: profile.nickname || profile.email || profile.username || '-',
|
||||
username: profile.username || profile.email || '-',
|
||||
levelName: profile.level_name || (profile.level && profile.level.name) || (Number(profile.enabled) === 0 ? '账号已停用' : '会员用户'),
|
||||
enabled: Number(profile.enabled) !== 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[scanapp] 获取首页会员信息失败', error)
|
||||
}
|
||||
},
|
||||
isLoggedIn() {
|
||||
return Boolean(uni.getStorageSync('scanapp_token'))
|
||||
},
|
||||
requireLogin(redirectUrl) {
|
||||
if (this.isLoggedIn()) {
|
||||
uni.navigateTo({
|
||||
url: redirectUrl
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '需要登录',
|
||||
content: '登录后可访问个人资料',
|
||||
confirmText: '去登录',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/auth/login?redirect=${encodeURIComponent(redirectUrl)}`
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
openProfile() {
|
||||
this.requireLogin('/pages/profile/index')
|
||||
},
|
||||
handleMenuTap(item) {
|
||||
if (item.url) {
|
||||
uni.navigateTo({
|
||||
url: item.url
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.showToast({
|
||||
title: `${item.title}功能待接入`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
padding: 34rpx 24rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #eaf4ff 0%, #f4f6fb 34%, #f4f6fb 100%);
|
||||
}
|
||||
|
||||
.home-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12rpx 8rpx 30rpx;
|
||||
}
|
||||
|
||||
.hero-title,
|
||||
.hero-subtitle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
color: #17233d;
|
||||
font-size: 38rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
margin-top: 8rpx;
|
||||
color: #7d8798;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hero-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 12rpx 32rpx rgba(23, 35, 61, 0.08);
|
||||
}
|
||||
|
||||
.user-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 36rpx 28rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 28rpx;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8fbff 100%);
|
||||
box-shadow: 0 18rpx 44rpx rgba(23, 35, 61, 0.08);
|
||||
}
|
||||
|
||||
.user-card::after {
|
||||
position: absolute;
|
||||
top: -80rpx;
|
||||
right: -70rpx;
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(22, 119, 255, 0.08);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
margin-right: 24rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #35bfa3 0%, #1677ff 100%);
|
||||
box-shadow: 0 12rpx 28rpx rgba(53, 191, 163, 0.28);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-account {
|
||||
color: #111111;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin-top: 8rpx;
|
||||
color: #8b8f98;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.user-plan-pill {
|
||||
flex: none;
|
||||
margin-left: 20rpx;
|
||||
padding: 6rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(25, 190, 107, 0.1);
|
||||
color: #19be6b;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.user-plan-pill.disabled {
|
||||
background: rgba(233, 31, 63, 0.1);
|
||||
color: #e91f3f;
|
||||
}
|
||||
|
||||
.menu-panel {
|
||||
margin-top: 34rpx;
|
||||
overflow: hidden;
|
||||
padding: 18rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 28rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 16rpx 42rpx rgba(23, 35, 61, 0.06);
|
||||
}
|
||||
|
||||
.workspace-header {
|
||||
padding: 6rpx 8rpx 20rpx;
|
||||
}
|
||||
|
||||
.workspace-title {
|
||||
color: #17233d;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.menu-group {
|
||||
overflow: hidden;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.menu-group + .menu-group {
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.menu-group-0 {
|
||||
background: linear-gradient(135deg, rgba(22, 119, 255, 0.08) 0%, rgba(233, 31, 63, 0.05) 100%);
|
||||
}
|
||||
|
||||
.menu-group-1 {
|
||||
background: linear-gradient(135deg, rgba(25, 190, 107, 0.08) 0%, rgba(159, 134, 232, 0.06) 100%);
|
||||
}
|
||||
|
||||
.menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 196rpx;
|
||||
padding: 26rpx 10rpx;
|
||||
box-sizing: border-box;
|
||||
border-right: 1rpx solid #edf0f5;
|
||||
border-bottom: 1rpx solid #edf0f5;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.menu-item--active {
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.menu-item:nth-child(3n) {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.menu-item:nth-last-child(-n + 3) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
margin-bottom: 28rpx;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 10rpx 24rpx rgba(23, 35, 61, 0.12);
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
color: #111111;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.35;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-desc {
|
||||
margin-top: 4rpx;
|
||||
color: #9a9fa8;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.25;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart-panel {
|
||||
margin-top: 34rpx;
|
||||
padding: 28rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 28rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 16rpx 42rpx rgba(23, 35, 61, 0.06);
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.chart-title,
|
||||
.chart-subtitle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
color: #17233d;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.chart-subtitle {
|
||||
margin-top: 6rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chart-badge {
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(22, 119, 255, 0.1);
|
||||
color: #1677ff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16rpx;
|
||||
margin-bottom: 22rpx;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 112rpx;
|
||||
border-radius: 22rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
color: #17233d;
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
margin-top: 8rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.trend-card,
|
||||
.progress-card {
|
||||
padding: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.progress-card {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.chart-section-title {
|
||||
margin-bottom: 22rpx;
|
||||
color: #17233d;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
height: 210rpx;
|
||||
}
|
||||
|
||||
.bar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bar-track {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
width: 28rpx;
|
||||
height: 156rpx;
|
||||
overflow: hidden;
|
||||
border-radius: 999rpx;
|
||||
background: #eaf0f8;
|
||||
}
|
||||
|
||||
.bar-fill {
|
||||
width: 100%;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
margin-top: 14rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.progress-row + .progress-row {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.progress-label,
|
||||
.progress-value {
|
||||
font-size: 22rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
color: #5f6b7a;
|
||||
}
|
||||
|
||||
.progress-value {
|
||||
color: #17233d;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
height: 14rpx;
|
||||
overflow: hidden;
|
||||
border-radius: 999rpx;
|
||||
background: #eaf0f8;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
.theme-dark.home-page {
|
||||
background: linear-gradient(180deg, #111827 0%, #151923 34%, #151923 100%);
|
||||
}
|
||||
|
||||
.theme-dark .hero-action {
|
||||
background: #222936;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-dark .user-card,
|
||||
.theme-dark .menu-panel,
|
||||
.theme-dark .chart-panel {
|
||||
border-color: #364152;
|
||||
background: #222936;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-dark .menu-group-0,
|
||||
.theme-dark .menu-group-1 {
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .menu-item {
|
||||
border-color: #364152;
|
||||
}
|
||||
|
||||
.theme-dark .menu-item--active,
|
||||
.theme-dark .summary-card,
|
||||
.theme-dark .trend-card,
|
||||
.theme-dark .progress-card {
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .user-account,
|
||||
.theme-dark .menu-title,
|
||||
.theme-dark .workspace-title,
|
||||
.theme-dark .chart-title,
|
||||
.theme-dark .chart-section-title,
|
||||
.theme-dark .summary-value,
|
||||
.theme-dark .progress-value {
|
||||
color: #f2f5fa;
|
||||
}
|
||||
|
||||
.theme-dark .hero-subtitle,
|
||||
.theme-dark .user-name,
|
||||
.theme-dark .menu-desc,
|
||||
.theme-dark .chart-subtitle,
|
||||
.theme-dark .summary-label,
|
||||
.theme-dark .bar-label,
|
||||
.theme-dark .progress-label {
|
||||
color: #aeb9ca;
|
||||
}
|
||||
|
||||
.theme-dark .bar-track,
|
||||
.theme-dark .progress-track {
|
||||
background: #364152;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<view class="profile-form-page" :class="`theme-${themeMode}`">
|
||||
<view class="form-card">
|
||||
<view class="form-header">
|
||||
<text class="form-title">修改资料</text>
|
||||
<text class="form-desc">用户名、邮箱和手机号为账号信息,暂不支持修改</text>
|
||||
</view>
|
||||
|
||||
<u-form :model="form" :border-bottom="false" label-position="top" label-width="120">
|
||||
<u-form-item label="用户名" :border-bottom="false">
|
||||
<view class="field-card locked">
|
||||
<text class="field-text locked-text">{{ form.username || '-' }}</text>
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item label="邮箱" :border-bottom="false">
|
||||
<view class="field-card locked">
|
||||
<text class="field-text locked-text">{{ form.email || '-' }}</text>
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item label="手机号" :border-bottom="false">
|
||||
<view class="field-card locked">
|
||||
<text class="field-text locked-text">{{ form.phone || '暂未绑定手机号' }}</text>
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item label="昵称" prop="nickname" required :border-bottom="false">
|
||||
<view class="field-card">
|
||||
<u-input v-model="form.nickname" clearable placeholder="请输入昵称" placeholder-style="color: #a0a7b5;" />
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item label="性别" prop="gender" :border-bottom="false">
|
||||
<view class="gender-options">
|
||||
<view
|
||||
v-for="item in genderOptions"
|
||||
:key="item.value"
|
||||
class="gender-option"
|
||||
:class="{ active: form.gender === item.value }"
|
||||
@tap="form.gender = item.value"
|
||||
>
|
||||
<view class="gender-dot"></view>
|
||||
<text>{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item label="生日" prop="birthday" :border-bottom="false">
|
||||
<picker mode="date" :value="form.birthday || today" start="1900-01-01" :end="today" @change="handleBirthdayChange">
|
||||
<view class="field-card picker-card">
|
||||
<text class="field-text" :class="{ placeholder: !form.birthday }">{{ form.birthday || '请选择生日' }}</text>
|
||||
<u-icon name="calendar" color="#a0a7b5" size="24" />
|
||||
</view>
|
||||
</picker>
|
||||
</u-form-item>
|
||||
<u-form-item label="个人简介" prop="bio" :border-bottom="false">
|
||||
<view class="textarea-card">
|
||||
<textarea v-model="form.bio" maxlength="120" placeholder="请输入个人简介" placeholder-style="color: #a0a7b5;" />
|
||||
</view>
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
</view>
|
||||
|
||||
<view class="bottom-actions">
|
||||
<u-button type="primary" shape="circle" size="large" :loading="submitting" @click="submitForm">保存资料</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUserInfo, updateUserInfo } from '@/api/member.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
submitting: false,
|
||||
today: '',
|
||||
form: {
|
||||
username: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
nickname: '',
|
||||
gender: 0,
|
||||
birthday: '',
|
||||
bio: ''
|
||||
},
|
||||
genderOptions: [
|
||||
{ label: '男', value: 1 },
|
||||
{ label: '女', value: 2 },
|
||||
{ label: '其他', value: 0 }
|
||||
]
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.today = this.getToday()
|
||||
this.loadProfile()
|
||||
},
|
||||
methods: {
|
||||
getToday() {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
},
|
||||
async loadProfile() {
|
||||
try {
|
||||
const profile = await getUserInfo()
|
||||
this.form = {
|
||||
username: profile.username || '',
|
||||
email: profile.email || '',
|
||||
phone: profile.phone || '',
|
||||
nickname: profile.nickname || profile.username || '',
|
||||
gender: Number(profile.gender || 0),
|
||||
birthday: profile.birthday || '',
|
||||
bio: profile.bio || ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[scanapp] 加载资料失败', error)
|
||||
}
|
||||
},
|
||||
handleBirthdayChange(e) {
|
||||
this.form.birthday = e.detail.value
|
||||
},
|
||||
validateForm() {
|
||||
if (!this.form.nickname.trim()) {
|
||||
uni.showToast({ title: '请输入昵称', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
async submitForm() {
|
||||
if (this.submitting || !this.validateForm()) {
|
||||
return
|
||||
}
|
||||
this.submitting = true
|
||||
try {
|
||||
await updateUserInfo({
|
||||
nickname: this.form.nickname.trim(),
|
||||
gender: this.form.gender,
|
||||
birthday: this.form.birthday,
|
||||
bio: this.form.bio
|
||||
})
|
||||
const profile = await getUserInfo()
|
||||
uni.setStorageSync('scanapp_user', profile)
|
||||
uni.showToast({ title: '资料已保存', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 400)
|
||||
} catch (error) {
|
||||
console.error('[scanapp] 保存资料失败', error)
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.profile-form-page {
|
||||
min-height: 100vh;
|
||||
padding: 28rpx 24rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
background: #f4f6fb;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
padding: 34rpx 30rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.75);
|
||||
border-radius: 28rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 16rpx 40rpx rgba(23, 35, 61, 0.06);
|
||||
}
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.form-title,
|
||||
.form-desc,
|
||||
.field-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
color: #17233d;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.form-desc {
|
||||
margin-top: 10rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.field-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
border: 1rpx solid #edf1f7;
|
||||
border-radius: 22rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.field-card.locked {
|
||||
border-color: #edf0f5;
|
||||
background: #f4f6fb;
|
||||
}
|
||||
|
||||
.field-card.picker-card {
|
||||
padding-right: 20rpx;
|
||||
}
|
||||
|
||||
.field-text {
|
||||
color: #17233d;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.field-text.locked-text,
|
||||
.field-text.placeholder {
|
||||
color: #a0a7b5;
|
||||
}
|
||||
|
||||
.gender-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
column-gap: 16rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gender-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 76rpx;
|
||||
border: 1rpx solid #edf1f7;
|
||||
border-radius: 20rpx;
|
||||
background: #f8fbff;
|
||||
color: #6b778c;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gender-dot {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
margin-right: 10rpx;
|
||||
border: 3rpx solid #c5ccda;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.gender-option.active {
|
||||
border-color: #1677ff;
|
||||
background: rgba(22, 119, 255, 0.1);
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.gender-option.active .gender-dot {
|
||||
border-color: #1677ff;
|
||||
background: #1677ff;
|
||||
box-shadow: 0 0 0 4rpx rgba(22, 119, 255, 0.16);
|
||||
}
|
||||
|
||||
.textarea-card {
|
||||
width: 100%;
|
||||
min-height: 156rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
border: 1rpx solid #edf1f7;
|
||||
border-radius: 22rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.textarea-card textarea {
|
||||
width: 100%;
|
||||
height: 116rpx;
|
||||
color: #17233d;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
margin-top: 36rpx;
|
||||
}
|
||||
.theme-dark.profile-form-page {
|
||||
background: #151923;
|
||||
}
|
||||
|
||||
.theme-dark .form-card,
|
||||
.theme-dark .field-card,
|
||||
.theme-dark .gender-option,
|
||||
.theme-dark .textarea-card {
|
||||
border-color: #364152;
|
||||
background: #222936;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-dark .field-card.locked {
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .form-title,
|
||||
.theme-dark .field-text,
|
||||
.theme-dark .textarea-card textarea {
|
||||
color: #f2f5fa;
|
||||
}
|
||||
|
||||
.theme-dark .form-desc,
|
||||
.theme-dark .field-text.locked-text,
|
||||
.theme-dark .field-text.placeholder,
|
||||
.theme-dark .gender-option {
|
||||
color: #aeb9ca;
|
||||
}
|
||||
|
||||
.theme-dark :deep(textarea),
|
||||
.theme-dark :deep(.u-input__input) {
|
||||
color: #f2f5fa !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,384 @@
|
||||
<template>
|
||||
<view class="profile-page" :class="`theme-${themeMode}`">
|
||||
<view class="profile-header">
|
||||
<view class="profile-top">
|
||||
<view class="avatar-wrap">
|
||||
<u-icon name="account-fill" color="#ffffff" size="52" />
|
||||
</view>
|
||||
<view class="profile-meta">
|
||||
<text class="profile-name">{{ user.name }}</text>
|
||||
<text class="profile-account">{{ user.account }}</text>
|
||||
</view>
|
||||
<text class="profile-role">{{ user.role }}</text>
|
||||
</view>
|
||||
<view class="profile-stat-row">
|
||||
<view class="stat-item" v-for="item in stats" :key="item.label">
|
||||
<text class="stat-value">{{ item.value }}</text>
|
||||
<text class="stat-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="profile-card">
|
||||
<view class="card-title">基本信息</view>
|
||||
<view class="info-list">
|
||||
<view class="info-row">
|
||||
<text class="info-label">手机号</text>
|
||||
<text class="info-value">{{ user.phone }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">所属组织</text>
|
||||
<text class="info-value">{{ user.department }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">账号状态</text>
|
||||
<text class="info-value success">{{ user.status }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="profile-card">
|
||||
<view class="card-title">快捷操作</view>
|
||||
<view class="action-list">
|
||||
<view class="action-item" @tap="handleAction('修改资料')">
|
||||
<view class="action-left">
|
||||
<u-icon name="edit-pen" color="#2f6cf6" size="30" />
|
||||
<text>修改资料</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#c1c7d0" size="24" />
|
||||
</view>
|
||||
<view class="action-item" @tap="handleAction('修改密码')">
|
||||
<view class="action-left">
|
||||
<u-icon name="lock-fill" color="#f7b733" size="30" />
|
||||
<text>修改密码</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#c1c7d0" size="24" />
|
||||
</view>
|
||||
<view class="action-item" @tap="handleAction('切换账号')">
|
||||
<view class="action-left">
|
||||
<u-icon name="account" color="#35bfa3" size="30" />
|
||||
<text>切换账号</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#c1c7d0" size="24" />
|
||||
</view>
|
||||
<view class="action-item danger" @tap="handleAction('退出登录')">
|
||||
<view class="action-left">
|
||||
<u-icon name="logout" color="#ef4f4f" size="30" />
|
||||
<text>退出登录</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#c1c7d0" size="24" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUserInfo } from '@/api/member.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
user: {
|
||||
name: '-',
|
||||
account: '-',
|
||||
role: '会员用户',
|
||||
phone: '-',
|
||||
department: '-',
|
||||
status: '正常'
|
||||
},
|
||||
stats: [
|
||||
{ label: '今日扫码', value: '-' },
|
||||
{ label: '异常处理', value: '-' },
|
||||
{ label: '累计天数', value: '-' }
|
||||
]
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadUserInfo()
|
||||
},
|
||||
methods: {
|
||||
async loadUserInfo() {
|
||||
try {
|
||||
const profile = await getUserInfo()
|
||||
uni.setStorageSync('scanapp_user', profile)
|
||||
this.user = {
|
||||
name: profile.nickname || profile.username || '-',
|
||||
account: profile.username || profile.email || '-',
|
||||
role: '会员用户',
|
||||
phone: profile.phone || '-',
|
||||
department: profile.department_name || '-',
|
||||
status: Number(profile.enabled) === 0 ? '已停用' : '正常'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[scanapp] 获取用户信息失败', error)
|
||||
}
|
||||
},
|
||||
handleAction(title) {
|
||||
const actionMap = {
|
||||
'修改资料': this.goEditProfile,
|
||||
'修改密码': this.goChangePassword,
|
||||
'切换账号': this.switchAccount,
|
||||
'退出登录': this.logout
|
||||
}
|
||||
const action = actionMap[title]
|
||||
if (action) {
|
||||
action()
|
||||
}
|
||||
},
|
||||
goEditProfile() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/profile/edit'
|
||||
})
|
||||
},
|
||||
goChangePassword() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/profile/password'
|
||||
})
|
||||
},
|
||||
switchAccount() {
|
||||
uni.showModal({
|
||||
title: '切换账号',
|
||||
content: '将清除当前登录状态并返回登录页,是否继续?',
|
||||
confirmText: '切换',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.clearLoginState()
|
||||
uni.reLaunch({ url: '/pages/auth/login' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
logout() {
|
||||
uni.showModal({
|
||||
title: '退出登录',
|
||||
content: '确认退出当前账号?',
|
||||
confirmText: '退出',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.clearLoginState()
|
||||
uni.reLaunch({ url: '/pages/auth/login' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
clearLoginState() {
|
||||
uni.removeStorageSync('scanapp_token')
|
||||
uni.removeStorageSync('scanapp_refresh_token')
|
||||
uni.removeStorageSync('scanapp_user')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
padding: 28rpx 24rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #eef6ff 0%, #f4f6fb 36%, #f4f6fb 100%);
|
||||
}
|
||||
|
||||
.profile-header,
|
||||
.profile-card {
|
||||
padding: 38rpx 34rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.75);
|
||||
border-radius: 28rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 16rpx 40rpx rgba(23, 35, 61, 0.06);
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.profile-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.profile-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 36rpx;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 92rpx;
|
||||
height: 92rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #35bfa3 0%, #1677ff 100%);
|
||||
box-shadow: 0 12rpx 28rpx rgba(53, 191, 163, 0.28);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.profile-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
display: block;
|
||||
color: #17233d;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.profile-account,
|
||||
.profile-role,
|
||||
.info-label,
|
||||
.info-value,
|
||||
.action-item text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profile-account {
|
||||
margin-top: 14rpx;
|
||||
color: #7d8798;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.profile-role {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: none;
|
||||
margin-left: 24rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(25, 190, 107, 0.1);
|
||||
color: #19be6b;
|
||||
font-size: 18rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.profile-stat-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16rpx;
|
||||
margin-top: 34rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: 24rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #f7f9fc;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
color: #1677ff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 18rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #17233d;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.info-list {
|
||||
margin-top: 26rpx;
|
||||
}
|
||||
|
||||
.info-row,
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 26rpx 0;
|
||||
border-bottom: 1rpx solid #eef1f6;
|
||||
}
|
||||
|
||||
.info-row:last-child,
|
||||
.action-item:last-child {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #8a94a6;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #17233d;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.info-value.success {
|
||||
color: #19be6b;
|
||||
}
|
||||
|
||||
.action-list {
|
||||
margin-top: 26rpx;
|
||||
}
|
||||
|
||||
.action-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
color: #17233d;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.action-item.danger .action-left {
|
||||
color: #ef4f4f;
|
||||
}
|
||||
.theme-dark.profile-page {
|
||||
background: #151923;
|
||||
}
|
||||
|
||||
.theme-dark .profile-header,
|
||||
.theme-dark .profile-card {
|
||||
border-color: #364152;
|
||||
background: #222936;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-dark .stat-item {
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .profile-name,
|
||||
.theme-dark .card-title,
|
||||
.theme-dark .info-value,
|
||||
.theme-dark .action-left {
|
||||
color: #f2f5fa;
|
||||
}
|
||||
|
||||
.theme-dark .profile-account,
|
||||
.theme-dark .info-label,
|
||||
.theme-dark .stat-label {
|
||||
color: #aeb9ca;
|
||||
}
|
||||
|
||||
.theme-dark .info-row,
|
||||
.theme-dark .action-item {
|
||||
border-color: #364152;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<view class="password-page" :class="`theme-${themeMode}`">
|
||||
<view class="form-card">
|
||||
<view class="form-header">
|
||||
<text class="form-title">修改密码</text>
|
||||
<text class="form-desc">修改成功后需要重新登录</text>
|
||||
</view>
|
||||
|
||||
<u-form :model="form" :border-bottom="false" label-position="top" label-width="120">
|
||||
<u-form-item label="原密码" prop="oldPassword" :border-bottom="false">
|
||||
<view class="field-card">
|
||||
<u-input v-model="form.oldPassword" type="password" clearable password-icon placeholder="请输入原密码" placeholder-style="color: #a0a7b5;" />
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item label="新密码" prop="newPassword" :border-bottom="false">
|
||||
<view class="field-card">
|
||||
<u-input v-model="form.newPassword" type="password" clearable password-icon placeholder="请输入新密码" placeholder-style="color: #a0a7b5;" />
|
||||
</view>
|
||||
</u-form-item>
|
||||
<u-form-item label="确认新密码" prop="confirmPassword" :border-bottom="false">
|
||||
<view class="field-card">
|
||||
<u-input v-model="form.confirmPassword" type="password" clearable password-icon placeholder="请再次输入新密码" placeholder-style="color: #a0a7b5;" />
|
||||
</view>
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
</view>
|
||||
|
||||
<view class="tips-card">
|
||||
<text class="tips-title">密码要求</text>
|
||||
<text class="tips-text">长度不少于 6 位,建议同时包含字母、数字或符号。</text>
|
||||
</view>
|
||||
|
||||
<view class="bottom-actions">
|
||||
<u-button type="primary" shape="circle" size="large" :loading="submitting" @click="submitForm">确认修改</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { changePassword } from '@/api/member.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
submitting: false,
|
||||
form: {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validateForm() {
|
||||
if (!this.form.oldPassword) {
|
||||
uni.showToast({ title: '请输入原密码', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (!this.form.newPassword || this.form.newPassword.length < 6) {
|
||||
uni.showToast({ title: '新密码至少 6 位', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (this.form.newPassword !== this.form.confirmPassword) {
|
||||
uni.showToast({ title: '两次新密码不一致', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
if (this.form.oldPassword === this.form.newPassword) {
|
||||
uni.showToast({ title: '新密码不能与原密码相同', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
async submitForm() {
|
||||
if (this.submitting || !this.validateForm()) {
|
||||
return
|
||||
}
|
||||
this.submitting = true
|
||||
try {
|
||||
await changePassword({
|
||||
old_password: this.form.oldPassword,
|
||||
new_password: this.form.newPassword
|
||||
})
|
||||
uni.removeStorageSync('scanapp_token')
|
||||
uni.removeStorageSync('scanapp_refresh_token')
|
||||
uni.removeStorageSync('scanapp_user')
|
||||
uni.showToast({ title: '密码已修改,请重新登录', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/auth/login' })
|
||||
}, 700)
|
||||
} catch (error) {
|
||||
console.error('[scanapp] 修改密码失败', error)
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.password-page {
|
||||
min-height: 100vh;
|
||||
padding: 28rpx 24rpx 48rpx;
|
||||
box-sizing: border-box;
|
||||
background: #f4f6fb;
|
||||
}
|
||||
|
||||
.form-card,
|
||||
.tips-card {
|
||||
padding: 34rpx 30rpx;
|
||||
border-radius: 28rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 16rpx 40rpx rgba(23, 35, 61, 0.06);
|
||||
}
|
||||
|
||||
.form-header {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.form-title,
|
||||
.form-desc,
|
||||
.tips-title,
|
||||
.tips-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
color: #17233d;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.form-desc {
|
||||
margin-top: 10rpx;
|
||||
color: #8a94a6;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.field-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
border: 1rpx solid #edf1f7;
|
||||
border-radius: 22rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.tips-card {
|
||||
margin-top: 24rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.tips-title {
|
||||
color: #17233d;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tips-text {
|
||||
margin-top: 10rpx;
|
||||
color: #6b778c;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.bottom-actions {
|
||||
margin-top: 36rpx;
|
||||
}
|
||||
.theme-dark.password-page {
|
||||
background: #151923;
|
||||
}
|
||||
|
||||
.theme-dark .form-card,
|
||||
.theme-dark .tips-card,
|
||||
.theme-dark .field-card {
|
||||
border-color: #364152;
|
||||
background: #222936;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-dark .tips-card,
|
||||
.theme-dark .field-card {
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .form-title,
|
||||
.theme-dark .tips-title {
|
||||
color: #f2f5fa;
|
||||
}
|
||||
|
||||
.theme-dark .form-desc,
|
||||
.theme-dark .tips-text {
|
||||
color: #aeb9ca;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.u-input__input) {
|
||||
color: #f2f5fa !important;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,980 @@
|
||||
<template>
|
||||
<view class="repair-page" :class="`theme-${themeMode}`">
|
||||
<view class="repair-hero">
|
||||
<view class="hero-copy">
|
||||
<text class="hero-tag">维修工作台</text>
|
||||
<text class="hero-title">维修处理</text>
|
||||
<text class="hero-subtitle">扫描设备,快速完成故障判断与维修记录</text>
|
||||
</view>
|
||||
<view class="hero-icon">
|
||||
<u-icon name="setting-fill" color="#ffffff" size="46" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="summary-grid">
|
||||
<view v-for="item in summaries" :key="item.label" class="summary-item" @tap="handleSummaryTap(item)">
|
||||
<text class="summary-value" :class="item.type">{{ item.value }}</text>
|
||||
<text class="summary-label">{{ item.label }}</text>
|
||||
<view v-if="item.action === 'pending'" class="summary-arrow">
|
||||
<u-icon name="arrow-right" color="#1677ff" size="16" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-card scan-card">
|
||||
<view class="section-header">
|
||||
<view>
|
||||
<text class="section-title">1. 扫描或录入设备</text>
|
||||
<text class="section-desc">支持订单号、整机编码或设备条码</text>
|
||||
</view>
|
||||
<text class="required-tag">必填</text>
|
||||
</view>
|
||||
<view class="scan-input-row">
|
||||
<view class="scan-input">
|
||||
<u-icon name="search" color="#9aa4b4" size="32" />
|
||||
<u-input v-model="form.deviceNo" :input-mode="pdaMode ? 'none' : 'text'" placeholder="请输入订单号 / 整机编码" placeholder-style="color: #a0a7b5;" @focus="handleScanInputFocus" @blur="loadRepairTask" />
|
||||
</view>
|
||||
<view class="scan-button" @tap="scanDevice">
|
||||
<u-icon name="scan" color="#ffffff" size="30" />
|
||||
<text>扫码</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="repairTask" class="device-found">
|
||||
<view class="device-found-icon"><u-icon name="checkmark" color="#ffffff" size="28" /></view>
|
||||
<view class="device-found-info">
|
||||
<text class="device-found-title">已识别维修对象</text>
|
||||
<text class="device-found-value">{{ repairTask.deviceNo }}</text>
|
||||
</view>
|
||||
<text class="device-found-status">{{ repairTask.statusName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<view>
|
||||
<text class="section-title">2. 异常信息</text>
|
||||
<text class="section-desc">来自异常提报的维修任务</text>
|
||||
</view>
|
||||
<text class="order-time">{{ repairTask ? repairTask.reportedDate : '待扫描设备' }}</text>
|
||||
</view>
|
||||
<view v-if="repairTask" class="exception-info">
|
||||
<view class="info-row"><text class="info-label">异常类型</text><text class="info-value">{{ repairTask.typeName || '-' }}</text></view>
|
||||
<view class="info-row"><text class="info-label">异常原因</text><text class="info-value">{{ repairTask.reasonText || '-' }}</text></view>
|
||||
<view class="info-row"><text class="info-label">提报人员</text><text class="info-value">{{ repairTask.reporterText || '-' }}</text></view>
|
||||
</view>
|
||||
<view v-else class="exception-empty">扫描或录入设备后加载异常任务</view>
|
||||
</view>
|
||||
|
||||
<view class="section-card">
|
||||
<view class="section-header">
|
||||
<view>
|
||||
<text class="section-title">3. 维修判定</text>
|
||||
<text class="section-desc">选择本次维修的不良类型</text>
|
||||
</view>
|
||||
<text class="required-tag">必选</text>
|
||||
</view>
|
||||
<view class="result-grid">
|
||||
<view v-for="item in resultOptions" :key="item.value" class="result-item" :class="{ active: form.result === item.value }" @tap="selectRepairResult(item.value)">
|
||||
<view class="result-icon" :style="{ backgroundColor: form.result === item.value ? item.color : '#f2f5fa' }">
|
||||
<u-icon :name="item.icon" :color="form.result === item.value ? '#ffffff' : item.color" size="32" />
|
||||
</view>
|
||||
<text class="result-label">{{ item.label }}</text>
|
||||
<text class="result-desc">{{ item.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="form.result === 'assembly_mismatch'" class="section-card">
|
||||
<view class="section-header">
|
||||
<view>
|
||||
<text class="section-title">4. 搭配错误分类</text>
|
||||
<text class="section-desc">选择与本次异常对应的物料分类</text>
|
||||
</view>
|
||||
<text class="required-tag">必选</text>
|
||||
</view>
|
||||
<view v-if="loadingMaterialCategories" class="repair-actions-tip">正在加载物料分类...</view>
|
||||
<view v-else-if="!mismatchCategoryOptions.length" class="repair-actions-tip">暂无可用物料分类</view>
|
||||
<view v-else class="repair-action-list">
|
||||
<view
|
||||
v-for="item in mismatchCategoryOptions"
|
||||
:key="item.id"
|
||||
class="repair-action-item"
|
||||
:class="{ active: isMismatchCategorySelected(item.id) }"
|
||||
@tap="toggleMismatchCategory(item)"
|
||||
>
|
||||
<u-icon :name="isMismatchCategorySelected(item.id) ? 'checkmark-circle-fill' : 'plus-circle'" :color="isMismatchCategorySelected(item.id) ? '#1677ff' : '#9aa4b4'" size="28" />
|
||||
<text>{{ item.category_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="form.result === 'operation_defect'" class="section-card responsibility-card">
|
||||
<view class="section-header">
|
||||
<view>
|
||||
<text class="section-title">4. 作业不良责任</text>
|
||||
<text class="section-desc">选择责任人员,工序步骤随人员记录一并保存</text>
|
||||
</view>
|
||||
<text class="required-tag">必选</text>
|
||||
</view>
|
||||
<view v-if="loadingOrderRecords" class="repair-actions-tip">正在加载历史扫码记录...</view>
|
||||
<view v-else-if="!orderScanRecords.length" class="repair-actions-tip">当前订单暂无可定责的历史扫码记录</view>
|
||||
<view v-else>
|
||||
<text class="responsibility-title">责任人员</text>
|
||||
<view class="responsibility-list">
|
||||
<view v-for="item in orderScanRecords" :key="`operator-${item.id}`" class="responsibility-item" :class="{ active: isResponsibleOperatorSelected(item.id) }" @tap="toggleResponsibleOperator(item.id)">
|
||||
<u-icon :name="isResponsibleOperatorSelected(item.id) ? 'checkmark-circle-fill' : 'plus-circle'" :color="isResponsibleOperatorSelected(item.id) ? '#1677ff' : '#9aa4b4'" size="28" />
|
||||
<view><text class="responsibility-name">{{ item.operator || item.userCode || '未记录人员' }}</text><text class="responsibility-meta">{{ item.processName || '未设置工序' }} · {{ item.stepName || '未设置步骤' }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-card">
|
||||
<view class="section-header repair-record-header">
|
||||
<view>
|
||||
<text class="section-title">{{ form.result === 'material_defect' ? '4. 维修记录' : '5. 维修记录' }}</text>
|
||||
<text class="section-desc">记录处理过程,便于后续追溯</text>
|
||||
</view>
|
||||
<view class="parts-actions">
|
||||
<text class="parts-action scan-part-action" @tap="scanPart">扫码</text>
|
||||
<text class="parts-action input-part-action" @tap="inputPart">输入</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="parts-header">
|
||||
<text class="parts-title">更换备件</text>
|
||||
</view>
|
||||
<view v-for="(part, index) in form.parts" :key="part.id" class="part-row">
|
||||
<view class="part-index">{{ index + 1 }}</view>
|
||||
<view class="part-info">
|
||||
<text class="part-name">{{ part.name }}</text>
|
||||
<text class="part-code">{{ part.code }}</text>
|
||||
<text v-if="part.categoryName" class="part-category">{{ part.categoryName }}</text>
|
||||
<view class="part-source-options">
|
||||
<view
|
||||
v-for="item in partSourceOptions"
|
||||
:key="item.value"
|
||||
class="part-source-option"
|
||||
:class="{ active: part.source === item.value }"
|
||||
@tap="part.source = item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="part-fault-entry" @tap="openFaultPointPopup(part)">
|
||||
<text class="part-fault-entry-label">故障点</text>
|
||||
<text class="part-fault-entry-value">{{ getPartFaultSummary(part) || '请选择' }}</text>
|
||||
<u-icon name="arrow-right" color="#9aa4b4" size="20" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="part-stepper">
|
||||
<view class="stepper-button" @tap.stop="changePartQuantity(part, -1)">
|
||||
<u-icon name="minus" color="#56647a" size="22" />
|
||||
</view>
|
||||
<text class="stepper-value">{{ part.quantity }}</text>
|
||||
<view class="stepper-button" @tap.stop="changePartQuantity(part, 1)">
|
||||
<u-icon name="plus" color="#1677ff" size="22" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="part-remove" @tap.stop="removePart(index)">
|
||||
<u-icon name="trash" color="#ef625f" size="30" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="repair-actions-tip">每个备件分别选择对应分类的故障点</view>
|
||||
<view v-if="form.remark" class="repair-action-summary">{{ form.remark }}</view>
|
||||
</view>
|
||||
|
||||
<u-popup v-model="partInputVisible" mode="center" border-radius="20">
|
||||
<view class="part-input-dialog">
|
||||
<text class="part-input-title">输入商品编码</text>
|
||||
<u-input v-model="partInputCode" :input-mode="pdaMode ? 'none' : 'text'" placeholder="请输入商品编码" border @focus="handleScanInputFocus" />
|
||||
<view class="part-input-actions">
|
||||
<view class="part-input-cancel" @tap="closePartInput">取消</view>
|
||||
<view class="part-input-confirm" @tap="confirmPartInput">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<u-popup v-model="faultPointPopupVisible" mode="bottom" border-radius="24">
|
||||
<view class="fault-popup">
|
||||
<view class="fault-popup-header">
|
||||
<view>
|
||||
<text class="fault-popup-title">选择故障点</text>
|
||||
<text class="fault-popup-subtitle">{{ currentFaultPart ? currentFaultPart.name : '' }}</text>
|
||||
</view>
|
||||
<text class="fault-popup-close" @tap="closeFaultPointPopup">关闭</text>
|
||||
</view>
|
||||
<view v-if="loadingFaultPoints" class="repair-actions-tip">正在加载故障描述...</view>
|
||||
<view v-else-if="!currentFaultPartOptions.length" class="repair-actions-tip">当前备件分类暂无故障描述</view>
|
||||
<scroll-view v-else scroll-y class="fault-popup-scroll">
|
||||
<view class="repair-action-list fault-popup-list">
|
||||
<view
|
||||
v-for="item in currentFaultPartOptions"
|
||||
:key="item.id"
|
||||
class="repair-action-item"
|
||||
:class="{ active: isFaultPointSelected(item.id) }"
|
||||
@tap="toggleFaultPoint(item)"
|
||||
>
|
||||
<u-icon
|
||||
:name="isFaultPointSelected(item.id) ? 'checkmark-circle-fill' : 'plus-circle'"
|
||||
:color="isFaultPointSelected(item.id) ? '#1677ff' : '#9aa4b4'"
|
||||
size="28"
|
||||
/>
|
||||
<text>{{ item.description }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="fault-popup-footer">
|
||||
<view class="fault-popup-button fault-popup-clear" @tap="clearCurrentPartFaults">清空</view>
|
||||
<view class="fault-popup-button fault-popup-confirm" @tap="closeFaultPointPopup">完成</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<view class="section-card evidence-card">
|
||||
<view class="section-header">
|
||||
<view>
|
||||
<text class="section-title">{{ form.result === 'material_defect' ? '5. 维修凭证' : '6. 维修凭证' }}</text>
|
||||
<text class="section-desc">可上传维修完成后的现场照片</text>
|
||||
</view>
|
||||
<text class="optional-tag">选填</text>
|
||||
</view>
|
||||
<view class="evidence-row">
|
||||
<view v-if="form.images.length < 4" class="upload-box" @tap="chooseImages">
|
||||
<u-icon name="camera-fill" color="#ff8a3d" size="38" />
|
||||
<text>拍照上传</text>
|
||||
</view>
|
||||
<view v-for="(image, index) in form.images" :key="image" class="evidence-image">
|
||||
<image :src="image" 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 class="bottom-actions">
|
||||
<view class="draft-button" @tap="saveDraft">保存草稿</view>
|
||||
<view class="complete-button" @tap="completeRepair">完成维修</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// UTS 原生扫码插件仅在 App-Plus 真机/自定义基座中解析,Web 环境不加载。
|
||||
// #ifdef APP-PLUS
|
||||
import { openCodeScan } from '@/uni_modules/mas-code-scan'
|
||||
// #endif
|
||||
|
||||
import { getHardwareFaultDescriptionOptions, getOrderRecentRecords, getScanAppMaterialCategories, lookupMaterial } from '@/api/produce/data-entry.js'
|
||||
import { getRepairStatistics, getRepairTask, submitRepairRecord } from '@/api/produce/exception.js'
|
||||
import { uploadImage } from '@/api/upload.js'
|
||||
import { getScanRegionSetting } from '@/utils/scan-settings.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
partInputVisible: false,
|
||||
partInputCode: '',
|
||||
scanning: false,
|
||||
pdaMode: false,
|
||||
loadingRepairTask: false,
|
||||
loadingOrderRecords: false,
|
||||
submitting: false,
|
||||
repairTask: null,
|
||||
orderScanRecords: [],
|
||||
loadingFaultPoints: false,
|
||||
loadingMaterialCategories: false,
|
||||
faultPointRequestKey: '',
|
||||
faultPointPopupVisible: false,
|
||||
currentFaultPartId: '',
|
||||
faultPointOptions: [],
|
||||
summaries: [
|
||||
{ label: '待维修', value: 0, type: 'warning', action: 'pending' },
|
||||
{ label: '处理中', value: 0, type: 'primary' },
|
||||
{ label: '今日完成', value: 0, type: 'success' }
|
||||
],
|
||||
resultOptions: [
|
||||
{ label: '物料不良', value: 'material_defect', desc: '物料本身存在异常', icon: 'bag-fill', color: '#ef625f' },
|
||||
{ label: '作业不良', value: 'operation_defect', desc: '作业过程导致异常', icon: 'edit-pen-fill', color: '#f2a93b' },
|
||||
{ label: '搭配错误', value: 'assembly_mismatch', desc: '物料搭配或装配错误', icon: 'grid-fill', color: '#2f6cf6' }
|
||||
],
|
||||
mismatchCategoryOptions: [],
|
||||
partSourceOptions: [
|
||||
{ label: '新料', value: 'new' },
|
||||
{ label: '售后', value: 'after_sales' },
|
||||
{ label: '返修', value: 'refurbished' }
|
||||
],
|
||||
form: {
|
||||
deviceNo: '',
|
||||
result: 'material_defect',
|
||||
remark: '',
|
||||
parts: [],
|
||||
responsibleOperatorIds: [],
|
||||
mismatchCategoryIds: [],
|
||||
images: [],
|
||||
status: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.loadPdaMode()
|
||||
this.loadMaterialCategories()
|
||||
this.loadRepairStatistics()
|
||||
const deviceNo = String(options?.device_no || '').trim()
|
||||
if (deviceNo) {
|
||||
this.form.deviceNo = deviceNo
|
||||
this.loadRepairTask()
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.loadPdaMode()
|
||||
},
|
||||
computed: {
|
||||
currentFaultPart() {
|
||||
return this.form.parts.find((part) => part.id === this.currentFaultPartId) || null
|
||||
},
|
||||
currentFaultPartOptions() {
|
||||
if (!this.currentFaultPart) return []
|
||||
return this.faultPointOptions.filter((item) => item.categoryId === String(this.currentFaultPart.categoryId || ''))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCurrentUserCode() {
|
||||
const user = uni.getStorageSync('scanapp_user') || {}
|
||||
return user.username || user.nickname || 'U001'
|
||||
},
|
||||
loadPdaMode() {
|
||||
const settings = uni.getStorageSync(`scanapp_settings_${this.getCurrentUserCode()}`)
|
||||
this.pdaMode = settings?.pdaMode === true
|
||||
},
|
||||
handleScanInputFocus() {
|
||||
// #ifdef APP-PLUS
|
||||
try {
|
||||
const activity = plus.android.runtimeMainActivity()
|
||||
const focusedView = activity.getWindow().getDecorView().findFocus()
|
||||
if (focusedView) plus.android.invoke(focusedView, 'setShowSoftInputOnFocus', !this.pdaMode)
|
||||
if (this.pdaMode) uni.hideKeyboard()
|
||||
} catch (error) {
|
||||
if (this.pdaMode) uni.hideKeyboard()
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
handleSummaryTap(item) {
|
||||
if (item.action !== 'pending') return
|
||||
uni.navigateTo({ url: '/pages/repair/tasks' })
|
||||
},
|
||||
async loadRepairStatistics() {
|
||||
try {
|
||||
const user = uni.getStorageSync('scanapp_user') || {}
|
||||
const userCode = String(user.username || user.user_code || '').trim()
|
||||
const statistics = await getRepairStatistics({ user_code: userCode })
|
||||
const data = statistics?.data || statistics || {}
|
||||
this.summaries = [
|
||||
{ label: '待维修', value: Number(data.pending || 0), type: 'warning', action: 'pending' },
|
||||
{ label: '处理中', value: Number(data.processing || 0), type: 'primary' },
|
||||
{ label: '今日完成', value: Number(data.completed_today || 0), type: 'success' }
|
||||
]
|
||||
} catch (error) {
|
||||
this.summaries = [
|
||||
{ label: '待维修', value: 0, type: 'warning', action: 'pending' },
|
||||
{ label: '处理中', value: 0, type: 'primary' },
|
||||
{ label: '今日完成', value: 0, type: 'success' }
|
||||
]
|
||||
}
|
||||
},
|
||||
async loadMaterialCategories() {
|
||||
this.loadingMaterialCategories = true
|
||||
try {
|
||||
const items = await getScanAppMaterialCategories()
|
||||
this.mismatchCategoryOptions = (Array.isArray(items) ? items : []).map((item) => ({
|
||||
id: String(item.id || ''),
|
||||
category_name: String(item.category_name || item.name || '')
|
||||
})).filter((item) => item.id && item.category_name)
|
||||
const availableIds = new Set(this.mismatchCategoryOptions.map((item) => item.id))
|
||||
this.form.mismatchCategoryIds = this.form.mismatchCategoryIds.filter((id) => availableIds.has(id))
|
||||
} catch (error) {
|
||||
this.mismatchCategoryOptions = []
|
||||
this.form.mismatchCategoryIds = []
|
||||
uni.showToast({ title: '物料分类加载失败,请稍后重试', icon: 'none' })
|
||||
} finally {
|
||||
this.loadingMaterialCategories = false
|
||||
}
|
||||
},
|
||||
async loadRepairTask() {
|
||||
const deviceNo = this.form.deviceNo.trim()
|
||||
this.repairTask = null
|
||||
this.orderScanRecords = []
|
||||
this.form.responsibleOperatorIds = []
|
||||
if (!deviceNo || this.loadingRepairTask) return
|
||||
this.loadingRepairTask = true
|
||||
try {
|
||||
const task = await getRepairTask(deviceNo)
|
||||
if (this.form.deviceNo.trim() !== deviceNo) return
|
||||
this.form.deviceNo = String(task.device_no || deviceNo)
|
||||
this.repairTask = {
|
||||
id: String(task.id || ''),
|
||||
deviceNo: String(task.device_no || deviceNo),
|
||||
typeName: String(task.type_name || ''),
|
||||
reasonText: [task.reason_name, task.issue_name].filter(Boolean).join(' · '),
|
||||
reporterText: task.reporter ? `${task.reporter}${task.user_code ? `(${task.user_code})` : ''}` : String(task.user_code || ''),
|
||||
reportedDate: String(task.reported_date || ''),
|
||||
statusName: String(task.status_name || '待维修')
|
||||
}
|
||||
await this.loadOrderScanRecords(this.repairTask.deviceNo)
|
||||
} catch (error) {
|
||||
if (this.form.deviceNo.trim() === deviceNo) this.repairTask = null
|
||||
} finally {
|
||||
this.loadingRepairTask = false
|
||||
}
|
||||
},
|
||||
async loadOrderScanRecords(orderNo) {
|
||||
this.loadingOrderRecords = true
|
||||
try {
|
||||
const result = await getOrderRecentRecords({ order_no: String(orderNo || '').trim(), limit: 50 })
|
||||
const items = Array.isArray(result?.items) ? result.items : []
|
||||
this.orderScanRecords = items.map((item) => ({
|
||||
id: String(item.id || ''),
|
||||
processName: String(item.process_name || item.process?.process_name || ''),
|
||||
stepName: String(item.step_name || item.step?.step_name || ''),
|
||||
operator: String(item.operator || ''),
|
||||
userCode: String(item.user_code || '')
|
||||
})).filter((item) => item.id)
|
||||
} catch (error) {
|
||||
this.orderScanRecords = []
|
||||
} finally {
|
||||
this.loadingOrderRecords = false
|
||||
}
|
||||
},
|
||||
selectRepairResult(result) {
|
||||
this.form.result = result
|
||||
if (result !== 'operation_defect') this.form.responsibleOperatorIds = []
|
||||
if (result !== 'assembly_mismatch') this.form.mismatchCategoryIds = []
|
||||
},
|
||||
isMismatchCategorySelected(categoryId) {
|
||||
return this.form.mismatchCategoryIds.includes(String(categoryId))
|
||||
},
|
||||
toggleMismatchCategory(category) {
|
||||
const id = String(category.id)
|
||||
const ids = this.form.mismatchCategoryIds
|
||||
this.form.mismatchCategoryIds = ids.includes(id) ? ids.filter((item) => item !== id) : [...ids, id]
|
||||
},
|
||||
isResponsibleOperatorSelected(scanId) {
|
||||
return this.form.responsibleOperatorIds.includes(String(scanId))
|
||||
},
|
||||
toggleResponsibleOperator(scanId) {
|
||||
const id = String(scanId)
|
||||
const ids = this.form.responsibleOperatorIds
|
||||
this.form.responsibleOperatorIds = ids.includes(id) ? ids.filter((item) => item !== id) : [...ids, id]
|
||||
},
|
||||
scanDevice() {
|
||||
if (this.scanning || this.submitting) return
|
||||
// #ifdef APP-PLUS
|
||||
this.scanning = true
|
||||
openCodeScan({
|
||||
smartZoom: true,
|
||||
vibrate: true,
|
||||
scanRegion: getScanRegionSetting(),
|
||||
success: async (result) => {
|
||||
const value = String(result?.content || '').trim()
|
||||
if (!value) return
|
||||
this.form.deviceNo = value
|
||||
await this.loadRepairTask()
|
||||
},
|
||||
fail: (error) => this.handleScanFail(error),
|
||||
complete: () => {
|
||||
this.scanning = false
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
uni.showToast({ title: '请在 Android App 中使用扫码功能', icon: 'none' })
|
||||
// #endif
|
||||
},
|
||||
handleScanFail(error) {
|
||||
const code = Number(error?.errCode)
|
||||
if (code === 9021003) return
|
||||
const messageMap = {
|
||||
9021001: '当前环境不支持扫码,请在 Android 自定义调试基座或正式安装包中使用',
|
||||
9021002: '相机权限被拒绝,请到系统设置中开启相机权限',
|
||||
9021004: '未获取到有效扫码结果,请重试'
|
||||
}
|
||||
uni.showToast({ title: messageMap[code] || String(error?.errMsg || '扫码插件调用异常'), icon: 'none' })
|
||||
},
|
||||
chooseImages() {
|
||||
if (this.submitting || this.form.images.length >= 4) return
|
||||
uni.chooseImage({
|
||||
count: 4 - this.form.images.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['camera', 'album'],
|
||||
success: (res) => {
|
||||
this.form.images = [...this.form.images, ...res.tempFilePaths].slice(0, 4)
|
||||
}
|
||||
})
|
||||
},
|
||||
removeImage(index) {
|
||||
if (this.submitting) return
|
||||
this.form.images.splice(index, 1)
|
||||
},
|
||||
getFaultDescriptionById(id) {
|
||||
return this.faultPointOptions.find((item) => item.id === id)?.description || ''
|
||||
},
|
||||
getPartFaultSummary(part) {
|
||||
return (part.selectedFaultPointIds || [])
|
||||
.map((id) => this.getFaultDescriptionById(id))
|
||||
.filter(Boolean)
|
||||
.join(';')
|
||||
},
|
||||
openFaultPointPopup(part) {
|
||||
if (!part.categoryId) {
|
||||
uni.showToast({ title: '当前备件缺少分类信息', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.currentFaultPartId = part.id
|
||||
this.faultPointPopupVisible = true
|
||||
},
|
||||
closeFaultPointPopup() {
|
||||
this.faultPointPopupVisible = false
|
||||
this.currentFaultPartId = ''
|
||||
},
|
||||
isFaultPointSelected(faultPointId) {
|
||||
return !!this.currentFaultPart && (this.currentFaultPart.selectedFaultPointIds || []).includes(faultPointId)
|
||||
},
|
||||
toggleFaultPoint(faultPoint) {
|
||||
if (!this.currentFaultPart) return
|
||||
const selectedFaultPointIds = this.currentFaultPart.selectedFaultPointIds || []
|
||||
const index = selectedFaultPointIds.indexOf(faultPoint.id)
|
||||
if (index === -1) {
|
||||
selectedFaultPointIds.push(faultPoint.id)
|
||||
} else {
|
||||
selectedFaultPointIds.splice(index, 1)
|
||||
}
|
||||
this.currentFaultPart.selectedFaultPointIds = [...selectedFaultPointIds]
|
||||
this.syncFaultPointRemark()
|
||||
},
|
||||
clearCurrentPartFaults() {
|
||||
if (!this.currentFaultPart) return
|
||||
this.currentFaultPart.selectedFaultPointIds = []
|
||||
this.syncFaultPointRemark()
|
||||
},
|
||||
syncFaultPointRemark() {
|
||||
const summaries = this.form.parts
|
||||
.map((part, index) => {
|
||||
const summary = this.getPartFaultSummary(part)
|
||||
if (!summary) return ''
|
||||
return `备件${index + 1}:${summary}`
|
||||
})
|
||||
.filter(Boolean)
|
||||
this.form.remark = summaries.join(';')
|
||||
},
|
||||
async reloadFaultPointOptions() {
|
||||
const categoryIds = [...new Set(this.form.parts.map((part) => String(part.categoryId || '')).filter(Boolean))].sort()
|
||||
const requestKey = categoryIds.join(',')
|
||||
this.faultPointRequestKey = requestKey
|
||||
if (!categoryIds.length) {
|
||||
this.faultPointOptions = []
|
||||
this.form.parts = this.form.parts.map((part) => ({
|
||||
...part,
|
||||
selectedFaultPointIds: []
|
||||
}))
|
||||
this.syncFaultPointRemark()
|
||||
return
|
||||
}
|
||||
|
||||
this.loadingFaultPoints = true
|
||||
try {
|
||||
const items = await getHardwareFaultDescriptionOptions(categoryIds)
|
||||
if (this.faultPointRequestKey !== requestKey) return
|
||||
this.faultPointOptions = (Array.isArray(items) ? items : []).map((item) => ({
|
||||
id: String(item.id),
|
||||
categoryId: String(item.category_id),
|
||||
description: String(item.description || ''),
|
||||
useCount: Number(item.use_count || 0)
|
||||
}))
|
||||
const availableIds = new Set(this.faultPointOptions.map((item) => item.id))
|
||||
this.form.parts = this.form.parts.map((part) => ({
|
||||
...part,
|
||||
selectedFaultPointIds: (part.selectedFaultPointIds || []).filter((id) => availableIds.has(id))
|
||||
}))
|
||||
this.syncFaultPointRemark()
|
||||
} catch (error) {
|
||||
if (this.faultPointRequestKey === requestKey) this.faultPointOptions = []
|
||||
} finally {
|
||||
if (this.faultPointRequestKey === requestKey) this.loadingFaultPoints = false
|
||||
}
|
||||
},
|
||||
async removePart(index) {
|
||||
if (this.form.parts[index] && this.form.parts[index].id === this.currentFaultPartId) {
|
||||
this.closeFaultPointPopup()
|
||||
}
|
||||
this.form.parts.splice(index, 1)
|
||||
await this.reloadFaultPointOptions()
|
||||
},
|
||||
changePartQuantity(part, delta) {
|
||||
const current = Math.max(1, Number.parseInt(part.quantity, 10) || 1)
|
||||
part.quantity = String(Math.max(1, current + delta))
|
||||
},
|
||||
async addPartByCode(code) {
|
||||
const productCode = String(code || '').trim()
|
||||
if (!productCode) return
|
||||
if (this.form.parts.some((part) => part.code === productCode)) {
|
||||
uni.showToast({ title: '该物料已添加', icon: 'none' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const material = await lookupMaterial(productCode)
|
||||
this.form.parts.push({
|
||||
id: `${Date.now()}-${productCode}`,
|
||||
name: material.product_name,
|
||||
code: material.product_code || productCode,
|
||||
categoryId: String(material.category_id || ''),
|
||||
categoryName: String(material.category_name || ''),
|
||||
selectedFaultPointIds: [],
|
||||
source: 'new',
|
||||
quantity: '1'
|
||||
})
|
||||
await this.reloadFaultPointOptions()
|
||||
} catch (error) {
|
||||
// 请求层已显示服务端错误提示,避免重复弹出。
|
||||
}
|
||||
},
|
||||
scanPart() {
|
||||
if (this.scanning || this.submitting) return
|
||||
// #ifdef APP-PLUS
|
||||
this.scanning = true
|
||||
openCodeScan({
|
||||
smartZoom: true,
|
||||
vibrate: true,
|
||||
scanRegion: getScanRegionSetting(),
|
||||
success: (result) => {
|
||||
const value = String(result?.content || '').trim()
|
||||
if (value) this.addPartByCode(value)
|
||||
},
|
||||
fail: (error) => this.handleScanFail(error),
|
||||
complete: () => {
|
||||
this.scanning = false
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
uni.showToast({ title: '请在 Android App 中使用扫码功能', icon: 'none' })
|
||||
// #endif
|
||||
},
|
||||
inputPart() {
|
||||
this.partInputCode = ''
|
||||
this.partInputVisible = true
|
||||
},
|
||||
closePartInput() {
|
||||
this.partInputVisible = false
|
||||
},
|
||||
async confirmPartInput() {
|
||||
const productCode = this.partInputCode.trim()
|
||||
if (!productCode) {
|
||||
uni.showToast({ title: '请输入商品编码', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.partInputVisible = false
|
||||
await this.addPartByCode(productCode)
|
||||
},
|
||||
saveDraft() {
|
||||
uni.showToast({ title: '维修草稿已保存', icon: 'success' })
|
||||
},
|
||||
completeRepair() {
|
||||
if (this.submitting) return
|
||||
if (!this.form.deviceNo.trim()) {
|
||||
uni.showToast({ title: '请先扫描或录入设备', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.repairTask?.id) {
|
||||
uni.showToast({ title: '请先加载有效的维修任务', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!this.form.result) {
|
||||
uni.showToast({ title: '请选择维修判定', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (this.form.result === 'operation_defect' && !this.form.responsibleOperatorIds.length) {
|
||||
uni.showToast({ title: '请选择责任人员', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (this.form.result === 'assembly_mismatch' && !this.form.mismatchCategoryIds.length) {
|
||||
uni.showToast({ title: '请选择搭配错误分类', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const user = uni.getStorageSync('scanapp_user') || {}
|
||||
const userCode = String(user.username || user.user_code || '').trim()
|
||||
if (!userCode) {
|
||||
uni.showToast({ title: '登录信息已失效,请重新登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '确认完成维修',
|
||||
content: `确定提交设备 ${this.form.deviceNo} 的维修记录吗?`,
|
||||
confirmText: '确认完成',
|
||||
success: async (res) => {
|
||||
if (!res.confirm || this.submitting) return
|
||||
this.submitting = true
|
||||
try {
|
||||
const uploadedImages = []
|
||||
for (const filePath of this.form.images) {
|
||||
uploadedImages.push(await uploadImage(filePath, 'repair'))
|
||||
}
|
||||
const payload = {
|
||||
exception_id: this.repairTask.id,
|
||||
user_code: userCode,
|
||||
repair_result: this.form.result,
|
||||
images: uploadedImages,
|
||||
parts: this.form.parts.map((part) => ({
|
||||
product_code: String(part.code || '').trim(),
|
||||
source: part.source || 'new',
|
||||
quantity: Math.max(1, Number.parseInt(part.quantity, 10) || 1),
|
||||
fault_description_ids: (part.selectedFaultPointIds || []).map((id) => String(id))
|
||||
})),
|
||||
operation_fault_steps: this.form.responsibleOperatorIds,
|
||||
responsible_operators: this.form.responsibleOperatorIds,
|
||||
remark: this.form.remark.trim()
|
||||
}
|
||||
if (this.form.result === 'assembly_mismatch') {
|
||||
payload.category_ids = this.form.mismatchCategoryIds
|
||||
}
|
||||
await submitRepairRecord(payload)
|
||||
uni.showToast({ title: '维修记录已完成', icon: 'success' })
|
||||
this.repairTask = null
|
||||
this.orderScanRecords = []
|
||||
this.faultPointOptions = []
|
||||
this.form.mismatchCategoryIds = []
|
||||
this.form = {
|
||||
deviceNo: '',
|
||||
result: 'material_defect',
|
||||
remark: '',
|
||||
parts: [],
|
||||
responsibleOperatorIds: [],
|
||||
mismatchCategoryIds: [],
|
||||
images: []
|
||||
}
|
||||
this.loadRepairStatistics()
|
||||
} catch (error) {
|
||||
console.error('[scanapp] 维修记录提交失败', error)
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.repair-page {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx 24rpx 210rpx;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(180deg, #eaf4ff 0%, #f4f6fb 24%, #f4f6fb 100%);
|
||||
}
|
||||
|
||||
.repair-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 8rpx 28rpx;
|
||||
}
|
||||
|
||||
.hero-copy text { display: block; }
|
||||
.hero-tag { color: #1677ff; font-size: 20rpx; line-height: 1.35; }
|
||||
.hero-title { margin-top: 6rpx; color: #17233d; font-size: 38rpx; font-weight: 800; line-height: 1.35; }
|
||||
.hero-subtitle { margin-top: 8rpx; color: #7d899d; font-size: 20rpx; line-height: 1.4; }
|
||||
.hero-icon { display: flex; align-items: center; justify-content: center; width: 88rpx; height: 88rpx; border-radius: 28rpx; background: linear-gradient(135deg, #2f6cf6 0%, #35bfa3 100%); box-shadow: 0 16rpx 28rpx rgba(47, 108, 246, 0.2); }
|
||||
|
||||
.summary-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12rpx; margin-bottom: 18rpx; }
|
||||
.summary-item { position: relative; padding: 18rpx 12rpx; border-radius: 20rpx; background: #ffffff; text-align: center; box-shadow: 0 8rpx 24rpx rgba(36, 59, 92, 0.05); }
|
||||
.summary-arrow { position: absolute; top: 50%; right: 12rpx; display: flex; align-items: center; justify-content: center; width: 34rpx; height: 34rpx; border-radius: 50%; background: #edf4ff; transform: translateY(-50%); }
|
||||
.summary-value { display: block; font-size: 32rpx; font-weight: 800; line-height: 1.2; }
|
||||
.summary-value.warning { color: #f2a93b; }
|
||||
.summary-value.primary { color: #1677ff; }
|
||||
.summary-value.success { color: #26b36c; }
|
||||
.summary-label { display: block; margin-top: 8rpx; color: #8a94a6; font-size: 18rpx; line-height: 1.35; }
|
||||
|
||||
.section-card { margin-bottom: 18rpx; padding: 24rpx; border-radius: 24rpx; background: #ffffff; box-shadow: 0 8rpx 24rpx rgba(36, 59, 92, 0.05); }
|
||||
.section-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 14rpx; }
|
||||
.section-title, .section-desc { display: block; }
|
||||
.section-title { color: #17233d; font-size: 28rpx; font-weight: 700; line-height: 1.35; }
|
||||
.section-desc { margin-top: 8rpx; color: #9aa4b4; font-size: 20rpx; line-height: 1.35; }
|
||||
.required-tag, .optional-tag, .order-time { flex: none; font-size: 18rpx; line-height: 1.35; }
|
||||
.required-tag { padding: 6rpx 12rpx; border-radius: 999rpx; color: #1677ff; background: #edf4ff; }
|
||||
.optional-tag, .order-time { color: #9aa4b4; }
|
||||
|
||||
.scan-input-row { display: flex; align-items: center; margin-top: 18rpx; }
|
||||
.scan-input { display: flex; align-items: center; flex: 1; min-width: 0; padding: 0 14rpx; border: 1rpx solid #e7edf5; border-radius: 18rpx; background: #f8fbff; }
|
||||
.scan-button { display: flex; align-items: center; justify-content: center; gap: 8rpx; width: 116rpx; height: 78rpx; margin-left: 14rpx; border-radius: 18rpx; color: #ffffff; background: linear-gradient(135deg, #2f6cf6 0%, #35bfa3 100%); font-size: 22rpx; flex: none; }
|
||||
.device-found { display: flex; align-items: center; margin-top: 16rpx; padding: 16rpx; border-radius: 18rpx; background: #f0fbf6; }
|
||||
.device-found-icon { display: flex; align-items: center; justify-content: center; width: 46rpx; height: 46rpx; margin-right: 12rpx; border-radius: 50%; background: #26b36c; }
|
||||
.device-found-info { flex: 1; min-width: 0; }
|
||||
.device-found-title, .device-found-value { display: block; }
|
||||
.device-found-title { color: #26b36c; font-size: 18rpx; line-height: 1.35; }
|
||||
.device-found-value { margin-top: 4rpx; color: #17233d; font-size: 24rpx; font-weight: 700; word-break: break-all; }
|
||||
.device-found-status { color: #f2a93b; font-size: 18rpx; }
|
||||
|
||||
.exception-info { margin-top: 18rpx; padding: 4rpx 18rpx; border-radius: 18rpx; background: #f8fafc; }
|
||||
.exception-empty { margin-top: 18rpx; padding: 24rpx 18rpx; border-radius: 18rpx; color: #9aa4b4; background: #f8fafc; font-size: 20rpx; text-align: center; }
|
||||
.info-row { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 0; border-bottom: 1rpx solid #edf0f5; }
|
||||
.info-row:last-child { border-bottom: 0; }
|
||||
.info-label { color: #8a94a6; font-size: 20rpx; }
|
||||
.info-value { max-width: 62%; color: #2b3853; font-size: 22rpx; font-weight: 600; text-align: right; word-break: break-all; }
|
||||
|
||||
.result-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12rpx; margin-top: 18rpx; }
|
||||
.result-item { padding: 16rpx 8rpx; border: 2rpx solid #edf0f5; border-radius: 18rpx; text-align: center; }
|
||||
.result-item.active { border-color: #2f6cf6; background: #f4f8ff; }
|
||||
.result-icon { display: flex; align-items: center; justify-content: center; width: 56rpx; height: 56rpx; margin: 0 auto 10rpx; border-radius: 18rpx; }
|
||||
.result-label { display: block; color: #2b3853; font-size: 22rpx; font-weight: 700; line-height: 1.35; }
|
||||
.result-desc { display: block; margin-top: 6rpx; color: #9aa4b4; font-size: 18rpx; line-height: 1.35; }
|
||||
|
||||
.repair-actions-tip { margin-top: 18rpx; color: #9aa4b4; font-size: 20rpx; }
|
||||
.responsibility-title { display: block; margin-top: 20rpx; color: #2b3853; font-size: 22rpx; font-weight: 700; }
|
||||
.responsibility-list { display: flex; flex-wrap: wrap; margin-top: 12rpx; }
|
||||
.responsibility-item { display: flex; align-items: center; min-width: 44%; margin: 0 12rpx 12rpx 0; padding: 14rpx; border: 2rpx solid #e7edf5; border-radius: 14rpx; color: #56647a; background: #f8fafc; font-size: 20rpx; }
|
||||
.responsibility-item.active { border-color: #1677ff; color: #1677ff; background: #edf4ff; }
|
||||
.responsibility-item > view { margin-left: 8rpx; min-width: 0; }
|
||||
.responsibility-name, .responsibility-meta { display: block; }
|
||||
.responsibility-name { color: #2b3853; font-size: 20rpx; font-weight: 600; line-height: 1.35; }
|
||||
.responsibility-meta { margin-top: 4rpx; color: #9aa4b4; font-size: 17rpx; line-height: 1.35; }
|
||||
.repair-action-list { display: flex; flex-wrap: wrap; margin-top: 12rpx; }
|
||||
.repair-action-item { display: flex; align-items: center; min-height: 64rpx; margin: 0 12rpx 12rpx 0; padding: 0 18rpx; border: 2rpx solid #e7edf5; border-radius: 14rpx; color: #56647a; background: #f8fafc; font-size: 22rpx; flex-shrink: 0; }
|
||||
.repair-action-item text { margin-left: 8rpx; }
|
||||
.repair-action-item.active { border-color: #1677ff; color: #1677ff; background: #edf4ff; }
|
||||
.repair-action-summary { margin-top: 2rpx; padding: 16rpx; border-radius: 14rpx; color: #2b3853; background: #f8fbff; font-size: 20rpx; line-height: 1.5; word-break: break-all; }
|
||||
.fault-popup { max-height: 78vh; padding: 28rpx 24rpx calc(24rpx + env(safe-area-inset-bottom)); box-sizing: border-box; background: #ffffff; }
|
||||
.fault-popup-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16rpx; }
|
||||
.fault-popup-title, .fault-popup-subtitle { display: block; }
|
||||
.fault-popup-title { color: #17233d; font-size: 28rpx; font-weight: 700; line-height: 1.35; }
|
||||
.fault-popup-subtitle { margin-top: 8rpx; color: #7d899d; font-size: 20rpx; line-height: 1.35; }
|
||||
.fault-popup-close { color: #1677ff; font-size: 22rpx; line-height: 1.35; flex-shrink: 0; }
|
||||
.fault-popup-scroll { max-height: 48vh; margin-top: 20rpx; }
|
||||
.fault-popup-list { margin-top: 0; }
|
||||
.fault-popup-footer { display: flex; align-items: center; margin-top: 12rpx; }
|
||||
.fault-popup-button { display: flex; align-items: center; justify-content: center; height: 78rpx; border-radius: 18rpx; font-size: 24rpx; font-weight: 700; }
|
||||
.fault-popup-clear { width: 30%; color: #56647a; background: #f0f3f8; }
|
||||
.fault-popup-confirm { flex: 1; margin-left: 14rpx; color: #ffffff; background: #1677ff; }
|
||||
.repair-record-header { align-items: center; }
|
||||
.parts-header { margin-top: 24rpx; }
|
||||
.parts-title { color: #2b3853; font-size: 24rpx; font-weight: 700; }
|
||||
.part-source-options { display: flex; align-items: center; width: fit-content; margin-top: 10rpx; overflow: hidden; border: 1rpx solid #dce6f3; border-radius: 12rpx; }
|
||||
.part-source-option { min-width: 88rpx; padding: 8rpx 14rpx; color: #7d899d; background: #ffffff; font-size: 18rpx; line-height: 1.35; text-align: center; }
|
||||
.part-source-option + .part-source-option { border-left: 1rpx solid #dce6f3; }
|
||||
.part-source-option.active { color: #ffffff; background: #1677ff; }
|
||||
.parts-actions { display: flex; align-items: center; flex: none; }
|
||||
.parts-action { display: flex; align-items: center; justify-content: center; width: 100rpx; height: 64rpx; border-radius: 16rpx; font-size: 24rpx; flex-shrink: 0; }
|
||||
.scan-part-action { color: #ffffff; background: #1677ff; }
|
||||
.input-part-action { margin-left: 16rpx; color: #1677ff; background: #edf4ff; }
|
||||
.part-row { display: flex; align-items: center; margin-top: 12rpx; padding: 12rpx; border-radius: 16rpx; background: #f8fafc; }
|
||||
.part-index { display: flex; align-items: center; justify-content: center; width: 42rpx; height: 42rpx; margin-right: 12rpx; border-radius: 50%; color: #1677ff; background: #eaf2ff; font-size: 20rpx; }
|
||||
.part-info { flex: 1; min-width: 0; }
|
||||
.part-name, .part-code, .part-category { display: block; }
|
||||
.part-name { color: #2b3853; font-size: 22rpx; font-weight: 600; }
|
||||
.part-code { margin-top: 4rpx; color: #9aa4b4; font-size: 18rpx; }
|
||||
.part-category { margin-top: 4rpx; color: #1677ff; font-size: 18rpx; }
|
||||
.part-fault-entry { display: flex; align-items: center; margin-top: 12rpx; padding: 14rpx 16rpx; border-radius: 14rpx; background: #ffffff; }
|
||||
.part-fault-entry-label { color: #7d899d; font-size: 20rpx; line-height: 1.35; flex-shrink: 0; }
|
||||
.part-fault-entry-value { margin: 0 12rpx; color: #2b3853; font-size: 20rpx; line-height: 1.4; flex: 1; min-width: 0; word-break: break-all; }
|
||||
.part-stepper { display: flex; align-items: center; width: 172rpx; height: 52rpx; overflow: hidden; border: 1rpx solid #dce6f3; border-radius: 12rpx; background: #ffffff; flex: none; }
|
||||
.stepper-button { display: flex; align-items: center; justify-content: center; width: 52rpx; height: 52rpx; background: #f8fafc; flex: none; }
|
||||
.stepper-button + .stepper-value { border-left: 1rpx solid #dce6f3; }
|
||||
.stepper-value { display: flex; align-items: center; justify-content: center; height: 52rpx; color: #2b3853; font-size: 22rpx; font-weight: 600; flex: 1; }
|
||||
.stepper-value + .stepper-button { border-left: 1rpx solid #dce6f3; }
|
||||
.part-remove { display: flex; align-items: center; justify-content: center; width: 52rpx; height: 52rpx; margin-left: 10rpx; flex: none; }
|
||||
.part-input-dialog { width: 580rpx; padding: 32rpx; box-sizing: border-box; background: #ffffff; }
|
||||
.part-input-title { display: block; margin-bottom: 24rpx; color: #17233d; font-size: 28rpx; font-weight: 700; }
|
||||
.part-input-actions { display: flex; justify-content: flex-end; margin-top: 28rpx; }
|
||||
.part-input-cancel, .part-input-confirm { padding: 14rpx 28rpx; border-radius: 12rpx; font-size: 24rpx; flex-shrink: 0; }
|
||||
.part-input-cancel { color: #56647a; background: #f0f3f8; }
|
||||
.part-input-confirm { margin-left: 16rpx; color: #ffffff; background: #1677ff; }
|
||||
|
||||
.evidence-row { display: flex; flex-wrap: nowrap; align-items: flex-start; overflow-x: auto; margin-top: 18rpx; padding-bottom: 4rpx; }
|
||||
.evidence-row > * { margin-right: 14rpx; margin-bottom: 14rpx; }
|
||||
.upload-box, .evidence-image { display: flex; flex-direction: column; align-items: center; justify-content: center; width: 144rpx; height: 144rpx; border-radius: 20rpx; font-size: 20rpx; }
|
||||
.upload-box { border: 2rpx dashed #ffb47e; color: #ff8a3d; background: #fffaf6; }
|
||||
.evidence-image { position: relative; overflow: hidden; color: #ffffff; background: #b7c6d8; }
|
||||
.evidence-image image { width: 100%; height: 100%; }
|
||||
.upload-label { position: absolute; right: 8rpx; bottom: 8rpx; padding: 4rpx 8rpx; border-radius: 8rpx; 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); }
|
||||
|
||||
.bottom-actions { position: fixed; right: 0; bottom: 0; left: 0; z-index: 10; display: flex; padding: 22rpx 24rpx calc(22rpx + env(safe-area-inset-bottom)); background: rgba(255, 255, 255, 0.98); box-shadow: 0 -8rpx 24rpx rgba(36, 59, 92, 0.08); }
|
||||
.draft-button, .complete-button { display: flex; align-items: center; justify-content: center; height: 82rpx; border-radius: 20rpx; font-size: 24rpx; font-weight: 700; flex-shrink: 0; }
|
||||
.draft-button { width: 30%; color: #56647a; background: #f0f3f8; }
|
||||
.complete-button { flex: 1; margin-left: 14rpx; color: #ffffff; background: linear-gradient(135deg, #2f6cf6 0%, #35bfa3 100%); }
|
||||
.theme-dark.repair-page {
|
||||
background: #151923;
|
||||
}
|
||||
|
||||
.theme-dark .summary-item,
|
||||
.theme-dark .section-card,
|
||||
.theme-dark .evidence-panel,
|
||||
.theme-dark .remark-panel,
|
||||
.theme-dark .fault-popup,
|
||||
.theme-dark .part-input-dialog,
|
||||
.theme-dark .bottom-actions {
|
||||
border-color: #364152;
|
||||
background: #222936;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-dark .scan-input,
|
||||
.theme-dark .device-found,
|
||||
.theme-dark .exception-info,
|
||||
.theme-dark .exception-empty,
|
||||
.theme-dark .result-item,
|
||||
.theme-dark .responsibility-item,
|
||||
.theme-dark .repair-action-item,
|
||||
.theme-dark .repair-action-summary,
|
||||
.theme-dark .part-row,
|
||||
.theme-dark .part-fault-entry,
|
||||
.theme-dark .part-source-option,
|
||||
.theme-dark .upload-box {
|
||||
border-color: #364152;
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .section-title,
|
||||
.theme-dark .device-found-value,
|
||||
.theme-dark .info-value,
|
||||
.theme-dark .result-label,
|
||||
.theme-dark .responsibility-title,
|
||||
.theme-dark .responsibility-name,
|
||||
.theme-dark .part-name,
|
||||
.theme-dark .part-fault-entry-value,
|
||||
.theme-dark .fault-popup-title,
|
||||
.theme-dark .part-input-title {
|
||||
color: #f2f5fa;
|
||||
}
|
||||
|
||||
.theme-dark .section-desc,
|
||||
.theme-dark .info-label,
|
||||
.theme-dark .result-desc,
|
||||
.theme-dark .responsibility-meta,
|
||||
.theme-dark .repair-actions-tip,
|
||||
.theme-dark .part-code,
|
||||
.theme-dark .part-category,
|
||||
.theme-dark .part-fault-entry-label,
|
||||
.theme-dark .fault-popup-subtitle {
|
||||
color: #aeb9ca;
|
||||
}
|
||||
|
||||
.theme-dark .info-row,
|
||||
.theme-dark .record-item,
|
||||
.theme-dark .part-source-option + .part-source-option,
|
||||
.theme-dark .stepper-button + .stepper-value,
|
||||
.theme-dark .stepper-value + .stepper-button {
|
||||
border-color: #364152;
|
||||
}
|
||||
|
||||
.theme-dark .part-stepper,
|
||||
.theme-dark .stepper-button {
|
||||
border-color: #364152;
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.u-input__input),
|
||||
.theme-dark :deep(textarea) {
|
||||
color: #f2f5fa !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<view class="repair-tasks-page" :class="`theme-${themeMode}`">
|
||||
<view class="page-header">
|
||||
<view>
|
||||
<text class="page-title">待维修订单</text>
|
||||
<text class="page-subtitle">选择订单查看异常详情并进入维修操作</text>
|
||||
</view>
|
||||
<view class="task-count">{{ tasks.length }} 条</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loading" class="loading-card">
|
||||
<u-loading mode="circle" color="#1677ff" />
|
||||
<text>正在加载待维修订单</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="tasks.length" class="task-list">
|
||||
<view v-for="task in tasks" :key="task.id" class="task-card" @tap="openTaskDetail(task)">
|
||||
<view class="task-card-header">
|
||||
<view class="task-no-wrap">
|
||||
<u-icon name="file-text" color="#1677ff" size="28" />
|
||||
<text class="task-no">{{ task.deviceNo }}</text>
|
||||
</view>
|
||||
<text class="task-status">待维修</text>
|
||||
</view>
|
||||
<view class="task-main-row"><text>异常类型</text><text>{{ task.typeName || '-' }}</text></view>
|
||||
<view class="task-main-row"><text>异常原因</text><text>{{ task.reasonText || '-' }}</text></view>
|
||||
<view class="task-footer">
|
||||
<text>{{ task.reportedDate || '-' }}</text>
|
||||
<text>{{ task.reporterText || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<u-empty v-else mode="data" text="暂无待维修订单" />
|
||||
|
||||
<view v-if="selectedTask" class="detail-mask" @tap="closeTaskDetail">
|
||||
<view class="detail-sheet" @tap.stop>
|
||||
<view class="sheet-handle"></view>
|
||||
<view class="detail-header">
|
||||
<text>待维修订单详情</text>
|
||||
<u-icon name="close" color="#7d8798" size="30" @tap="closeTaskDetail" />
|
||||
</view>
|
||||
<text class="detail-device-no">{{ selectedTask.deviceNo }}</text>
|
||||
<view class="detail-list">
|
||||
<view class="detail-item"><text>异常类型</text><text>{{ selectedTask.typeName || '-' }}</text></view>
|
||||
<view class="detail-item"><text>异常原因</text><text>{{ selectedTask.reasonText || '-' }}</text></view>
|
||||
<view class="detail-item"><text>提报人员</text><text>{{ selectedTask.reporterText || '-' }}</text></view>
|
||||
<view class="detail-item"><text>提报时间</text><text>{{ selectedTask.reportedDate || '-' }}</text></view>
|
||||
</view>
|
||||
<view class="repair-button" @tap="startRepair">开始维修</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getRepairTasks } from '@/api/produce/exception.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
tasks: [],
|
||||
selectedTask: null
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadTasks()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.loadTasks().finally(() => uni.stopPullDownRefresh())
|
||||
},
|
||||
methods: {
|
||||
async loadTasks() {
|
||||
this.loading = true
|
||||
try {
|
||||
const result = await getRepairTasks({ limit: 100 })
|
||||
const items = Array.isArray(result) ? result : (Array.isArray(result?.data) ? result.data : [])
|
||||
this.tasks = items.map((item) => ({
|
||||
id: String(item.id || ''),
|
||||
deviceNo: String(item.device_no || ''),
|
||||
typeName: String(item.type_name || ''),
|
||||
reasonText: [item.reason_name, item.issue_name].filter(Boolean).join(' · '),
|
||||
reporterText: item.reporter ? `${item.reporter}${item.user_code ? `(${item.user_code})` : ''}` : String(item.user_code || ''),
|
||||
reportedDate: String(item.reported_date || '')
|
||||
})).filter((item) => item.id && item.deviceNo)
|
||||
} catch (error) {
|
||||
this.tasks = []
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
openTaskDetail(task) {
|
||||
this.selectedTask = task
|
||||
},
|
||||
closeTaskDetail() {
|
||||
this.selectedTask = null
|
||||
},
|
||||
startRepair() {
|
||||
if (!this.selectedTask?.deviceNo) return
|
||||
uni.navigateTo({
|
||||
url: `/pages/repair/index?device_no=${encodeURIComponent(this.selectedTask.deviceNo)}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.repair-tasks-page { min-height: 100vh; padding: 24rpx; box-sizing: border-box; background: #f4f6fb; }
|
||||
.page-header { display: flex; align-items: flex-start; justify-content: space-between; padding: 8rpx 8rpx 26rpx; }
|
||||
.page-title, .page-subtitle { display: block; }
|
||||
.page-title { color: #17233d; font-size: 38rpx; font-weight: 800; line-height: 1.35; }
|
||||
.page-subtitle { margin-top: 8rpx; color: #7d899d; font-size: 20rpx; line-height: 1.4; }
|
||||
.task-count { padding: 8rpx 14rpx; border-radius: 999rpx; color: #1677ff; background: #eaf2ff; font-size: 20rpx; line-height: 1.35; }
|
||||
.loading-card { display: flex; flex-direction: column; align-items: center; padding: 80rpx 0; color: #7d899d; font-size: 20rpx; }
|
||||
.loading-card text { margin-top: 18rpx; }
|
||||
.task-card { margin-bottom: 16rpx; padding: 22rpx; border-radius: 22rpx; background: #ffffff; box-shadow: 0 8rpx 24rpx rgba(36, 59, 92, 0.05); }
|
||||
.task-card-header, .task-no-wrap, .task-footer, .detail-header, .detail-item { display: flex; align-items: center; }
|
||||
.task-card-header, .detail-header, .detail-item { justify-content: space-between; }
|
||||
.task-no-wrap { min-width: 0; }
|
||||
.task-no { margin-left: 10rpx; color: #17233d; font-size: 26rpx; font-weight: 700; word-break: break-all; }
|
||||
.task-status { padding: 6rpx 12rpx; border-radius: 999rpx; color: #e58b12; background: #fff5e8; font-size: 18rpx; line-height: 1.35; flex-shrink: 0; }
|
||||
.task-main-row { display: flex; justify-content: space-between; margin-top: 18rpx; color: #7d899d; font-size: 20rpx; line-height: 1.45; }
|
||||
.task-main-row text:last-child { max-width: 68%; color: #2b3853; text-align: right; word-break: break-all; }
|
||||
.task-footer { justify-content: space-between; margin-top: 20rpx; padding-top: 16rpx; border-top: 1rpx solid #edf0f5; color: #9aa4b4; font-size: 18rpx; }
|
||||
.detail-mask { position: fixed; z-index: 99; top: 0; right: 0; bottom: 0; left: 0; display: flex; align-items: flex-end; background: rgba(22, 35, 61, 0.45); }
|
||||
.detail-sheet { width: 100%; padding: 18rpx 24rpx calc(30rpx + env(safe-area-inset-bottom)); box-sizing: border-box; border-radius: 28rpx 28rpx 0 0; background: #ffffff; }
|
||||
.sheet-handle { width: 72rpx; height: 8rpx; margin: 0 auto 22rpx; border-radius: 999rpx; background: #dce3ee; }
|
||||
.detail-header { color: #17233d; font-size: 28rpx; font-weight: 700; }
|
||||
.detail-device-no { display: block; margin-top: 22rpx; color: #1677ff; font-size: 32rpx; font-weight: 800; word-break: break-all; }
|
||||
.detail-list { margin-top: 18rpx; padding: 2rpx 18rpx; border-radius: 18rpx; background: #f8fafc; }
|
||||
.detail-item { padding: 18rpx 0; border-bottom: 1rpx solid #e8edf4; color: #7d899d; font-size: 20rpx; line-height: 1.4; }
|
||||
.detail-item:last-child { border-bottom: 0; }
|
||||
.detail-item text:last-child { max-width: 64%; color: #2b3853; text-align: right; word-break: break-all; }
|
||||
.repair-button { display: flex; align-items: center; justify-content: center; height: 84rpx; margin-top: 24rpx; border-radius: 18rpx; color: #ffffff; background: #1677ff; font-size: 24rpx; font-weight: 700; }
|
||||
.theme-dark.repair-tasks-page {
|
||||
background: #151923;
|
||||
}
|
||||
|
||||
.theme-dark .task-card,
|
||||
.theme-dark .detail-sheet {
|
||||
border-color: #364152;
|
||||
background: #222936;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-dark .task-no,
|
||||
.theme-dark .task-main-row text:last-child,
|
||||
.theme-dark .detail-header,
|
||||
.theme-dark .detail-item text:last-child {
|
||||
color: #f2f5fa;
|
||||
}
|
||||
|
||||
.theme-dark .page-subtitle,
|
||||
.theme-dark .loading-card,
|
||||
.theme-dark .task-main-row,
|
||||
.theme-dark .task-footer,
|
||||
.theme-dark .detail-item {
|
||||
color: #aeb9ca;
|
||||
}
|
||||
|
||||
.theme-dark .task-footer,
|
||||
.theme-dark .detail-item {
|
||||
border-color: #364152;
|
||||
}
|
||||
|
||||
.theme-dark .detail-list {
|
||||
background: #1c2431;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,615 @@
|
||||
<template>
|
||||
<view class="statistics-page" :class="`theme-${themeMode}`">
|
||||
<view class="hero-card">
|
||||
<view class="hero-main">
|
||||
<text class="hero-title">个人统计</text>
|
||||
<text class="hero-subtitle">查看当前登录用户的扫码与处理数据</text>
|
||||
</view>
|
||||
<view class="hero-avatar">
|
||||
<u-icon name="account-fill" color="#ffffff" size="44" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="profile-card">
|
||||
<view class="profile-row">
|
||||
<view class="profile-badge">{{ profile.userCode }}</view>
|
||||
<view class="profile-meta">
|
||||
<text class="profile-name">{{ profile.name }}</text>
|
||||
<text class="profile-desc">岗位:{{ profile.position }} · 班组:{{ profile.team }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-extra">
|
||||
<view class="profile-chip">{{ profile.department }}</view>
|
||||
<view class="profile-chip success">{{ profile.level }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="metric-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">核心指标</text>
|
||||
<text class="section-subtitle">近 30 天累计</text>
|
||||
</view>
|
||||
<view class="metric-grid">
|
||||
<view v-for="item in metrics" :key="item.label" class="metric-item">
|
||||
<text class="metric-value" :class="item.type">{{ item.value }}</text>
|
||||
<text class="metric-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="trend-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">个人趋势</text>
|
||||
<text class="section-subtitle">最近 7 天</text>
|
||||
</view>
|
||||
<view class="bar-chart">
|
||||
<view v-for="item in trendData" :key="item.label" class="bar-item">
|
||||
<view class="bar-track">
|
||||
<view class="bar-fill" :style="{ height: item.percent + '%', backgroundColor: item.color }"></view>
|
||||
</view>
|
||||
<text class="bar-label">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="record-card">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最近明细</text>
|
||||
<text class="section-subtitle">最新 5 条</text>
|
||||
</view>
|
||||
<view v-for="item in recentRecords" :key="item.id" class="record-item">
|
||||
<view class="record-top">
|
||||
<view>
|
||||
<text class="record-main">{{ item.title }}</text>
|
||||
<text class="record-sub">{{ item.subtitle }}</text>
|
||||
</view>
|
||||
<view class="record-pill" :class="item.statusClass">{{ item.status }}</view>
|
||||
</view>
|
||||
<view class="record-bottom">
|
||||
<text>{{ item.time }}</text>
|
||||
<text>{{ item.count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="split-card">
|
||||
<view class="split-section">
|
||||
<view class="section-header compact">
|
||||
<text class="section-title">扫码类型占比</text>
|
||||
</view>
|
||||
<view v-for="item in typeRate" :key="item.label" class="progress-row">
|
||||
<view class="progress-info">
|
||||
<text class="progress-label">{{ item.label }}</text>
|
||||
<text class="progress-value">{{ item.value }}%</text>
|
||||
</view>
|
||||
<view class="progress-track">
|
||||
<view class="progress-fill" :style="{ width: item.value + '%', backgroundColor: item.color }"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="split-section">
|
||||
<view class="section-header compact">
|
||||
<text class="section-title">异常统计</text>
|
||||
</view>
|
||||
<view v-for="item in exceptionData" :key="item.label" class="progress-row">
|
||||
<view class="progress-info">
|
||||
<text class="progress-label">{{ item.label }}</text>
|
||||
<text class="progress-value">{{ item.value }}%</text>
|
||||
</view>
|
||||
<view class="progress-track">
|
||||
<view class="progress-fill" :style="{ width: item.value + '%', backgroundColor: item.color }"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUserInfo } from '@/api/member.js'
|
||||
import { getOrderRecentRecords, getOrderStatistics, getProductRecentRecords, getProductStatistics } from '@/api/produce/data-entry.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
profile: {
|
||||
userCode: 'U001',
|
||||
name: '-',
|
||||
position: '-',
|
||||
team: '-',
|
||||
department: '-',
|
||||
level: '-'
|
||||
},
|
||||
metrics: [
|
||||
{ label: '今日扫码', value: '0', type: 'primary' },
|
||||
{ label: '本月累计', value: '0', type: 'success' },
|
||||
{ label: '成功率', value: '0%', type: 'success' },
|
||||
{ label: '异常数', value: '0', type: 'danger' }
|
||||
],
|
||||
trendData: [],
|
||||
recentRecords: [],
|
||||
typeRate: [
|
||||
{ label: '订单模式', value: 0, color: '#2f6cf6' },
|
||||
{ label: '整机模式', value: 0, color: '#35bfa3' },
|
||||
{ label: '异常记录', value: 0, color: '#f7b733' }
|
||||
],
|
||||
exceptionData: [
|
||||
{ label: '已处理', value: 0, color: '#19be6b' },
|
||||
{ label: '待复核', value: 0, color: '#f7b733' },
|
||||
{ label: '待处理', value: 0, color: '#e91f3f' }
|
||||
]
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadPageData()
|
||||
},
|
||||
methods: {
|
||||
getCurrentUserCode() {
|
||||
const user = uni.getStorageSync('scanapp_user') || {}
|
||||
return user.username || user.nickname || 'U001'
|
||||
},
|
||||
async loadPageData() {
|
||||
const userCode = this.getCurrentUserCode()
|
||||
try {
|
||||
const [profile, orderStats, productStats, orderRecent, productRecent] = await Promise.all([
|
||||
getUserInfo(),
|
||||
getOrderStatistics({ user_code: userCode }),
|
||||
getProductStatistics({ user_code: userCode }),
|
||||
getOrderRecentRecords({ user_code: userCode, limit: 3 }),
|
||||
getProductRecentRecords({ user_code: userCode, limit: 3 })
|
||||
])
|
||||
|
||||
this.profile = {
|
||||
userCode,
|
||||
name: profile.nickname || profile.username || '-',
|
||||
position: profile.position || profile.role_name || '-',
|
||||
team: profile.team_name || profile.group_name || '-',
|
||||
department: profile.department_name || '-',
|
||||
level: Number(profile.enabled) === 0 ? '已停用' : '稳定输出'
|
||||
}
|
||||
|
||||
const todayTotal = Number(orderStats?.today_total || 0) + Number(productStats?.today_total || 0)
|
||||
const successTotal = Number(orderStats?.success_total || 0) + Number(productStats?.success_total || 0)
|
||||
const failedTotal = Number(orderStats?.failed_total || 0) + Number(productStats?.failed_total || 0)
|
||||
const successRate = todayTotal > 0 ? ((successTotal / todayTotal) * 100).toFixed(1) : '0.0'
|
||||
this.metrics = [
|
||||
{ label: '今日扫码', value: String(todayTotal), type: 'primary' },
|
||||
{ label: '本月累计', value: String(todayTotal), type: 'success' },
|
||||
{ label: '成功率', value: `${successRate}%`, type: 'success' },
|
||||
{ label: '异常数', value: String(failedTotal), type: 'danger' }
|
||||
]
|
||||
|
||||
const orderItems = Array.isArray(orderRecent?.items) ? orderRecent.items : []
|
||||
const productItems = Array.isArray(productRecent?.items) ? productRecent.items : []
|
||||
this.recentRecords = [...orderItems, ...productItems].slice(0, 5).map((item, index) => ({
|
||||
id: item.id || index,
|
||||
title: item.order_no || item.process_order_no || '-',
|
||||
subtitle: item.process_name && item.step_name ? `${item.process_name} / ${item.step_name}` : '生产记录',
|
||||
status: Number(item.scan_result) === 0 ? '失败' : '成功',
|
||||
statusClass: Number(item.scan_result) === 0 ? 'failed' : 'success',
|
||||
time: item.scan_date || item.created_date || '-',
|
||||
count: item.quantity ? `+${item.quantity}` : '1'
|
||||
}))
|
||||
|
||||
this.typeRate = [
|
||||
{ label: '订单模式', value: orderItems.length, color: '#2f6cf6' },
|
||||
{ label: '整机模式', value: productItems.length, color: '#35bfa3' },
|
||||
{ label: '异常记录', value: failedTotal, color: '#f7b733' }
|
||||
]
|
||||
|
||||
this.exceptionData = [
|
||||
{ label: '已处理', value: successTotal, color: '#19be6b' },
|
||||
{ label: '待复核', value: failedTotal > 0 ? Math.min(failedTotal, 18) : 0, color: '#f7b733' },
|
||||
{ label: '待处理', value: failedTotal > 18 ? failedTotal - 18 : 0, color: '#e91f3f' }
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('[scanapp] 统计页数据加载失败', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.statistics-page {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx 22rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
background: #f5f7fb;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.profile-card,
|
||||
.metric-card,
|
||||
.trend-card,
|
||||
.record-card,
|
||||
.split-card {
|
||||
margin-top: 18rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 22rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 10rpx 26rpx rgba(23, 35, 61, 0.05);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 0;
|
||||
padding: 30rpx 26rpx;
|
||||
background: linear-gradient(135deg, #2f6cf6 0%, #35bfa3 100%);
|
||||
}
|
||||
|
||||
.hero-title,
|
||||
.hero-subtitle,
|
||||
.section-title,
|
||||
.section-subtitle,
|
||||
.profile-name,
|
||||
.profile-desc,
|
||||
.metric-label,
|
||||
.record-main,
|
||||
.record-sub,
|
||||
.record-bottom,
|
||||
.progress-label,
|
||||
.progress-value {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
color: #ffffff;
|
||||
font-size: 38rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
margin-top: 8rpx;
|
||||
color: rgba(255, 255, 255, 0.84);
|
||||
font-size: 22rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.hero-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
border-radius: 26rpx;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.profile-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.profile-badge {
|
||||
min-width: 98rpx;
|
||||
margin-right: 24rpx;
|
||||
padding: 18rpx 14rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(47, 108, 246, 0.1);
|
||||
color: #2f6cf6;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.profile-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
color: #17233d;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.profile-desc {
|
||||
margin-top: 6rpx;
|
||||
color: #7d8798;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.profile-extra {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
margin-top: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.profile-chip {
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #eef3fb;
|
||||
color: #6b778c;
|
||||
font-size: 18rpx;
|
||||
line-height: 1.35;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.profile-chip + .profile-chip {
|
||||
margin-left: 14rpx;
|
||||
}
|
||||
|
||||
.profile-chip.success {
|
||||
background: rgba(25, 190, 107, 0.12);
|
||||
color: #19be6b;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.section-header.compact {
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #17233d;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
color: #9aa4b4;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
padding: 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: #17233d;
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.metric-value.primary {
|
||||
color: #2f6cf6;
|
||||
}
|
||||
|
||||
.metric-value.success {
|
||||
color: #19be6b;
|
||||
}
|
||||
|
||||
.metric-value.danger {
|
||||
color: #e91f3f;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
margin-top: 6rpx;
|
||||
color: #7d8798;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 12rpx;
|
||||
align-items: end;
|
||||
height: 220rpx;
|
||||
}
|
||||
|
||||
.bar-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bar-track {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 170rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #f3f6fb;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar-fill {
|
||||
width: 72%;
|
||||
border-radius: 16rpx 16rpx 0 0;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
margin-top: 10rpx;
|
||||
color: #9aa4b4;
|
||||
font-size: 18rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
padding: 18rpx 0;
|
||||
border-bottom: 1rpx solid #eef1f6;
|
||||
}
|
||||
|
||||
.record-item:last-child {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.record-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.record-main {
|
||||
color: #17233d;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.record-sub {
|
||||
margin-top: 5rpx;
|
||||
color: #7d8798;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.record-pill {
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 18rpx;
|
||||
line-height: 1.35;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.record-pill.success {
|
||||
background: rgba(25, 190, 107, 0.12);
|
||||
color: #19be6b;
|
||||
}
|
||||
|
||||
.record-pill.failed {
|
||||
background: rgba(233, 31, 63, 0.12);
|
||||
color: #e91f3f;
|
||||
}
|
||||
|
||||
.record-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 10rpx;
|
||||
color: #9aa4b4;
|
||||
font-size: 18rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.split-card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.split-section {
|
||||
padding: 20rpx;
|
||||
border-radius: 18rpx;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.progress-row {
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.progress-label,
|
||||
.progress-value {
|
||||
color: #7d8798;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.progress-value {
|
||||
color: #17233d;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
height: 14rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #e9edf4;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
.theme-dark.statistics-page {
|
||||
background: #151923;
|
||||
}
|
||||
|
||||
.theme-dark .hero-card,
|
||||
.theme-dark .profile-card,
|
||||
.theme-dark .metric-card,
|
||||
.theme-dark .trend-card,
|
||||
.theme-dark .record-card,
|
||||
.theme-dark .split-card {
|
||||
border-color: #364152;
|
||||
background: #222936;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-dark .metric-item,
|
||||
.theme-dark .split-section,
|
||||
.theme-dark .profile-chip {
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .profile-name,
|
||||
.theme-dark .section-title,
|
||||
.theme-dark .metric-value,
|
||||
.theme-dark .record-main,
|
||||
.theme-dark .progress-value {
|
||||
color: #f2f5fa;
|
||||
}
|
||||
|
||||
.theme-dark .profile-desc,
|
||||
.theme-dark .section-subtitle,
|
||||
.theme-dark .metric-label,
|
||||
.theme-dark .bar-label,
|
||||
.theme-dark .record-sub,
|
||||
.theme-dark .record-bottom,
|
||||
.theme-dark .progress-label,
|
||||
.theme-dark .profile-chip {
|
||||
color: #aeb9ca;
|
||||
}
|
||||
|
||||
.theme-dark .record-item,
|
||||
.theme-dark .progress-track {
|
||||
border-color: #364152;
|
||||
}
|
||||
|
||||
.theme-dark .bar-track,
|
||||
.theme-dark .progress-track {
|
||||
background: #1c2431;
|
||||
}
|
||||
|
||||
.theme-dark .profile-badge {
|
||||
background: rgba(47, 108, 246, 0.2);
|
||||
}
|
||||
|
||||
.theme-dark .record-pill.success,
|
||||
.theme-dark .record-pill.failed {
|
||||
background: rgba(25, 190, 107, 0.18);
|
||||
}
|
||||
|
||||
.theme-dark .record-pill.failed {
|
||||
background: rgba(233, 31, 63, 0.18);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user