You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

614 lines
26 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="page-container">
<NavBar :title="t('equipmentLedger.moduleName')" :subTitle="t('equipmentLedger.subTitle')" />
<!-- 搜索区域 -->
<view class="search-card">
<view class="search-row">
<view class="search-input-wrap">
<text class="iconfont icon-search search-icon"></text>
<input v-model="searchKeyword" class="search-input" :placeholder="t('equipmentLedger.searchPlaceholder')" @confirm="handleSearch" />
</view>
<view class="search-btn" @click="handleSearch">{{ t('functionCommon.search') }}</view>
</view>
</view>
<!-- 列表区域 -->
<scroll-view scroll-y class="list-scroll" :scroll-top="scrollTop" @scroll="onScroll" @scrolltolower="loadMore" :lower-threshold="80">
<view class="list-wrap">
<view v-for="item in list" :key="item.id" class="type-card" @click="openDetail(item)">
<view class="card-header">
<view class="header-left">
<text class="type-name">{{ textValue(item.deviceName) }}</text>
<text class="type-code">{{ t('equipmentLedger.deviceCode') }}: {{ textValue(item.deviceCode) }}</text>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">{{ t('equipmentLedger.deviceType') }}</text>
<text class="value">{{ textValue(item.deviceTypeName) }}</text>
</view>
<view class="row">
<text class="label">{{ t('equipmentLedger.deviceStatus') }}</text>
<text :class="['value', getStatusClass(item.deviceStatus)]">{{ getStatusText(item.deviceStatus) }}</text>
</view>
<view class="row">
<text class="label">{{ t('equipmentLedger.deviceSpec') }}</text>
<text class="value">{{ textValue(item.deviceSpec) }}</text>
</view>
<view class="row">
<text class="label">{{ t('equipmentLedger.deviceLocation') }}</text>
<text class="value">{{ textValue(item.deviceLocation) }}</text>
</view>
</view>
<view class="card-actions">
<view class="action-btn edit-btn" @click.stop="openEdit(item)">
<uni-icons type="compose" size="18" color="#ffffff"></uni-icons>
</view>
<view class="action-btn delete-btn" @click.stop="confirmDelete(item)">
<uni-icons type="trash" size="18" color="#ffffff"></uni-icons>
</view>
</view>
</view>
<view v-if="loading && pageNo === 1" class="loading-text">{{ t('functionCommon.loading') }}</view>
<view v-else-if="!list.length" class="empty-text">{{ t('equipmentLedger.empty') }}</view>
<view v-else-if="loadingMore" class="loading-text">{{ t('functionCommon.loadingMore') }}</view>
<view v-else-if="finished" class="finished-text">{{ t('functionCommon.noMore') }}</view>
</view>
</scroll-view>
<!-- 新增悬浮按钮 -->
<view class="add-btn" @click="openCreate">
<text class="add-icon">+</text>
</view>
<!-- 返回顶部 -->
<view v-if="showGoTop" class="go-top-btn" @click="goTop">
<text class="go-top-icon">↑</text>
</view>
<!-- 新增/编辑弹框 -->
<uni-popup ref="formPopupRef" type="center" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">{{ formMode === 'create' ? t('equipmentLedger.createTitle') : t('equipmentLedger.editTitle') }}</text>
<view class="popup-close" @click="closeForm">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view scroll-y class="form-scroll">
<view class="form-content">
<!-- 设备编码 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.deviceCode') }} <text class="required-star">*</text></text>
<view class="code-row">
<input v-model="formData.deviceCode" class="form-input code-input" type="text" :placeholder="t('equipmentLedger.placeholderDeviceCode')" :disabled="formData.isCode || formMode === 'update'" />
<view class="auto-code-wrap" @click="toggleAutoCode">
<text class="auto-code-label">{{ t('equipmentLedger.autoCode') }}</text>
<view :class="['auto-code-switch', formData.isCode ? 'active' : '']">
<text class="switch-dot"></text>
</view>
</view>
</view>
</view>
<!-- 设备名称 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.deviceName') }} <text class="required-star">*</text></text>
<input v-model="formData.deviceName" class="form-input" type="text" :placeholder="t('equipmentLedger.placeholderDeviceName')" />
</view>
<!-- 设备类型 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.deviceType') }} <text class="required-star">*</text></text>
<picker :range="deviceTypeLabels" @change="onDeviceTypeChange">
<view class="form-picker">
<text :class="['picker-text', !formData.deviceType ? 'placeholder' : '']">{{ selectedDeviceTypeLabel }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<!-- 设备规格 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.deviceSpec') }}</text>
<input v-model="formData.deviceSpec" class="form-input" type="text" :placeholder="t('equipmentLedger.placeholderDeviceSpec')" />
</view>
<!-- 是否排产 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.isScheduled') }}</text>
<view class="switch-row" @click="toggleScheduled">
<text :class="['switch-text', formData.isScheduled === 1 ? 'active' : '']">{{ formData.isScheduled === 1 ? t('equipmentLedger.yes') : t('equipmentLedger.no') }}</text>
<view :class="['auto-code-switch', formData.isScheduled === 1 ? 'active' : '']">
<text class="switch-dot"></text>
</view>
</view>
</view>
<!-- 额定产能 -->
<view v-if="formData.isScheduled === 1" class="form-item">
<text class="form-label">{{ t('equipmentLedger.ratedCapacity') }} <text class="required-star">*</text></text>
<input v-model="formData.ratedCapacity" class="form-input" type="digit" :placeholder="t('equipmentLedger.placeholderRatedCapacity')" />
</view>
<!-- 每日报工平均值 -->
<view v-if="formData.isScheduled === 1" class="form-item">
<text class="form-label">{{ t('equipmentLedger.dailyAverageValue') }} <text class="required-star">*</text></text>
<input v-model="formData.dailyAverageValue" class="form-input" type="digit" :placeholder="t('equipmentLedger.placeholderDailyAverageValue')" />
</view>
<!-- 数据采集产能 -->
<view v-if="formData.isScheduled === 1" class="form-item">
<text class="form-label">{{ t('equipmentLedger.dataCollectionCapacity') }} <text class="required-star">*</text></text>
<input v-model="formData.dataCollectionCapacity" class="form-input" type="digit" :placeholder="t('equipmentLedger.placeholderDataCollectionCapacity')" />
</view>
<!-- 生产日期 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.productionDate') }} <text class="required-star">*</text></text>
<picker mode="date" :value="formData.productionDate" @change="onProductionDateChange">
<view class="form-picker">
<text :class="['picker-text', !formData.productionDate ? 'placeholder' : '']">{{ formData.productionDate || t('equipmentLedger.placeholderProductionDate') }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<!-- 入厂日期 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.factoryEntryDate') }} <text class="required-star">*</text></text>
<picker mode="date" :value="formData.factoryEntryDate" @change="onFactoryEntryDateChange">
<view class="form-picker">
<text :class="['picker-text', !formData.factoryEntryDate ? 'placeholder' : '']">{{ formData.factoryEntryDate || t('equipmentLedger.placeholderFactoryEntryDate') }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<!-- 设备位置 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.deviceLocation') }}</text>
<input v-model="formData.deviceLocation" class="form-input" type="text" :placeholder="t('equipmentLedger.placeholderDeviceLocation')" />
</view>
<!-- 备注 -->
<view class="form-item">
<text class="form-label">{{ t('equipmentLedger.remark') }}</text>
<textarea v-model="formData.remark" class="form-textarea" :placeholder="t('equipmentLedger.placeholderRemark')" :maxlength="200" />
</view>
</view>
</scroll-view>
<view class="form-footer">
<view class="footer-btn cancel-btn" @click="closeForm">
<text class="btn-text">{{ t('functionCommon.cancel') }}</text>
</view>
<view class="footer-btn confirm-btn" @click="submitForm">
<text class="btn-text">{{ t('functionCommon.save') }}</text>
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useI18n } from 'vue-i18n'
import NavBar from '@/components/common/NavBar.vue'
import { getDeviceLedgerPage, getDeviceLedger, createDeviceLedger, updateDeviceLedger, deleteDeviceLedger } from '@/api/mes/deviceLedger'
import { getDeviceTypeTree } from '@/api/mes/deviceType'
import { DICT_TYPE, getDictLabel, initAllDict } from '@/utils/dict'
const { t } = useI18n()
const formPopupRef = ref(null)
const searchKeyword = ref('')
const list = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const finished = ref(false)
const pageNo = ref(1)
const pageSize = ref(10)
const total = ref(0)
const scrollTop = ref(0)
const showGoTop = ref(false)
const formMode = ref('create')
const deviceTypeList = ref([])
const formData = reactive({
id: undefined,
deviceCode: '',
isCode: false,
deviceName: '',
deviceType: '',
deviceSpec: '',
isScheduled: 0,
ratedCapacity: '',
dailyAverageValue: '',
dataCollectionCapacity: '',
productionDate: '',
factoryEntryDate: '',
deviceLocation: '',
remark: ''
})
onLoad(async () => {
await initAllDict()
await fetchDeviceTypeList()
await fetchList(true)
})
async function fetchDeviceTypeList() {
try {
const res = await getDeviceTypeTree({})
const root = res && res.data !== undefined ? res.data : res
const treeData = Array.isArray(root) ? root : (Array.isArray(root?.data) ? root.data : [])
deviceTypeList.value = flattenTree(treeData)
} catch (e) {}
}
function flattenTree(nodes) {
const result = []
nodes.forEach(node => {
result.push(node)
if (node.children && node.children.length) {
result.push(...flattenTree(node.children))
}
})
return result
}
const deviceTypeLabels = computed(() => deviceTypeList.value.map(item => item.name || ''))
const selectedDeviceTypeLabel = computed(() => {
if (!formData.deviceType) return t('equipmentLedger.placeholderDeviceType')
const found = deviceTypeList.value.find(item => String(item.id) === String(formData.deviceType))
return found ? found.name : String(formData.deviceType)
})
function onDeviceTypeChange(e) {
const idx = e.detail.value
const item = deviceTypeList.value[idx]
formData.deviceType = item?.id ?? ''
}
async function fetchList(reset) {
if (reset) {
pageNo.value = 1
finished.value = false
}
if (pageNo.value === 1) {
loading.value = true
} else {
loadingMore.value = true
}
try {
const params = {
pageNo: pageNo.value,
pageSize: pageSize.value,
deviceCode: searchKeyword.value.trim() || undefined,
deviceName: searchKeyword.value.trim() || undefined
}
const res = await getDeviceLedgerPage(params)
const page = normalizePageData(res)
total.value = page.total
if (reset) {
list.value = page.list
} else {
list.value = [...list.value, ...page.list]
}
const loadedCount = list.value.length
finished.value = loadedCount >= total.value || page.list.length < pageSize.value
} catch (e) {
if (!reset) pageNo.value = Math.max(1, pageNo.value - 1)
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
} finally {
loading.value = false
loadingMore.value = false
}
}
function normalizePageData(res) {
const root = res && res.data !== undefined ? res.data : res
const candidateList = root?.list || root?.rows || root?.records || root?.data?.list || root?.data?.rows || root?.data?.records || []
const candidateTotal = root?.total ?? root?.data?.total ?? (Array.isArray(candidateList) ? candidateList.length : 0)
return { list: Array.isArray(candidateList) ? candidateList : [], total: Number(candidateTotal || 0) }
}
function getStatusText(status) {
return getDictLabel(DICT_TYPE.MES_TZ_STATUS, status, textValue(status))
}
function getStatusClass(status) {
const s = String(status)
if (s === '0' || s === '1') return 'text-success'
return 'text-danger'
}
async function handleSearch() {
await fetchList(true)
}
function onScroll(e) {
const top = e?.detail?.scrollTop || 0
showGoTop.value = top > 600
}
function goTop() {
scrollTop.value = 0
}
async function loadMore() {
if (loading.value || loadingMore.value || finished.value) return
pageNo.value += 1
await fetchList(false)
}
function openCreate() {
formMode.value = 'create'
resetForm()
formPopupRef.value?.open()
}
async function openEdit(item) {
const id = item?.id
if (id === undefined || id === null) {
uni.showToast({ title: t('equipmentLedger.noId'), icon: 'none' })
return
}
try {
const res = await getDeviceLedger(id)
const detail = normalizeDetail(res)
formMode.value = 'update'
formData.id = detail?.id
formData.deviceCode = textValueForInput(detail?.deviceCode)
formData.isCode = Boolean(detail?.isCode)
formData.deviceName = textValueForInput(detail?.deviceName)
formData.deviceType = detail?.deviceType ?? ''
formData.deviceSpec = textValueForInput(detail?.deviceSpec)
formData.isScheduled = detail?.isSchedueld ?? detail?.isScheduled ?? 0
formData.ratedCapacity = textValueForInput(detail?.ratedCapacity)
formData.dailyAverageValue = textValueForInput(detail?.dailyAverageValue)
formData.dataCollectionCapacity = textValueForInput(detail?.dataCollectionCapacity)
formData.productionDate = formatDateForInput(detail?.productionDate)
formData.factoryEntryDate = formatDateForInput(detail?.factoryEntryDate)
formData.deviceLocation = textValueForInput(detail?.deviceLocation)
formData.remark = textValueForInput(detail?.remark)
formPopupRef.value?.open()
} catch (e) {
uni.showToast({ title: t('equipmentLedger.loadEditFailed'), icon: 'none' })
}
}
function confirmDelete(item) {
const id = item?.id
if (id === undefined || id === null) return
uni.showModal({
title: t('functionCommon.confirmTitle'),
content: t('equipmentLedger.confirmDeleteContent', { name: textValue(item?.deviceName) }),
success: async (res) => {
if (!res.confirm) return
try {
await deleteDeviceLedger(String(id))
uni.showToast({ title: t('functionCommon.deleteSuccess'), icon: 'success' })
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.deleteFailed'), icon: 'none' })
}
}
})
}
function closeForm() {
formPopupRef.value?.close()
}
function toggleAutoCode() {
if (formMode.value === 'update') return
formData.isCode = !formData.isCode
if (formData.isCode) formData.deviceCode = ''
}
function toggleScheduled() {
formData.isScheduled = formData.isScheduled === 1 ? 0 : 1
if (formData.isScheduled !== 1) {
formData.ratedCapacity = ''
formData.dailyAverageValue = ''
formData.dataCollectionCapacity = ''
}
}
function onProductionDateChange(e) {
formData.productionDate = e.detail.value
}
function onFactoryEntryDateChange(e) {
formData.factoryEntryDate = e.detail.value
}
async function submitForm() {
if (!formData.isCode && !formData.deviceCode.trim()) {
uni.showToast({ title: t('equipmentLedger.validatorDeviceCodeRequired'), icon: 'none' })
return
}
if (!formData.deviceName.trim()) {
uni.showToast({ title: t('equipmentLedger.validatorDeviceNameRequired'), icon: 'none' })
return
}
if (!formData.deviceType) {
uni.showToast({ title: t('equipmentLedger.validatorDeviceTypeRequired'), icon: 'none' })
return
}
if (!formData.productionDate) {
uni.showToast({ title: t('equipmentLedger.validatorProductionDateRequired'), icon: 'none' })
return
}
if (!formData.factoryEntryDate) {
uni.showToast({ title: t('equipmentLedger.validatorFactoryEntryDateRequired'), icon: 'none' })
return
}
if (formData.isScheduled === 1) {
if (!formData.ratedCapacity && formData.ratedCapacity !== 0) {
uni.showToast({ title: t('equipmentLedger.validatorRatedCapacityRequired'), icon: 'none' })
return
}
if (!formData.dailyAverageValue && formData.dailyAverageValue !== 0) {
uni.showToast({ title: t('equipmentLedger.validatorDailyAverageValueRequired'), icon: 'none' })
return
}
if (!formData.dataCollectionCapacity && formData.dataCollectionCapacity !== 0) {
uni.showToast({ title: t('equipmentLedger.validatorDataCollectionCapacityRequired'), icon: 'none' })
return
}
}
const payload = {
id: formMode.value === 'update' ? formData.id : undefined,
deviceCode: formData.isCode ? undefined : formData.deviceCode.trim(),
isCode: formData.isCode,
deviceName: formData.deviceName.trim(),
deviceType: formData.deviceType,
deviceSpec: formData.deviceSpec.trim() || undefined,
isScheduled: formData.isScheduled,
ratedCapacity: formData.isScheduled === 1 ? toNumberOrUndefined(formData.ratedCapacity) : undefined,
dailyAverageValue: formData.isScheduled === 1 ? toNumberOrUndefined(formData.dailyAverageValue) : undefined,
dataCollectionCapacity: formData.isScheduled === 1 ? toNumberOrUndefined(formData.dataCollectionCapacity) : undefined,
productionDate: formData.productionDate,
factoryEntryDate: formData.factoryEntryDate,
deviceLocation: formData.deviceLocation.trim() || undefined,
remark: formData.remark.trim() || undefined
}
try {
if (formMode.value === 'create') {
await createDeviceLedger(payload)
uni.showToast({ title: t('functionCommon.createSuccess'), icon: 'success' })
} else {
await updateDeviceLedger(payload)
uni.showToast({ title: t('functionCommon.updateSuccess'), icon: 'success' })
}
closeForm()
await fetchList(true)
} catch (e) {
uni.showToast({ title: t('functionCommon.saveFailed'), icon: 'none' })
}
}
function resetForm() {
formData.id = undefined
formData.deviceCode = ''
formData.isCode = false
formData.deviceName = ''
formData.deviceType = ''
formData.deviceSpec = ''
formData.isScheduled = 0
formData.ratedCapacity = ''
formData.dailyAverageValue = ''
formData.dataCollectionCapacity = ''
formData.productionDate = ''
formData.factoryEntryDate = ''
formData.deviceLocation = ''
formData.remark = ''
}
function openDetail(item) {
const id = item?.id
if (!id && id !== 0) return
uni.navigateTo({
url: `/pages_function/pages/equipmentLedger/detail?id=${encodeURIComponent(String(id))}`
})
}
function normalizeDetail(res) {
const root = res && res.data !== undefined ? res.data : res
if (root?.data && typeof root.data === 'object') return root.data
if (root && typeof root === 'object') return root
return {}
}
function textValue(value) {
if (value === 0) return '0'
if (value === false) return t('functionCommon.no')
if (value === true) return t('functionCommon.yes')
if (value === null || value === undefined) return '-'
const text = String(value).trim()
return text || '-'
}
function textValueForInput(value) {
if (value === null || value === undefined) return ''
return String(value)
}
function formatDateForInput(value) {
if (!value) return ''
if (Array.isArray(value) && value.length >= 3) {
const [y, m, d] = value
const pad = (n) => String(n).padStart(2, '0')
return `${y}-${pad(m)}-${pad(d)}`
}
const text = String(value).trim()
if (!text) return ''
return text.split(' ')[0]
}
function toNumberOrUndefined(value) {
if (value === null || value === undefined || String(value).trim() === '') return undefined
const num = Number(value)
return Number.isFinite(num) ? num : undefined
}
</script>
<style lang="scss" scoped>
.page-container { min-height: 100vh; background-color: #f0f2f5; }
.search-card { background: #ffffff; margin: 20rpx 24rpx; border-radius: 18rpx; padding: 20rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.04); }
.search-row { display: flex; align-items: center; gap: 16rpx; }
.search-input-wrap { flex: 1; display: flex; align-items: center; background: #f5f7fa; border-radius: 44rpx; padding: 0 20rpx; }
.search-icon { margin-right: 12rpx; font-size: 30rpx; color: #999; }
.search-input { flex: 1; height: 72rpx; font-size: 28rpx; color: #333; background: transparent; }
.search-btn { min-width: 120rpx; height: 72rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); border-radius: 14rpx; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 28rpx; font-weight: 600; }
.list-scroll { height: calc(100vh - 360rpx); }
.list-wrap { padding: 0 24rpx 30rpx; }
.type-card { background: #ffffff; border-radius: 18rpx; padding: 24rpx; margin-bottom: 18rpx; box-shadow: 0 4rpx 18rpx rgba(0, 0, 0, 0.05); }
.card-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 18rpx; border-bottom: 1rpx solid #edf0f3; }
.header-left { display: flex; flex-direction: column; }
.type-name { font-size: 32rpx; font-weight: 600; color: #1a3a5c; margin-bottom: 8rpx; }
.type-code { font-size: 24rpx; color: #8a9099; }
.card-body { padding-top: 16rpx; }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.label { font-size: 26rpx; color: #8a9099; }
.value { font-size: 27rpx; color: #30363d; max-width: 62%; text-align: right; }
.text-success { color: #52c41a; }
.text-danger { color: #ff4d4f; }
.card-actions { margin-top: 24rpx; display: flex; justify-content: flex-end; gap: 14rpx; }
.action-btn { width: 60rpx; height: 60rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.edit-btn { background: #1a3a5c; }
.delete-btn { background: #ff4d4f; }
.add-btn { position: fixed; right: 28rpx; bottom: 140rpx; width: 96rpx; height: 96rpx; border-radius: 48rpx; background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); display: flex; align-items: center; justify-content: center; box-shadow: 0 8rpx 24rpx rgba(26, 58, 92, 0.32); z-index: 99; }
.add-icon { color: #ffffff; font-size: 56rpx; line-height: 1; }
.go-top-btn { position: fixed; right: 28rpx; bottom: 254rpx; width: 88rpx; height: 88rpx; border-radius: 44rpx; background: rgba(26, 58, 92, 0.9); display: flex; align-items: center; justify-content: center; z-index: 99; box-shadow: 0 6rpx 16rpx rgba(26, 58, 92, 0.25); }
.go-top-icon { color: #ffffff; font-size: 36rpx; font-weight: 700; }
.popup-content { width: 680rpx; max-height: 80vh; border-radius: 20rpx; overflow: hidden; }
.popup-header { padding: 24rpx; display: flex; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #edf0f3; }
.popup-title { font-size: 32rpx; color: #1a3a5c; font-weight: 700; }
.close-icon { font-size: 38rpx; color: #8e95a0; }
.form-scroll { max-height: 56vh; }
.form-content { padding: 24rpx; }
.form-item { margin-bottom: 18rpx; }
.form-label { display: block; margin-bottom: 8rpx; font-size: 26rpx; color: #8a9099; }
.required-star { color: #ff4d4f; }
.form-input { height: 76rpx; padding: 0 20rpx; border-radius: 12rpx; background: #f5f7fa; font-size: 28rpx; color: #30363d; }
.code-row { display: flex; align-items: center; gap: 16rpx; }
.code-input { flex: 1; }
.auto-code-wrap { display: flex; align-items: center; gap: 8rpx; }
.auto-code-label { font-size: 24rpx; color: #8a9099; white-space: nowrap; }
.auto-code-switch { width: 64rpx; height: 36rpx; border-radius: 18rpx; background: #dcdfe6; position: relative; transition: background 0.3s; }
.auto-code-switch.active { background: #1a3a5c; }
.switch-dot { position: absolute; top: 4rpx; left: 4rpx; width: 28rpx; height: 28rpx; border-radius: 14rpx; background: #fff; transition: transform 0.3s; }
.auto-code-switch.active .switch-dot { transform: translateX(28rpx); }
.switch-row { display: flex; align-items: center; gap: 16rpx; }
.switch-text { font-size: 28rpx; color: #8a9099; }
.switch-text.active { color: #1a3a5c; font-weight: 600; }
.form-picker { height: 76rpx; padding: 0 20rpx; border-radius: 12rpx; background: #f5f7fa; display: flex; align-items: center; justify-content: space-between; }
.picker-text { font-size: 28rpx; color: #30363d; }
.picker-text.placeholder { color: #999; }
.picker-arrow { font-size: 30rpx; color: #999; }
.form-textarea { width: 100%; min-height: 130rpx; padding: 18rpx 20rpx; border-radius: 12rpx; background: #f5f7fa; font-size: 28rpx; color: #30363d; }
.form-footer { display: flex; gap: 14rpx; padding: 20rpx 24rpx 24rpx; }
.footer-btn { flex: 1; height: 76rpx; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; }
.cancel-btn { background: #edf0f4; }
.confirm-btn { background: linear-gradient(135deg, #1a3a5c 0%, #2d5a87 100%); }
.btn-text { font-size: 28rpx; font-weight: 600; color: #ffffff; }
.cancel-btn .btn-text { color: #586070; }
.loading-text, .empty-text, .finished-text { text-align: center; padding: 28rpx 0; color: #99a1aa; font-size: 26rpx; }
</style>