Commit 940fb6ea83ce997c7907738bfb0837a7312a50ad
1 parent
b165f94a
又改了一个版本,这泰额太纠结了,一个设计,还要看示例数据对不对。
Showing
32 changed files
with
3586 additions
and
777 deletions
美国版/Food Labeling Management App UniApp/README.md
| @@ -52,7 +52,7 @@ npm run build:h5 | @@ -52,7 +52,7 @@ npm run build:h5 | ||
| 52 | 52 | ||
| 53 | ## 设计规范(与 React 版一致) | 53 | ## 设计规范(与 React 版一致) |
| 54 | 54 | ||
| 55 | -- 主色: #2563eb (Enterprise Blue) | 55 | +- 主色: #1F3A8A (Enterprise Blue) |
| 56 | - 最大宽度: 480px (约 960rpx) | 56 | - 最大宽度: 480px (约 960rpx) |
| 57 | - 底部导航: Dashboard / Labels / More | 57 | - 底部导航: Dashboard / Labels / More |
| 58 | - 触控高度: ≥ 48px (96rpx) | 58 | - 触控高度: ≥ 48px (96rpx) |
美国版/Food Labeling Management App UniApp/src/App.vue
| @@ -15,17 +15,30 @@ onHide(() => { | @@ -15,17 +15,30 @@ onHide(() => { | ||
| 15 | </script> | 15 | </script> |
| 16 | 16 | ||
| 17 | <style> | 17 | <style> |
| 18 | -page { | ||
| 19 | - /* ====== 主题色配置 - 修改这里即可全局换色 ====== */ | ||
| 20 | - --theme-primary: #4278bd; | ||
| 21 | - --theme-primary-dark: #2a288f; | ||
| 22 | - --theme-primary-light: #eef4fb; | ||
| 23 | - --theme-primary-shadow: rgba(66, 120, 189, 0.35); | ||
| 24 | - --theme-primary-shadow-light: rgba(66, 120, 189, 0.2); | ||
| 25 | - /* ============================================== */ | 18 | +/* 主题色 - :root 确保 H5 等环境下 var(--theme-primary) 可用 */ |
| 19 | +:root { | ||
| 20 | + --theme-primary: #1F3A8A; | ||
| 21 | + --theme-primary-dark: #142a6c; | ||
| 22 | + --theme-primary-light: #e8ecf5; | ||
| 23 | + --theme-primary-shadow: rgba(31, 58, 138, 0.35); | ||
| 24 | + --theme-primary-shadow-light: rgba(31, 58, 138, 0.2); | ||
| 25 | +} | ||
| 26 | 26 | ||
| 27 | +page { | ||
| 27 | background: #f9fafb; | 28 | background: #f9fafb; |
| 28 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; | 29 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; |
| 29 | color: #111827; | 30 | color: #111827; |
| 31 | + overflow-x: hidden; | ||
| 32 | + overflow-x: clip; | ||
| 33 | + width: 100%; | ||
| 34 | + max-width: 100vw; | ||
| 35 | +} | ||
| 36 | + | ||
| 37 | +/* H5 移动端防横向溢出 */ | ||
| 38 | +html, body, #app { | ||
| 39 | + overflow-x: hidden; | ||
| 40 | + overflow-x: clip; | ||
| 41 | + width: 100%; | ||
| 42 | + max-width: 100vw; | ||
| 30 | } | 43 | } |
| 31 | </style> | 44 | </style> |
美国版/Food Labeling Management App UniApp/src/components/AppIcon.vue
| @@ -50,6 +50,9 @@ const icons: Record<string, string> = { | @@ -50,6 +50,9 @@ const icons: Record<string, string> = { | ||
| 50 | bluetooth: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"/></svg>', | 50 | bluetooth: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"/></svg>', |
| 51 | minus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/></svg>', | 51 | minus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/></svg>', |
| 52 | eye: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>', | 52 | eye: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>', |
| 53 | + lock: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>', | ||
| 54 | + chevronDown: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>', | ||
| 55 | + chevronUp: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"/></svg>', | ||
| 53 | } | 56 | } |
| 54 | 57 | ||
| 55 | const svgContent = computed(() => icons[props.name] || icons.food) | 58 | const svgContent = computed(() => icons[props.name] || icons.food) |
| @@ -80,9 +83,9 @@ const wrapStyle = computed(() => ({})) | @@ -80,9 +83,9 @@ const wrapStyle = computed(() => ({})) | ||
| 80 | .icon-lg { width: 64rpx; height: 64rpx; } | 83 | .icon-lg { width: 64rpx; height: 64rpx; } |
| 81 | 84 | ||
| 82 | .icon-gray :deep(svg) { stroke: #6b7280; } | 85 | .icon-gray :deep(svg) { stroke: #6b7280; } |
| 83 | -.icon-primary :deep(svg) { stroke: #2563eb; } | 86 | +.icon-primary :deep(svg) { stroke: var(--theme-primary, #1F3A8A); } |
| 84 | .icon-white :deep(svg) { stroke: #ffffff; } | 87 | .icon-white :deep(svg) { stroke: #ffffff; } |
| 85 | -.icon-blue :deep(svg) { stroke: #2563eb; } | 88 | +.icon-blue :deep(svg) { stroke: var(--theme-primary, #1F3A8A); } |
| 86 | .icon-orange :deep(svg) { stroke: #ea580c; } | 89 | .icon-orange :deep(svg) { stroke: #ea580c; } |
| 87 | .icon-green :deep(svg) { stroke: #16a34a; } | 90 | .icon-green :deep(svg) { stroke: #16a34a; } |
| 88 | .icon-purple :deep(svg) { stroke: #7c3aed; } | 91 | .icon-purple :deep(svg) { stroke: #7c3aed; } |
美国版/Food Labeling Management App UniApp/src/components/LocationPicker.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="loc-root"> | ||
| 3 | + <view class="loc-trigger" @click.stop="showPicker = true"> | ||
| 4 | + <text class="loc-text">LOC-{{ currentId }}</text> | ||
| 5 | + <AppIcon name="chevronDown" size="sm" color="white" /> | ||
| 6 | + </view> | ||
| 7 | + | ||
| 8 | + <view v-if="showPicker" class="picker-mask" @click="showPicker = false"> | ||
| 9 | + <view class="picker-body" @click.stop> | ||
| 10 | + <view class="picker-header"> | ||
| 11 | + <text class="picker-title">Switch Location</text> | ||
| 12 | + <view class="picker-close" @click="showPicker = false"> | ||
| 13 | + <AppIcon name="plus" size="sm" color="gray" /> | ||
| 14 | + </view> | ||
| 15 | + </view> | ||
| 16 | + <view class="picker-list"> | ||
| 17 | + <view | ||
| 18 | + v-for="s in stores" | ||
| 19 | + :key="s.id" | ||
| 20 | + class="picker-item" | ||
| 21 | + :class="{ active: currentId === s.id }" | ||
| 22 | + @click="handleSelect(s)" | ||
| 23 | + > | ||
| 24 | + <view class="picker-icon" :class="{ active: currentId === s.id }"> | ||
| 25 | + <AppIcon name="mapPin" size="sm" :color="currentId === s.id ? 'white' : 'gray'" /> | ||
| 26 | + </view> | ||
| 27 | + <view class="picker-info"> | ||
| 28 | + <text class="picker-name">{{ t(s.nameKey) }}</text> | ||
| 29 | + <text class="picker-addr">{{ s.address }}, {{ s.city }}</text> | ||
| 30 | + </view> | ||
| 31 | + <view v-if="currentId === s.id" class="picker-check"> | ||
| 32 | + <AppIcon name="check" size="sm" color="white" /> | ||
| 33 | + </view> | ||
| 34 | + </view> | ||
| 35 | + </view> | ||
| 36 | + </view> | ||
| 37 | + </view> | ||
| 38 | + </view> | ||
| 39 | +</template> | ||
| 40 | + | ||
| 41 | +<script setup lang="ts"> | ||
| 42 | +import { ref } from 'vue' | ||
| 43 | +import { useI18n } from 'vue-i18n' | ||
| 44 | +import AppIcon from './AppIcon.vue' | ||
| 45 | +import { storeList, getCurrentStoreId, switchStore } from '../utils/stores' | ||
| 46 | + | ||
| 47 | +const { t } = useI18n() | ||
| 48 | +const stores = storeList | ||
| 49 | +const currentId = ref(getCurrentStoreId()) | ||
| 50 | +const showPicker = ref(false) | ||
| 51 | + | ||
| 52 | +const handleSelect = (s: typeof stores[0]) => { | ||
| 53 | + if (s.id === currentId.value) { | ||
| 54 | + showPicker.value = false | ||
| 55 | + return | ||
| 56 | + } | ||
| 57 | + const name = t(s.nameKey) | ||
| 58 | + switchStore(s.id, name) | ||
| 59 | + currentId.value = s.id | ||
| 60 | + showPicker.value = false | ||
| 61 | + uni.showToast({ title: 'Switched to ' + name, icon: 'success' }) | ||
| 62 | + setTimeout(() => { | ||
| 63 | + const pages = getCurrentPages() | ||
| 64 | + const cur = pages[pages.length - 1] as any | ||
| 65 | + if (cur && cur.route) { | ||
| 66 | + uni.redirectTo({ url: '/' + cur.route }) | ||
| 67 | + } | ||
| 68 | + }, 800) | ||
| 69 | +} | ||
| 70 | +</script> | ||
| 71 | + | ||
| 72 | +<style scoped> | ||
| 73 | +.loc-trigger { | ||
| 74 | + display: flex; | ||
| 75 | + align-items: center; | ||
| 76 | + gap: 4rpx; | ||
| 77 | + padding: 4rpx 16rpx 4rpx 20rpx; | ||
| 78 | + background: rgba(255, 255, 255, 0.15); | ||
| 79 | + border-radius: 999rpx; | ||
| 80 | + margin-top: 6rpx; | ||
| 81 | +} | ||
| 82 | + | ||
| 83 | +.loc-text { | ||
| 84 | + font-size: 22rpx; | ||
| 85 | + color: rgba(255, 255, 255, 0.9); | ||
| 86 | + font-weight: 500; | ||
| 87 | +} | ||
| 88 | + | ||
| 89 | +.loc-trigger .icon-wrap { | ||
| 90 | + opacity: 0.7; | ||
| 91 | +} | ||
| 92 | + | ||
| 93 | +/* Picker modal */ | ||
| 94 | +.picker-mask { | ||
| 95 | + position: fixed; | ||
| 96 | + top: 0; | ||
| 97 | + right: 0; | ||
| 98 | + bottom: 0; | ||
| 99 | + left: 0; | ||
| 100 | + background: rgba(0, 0, 0, 0.5); | ||
| 101 | + z-index: 2000; | ||
| 102 | + display: flex; | ||
| 103 | + align-items: flex-end; | ||
| 104 | + justify-content: center; | ||
| 105 | +} | ||
| 106 | + | ||
| 107 | +.picker-body { | ||
| 108 | + width: 100%; | ||
| 109 | + background: #fff; | ||
| 110 | + border-radius: 32rpx 32rpx 0 0; | ||
| 111 | + padding: 32rpx; | ||
| 112 | + max-height: 70vh; | ||
| 113 | + overflow-y: auto; | ||
| 114 | +} | ||
| 115 | + | ||
| 116 | +.picker-header { | ||
| 117 | + display: flex; | ||
| 118 | + align-items: center; | ||
| 119 | + justify-content: space-between; | ||
| 120 | + margin-bottom: 24rpx; | ||
| 121 | +} | ||
| 122 | + | ||
| 123 | +.picker-title { | ||
| 124 | + font-size: 36rpx; | ||
| 125 | + font-weight: 700; | ||
| 126 | + color: #111827; | ||
| 127 | +} | ||
| 128 | + | ||
| 129 | +.picker-close { | ||
| 130 | + width: 56rpx; | ||
| 131 | + height: 56rpx; | ||
| 132 | + border-radius: 50%; | ||
| 133 | + background: #f3f4f6; | ||
| 134 | + display: flex; | ||
| 135 | + align-items: center; | ||
| 136 | + justify-content: center; | ||
| 137 | + transform: rotate(45deg); | ||
| 138 | +} | ||
| 139 | + | ||
| 140 | +.picker-list { | ||
| 141 | + display: flex; | ||
| 142 | + flex-direction: column; | ||
| 143 | + gap: 12rpx; | ||
| 144 | +} | ||
| 145 | + | ||
| 146 | +.picker-item { | ||
| 147 | + display: flex; | ||
| 148 | + align-items: center; | ||
| 149 | + gap: 20rpx; | ||
| 150 | + padding: 28rpx 24rpx; | ||
| 151 | + border-radius: 16rpx; | ||
| 152 | + border: 2rpx solid #e5e7eb; | ||
| 153 | + background: #fff; | ||
| 154 | +} | ||
| 155 | + | ||
| 156 | +.picker-item.active { | ||
| 157 | + border-color: var(--theme-primary); | ||
| 158 | + background: var(--theme-primary-light); | ||
| 159 | +} | ||
| 160 | + | ||
| 161 | +.picker-icon { | ||
| 162 | + width: 56rpx; | ||
| 163 | + height: 56rpx; | ||
| 164 | + border-radius: 14rpx; | ||
| 165 | + background: #f3f4f6; | ||
| 166 | + display: flex; | ||
| 167 | + align-items: center; | ||
| 168 | + justify-content: center; | ||
| 169 | + flex-shrink: 0; | ||
| 170 | +} | ||
| 171 | + | ||
| 172 | +.picker-icon.active { | ||
| 173 | + background: var(--theme-primary); | ||
| 174 | +} | ||
| 175 | + | ||
| 176 | +.picker-info { | ||
| 177 | + flex: 1; | ||
| 178 | + min-width: 0; | ||
| 179 | +} | ||
| 180 | + | ||
| 181 | +.picker-name { | ||
| 182 | + font-size: 30rpx; | ||
| 183 | + font-weight: 600; | ||
| 184 | + color: #111827; | ||
| 185 | + display: block; | ||
| 186 | + margin-bottom: 4rpx; | ||
| 187 | +} | ||
| 188 | + | ||
| 189 | +.picker-addr { | ||
| 190 | + font-size: 24rpx; | ||
| 191 | + color: #6b7280; | ||
| 192 | + display: block; | ||
| 193 | +} | ||
| 194 | + | ||
| 195 | +.picker-check { | ||
| 196 | + width: 44rpx; | ||
| 197 | + height: 44rpx; | ||
| 198 | + border-radius: 50%; | ||
| 199 | + background: var(--theme-primary); | ||
| 200 | + display: flex; | ||
| 201 | + align-items: center; | ||
| 202 | + justify-content: center; | ||
| 203 | + flex-shrink: 0; | ||
| 204 | +} | ||
| 205 | +</style> |
美国版/Food Labeling Management App UniApp/src/components/SideMenu.vue
| @@ -14,22 +14,56 @@ | @@ -14,22 +14,56 @@ | ||
| 14 | </view> | 14 | </view> |
| 15 | 15 | ||
| 16 | <view class="drawer-menu"> | 16 | <view class="drawer-menu"> |
| 17 | - <view | ||
| 18 | - v-for="item in items" | ||
| 19 | - :key="item.key" | ||
| 20 | - class="menu-item" | ||
| 21 | - :class="{ active: isActive(item.path), danger: item.danger }" | ||
| 22 | - @click="handleItemClick(item)" | ||
| 23 | - > | ||
| 24 | - <view class="menu-icon"> | ||
| 25 | - <AppIcon | ||
| 26 | - :name="item.icon" | ||
| 27 | - size="md" | ||
| 28 | - :color="isActive(item.path) || item.danger ? 'white' : 'gray'" | ||
| 29 | - /> | 17 | + <template v-for="item in items" :key="item.key"> |
| 18 | + <!-- 有子菜单的项 --> | ||
| 19 | + <view v-if="item.children" class="menu-group"> | ||
| 20 | + <view | ||
| 21 | + class="menu-item menu-item-parent" | ||
| 22 | + :class="{ expanded: expandedKey === item.key }" | ||
| 23 | + @click="toggleExpand(item.key)" | ||
| 24 | + > | ||
| 25 | + <view class="menu-icon"> | ||
| 26 | + <AppIcon :name="item.icon" size="md" color="gray" /> | ||
| 27 | + </view> | ||
| 28 | + <text class="menu-label">{{ t(item.labelKey) }}</text> | ||
| 29 | + <view class="menu-chevron"> | ||
| 30 | + <AppIcon | ||
| 31 | + :name="expandedKey === item.key ? 'chevronUp' : 'chevronDown'" | ||
| 32 | + size="sm" | ||
| 33 | + color="gray" | ||
| 34 | + /> | ||
| 35 | + </view> | ||
| 36 | + </view> | ||
| 37 | + <view v-show="expandedKey === item.key" class="menu-children"> | ||
| 38 | + <view | ||
| 39 | + v-for="child in item.children" | ||
| 40 | + :key="child.path" | ||
| 41 | + class="menu-item child" | ||
| 42 | + :class="{ active: isActive(child.path) }" | ||
| 43 | + @click="handleItemClick(child)" | ||
| 44 | + > | ||
| 45 | + <view class="menu-icon" /> | ||
| 46 | + <text class="menu-label">{{ t(child.labelKey) }}</text> | ||
| 47 | + </view> | ||
| 48 | + </view> | ||
| 30 | </view> | 49 | </view> |
| 31 | - <text class="menu-label">{{ t(item.labelKey) }}</text> | ||
| 32 | - </view> | 50 | + <!-- 普通菜单项 --> |
| 51 | + <view | ||
| 52 | + v-else | ||
| 53 | + class="menu-item" | ||
| 54 | + :class="{ active: isActive(item.path), danger: item.danger }" | ||
| 55 | + @click="handleItemClick(item)" | ||
| 56 | + > | ||
| 57 | + <view class="menu-icon"> | ||
| 58 | + <AppIcon | ||
| 59 | + :name="item.icon" | ||
| 60 | + size="md" | ||
| 61 | + :color="isActive(item.path) || item.danger ? 'white' : 'gray'" | ||
| 62 | + /> | ||
| 63 | + </view> | ||
| 64 | + <text class="menu-label">{{ t(item.labelKey) }}</text> | ||
| 65 | + </view> | ||
| 66 | + </template> | ||
| 33 | </view> | 67 | </view> |
| 34 | 68 | ||
| 35 | <view class="drawer-footer" :style="{ paddingBottom: (bottomSafeArea + 24) + 'px' }"> | 69 | <view class="drawer-footer" :style="{ paddingBottom: (bottomSafeArea + 24) + 'px' }"> |
| @@ -70,27 +104,43 @@ const storeName = computed(() => { | @@ -70,27 +104,43 @@ const storeName = computed(() => { | ||
| 70 | return stored || 'MedVantage' | 104 | return stored || 'MedVantage' |
| 71 | }) | 105 | }) |
| 72 | 106 | ||
| 107 | +const expandedKey = ref<string | null>(null) | ||
| 108 | + | ||
| 73 | const items = [ | 109 | const items = [ |
| 74 | - { key: 'home', path: '/pages/index/index', icon: 'home', labelKey: 'home' }, | ||
| 75 | - { key: 'labels', path: '/pages/labels/labels', icon: 'tag', labelKey: 'nav.labels' }, | 110 | + { key: 'home', path: '/pages/index/index', icon: 'home', labelKey: 'Home' }, |
| 111 | + { key: 'Labeling', path: '/pages/labels/labels', icon: 'tag', labelKey: 'Labeling' }, | ||
| 112 | + { | ||
| 113 | + key: 'report', | ||
| 114 | + icon: 'fileText', | ||
| 115 | + labelKey: 'more.report', | ||
| 116 | + children: [ | ||
| 117 | + { path: '/pages/more/print-log', labelKey: 'more.printLog' }, | ||
| 118 | + { path: '/pages/more/label-report', labelKey: 'more.labelReport' }, | ||
| 119 | + ], | ||
| 120 | + }, | ||
| 76 | { key: 'profile', path: '/pages/more/profile', icon: 'user', labelKey: 'more.profile' }, | 121 | { key: 'profile', path: '/pages/more/profile', icon: 'user', labelKey: 'more.profile' }, |
| 77 | { key: 'location', path: '/pages/more/location', icon: 'mapPin', labelKey: 'more.location' }, | 122 | { key: 'location', path: '/pages/more/location', icon: 'mapPin', labelKey: 'more.location' }, |
| 78 | { key: 'sync', path: '/pages/more/sync', icon: 'refresh', labelKey: 'more.sync' }, | 123 | { key: 'sync', path: '/pages/more/sync', icon: 'refresh', labelKey: 'more.sync' }, |
| 79 | - { key: 'language', path: '/pages/more/language', icon: 'globe', labelKey: 'more.language' }, | ||
| 80 | { key: 'support', path: '/pages/more/support', icon: 'help', labelKey: 'more.support' }, | 124 | { key: 'support', path: '/pages/more/support', icon: 'help', labelKey: 'more.support' }, |
| 81 | { key: 'logout', path: '__logout__', icon: 'logout', labelKey: 'more.logout', danger: true }, | 125 | { key: 'logout', path: '__logout__', icon: 'logout', labelKey: 'more.logout', danger: true }, |
| 82 | ] | 126 | ] |
| 83 | 127 | ||
| 128 | +const toggleExpand = (key: string) => { | ||
| 129 | + expandedKey.value = expandedKey.value === key ? null : key | ||
| 130 | +} | ||
| 131 | + | ||
| 84 | watch( | 132 | watch( |
| 85 | () => props.modelValue, | 133 | () => props.modelValue, |
| 86 | (val) => { | 134 | (val) => { |
| 87 | if (val) { | 135 | if (val) { |
| 88 | isShown.value = true | 136 | isShown.value = true |
| 137 | + if (currentPath.value.startsWith('/pages/more/print-log') || currentPath.value.startsWith('/pages/more/label-report')) { | ||
| 138 | + expandedKey.value = 'report' | ||
| 139 | + } | ||
| 89 | nextTick(() => { | 140 | nextTick(() => { |
| 90 | animClass.value = 'opening' | 141 | animClass.value = 'opening' |
| 91 | }) | 142 | }) |
| 92 | } else if (isShown.value) { | 143 | } else if (isShown.value) { |
| 93 | - // 先播放关闭动画,再卸载 | ||
| 94 | animClass.value = 'closing' | 144 | animClass.value = 'closing' |
| 95 | setTimeout(() => { | 145 | setTimeout(() => { |
| 96 | isShown.value = false | 146 | isShown.value = false |
| @@ -112,7 +162,7 @@ const currentPath = computed(() => { | @@ -112,7 +162,7 @@ const currentPath = computed(() => { | ||
| 112 | return route | 162 | return route |
| 113 | }) | 163 | }) |
| 114 | 164 | ||
| 115 | -const isActive = (path: string) => !!path && currentPath.value === path | 165 | +const isActive = (path: string) => !!path && path !== '__logout__' && currentPath.value === path |
| 116 | 166 | ||
| 117 | const handleLogout = () => { | 167 | const handleLogout = () => { |
| 118 | uni.removeStorageSync('isLoggedIn') | 168 | uni.removeStorageSync('isLoggedIn') |
| @@ -152,7 +202,7 @@ const handleItemClick = (item: any) => { | @@ -152,7 +202,7 @@ const handleItemClick = (item: any) => { | ||
| 152 | .drawer-panel { | 202 | .drawer-panel { |
| 153 | width: 76%; | 203 | width: 76%; |
| 154 | max-width: 640rpx; | 204 | max-width: 640rpx; |
| 155 | - background: #111827; | 205 | + background: #1F3A8A; |
| 156 | padding: 16rpx 32rpx 0; | 206 | padding: 16rpx 32rpx 0; |
| 157 | box-shadow: 4rpx 0 32rpx rgba(0, 0, 0, 0.5); | 207 | box-shadow: 4rpx 0 32rpx rgba(0, 0, 0, 0.5); |
| 158 | display: flex; | 208 | display: flex; |
| @@ -253,13 +303,35 @@ const handleItemClick = (item: any) => { | @@ -253,13 +303,35 @@ const handleItemClick = (item: any) => { | ||
| 253 | } | 303 | } |
| 254 | 304 | ||
| 255 | .menu-item.active { | 305 | .menu-item.active { |
| 256 | - background: var(--theme-primary); | 306 | + background: #1447E6; |
| 257 | } | 307 | } |
| 258 | 308 | ||
| 259 | .menu-item.active .menu-label { | 309 | .menu-item.active .menu-label { |
| 260 | color: #ffffff; | 310 | color: #ffffff; |
| 261 | } | 311 | } |
| 262 | 312 | ||
| 313 | +.menu-group { | ||
| 314 | + margin-bottom: 4rpx; | ||
| 315 | +} | ||
| 316 | + | ||
| 317 | +.menu-chevron { | ||
| 318 | + margin-left: auto; | ||
| 319 | +} | ||
| 320 | + | ||
| 321 | +.menu-children { | ||
| 322 | + padding-left: 76rpx; | ||
| 323 | + padding-bottom: 8rpx; | ||
| 324 | +} | ||
| 325 | + | ||
| 326 | +.menu-item.child { | ||
| 327 | + padding: 12rpx 12rpx; | ||
| 328 | +} | ||
| 329 | + | ||
| 330 | +.menu-item.child .menu-icon { | ||
| 331 | + width: 0; | ||
| 332 | + min-width: 0; | ||
| 333 | +} | ||
| 334 | + | ||
| 263 | .drawer-footer { | 335 | .drawer-footer { |
| 264 | padding: 24rpx 0 48rpx; | 336 | padding: 24rpx 0 48rpx; |
| 265 | border-top: 1rpx solid rgba(148, 163, 184, 0.2); | 337 | border-top: 1rpx solid rgba(148, 163, 184, 0.2); |
美国版/Food Labeling Management App UniApp/src/components/TabBar.vue
| @@ -78,7 +78,7 @@ const switchTab = (path: string) => { | @@ -78,7 +78,7 @@ const switchTab = (path: string) => { | ||
| 78 | } | 78 | } |
| 79 | 79 | ||
| 80 | .tab-label.active { | 80 | .tab-label.active { |
| 81 | - color: #2563eb; | 81 | + color: var(--theme-primary, #1F3A8A); |
| 82 | font-weight: 600; | 82 | font-weight: 600; |
| 83 | } | 83 | } |
| 84 | </style> | 84 | </style> |
美国版/Food Labeling Management App UniApp/src/locales/en.ts
| @@ -89,6 +89,9 @@ export default { | @@ -89,6 +89,9 @@ export default { | ||
| 89 | more: { | 89 | more: { |
| 90 | title: 'More', | 90 | title: 'More', |
| 91 | profile: 'My Profile', 'profile.desc': 'View and edit your profile', | 91 | profile: 'My Profile', 'profile.desc': 'View and edit your profile', |
| 92 | + report: 'Report', | ||
| 93 | + printLog: 'Print Log', | ||
| 94 | + labelReport: 'Label Report', | ||
| 92 | printers: 'Printer Settings', 'printers.desc': 'Manage connected printers', | 95 | printers: 'Printer Settings', 'printers.desc': 'Manage connected printers', |
| 93 | location: 'Location', 'location.desc': 'Change your work location', | 96 | location: 'Location', 'location.desc': 'Change your work location', |
| 94 | sync: 'Sync Status', 'sync.desc': 'View sync status and data', | 97 | sync: 'Sync Status', 'sync.desc': 'View sync status and data', |
美国版/Food Labeling Management App UniApp/src/locales/zh.ts
| @@ -89,6 +89,9 @@ export default { | @@ -89,6 +89,9 @@ export default { | ||
| 89 | more: { | 89 | more: { |
| 90 | title: '更多', | 90 | title: '更多', |
| 91 | profile: '我的资料', 'profile.desc': '查看和编辑您的个人资料', | 91 | profile: '我的资料', 'profile.desc': '查看和编辑您的个人资料', |
| 92 | + report: '报表', | ||
| 93 | + printLog: '打印日志', | ||
| 94 | + labelReport: '标签报表', | ||
| 92 | printers: '打印机设置', 'printers.desc': '管理连接的打印机', | 95 | printers: '打印机设置', 'printers.desc': '管理连接的打印机', |
| 93 | location: '工作地点', 'location.desc': '更改您的工作地点', | 96 | location: '工作地点', 'location.desc': '更改您的工作地点', |
| 94 | sync: '同步状态', 'sync.desc': '查看同步状态和数据', | 97 | sync: '同步状态', 'sync.desc': '查看同步状态和数据', |
美国版/Food Labeling Management App UniApp/src/manifest.json
美国版/Food Labeling Management App UniApp/src/pages.json
| @@ -50,6 +50,27 @@ | @@ -50,6 +50,27 @@ | ||
| 50 | } | 50 | } |
| 51 | }, | 51 | }, |
| 52 | { | 52 | { |
| 53 | + "path": "pages/more/print-log", | ||
| 54 | + "style": { | ||
| 55 | + "navigationBarTitleText": "Print Log", | ||
| 56 | + "navigationStyle": "custom" | ||
| 57 | + } | ||
| 58 | + }, | ||
| 59 | + { | ||
| 60 | + "path": "pages/more/label-report", | ||
| 61 | + "style": { | ||
| 62 | + "navigationBarTitleText": "Label Report", | ||
| 63 | + "navigationStyle": "custom" | ||
| 64 | + } | ||
| 65 | + }, | ||
| 66 | + { | ||
| 67 | + "path": "pages/more/print-detail", | ||
| 68 | + "style": { | ||
| 69 | + "navigationBarTitleText": "Print Detail", | ||
| 70 | + "navigationStyle": "custom" | ||
| 71 | + } | ||
| 72 | + }, | ||
| 73 | + { | ||
| 53 | "path": "pages/more/more", | 74 | "path": "pages/more/more", |
| 54 | "style": { | 75 | "style": { |
| 55 | "navigationBarTitleText": "More", | 76 | "navigationBarTitleText": "More", |
| @@ -97,6 +118,13 @@ | @@ -97,6 +118,13 @@ | ||
| 97 | "navigationBarTitleText": "Support", | 118 | "navigationBarTitleText": "Support", |
| 98 | "navigationStyle": "custom" | 119 | "navigationStyle": "custom" |
| 99 | } | 120 | } |
| 121 | + }, | ||
| 122 | + { | ||
| 123 | + "path": "pages/more/change-password", | ||
| 124 | + "style": { | ||
| 125 | + "navigationBarTitleText": "Change Password", | ||
| 126 | + "navigationStyle": "custom" | ||
| 127 | + } | ||
| 100 | } | 128 | } |
| 101 | ], | 129 | ], |
| 102 | "globalStyle": { | 130 | "globalStyle": { |
美国版/Food Labeling Management App UniApp/src/pages/index/index.vue
| @@ -5,12 +5,15 @@ | @@ -5,12 +5,15 @@ | ||
| 5 | <view class="top-left-empty" /> | 5 | <view class="top-left-empty" /> |
| 6 | <view class="top-center"> | 6 | <view class="top-center"> |
| 7 | <image class="header-logo" src="/static/logo_us.png" mode="aspectFit" /> | 7 | <image class="header-logo" src="/static/logo_us.png" mode="aspectFit" /> |
| 8 | - <text class="app-sub">{{ storeName }}</text> | ||
| 9 | </view> | 8 | </view> |
| 10 | <view class="top-right" @click="isMenuOpen = true"> | 9 | <view class="top-right" @click="isMenuOpen = true"> |
| 11 | <AppIcon name="menu" size="sm" color="white" /> | 10 | <AppIcon name="menu" size="sm" color="white" /> |
| 12 | </view> | 11 | </view> |
| 13 | </view> | 12 | </view> |
| 13 | + <view class="hero-info"> | ||
| 14 | + <text class="app-sub">{{ storeName }}</text> | ||
| 15 | + <LocationPicker /> | ||
| 16 | + </view> | ||
| 14 | </view> | 17 | </view> |
| 15 | 18 | ||
| 16 | <view class="main"> | 19 | <view class="main"> |
| @@ -40,6 +43,7 @@ import { ref, computed } from 'vue' | @@ -40,6 +43,7 @@ import { ref, computed } from 'vue' | ||
| 40 | import { useI18n } from 'vue-i18n' | 43 | import { useI18n } from 'vue-i18n' |
| 41 | import AppIcon from '../../components/AppIcon.vue' | 44 | import AppIcon from '../../components/AppIcon.vue' |
| 42 | import SideMenu from '../../components/SideMenu.vue' | 45 | import SideMenu from '../../components/SideMenu.vue' |
| 46 | +import LocationPicker from '../../components/LocationPicker.vue' | ||
| 43 | import { getStatusBarHeight } from '../../utils/statusBar' | 47 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 44 | 48 | ||
| 45 | const { t } = useI18n() | 49 | const { t } = useI18n() |
| @@ -50,7 +54,7 @@ const isMenuOpen = ref(false) | @@ -50,7 +54,7 @@ const isMenuOpen = ref(false) | ||
| 50 | 54 | ||
| 51 | const quickActions = computed(() => [ | 55 | const quickActions = computed(() => [ |
| 52 | { | 56 | { |
| 53 | - label: t('nav.labels'), | 57 | + label: t('Labeling'), |
| 54 | icon: 'tag', | 58 | icon: 'tag', |
| 55 | path: '/pages/labels/labels', | 59 | path: '/pages/labels/labels', |
| 56 | }, | 60 | }, |
| @@ -114,9 +118,17 @@ const navTo = (path: string) => { | @@ -114,9 +118,17 @@ const navTo = (path: string) => { | ||
| 114 | margin-bottom: 8rpx; | 118 | margin-bottom: 8rpx; |
| 115 | } | 119 | } |
| 116 | 120 | ||
| 121 | +.hero-info { | ||
| 122 | + display: flex; | ||
| 123 | + flex-direction: column; | ||
| 124 | + align-items: center; | ||
| 125 | + padding-top: 16rpx; | ||
| 126 | +} | ||
| 127 | + | ||
| 117 | .app-sub { | 128 | .app-sub { |
| 118 | font-size: 26rpx; | 129 | font-size: 26rpx; |
| 119 | color: rgba(255, 255, 255, 0.9); | 130 | color: rgba(255, 255, 255, 0.9); |
| 131 | + margin-bottom: 12rpx; | ||
| 120 | } | 132 | } |
| 121 | 133 | ||
| 122 | .main { | 134 | .main { |
美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue
| @@ -7,6 +7,7 @@ | @@ -7,6 +7,7 @@ | ||
| 7 | </view> | 7 | </view> |
| 8 | <view class="top-center"> | 8 | <view class="top-center"> |
| 9 | <text class="page-title">Bluetooth Printer</text> | 9 | <text class="page-title">Bluetooth Printer</text> |
| 10 | + <LocationPicker /> | ||
| 10 | </view> | 11 | </view> |
| 11 | <view class="top-right" @click="isMenuOpen = true"> | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | <AppIcon name="menu" size="sm" color="white" /> | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| @@ -15,13 +16,6 @@ | @@ -15,13 +16,6 @@ | ||
| 15 | </view> | 16 | </view> |
| 16 | 17 | ||
| 17 | <view class="content"> | 18 | <view class="content"> |
| 18 | - <!-- Error banner --> | ||
| 19 | - <view v-if="errorMsg" class="error-banner"> | ||
| 20 | - <AppIcon name="alert" size="sm" color="red" /> | ||
| 21 | - <text class="error-text">{{ errorMsg }}</text> | ||
| 22 | - </view> | ||
| 23 | - | ||
| 24 | - <!-- Current connection --> | ||
| 25 | <view v-if="connectedDevice" class="connected-card"> | 19 | <view v-if="connectedDevice" class="connected-card"> |
| 26 | <view class="connected-header"> | 20 | <view class="connected-header"> |
| 27 | <view class="connected-icon"> | 21 | <view class="connected-icon"> |
| @@ -37,45 +31,38 @@ | @@ -37,45 +31,38 @@ | ||
| 37 | </view> | 31 | </view> |
| 38 | </view> | 32 | </view> |
| 39 | 33 | ||
| 40 | - <!-- Scan section --> | ||
| 41 | <view class="section-header"> | 34 | <view class="section-header"> |
| 42 | - <text class="section-title">Available Devices</text> | 35 | + <text class="section-title">Available Printers</text> |
| 43 | <view class="btn-scan" :class="{ scanning: isScanning }" @click="handleScan"> | 36 | <view class="btn-scan" :class="{ scanning: isScanning }" @click="handleScan"> |
| 44 | <AppIcon name="refresh" size="sm" :color="isScanning ? 'white' : 'primary'" /> | 37 | <AppIcon name="refresh" size="sm" :color="isScanning ? 'white' : 'primary'" /> |
| 45 | <text class="btn-scan-text">{{ isScanning ? 'Scanning...' : 'Scan' }}</text> | 38 | <text class="btn-scan-text">{{ isScanning ? 'Scanning...' : 'Scan' }}</text> |
| 46 | </view> | 39 | </view> |
| 47 | </view> | 40 | </view> |
| 48 | 41 | ||
| 49 | - <view v-if="isScanning && devices.length === 0" class="scanning-wrap"> | 42 | + <view v-if="isScanning && !showDevices" class="scanning-wrap"> |
| 50 | <view class="scan-anim"> | 43 | <view class="scan-anim"> |
| 51 | <AppIcon name="bluetooth" size="lg" color="blue" /> | 44 | <AppIcon name="bluetooth" size="lg" color="blue" /> |
| 52 | </view> | 45 | </view> |
| 53 | - <text class="scan-text">Searching for nearby devices...</text> | ||
| 54 | - </view> | ||
| 55 | - | ||
| 56 | - <view v-else-if="!isScanning && devices.length === 0" class="empty-wrap"> | ||
| 57 | - <AppIcon name="bluetooth" size="lg" color="gray" /> | ||
| 58 | - <text class="empty-title">No Devices Found</text> | ||
| 59 | - <text class="empty-desc">Make sure your Bluetooth printer is turned on and in pairing mode, then tap Scan.</text> | 46 | + <text class="scan-text">Searching for nearby printers...</text> |
| 60 | </view> | 47 | </view> |
| 61 | 48 | ||
| 62 | <view v-else class="device-list"> | 49 | <view v-else class="device-list"> |
| 63 | <view | 50 | <view |
| 64 | - v-for="dev in devices" | 51 | + v-for="dev in mockPrinters" |
| 65 | :key="dev.deviceId" | 52 | :key="dev.deviceId" |
| 66 | class="device-card" | 53 | class="device-card" |
| 67 | :class="{ connecting: connectingId === dev.deviceId }" | 54 | :class="{ connecting: connectingId === dev.deviceId }" |
| 68 | @click="handleConnect(dev)" | 55 | @click="handleConnect(dev)" |
| 69 | > | 56 | > |
| 70 | - <view class="device-icon" :class="{ printer: dev.isPrinter }"> | ||
| 71 | - <AppIcon :name="dev.isPrinter ? 'printer' : 'bluetooth'" size="md" :color="dev.isPrinter ? 'blue' : 'gray'" /> | 57 | + <view class="device-icon printer"> |
| 58 | + <AppIcon name="printer" size="md" color="blue" /> | ||
| 72 | </view> | 59 | </view> |
| 73 | <view class="device-info"> | 60 | <view class="device-info"> |
| 74 | - <text class="device-name">{{ dev.name || 'Unknown Device' }}</text> | 61 | + <text class="device-name">{{ dev.name }}</text> |
| 75 | <text class="device-id">{{ dev.deviceId }}</text> | 62 | <text class="device-id">{{ dev.deviceId }}</text> |
| 76 | <view class="device-meta"> | 63 | <view class="device-meta"> |
| 77 | - <text v-if="dev.isPrinter" class="device-tag">Printer</text> | ||
| 78 | - <text v-if="dev.RSSI" class="device-rssi">Signal: {{ dev.RSSI }}dBm</text> | 64 | + <text class="device-tag">Printer</text> |
| 65 | + <text class="device-rssi">Signal: {{ dev.RSSI }}dBm</text> | ||
| 79 | </view> | 66 | </view> |
| 80 | </view> | 67 | </view> |
| 81 | <view v-if="connectingId === dev.deviceId" class="device-action"> | 68 | <view v-if="connectingId === dev.deviceId" class="device-action"> |
| @@ -102,40 +89,24 @@ | @@ -102,40 +89,24 @@ | ||
| 102 | </template> | 89 | </template> |
| 103 | 90 | ||
| 104 | <script setup lang="ts"> | 91 | <script setup lang="ts"> |
| 105 | -import { ref, computed, onMounted, onUnmounted } from 'vue' | 92 | +import { ref, computed } from 'vue' |
| 106 | import AppIcon from '../../components/AppIcon.vue' | 93 | import AppIcon from '../../components/AppIcon.vue' |
| 107 | import SideMenu from '../../components/SideMenu.vue' | 94 | import SideMenu from '../../components/SideMenu.vue' |
| 95 | +import LocationPicker from '../../components/LocationPicker.vue' | ||
| 108 | import { getStatusBarHeight } from '../../utils/statusBar' | 96 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 109 | 97 | ||
| 110 | const statusBarHeight = getStatusBarHeight() | 98 | const statusBarHeight = getStatusBarHeight() |
| 111 | const isMenuOpen = ref(false) | 99 | const isMenuOpen = ref(false) |
| 112 | const isScanning = ref(false) | 100 | const isScanning = ref(false) |
| 101 | +const showDevices = ref(true) | ||
| 113 | const connectingId = ref('') | 102 | const connectingId = ref('') |
| 114 | -const errorMsg = ref('') | ||
| 115 | -const btAdapterReady = ref(false) | ||
| 116 | - | ||
| 117 | -interface BtDevice { | ||
| 118 | - deviceId: string | ||
| 119 | - name: string | ||
| 120 | - isPrinter: boolean | ||
| 121 | - RSSI?: number | ||
| 122 | -} | ||
| 123 | - | ||
| 124 | -const devices = ref<BtDevice[]>([]) | ||
| 125 | -const discoveredIds = new Set<string>() | ||
| 126 | 103 | ||
| 127 | -const PRINTER_KEYWORDS = [ | ||
| 128 | - 'zebra', 'brother', 'tsc', 'epson', 'star', 'bixolon', 'honeywell', | ||
| 129 | - 'citizen', 'sato', 'godex', 'ql-', 'zd', 'zt', 'printer', 'print', | ||
| 130 | - 'label', 'receipt', 'pos', 'tdp', 'ttp', 'zq', 'ds-', 'rj-', | 104 | +const mockPrinters = [ |
| 105 | + { deviceId: 'BT:ZB:42:3A:8C:01', name: 'Zebra ZD421', RSSI: -45 }, | ||
| 106 | + { deviceId: 'BT:BR:55:1D:7E:02', name: 'Brother QL-820NWB', RSSI: -58 }, | ||
| 107 | + { deviceId: 'BT:EP:61:4F:2B:03', name: 'Epson TM-T88VI', RSSI: -72 }, | ||
| 131 | ] | 108 | ] |
| 132 | 109 | ||
| 133 | -const isPrinterDevice = (name: string): boolean => { | ||
| 134 | - if (!name) return false | ||
| 135 | - const lower = name.toLowerCase() | ||
| 136 | - return PRINTER_KEYWORDS.some(kw => lower.includes(kw)) | ||
| 137 | -} | ||
| 138 | - | ||
| 139 | const connectedDevice = computed(() => { | 110 | const connectedDevice = computed(() => { |
| 140 | const name = uni.getStorageSync('btDeviceName') | 111 | const name = uni.getStorageSync('btDeviceName') |
| 141 | const id = uni.getStorageSync('btDeviceId') | 112 | const id = uni.getStorageSync('btDeviceId') |
| @@ -145,171 +116,35 @@ const connectedDevice = computed(() => { | @@ -145,171 +116,35 @@ const connectedDevice = computed(() => { | ||
| 145 | 116 | ||
| 146 | const goBack = () => uni.navigateBack() | 117 | const goBack = () => uni.navigateBack() |
| 147 | 118 | ||
| 148 | -const initBluetooth = (): Promise<void> => { | ||
| 149 | - return new Promise((resolve, reject) => { | ||
| 150 | - uni.openBluetoothAdapter({ | ||
| 151 | - success: () => { | ||
| 152 | - btAdapterReady.value = true | ||
| 153 | - errorMsg.value = '' | ||
| 154 | - resolve() | ||
| 155 | - }, | ||
| 156 | - fail: (err: any) => { | ||
| 157 | - btAdapterReady.value = false | ||
| 158 | - if (err.errCode === 10001 || (err.errMsg && err.errMsg.includes('not available'))) { | ||
| 159 | - errorMsg.value = 'Bluetooth is turned off. Please enable Bluetooth in system settings.' | ||
| 160 | - } else { | ||
| 161 | - errorMsg.value = 'Failed to initialize Bluetooth: ' + (err.errMsg || 'Unknown error') | ||
| 162 | - } | ||
| 163 | - reject(err) | ||
| 164 | - }, | ||
| 165 | - }) | ||
| 166 | - }) | ||
| 167 | -} | ||
| 168 | - | ||
| 169 | -const startDiscovery = () => { | ||
| 170 | - uni.startBluetoothDevicesDiscovery({ | ||
| 171 | - allowDuplicatesKey: false, | ||
| 172 | - success: () => { | ||
| 173 | - isScanning.value = true | ||
| 174 | - errorMsg.value = '' | ||
| 175 | - }, | ||
| 176 | - fail: (err: any) => { | ||
| 177 | - isScanning.value = false | ||
| 178 | - errorMsg.value = 'Failed to start scanning: ' + (err.errMsg || 'Unknown error') | ||
| 179 | - }, | ||
| 180 | - }) | ||
| 181 | -} | ||
| 182 | - | ||
| 183 | -const stopDiscovery = () => { | ||
| 184 | - uni.stopBluetoothDevicesDiscovery({ | ||
| 185 | - complete: () => { | ||
| 186 | - isScanning.value = false | ||
| 187 | - }, | ||
| 188 | - }) | ||
| 189 | -} | ||
| 190 | - | ||
| 191 | -const onDeviceFound = (res: any) => { | ||
| 192 | - const foundDevices: any[] = res.devices || [] | ||
| 193 | - for (const d of foundDevices) { | ||
| 194 | - if (discoveredIds.has(d.deviceId)) continue | ||
| 195 | - if (!d.name && !d.localName) continue | ||
| 196 | - | ||
| 197 | - discoveredIds.add(d.deviceId) | ||
| 198 | - const name = d.localName || d.name || '' | ||
| 199 | - devices.value.push({ | ||
| 200 | - deviceId: d.deviceId, | ||
| 201 | - name, | ||
| 202 | - isPrinter: isPrinterDevice(name), | ||
| 203 | - RSSI: d.RSSI, | ||
| 204 | - }) | ||
| 205 | - } | ||
| 206 | - | ||
| 207 | - devices.value.sort((a, b) => { | ||
| 208 | - if (a.isPrinter && !b.isPrinter) return -1 | ||
| 209 | - if (!a.isPrinter && b.isPrinter) return 1 | ||
| 210 | - return (b.RSSI || -100) - (a.RSSI || -100) | ||
| 211 | - }) | ||
| 212 | -} | ||
| 213 | - | ||
| 214 | -const handleScan = async () => { | 119 | +const handleScan = () => { |
| 215 | if (isScanning.value) { | 120 | if (isScanning.value) { |
| 216 | - stopDiscovery() | 121 | + isScanning.value = false |
| 217 | return | 122 | return |
| 218 | } | 123 | } |
| 219 | - | ||
| 220 | - errorMsg.value = '' | ||
| 221 | - devices.value = [] | ||
| 222 | - discoveredIds.clear() | ||
| 223 | - | ||
| 224 | - try { | ||
| 225 | - if (!btAdapterReady.value) { | ||
| 226 | - await initBluetooth() | ||
| 227 | - } | ||
| 228 | - startDiscovery() | ||
| 229 | - | ||
| 230 | - setTimeout(() => { | ||
| 231 | - if (isScanning.value) stopDiscovery() | ||
| 232 | - }, 15000) | ||
| 233 | - } catch (_) { | ||
| 234 | - // error already set in initBluetooth | ||
| 235 | - } | 124 | + isScanning.value = true |
| 125 | + showDevices.value = false | ||
| 126 | + setTimeout(() => { | ||
| 127 | + showDevices.value = true | ||
| 128 | + isScanning.value = false | ||
| 129 | + }, 2000) | ||
| 236 | } | 130 | } |
| 237 | 131 | ||
| 238 | -const handleConnect = async (dev: BtDevice) => { | 132 | +const handleConnect = (dev: any) => { |
| 239 | if (connectingId.value) return | 133 | if (connectingId.value) return |
| 240 | connectingId.value = dev.deviceId | 134 | connectingId.value = dev.deviceId |
| 241 | - errorMsg.value = '' | ||
| 242 | - | ||
| 243 | - if (isScanning.value) stopDiscovery() | ||
| 244 | - | ||
| 245 | - uni.createBLEConnection({ | ||
| 246 | - deviceId: dev.deviceId, | ||
| 247 | - timeout: 10000, | ||
| 248 | - success: () => { | ||
| 249 | - uni.setStorageSync('btDeviceName', dev.name) | ||
| 250 | - uni.setStorageSync('btDeviceId', dev.deviceId) | ||
| 251 | - connectingId.value = '' | ||
| 252 | - uni.showToast({ title: 'Connected!', icon: 'success' }) | ||
| 253 | - }, | ||
| 254 | - fail: (err: any) => { | ||
| 255 | - connectingId.value = '' | ||
| 256 | - if (err.errCode === -1) { | ||
| 257 | - uni.setStorageSync('btDeviceName', dev.name) | ||
| 258 | - uni.setStorageSync('btDeviceId', dev.deviceId) | ||
| 259 | - uni.showToast({ title: 'Already connected', icon: 'success' }) | ||
| 260 | - } else { | ||
| 261 | - errorMsg.value = 'Connection failed: ' + (err.errMsg || 'Please try again') | ||
| 262 | - } | ||
| 263 | - }, | ||
| 264 | - }) | 135 | + setTimeout(() => { |
| 136 | + uni.setStorageSync('btDeviceName', dev.name) | ||
| 137 | + uni.setStorageSync('btDeviceId', dev.deviceId) | ||
| 138 | + connectingId.value = '' | ||
| 139 | + uni.showToast({ title: 'Connected!', icon: 'success' }) | ||
| 140 | + }, 1500) | ||
| 265 | } | 141 | } |
| 266 | 142 | ||
| 267 | const handleDisconnect = () => { | 143 | const handleDisconnect = () => { |
| 268 | - const id = uni.getStorageSync('btDeviceId') | ||
| 269 | - if (id) { | ||
| 270 | - uni.closeBLEConnection({ | ||
| 271 | - deviceId: id, | ||
| 272 | - complete: () => { | ||
| 273 | - uni.removeStorageSync('btDeviceName') | ||
| 274 | - uni.removeStorageSync('btDeviceId') | ||
| 275 | - uni.showToast({ title: 'Disconnected', icon: 'none' }) | ||
| 276 | - }, | ||
| 277 | - }) | ||
| 278 | - } else { | ||
| 279 | - uni.removeStorageSync('btDeviceName') | ||
| 280 | - uni.removeStorageSync('btDeviceId') | ||
| 281 | - } | 144 | + uni.removeStorageSync('btDeviceName') |
| 145 | + uni.removeStorageSync('btDeviceId') | ||
| 146 | + uni.showToast({ title: 'Disconnected', icon: 'none' }) | ||
| 282 | } | 147 | } |
| 283 | - | ||
| 284 | -onMounted(async () => { | ||
| 285 | - uni.onBluetoothDeviceFound(onDeviceFound) | ||
| 286 | - | ||
| 287 | - uni.onBluetoothAdapterStateChange((res: any) => { | ||
| 288 | - if (!res.available) { | ||
| 289 | - btAdapterReady.value = false | ||
| 290 | - isScanning.value = false | ||
| 291 | - errorMsg.value = 'Bluetooth has been turned off.' | ||
| 292 | - } else { | ||
| 293 | - btAdapterReady.value = true | ||
| 294 | - errorMsg.value = '' | ||
| 295 | - } | ||
| 296 | - }) | ||
| 297 | - | ||
| 298 | - try { | ||
| 299 | - await initBluetooth() | ||
| 300 | - startDiscovery() | ||
| 301 | - setTimeout(() => { | ||
| 302 | - if (isScanning.value) stopDiscovery() | ||
| 303 | - }, 15000) | ||
| 304 | - } catch (_) { | ||
| 305 | - // error already shown | ||
| 306 | - } | ||
| 307 | -}) | ||
| 308 | - | ||
| 309 | -onUnmounted(() => { | ||
| 310 | - if (isScanning.value) stopDiscovery() | ||
| 311 | - uni.offBluetoothDeviceFound(onDeviceFound) | ||
| 312 | -}) | ||
| 313 | </script> | 148 | </script> |
| 314 | 149 | ||
| 315 | <style scoped> | 150 | <style scoped> |
| @@ -343,7 +178,9 @@ onUnmounted(() => { | @@ -343,7 +178,9 @@ onUnmounted(() => { | ||
| 343 | 178 | ||
| 344 | .top-center { | 179 | .top-center { |
| 345 | flex: 1; | 180 | flex: 1; |
| 346 | - text-align: center; | 181 | + display: flex; |
| 182 | + flex-direction: column; | ||
| 183 | + align-items: center; | ||
| 347 | } | 184 | } |
| 348 | 185 | ||
| 349 | .page-title { | 186 | .page-title { |
| @@ -356,31 +193,12 @@ onUnmounted(() => { | @@ -356,31 +193,12 @@ onUnmounted(() => { | ||
| 356 | padding: 32rpx; | 193 | padding: 32rpx; |
| 357 | } | 194 | } |
| 358 | 195 | ||
| 359 | -/* Error banner */ | ||
| 360 | -.error-banner { | ||
| 361 | - display: flex; | ||
| 362 | - align-items: center; | ||
| 363 | - gap: 16rpx; | ||
| 364 | - padding: 24rpx 28rpx; | ||
| 365 | - background: #fef2f2; | ||
| 366 | - border: 1rpx solid #fecaca; | ||
| 367 | - border-radius: 16rpx; | ||
| 368 | - margin-bottom: 24rpx; | ||
| 369 | -} | ||
| 370 | -.error-text { | ||
| 371 | - flex: 1; | ||
| 372 | - font-size: 26rpx; | ||
| 373 | - color: #991b1b; | ||
| 374 | - line-height: 1.4; | ||
| 375 | -} | ||
| 376 | - | ||
| 377 | -/* Connected device card */ | ||
| 378 | .connected-card { | 196 | .connected-card { |
| 379 | background: #fff; | 197 | background: #fff; |
| 380 | padding: 32rpx; | 198 | padding: 32rpx; |
| 381 | border-radius: 20rpx; | 199 | border-radius: 20rpx; |
| 382 | margin-bottom: 32rpx; | 200 | margin-bottom: 32rpx; |
| 383 | - box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); | 201 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); |
| 384 | border: 2rpx solid #4ade80; | 202 | border: 2rpx solid #4ade80; |
| 385 | } | 203 | } |
| 386 | 204 | ||
| @@ -424,8 +242,8 @@ onUnmounted(() => { | @@ -424,8 +242,8 @@ onUnmounted(() => { | ||
| 424 | display: flex; | 242 | display: flex; |
| 425 | align-items: center; | 243 | align-items: center; |
| 426 | justify-content: center; | 244 | justify-content: center; |
| 427 | - cursor: pointer; | ||
| 428 | } | 245 | } |
| 246 | + | ||
| 429 | .btn-disconnect-text { | 247 | .btn-disconnect-text { |
| 430 | font-size: 28rpx; | 248 | font-size: 28rpx; |
| 431 | font-weight: 600; | 249 | font-weight: 600; |
| @@ -433,7 +251,6 @@ onUnmounted(() => { | @@ -433,7 +251,6 @@ onUnmounted(() => { | ||
| 433 | line-height: 1; | 251 | line-height: 1; |
| 434 | } | 252 | } |
| 435 | 253 | ||
| 436 | -/* Section header */ | ||
| 437 | .section-header { | 254 | .section-header { |
| 438 | display: flex; | 255 | display: flex; |
| 439 | justify-content: space-between; | 256 | justify-content: space-between; |
| @@ -456,7 +273,6 @@ onUnmounted(() => { | @@ -456,7 +273,6 @@ onUnmounted(() => { | ||
| 456 | background: #fff; | 273 | background: #fff; |
| 457 | border: 2rpx solid #e5e7eb; | 274 | border: 2rpx solid #e5e7eb; |
| 458 | border-radius: 14rpx; | 275 | border-radius: 14rpx; |
| 459 | - cursor: pointer; | ||
| 460 | } | 276 | } |
| 461 | 277 | ||
| 462 | .btn-scan-text { | 278 | .btn-scan-text { |
| @@ -475,7 +291,6 @@ onUnmounted(() => { | @@ -475,7 +291,6 @@ onUnmounted(() => { | ||
| 475 | color: #fff; | 291 | color: #fff; |
| 476 | } | 292 | } |
| 477 | 293 | ||
| 478 | -/* Scanning state */ | ||
| 479 | .scanning-wrap { | 294 | .scanning-wrap { |
| 480 | display: flex; | 295 | display: flex; |
| 481 | flex-direction: column; | 296 | flex-direction: column; |
| @@ -505,30 +320,6 @@ onUnmounted(() => { | @@ -505,30 +320,6 @@ onUnmounted(() => { | ||
| 505 | color: #6b7280; | 320 | color: #6b7280; |
| 506 | } | 321 | } |
| 507 | 322 | ||
| 508 | -/* Empty state */ | ||
| 509 | -.empty-wrap { | ||
| 510 | - display: flex; | ||
| 511 | - flex-direction: column; | ||
| 512 | - align-items: center; | ||
| 513 | - padding: 80rpx 24rpx; | ||
| 514 | -} | ||
| 515 | - | ||
| 516 | -.empty-title { | ||
| 517 | - font-size: 32rpx; | ||
| 518 | - font-weight: 600; | ||
| 519 | - color: #111827; | ||
| 520 | - margin-top: 24rpx; | ||
| 521 | - margin-bottom: 12rpx; | ||
| 522 | -} | ||
| 523 | - | ||
| 524 | -.empty-desc { | ||
| 525 | - font-size: 26rpx; | ||
| 526 | - color: #6b7280; | ||
| 527 | - text-align: center; | ||
| 528 | - line-height: 1.5; | ||
| 529 | -} | ||
| 530 | - | ||
| 531 | -/* Device list */ | ||
| 532 | .device-list { | 323 | .device-list { |
| 533 | margin-bottom: 32rpx; | 324 | margin-bottom: 32rpx; |
| 534 | } | 325 | } |
| @@ -541,9 +332,7 @@ onUnmounted(() => { | @@ -541,9 +332,7 @@ onUnmounted(() => { | ||
| 541 | display: flex; | 332 | display: flex; |
| 542 | align-items: center; | 333 | align-items: center; |
| 543 | gap: 24rpx; | 334 | gap: 24rpx; |
| 544 | - box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); | ||
| 545 | - cursor: pointer; | ||
| 546 | - transition: background 0.15s; | 335 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); |
| 547 | } | 336 | } |
| 548 | 337 | ||
| 549 | .device-card:active { | 338 | .device-card:active { |
| @@ -619,7 +408,6 @@ onUnmounted(() => { | @@ -619,7 +408,6 @@ onUnmounted(() => { | ||
| 619 | font-weight: 500; | 408 | font-weight: 500; |
| 620 | } | 409 | } |
| 621 | 410 | ||
| 622 | -/* Tips */ | ||
| 623 | .tips-card { | 411 | .tips-card { |
| 624 | background: #fffbeb; | 412 | background: #fffbeb; |
| 625 | border: 1rpx solid #fde68a; | 413 | border: 1rpx solid #fde68a; |
美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue.bak
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="page"> | ||
| 3 | + <view class="header-hero" :style="{ paddingTop: statusBarHeight + 'px' }"> | ||
| 4 | + <view class="top-bar"> | ||
| 5 | + <view class="top-left" @click="goBack"> | ||
| 6 | + <AppIcon name="chevronLeft" size="sm" color="white" /> | ||
| 7 | + </view> | ||
| 8 | + <view class="top-center"> | ||
| 9 | + <text class="page-title">Bluetooth Printer</text> | ||
| 10 | + <LocationPicker /> | ||
| 11 | + </view> | ||
| 12 | + <view class="top-right" @click="isMenuOpen = true"> | ||
| 13 | + <AppIcon name="menu" size="sm" color="white" /> | ||
| 14 | + </view> | ||
| 15 | + </view> | ||
| 16 | + </view> | ||
| 17 | + | ||
| 18 | + <view class="content"> | ||
| 19 | + <!-- Error banner --> | ||
| 20 | + <view v-if="errorMsg" class="error-banner"> | ||
| 21 | + <AppIcon name="alert" size="sm" color="red" /> | ||
| 22 | + <text class="error-text">{{ errorMsg }}</text> | ||
| 23 | + </view> | ||
| 24 | + | ||
| 25 | + <!-- Current connection --> | ||
| 26 | + <view v-if="connectedDevice" class="connected-card"> | ||
| 27 | + <view class="connected-header"> | ||
| 28 | + <view class="connected-icon"> | ||
| 29 | + <AppIcon name="bluetooth" size="md" color="white" /> | ||
| 30 | + </view> | ||
| 31 | + <view class="connected-info"> | ||
| 32 | + <text class="connected-name">{{ connectedDevice.name }}</text> | ||
| 33 | + <text class="connected-status">Connected</text> | ||
| 34 | + </view> | ||
| 35 | + </view> | ||
| 36 | + <view class="btn-disconnect" @click="handleDisconnect"> | ||
| 37 | + <text class="btn-disconnect-text">Disconnect</text> | ||
| 38 | + </view> | ||
| 39 | + </view> | ||
| 40 | + | ||
| 41 | + <!-- Scan section --> | ||
| 42 | + <view class="section-header"> | ||
| 43 | + <text class="section-title">Available Devices</text> | ||
| 44 | + <view class="btn-scan" :class="{ scanning: isScanning }" @click="handleScan"> | ||
| 45 | + <AppIcon name="refresh" size="sm" :color="isScanning ? 'white' : 'primary'" /> | ||
| 46 | + <text class="btn-scan-text">{{ isScanning ? 'Scanning...' : 'Scan' }}</text> | ||
| 47 | + </view> | ||
| 48 | + </view> | ||
| 49 | + | ||
| 50 | + <view v-if="isScanning && devices.length === 0" class="scanning-wrap"> | ||
| 51 | + <view class="scan-anim"> | ||
| 52 | + <AppIcon name="bluetooth" size="lg" color="blue" /> | ||
| 53 | + </view> | ||
| 54 | + <text class="scan-text">Searching for nearby devices...</text> | ||
| 55 | + </view> | ||
| 56 | + | ||
| 57 | + <view v-else-if="!isScanning && devices.length === 0" class="empty-wrap"> | ||
| 58 | + <AppIcon name="bluetooth" size="lg" color="gray" /> | ||
| 59 | + <text class="empty-title">No Devices Found</text> | ||
| 60 | + <text class="empty-desc">Make sure your Bluetooth printer is turned on and in pairing mode, then tap Scan.</text> | ||
| 61 | + </view> | ||
| 62 | + | ||
| 63 | + <view v-else class="device-list"> | ||
| 64 | + <view | ||
| 65 | + v-for="dev in devices" | ||
| 66 | + :key="dev.deviceId" | ||
| 67 | + class="device-card" | ||
| 68 | + :class="{ connecting: connectingId === dev.deviceId }" | ||
| 69 | + @click="handleConnect(dev)" | ||
| 70 | + > | ||
| 71 | + <view class="device-icon" :class="{ printer: dev.isPrinter }"> | ||
| 72 | + <AppIcon :name="dev.isPrinter ? 'printer' : 'bluetooth'" size="md" :color="dev.isPrinter ? 'blue' : 'gray'" /> | ||
| 73 | + </view> | ||
| 74 | + <view class="device-info"> | ||
| 75 | + <text class="device-name">{{ dev.name || 'Unknown Device' }}</text> | ||
| 76 | + <text class="device-id">{{ dev.deviceId }}</text> | ||
| 77 | + <view class="device-meta"> | ||
| 78 | + <text v-if="dev.isPrinter" class="device-tag">Printer</text> | ||
| 79 | + <text v-if="dev.RSSI" class="device-rssi">Signal: {{ dev.RSSI }}dBm</text> | ||
| 80 | + </view> | ||
| 81 | + </view> | ||
| 82 | + <view v-if="connectingId === dev.deviceId" class="device-action"> | ||
| 83 | + <text class="connecting-text">Connecting...</text> | ||
| 84 | + </view> | ||
| 85 | + <view v-else class="device-action"> | ||
| 86 | + <AppIcon name="chevronRight" size="sm" color="gray" /> | ||
| 87 | + </view> | ||
| 88 | + </view> | ||
| 89 | + </view> | ||
| 90 | + | ||
| 91 | + <view class="tips-card"> | ||
| 92 | + <text class="tips-title">Troubleshooting</text> | ||
| 93 | + <text class="tips-item">1. Ensure the printer is powered on</text> | ||
| 94 | + <text class="tips-item">2. Place the device within 10 meters</text> | ||
| 95 | + <text class="tips-item">3. Check if Bluetooth is enabled in system settings</text> | ||
| 96 | + <text class="tips-item">4. Grant Bluetooth & Location permissions when prompted</text> | ||
| 97 | + <text class="tips-item tips-item-last">5. Try restarting the printer if not visible</text> | ||
| 98 | + </view> | ||
| 99 | + </view> | ||
| 100 | + | ||
| 101 | + <SideMenu v-model="isMenuOpen" /> | ||
| 102 | + </view> | ||
| 103 | +</template> | ||
| 104 | + | ||
| 105 | +<script setup lang="ts"> | ||
| 106 | +import { ref, computed, onMounted, onUnmounted } from 'vue' | ||
| 107 | +import AppIcon from '../../components/AppIcon.vue' | ||
| 108 | +import SideMenu from '../../components/SideMenu.vue' | ||
| 109 | +import LocationPicker from '../../components/LocationPicker.vue' | ||
| 110 | +import { getStatusBarHeight } from '../../utils/statusBar' | ||
| 111 | + | ||
| 112 | +const statusBarHeight = getStatusBarHeight() | ||
| 113 | +const isMenuOpen = ref(false) | ||
| 114 | +const isScanning = ref(false) | ||
| 115 | +const connectingId = ref('') | ||
| 116 | +const errorMsg = ref('') | ||
| 117 | +const btAdapterReady = ref(false) | ||
| 118 | + | ||
| 119 | +interface BtDevice { | ||
| 120 | + deviceId: string | ||
| 121 | + name: string | ||
| 122 | + isPrinter: boolean | ||
| 123 | + RSSI?: number | ||
| 124 | +} | ||
| 125 | + | ||
| 126 | +const devices = ref<BtDevice[]>([]) | ||
| 127 | +const discoveredIds = new Set<string>() | ||
| 128 | + | ||
| 129 | +const PRINTER_KEYWORDS = [ | ||
| 130 | + 'zebra', 'brother', 'tsc', 'epson', 'star', 'bixolon', 'honeywell', | ||
| 131 | + 'citizen', 'sato', 'godex', 'ql-', 'zd', 'zt', 'printer', 'print', | ||
| 132 | + 'label', 'receipt', 'pos', 'tdp', 'ttp', 'zq', 'ds-', 'rj-', | ||
| 133 | +] | ||
| 134 | + | ||
| 135 | +const isPrinterDevice = (name: string): boolean => { | ||
| 136 | + if (!name) return false | ||
| 137 | + const lower = name.toLowerCase() | ||
| 138 | + return PRINTER_KEYWORDS.some(kw => lower.includes(kw)) | ||
| 139 | +} | ||
| 140 | + | ||
| 141 | +const connectedDevice = computed(() => { | ||
| 142 | + const name = uni.getStorageSync('btDeviceName') | ||
| 143 | + const id = uni.getStorageSync('btDeviceId') | ||
| 144 | + if (name && id) return { name, deviceId: id } | ||
| 145 | + return null | ||
| 146 | +}) | ||
| 147 | + | ||
| 148 | +const goBack = () => uni.navigateBack() | ||
| 149 | + | ||
| 150 | +const initBluetooth = (): Promise<void> => { | ||
| 151 | + return new Promise((resolve, reject) => { | ||
| 152 | + uni.openBluetoothAdapter({ | ||
| 153 | + success: () => { | ||
| 154 | + btAdapterReady.value = true | ||
| 155 | + errorMsg.value = '' | ||
| 156 | + resolve() | ||
| 157 | + }, | ||
| 158 | + fail: (err: any) => { | ||
| 159 | + btAdapterReady.value = false | ||
| 160 | + if (err.errCode === 10001 || (err.errMsg && err.errMsg.includes('not available'))) { | ||
| 161 | + errorMsg.value = 'Bluetooth is turned off. Please enable Bluetooth in system settings.' | ||
| 162 | + } else { | ||
| 163 | + errorMsg.value = 'Failed to initialize Bluetooth: ' + (err.errMsg || 'Unknown error') | ||
| 164 | + } | ||
| 165 | + reject(err) | ||
| 166 | + }, | ||
| 167 | + }) | ||
| 168 | + }) | ||
| 169 | +} | ||
| 170 | + | ||
| 171 | +const startDiscovery = () => { | ||
| 172 | + uni.startBluetoothDevicesDiscovery({ | ||
| 173 | + allowDuplicatesKey: false, | ||
| 174 | + success: () => { | ||
| 175 | + isScanning.value = true | ||
| 176 | + errorMsg.value = '' | ||
| 177 | + }, | ||
| 178 | + fail: (err: any) => { | ||
| 179 | + isScanning.value = false | ||
| 180 | + errorMsg.value = 'Failed to start scanning: ' + (err.errMsg || 'Unknown error') | ||
| 181 | + }, | ||
| 182 | + }) | ||
| 183 | +} | ||
| 184 | + | ||
| 185 | +const stopDiscovery = () => { | ||
| 186 | + uni.stopBluetoothDevicesDiscovery({ | ||
| 187 | + complete: () => { | ||
| 188 | + isScanning.value = false | ||
| 189 | + }, | ||
| 190 | + }) | ||
| 191 | +} | ||
| 192 | + | ||
| 193 | +const onDeviceFound = (res: any) => { | ||
| 194 | + const foundDevices: any[] = res.devices || [] | ||
| 195 | + for (const d of foundDevices) { | ||
| 196 | + if (discoveredIds.has(d.deviceId)) continue | ||
| 197 | + if (!d.name && !d.localName) continue | ||
| 198 | + | ||
| 199 | + discoveredIds.add(d.deviceId) | ||
| 200 | + const name = d.localName || d.name || '' | ||
| 201 | + devices.value.push({ | ||
| 202 | + deviceId: d.deviceId, | ||
| 203 | + name, | ||
| 204 | + isPrinter: isPrinterDevice(name), | ||
| 205 | + RSSI: d.RSSI, | ||
| 206 | + }) | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + devices.value.sort((a, b) => { | ||
| 210 | + if (a.isPrinter && !b.isPrinter) return -1 | ||
| 211 | + if (!a.isPrinter && b.isPrinter) return 1 | ||
| 212 | + return (b.RSSI || -100) - (a.RSSI || -100) | ||
| 213 | + }) | ||
| 214 | +} | ||
| 215 | + | ||
| 216 | +const handleScan = async () => { | ||
| 217 | + if (isScanning.value) { | ||
| 218 | + stopDiscovery() | ||
| 219 | + return | ||
| 220 | + } | ||
| 221 | + | ||
| 222 | + errorMsg.value = '' | ||
| 223 | + devices.value = [] | ||
| 224 | + discoveredIds.clear() | ||
| 225 | + | ||
| 226 | + try { | ||
| 227 | + if (!btAdapterReady.value) { | ||
| 228 | + await initBluetooth() | ||
| 229 | + } | ||
| 230 | + startDiscovery() | ||
| 231 | + | ||
| 232 | + setTimeout(() => { | ||
| 233 | + if (isScanning.value) stopDiscovery() | ||
| 234 | + }, 15000) | ||
| 235 | + } catch (_) { | ||
| 236 | + // error already set in initBluetooth | ||
| 237 | + } | ||
| 238 | +} | ||
| 239 | + | ||
| 240 | +const handleConnect = async (dev: BtDevice) => { | ||
| 241 | + if (connectingId.value) return | ||
| 242 | + connectingId.value = dev.deviceId | ||
| 243 | + errorMsg.value = '' | ||
| 244 | + | ||
| 245 | + if (isScanning.value) stopDiscovery() | ||
| 246 | + | ||
| 247 | + uni.createBLEConnection({ | ||
| 248 | + deviceId: dev.deviceId, | ||
| 249 | + timeout: 10000, | ||
| 250 | + success: () => { | ||
| 251 | + uni.setStorageSync('btDeviceName', dev.name) | ||
| 252 | + uni.setStorageSync('btDeviceId', dev.deviceId) | ||
| 253 | + connectingId.value = '' | ||
| 254 | + uni.showToast({ title: 'Connected!', icon: 'success' }) | ||
| 255 | + }, | ||
| 256 | + fail: (err: any) => { | ||
| 257 | + connectingId.value = '' | ||
| 258 | + if (err.errCode === -1) { | ||
| 259 | + uni.setStorageSync('btDeviceName', dev.name) | ||
| 260 | + uni.setStorageSync('btDeviceId', dev.deviceId) | ||
| 261 | + uni.showToast({ title: 'Already connected', icon: 'success' }) | ||
| 262 | + } else { | ||
| 263 | + errorMsg.value = 'Connection failed: ' + (err.errMsg || 'Please try again') | ||
| 264 | + } | ||
| 265 | + }, | ||
| 266 | + }) | ||
| 267 | +} | ||
| 268 | + | ||
| 269 | +const handleDisconnect = () => { | ||
| 270 | + const id = uni.getStorageSync('btDeviceId') | ||
| 271 | + if (id) { | ||
| 272 | + uni.closeBLEConnection({ | ||
| 273 | + deviceId: id, | ||
| 274 | + complete: () => { | ||
| 275 | + uni.removeStorageSync('btDeviceName') | ||
| 276 | + uni.removeStorageSync('btDeviceId') | ||
| 277 | + uni.showToast({ title: 'Disconnected', icon: 'none' }) | ||
| 278 | + }, | ||
| 279 | + }) | ||
| 280 | + } else { | ||
| 281 | + uni.removeStorageSync('btDeviceName') | ||
| 282 | + uni.removeStorageSync('btDeviceId') | ||
| 283 | + } | ||
| 284 | +} | ||
| 285 | + | ||
| 286 | +onMounted(async () => { | ||
| 287 | + uni.onBluetoothDeviceFound(onDeviceFound) | ||
| 288 | + | ||
| 289 | + uni.onBluetoothAdapterStateChange((res: any) => { | ||
| 290 | + if (!res.available) { | ||
| 291 | + btAdapterReady.value = false | ||
| 292 | + isScanning.value = false | ||
| 293 | + errorMsg.value = 'Bluetooth has been turned off.' | ||
| 294 | + } else { | ||
| 295 | + btAdapterReady.value = true | ||
| 296 | + errorMsg.value = '' | ||
| 297 | + } | ||
| 298 | + }) | ||
| 299 | + | ||
| 300 | + try { | ||
| 301 | + await initBluetooth() | ||
| 302 | + startDiscovery() | ||
| 303 | + setTimeout(() => { | ||
| 304 | + if (isScanning.value) stopDiscovery() | ||
| 305 | + }, 15000) | ||
| 306 | + } catch (_) { | ||
| 307 | + // error already shown | ||
| 308 | + } | ||
| 309 | +}) | ||
| 310 | + | ||
| 311 | +onUnmounted(() => { | ||
| 312 | + if (isScanning.value) stopDiscovery() | ||
| 313 | + uni.offBluetoothDeviceFound(onDeviceFound) | ||
| 314 | +}) | ||
| 315 | +</script> | ||
| 316 | + | ||
| 317 | +<style scoped> | ||
| 318 | +.page { | ||
| 319 | + min-height: 100vh; | ||
| 320 | + background: #f9fafb; | ||
| 321 | +} | ||
| 322 | + | ||
| 323 | +.header-hero { | ||
| 324 | + background: linear-gradient(135deg, var(--theme-primary), var(--theme-primary-dark)); | ||
| 325 | + padding: 16rpx 32rpx 24rpx; | ||
| 326 | +} | ||
| 327 | + | ||
| 328 | +.top-bar { | ||
| 329 | + height: 96rpx; | ||
| 330 | + display: flex; | ||
| 331 | + align-items: center; | ||
| 332 | + justify-content: space-between; | ||
| 333 | +} | ||
| 334 | + | ||
| 335 | +.top-left, | ||
| 336 | +.top-right { | ||
| 337 | + width: 64rpx; | ||
| 338 | + height: 64rpx; | ||
| 339 | + border-radius: 999rpx; | ||
| 340 | + background: rgba(255, 255, 255, 0.15); | ||
| 341 | + display: flex; | ||
| 342 | + align-items: center; | ||
| 343 | + justify-content: center; | ||
| 344 | +} | ||
| 345 | + | ||
| 346 | +.top-center { | ||
| 347 | + flex: 1; | ||
| 348 | + display: flex; | ||
| 349 | + flex-direction: column; | ||
| 350 | + align-items: center; | ||
| 351 | +} | ||
| 352 | + | ||
| 353 | +.page-title { | ||
| 354 | + font-size: 34rpx; | ||
| 355 | + font-weight: 600; | ||
| 356 | + color: #fff; | ||
| 357 | +} | ||
| 358 | + | ||
| 359 | +.content { | ||
| 360 | + padding: 32rpx; | ||
| 361 | +} | ||
| 362 | + | ||
| 363 | +/* Error banner */ | ||
| 364 | +.error-banner { | ||
| 365 | + display: flex; | ||
| 366 | + align-items: center; | ||
| 367 | + gap: 16rpx; | ||
| 368 | + padding: 24rpx 28rpx; | ||
| 369 | + background: #fef2f2; | ||
| 370 | + border: 1rpx solid #fecaca; | ||
| 371 | + border-radius: 16rpx; | ||
| 372 | + margin-bottom: 24rpx; | ||
| 373 | +} | ||
| 374 | +.error-text { | ||
| 375 | + flex: 1; | ||
| 376 | + font-size: 26rpx; | ||
| 377 | + color: #991b1b; | ||
| 378 | + line-height: 1.4; | ||
| 379 | +} | ||
| 380 | + | ||
| 381 | +/* Connected device card */ | ||
| 382 | +.connected-card { | ||
| 383 | + background: #fff; | ||
| 384 | + padding: 32rpx; | ||
| 385 | + border-radius: 20rpx; | ||
| 386 | + margin-bottom: 32rpx; | ||
| 387 | + box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); | ||
| 388 | + border: 2rpx solid #4ade80; | ||
| 389 | +} | ||
| 390 | + | ||
| 391 | +.connected-header { | ||
| 392 | + display: flex; | ||
| 393 | + align-items: center; | ||
| 394 | + gap: 24rpx; | ||
| 395 | + margin-bottom: 24rpx; | ||
| 396 | +} | ||
| 397 | + | ||
| 398 | +.connected-icon { | ||
| 399 | + width: 80rpx; | ||
| 400 | + height: 80rpx; | ||
| 401 | + border-radius: 20rpx; | ||
| 402 | + background: var(--theme-primary); | ||
| 403 | + display: flex; | ||
| 404 | + align-items: center; | ||
| 405 | + justify-content: center; | ||
| 406 | +} | ||
| 407 | + | ||
| 408 | +.connected-name { | ||
| 409 | + font-size: 32rpx; | ||
| 410 | + font-weight: 600; | ||
| 411 | + color: #111827; | ||
| 412 | + display: block; | ||
| 413 | + margin-bottom: 4rpx; | ||
| 414 | +} | ||
| 415 | + | ||
| 416 | +.connected-status { | ||
| 417 | + font-size: 26rpx; | ||
| 418 | + color: #16a34a; | ||
| 419 | + font-weight: 500; | ||
| 420 | +} | ||
| 421 | + | ||
| 422 | +.btn-disconnect { | ||
| 423 | + width: 100%; | ||
| 424 | + height: 80rpx; | ||
| 425 | + background: #fff; | ||
| 426 | + border: 2rpx solid var(--theme-primary); | ||
| 427 | + border-radius: 14rpx; | ||
| 428 | + display: flex; | ||
| 429 | + align-items: center; | ||
| 430 | + justify-content: center; | ||
| 431 | + cursor: pointer; | ||
| 432 | +} | ||
| 433 | +.btn-disconnect-text { | ||
| 434 | + font-size: 28rpx; | ||
| 435 | + font-weight: 600; | ||
| 436 | + color: var(--theme-primary); | ||
| 437 | + line-height: 1; | ||
| 438 | +} | ||
| 439 | + | ||
| 440 | +/* Section header */ | ||
| 441 | +.section-header { | ||
| 442 | + display: flex; | ||
| 443 | + justify-content: space-between; | ||
| 444 | + align-items: center; | ||
| 445 | + margin-bottom: 24rpx; | ||
| 446 | +} | ||
| 447 | + | ||
| 448 | +.section-title { | ||
| 449 | + font-size: 32rpx; | ||
| 450 | + font-weight: 600; | ||
| 451 | + color: #111827; | ||
| 452 | +} | ||
| 453 | + | ||
| 454 | +.btn-scan { | ||
| 455 | + display: flex; | ||
| 456 | + align-items: center; | ||
| 457 | + gap: 8rpx; | ||
| 458 | + height: 64rpx; | ||
| 459 | + padding: 0 24rpx; | ||
| 460 | + background: #fff; | ||
| 461 | + border: 2rpx solid #e5e7eb; | ||
| 462 | + border-radius: 14rpx; | ||
| 463 | + cursor: pointer; | ||
| 464 | +} | ||
| 465 | + | ||
| 466 | +.btn-scan-text { | ||
| 467 | + font-size: 26rpx; | ||
| 468 | + color: var(--theme-primary); | ||
| 469 | + font-weight: 500; | ||
| 470 | + line-height: 1; | ||
| 471 | +} | ||
| 472 | + | ||
| 473 | +.btn-scan.scanning { | ||
| 474 | + background: var(--theme-primary); | ||
| 475 | + border-color: var(--theme-primary); | ||
| 476 | +} | ||
| 477 | + | ||
| 478 | +.btn-scan.scanning .btn-scan-text { | ||
| 479 | + color: #fff; | ||
| 480 | +} | ||
| 481 | + | ||
| 482 | +/* Scanning state */ | ||
| 483 | +.scanning-wrap { | ||
| 484 | + display: flex; | ||
| 485 | + flex-direction: column; | ||
| 486 | + align-items: center; | ||
| 487 | + padding: 80rpx 24rpx; | ||
| 488 | +} | ||
| 489 | + | ||
| 490 | +.scan-anim { | ||
| 491 | + width: 120rpx; | ||
| 492 | + height: 120rpx; | ||
| 493 | + background: #eff6ff; | ||
| 494 | + border-radius: 50%; | ||
| 495 | + display: flex; | ||
| 496 | + align-items: center; | ||
| 497 | + justify-content: center; | ||
| 498 | + margin-bottom: 24rpx; | ||
| 499 | + animation: pulse 1.5s ease-in-out infinite; | ||
| 500 | +} | ||
| 501 | + | ||
| 502 | +@keyframes pulse { | ||
| 503 | + 0%, 100% { transform: scale(1); opacity: 1; } | ||
| 504 | + 50% { transform: scale(1.1); opacity: 0.7; } | ||
| 505 | +} | ||
| 506 | + | ||
| 507 | +.scan-text { | ||
| 508 | + font-size: 28rpx; | ||
| 509 | + color: #6b7280; | ||
| 510 | +} | ||
| 511 | + | ||
| 512 | +/* Empty state */ | ||
| 513 | +.empty-wrap { | ||
| 514 | + display: flex; | ||
| 515 | + flex-direction: column; | ||
| 516 | + align-items: center; | ||
| 517 | + padding: 80rpx 24rpx; | ||
| 518 | +} | ||
| 519 | + | ||
| 520 | +.empty-title { | ||
| 521 | + font-size: 32rpx; | ||
| 522 | + font-weight: 600; | ||
| 523 | + color: #111827; | ||
| 524 | + margin-top: 24rpx; | ||
| 525 | + margin-bottom: 12rpx; | ||
| 526 | +} | ||
| 527 | + | ||
| 528 | +.empty-desc { | ||
| 529 | + font-size: 26rpx; | ||
| 530 | + color: #6b7280; | ||
| 531 | + text-align: center; | ||
| 532 | + line-height: 1.5; | ||
| 533 | +} | ||
| 534 | + | ||
| 535 | +/* Device list */ | ||
| 536 | +.device-list { | ||
| 537 | + margin-bottom: 32rpx; | ||
| 538 | +} | ||
| 539 | + | ||
| 540 | +.device-card { | ||
| 541 | + background: #fff; | ||
| 542 | + padding: 28rpx 32rpx; | ||
| 543 | + border-radius: 16rpx; | ||
| 544 | + margin-bottom: 16rpx; | ||
| 545 | + display: flex; | ||
| 546 | + align-items: center; | ||
| 547 | + gap: 24rpx; | ||
| 548 | + box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); | ||
| 549 | + cursor: pointer; | ||
| 550 | + transition: background 0.15s; | ||
| 551 | +} | ||
| 552 | + | ||
| 553 | +.device-card:active { | ||
| 554 | + background: #f9fafb; | ||
| 555 | +} | ||
| 556 | + | ||
| 557 | +.device-card.connecting { | ||
| 558 | + opacity: 0.7; | ||
| 559 | +} | ||
| 560 | + | ||
| 561 | +.device-icon { | ||
| 562 | + width: 72rpx; | ||
| 563 | + height: 72rpx; | ||
| 564 | + border-radius: 16rpx; | ||
| 565 | + background: #f3f4f6; | ||
| 566 | + display: flex; | ||
| 567 | + align-items: center; | ||
| 568 | + justify-content: center; | ||
| 569 | +} | ||
| 570 | + | ||
| 571 | +.device-icon.printer { | ||
| 572 | + background: #eff6ff; | ||
| 573 | +} | ||
| 574 | + | ||
| 575 | +.device-info { | ||
| 576 | + flex: 1; | ||
| 577 | + min-width: 0; | ||
| 578 | +} | ||
| 579 | + | ||
| 580 | +.device-name { | ||
| 581 | + font-size: 30rpx; | ||
| 582 | + font-weight: 600; | ||
| 583 | + color: #111827; | ||
| 584 | + display: block; | ||
| 585 | + margin-bottom: 4rpx; | ||
| 586 | +} | ||
| 587 | + | ||
| 588 | +.device-id { | ||
| 589 | + font-size: 22rpx; | ||
| 590 | + color: #9ca3af; | ||
| 591 | + display: block; | ||
| 592 | + margin-bottom: 6rpx; | ||
| 593 | +} | ||
| 594 | + | ||
| 595 | +.device-meta { | ||
| 596 | + display: flex; | ||
| 597 | + align-items: center; | ||
| 598 | + gap: 12rpx; | ||
| 599 | +} | ||
| 600 | + | ||
| 601 | +.device-tag { | ||
| 602 | + display: inline-block; | ||
| 603 | + font-size: 20rpx; | ||
| 604 | + color: var(--theme-primary); | ||
| 605 | + background: var(--theme-primary-light); | ||
| 606 | + padding: 2rpx 12rpx; | ||
| 607 | + border-radius: 6rpx; | ||
| 608 | + font-weight: 500; | ||
| 609 | +} | ||
| 610 | + | ||
| 611 | +.device-rssi { | ||
| 612 | + font-size: 20rpx; | ||
| 613 | + color: #9ca3af; | ||
| 614 | +} | ||
| 615 | + | ||
| 616 | +.device-action { | ||
| 617 | + flex-shrink: 0; | ||
| 618 | +} | ||
| 619 | + | ||
| 620 | +.connecting-text { | ||
| 621 | + font-size: 24rpx; | ||
| 622 | + color: var(--theme-primary); | ||
| 623 | + font-weight: 500; | ||
| 624 | +} | ||
| 625 | + | ||
| 626 | +/* Tips */ | ||
| 627 | +.tips-card { | ||
| 628 | + background: #fffbeb; | ||
| 629 | + border: 1rpx solid #fde68a; | ||
| 630 | + border-radius: 16rpx; | ||
| 631 | + padding: 32rpx; | ||
| 632 | +} | ||
| 633 | + | ||
| 634 | +.tips-title { | ||
| 635 | + font-size: 28rpx; | ||
| 636 | + font-weight: 600; | ||
| 637 | + color: #92400e; | ||
| 638 | + display: block; | ||
| 639 | + margin-bottom: 16rpx; | ||
| 640 | +} | ||
| 641 | + | ||
| 642 | +.tips-item { | ||
| 643 | + font-size: 26rpx; | ||
| 644 | + color: #a16207; | ||
| 645 | + display: block; | ||
| 646 | + margin-bottom: 8rpx; | ||
| 647 | + line-height: 1.5; | ||
| 648 | +} | ||
| 649 | + | ||
| 650 | +.tips-item-last { | ||
| 651 | + margin-bottom: 0; | ||
| 652 | +} | ||
| 653 | +</style> |
美国版/Food Labeling Management App UniApp/src/pages/labels/food-select.vue
| @@ -75,10 +75,8 @@ const isMenuOpen = ref(false) | @@ -75,10 +75,8 @@ const isMenuOpen = ref(false) | ||
| 75 | const allFoods = [ | 75 | const allFoods = [ |
| 76 | { id: 'food-001', nameKey: 'food.chickenBreast', descKey: 'food.chickenBreast.desc', image: 'https://images.unsplash.com/photo-1532550907401-a500c9a57435?w=400', categoryKey: 'category.meat' }, | 76 | { id: 'food-001', nameKey: 'food.chickenBreast', descKey: 'food.chickenBreast.desc', image: 'https://images.unsplash.com/photo-1532550907401-a500c9a57435?w=400', categoryKey: 'category.meat' }, |
| 77 | { id: 'food-002', nameKey: 'food.caesarSalad', descKey: 'food.caesarSalad.desc', image: 'https://images.unsplash.com/photo-1546793665-c74683f339c1?w=400', categoryKey: 'category.salads' }, | 77 | { id: 'food-002', nameKey: 'food.caesarSalad', descKey: 'food.caesarSalad.desc', image: 'https://images.unsplash.com/photo-1546793665-c74683f339c1?w=400', categoryKey: 'category.salads' }, |
| 78 | - { id: 'food-003', nameKey: 'food.salmonFillet', descKey: 'food.salmonFillet.desc', image: 'https://images.unsplash.com/photo-1519708227418-c8fd9a32b7a2?w=400', categoryKey: 'category.seafood' }, | ||
| 79 | { id: 'food-004', nameKey: 'food.beefPatties', descKey: 'food.beefPatties.desc', image: 'https://images.unsplash.com/photo-1607623488235-e2e6794c30b5?w=400', categoryKey: 'category.meat' }, | 78 | { id: 'food-004', nameKey: 'food.beefPatties', descKey: 'food.beefPatties.desc', image: 'https://images.unsplash.com/photo-1607623488235-e2e6794c30b5?w=400', categoryKey: 'category.meat' }, |
| 80 | { id: 'food-005', nameKey: 'food.marinaraSauce', descKey: 'food.marinaraSauce.desc', image: 'https://images.unsplash.com/photo-1621996346565-e3dbc646d9a9?w=400', categoryKey: 'category.sauces' }, | 79 | { id: 'food-005', nameKey: 'food.marinaraSauce', descKey: 'food.marinaraSauce.desc', image: 'https://images.unsplash.com/photo-1621996346565-e3dbc646d9a9?w=400', categoryKey: 'category.sauces' }, |
| 81 | - { id: 'food-006', nameKey: 'food.vegetables', descKey: 'food.vegetables.desc', image: 'https://images.unsplash.com/photo-1540420773420-3366772f4999?w=400', categoryKey: 'category.vegetables' }, | ||
| 82 | { id: 'food-007', nameKey: 'food.brownie', descKey: 'food.brownie.desc', image: 'https://images.unsplash.com/photo-1606313564200-e75d5e30476c?w=400', categoryKey: 'category.desserts' }, | 80 | { id: 'food-007', nameKey: 'food.brownie', descKey: 'food.brownie.desc', image: 'https://images.unsplash.com/photo-1606313564200-e75d5e30476c?w=400', categoryKey: 'category.desserts' }, |
| 83 | { id: 'food-008', nameKey: 'food.shrimpPasta', descKey: 'food.shrimpPasta.desc', image: 'https://images.unsplash.com/photo-1563379926898-05f4575a45d8?w=400', categoryKey: 'category.prepared' }, | 81 | { id: 'food-008', nameKey: 'food.shrimpPasta', descKey: 'food.shrimpPasta.desc', image: 'https://images.unsplash.com/photo-1563379926898-05f4575a45d8?w=400', categoryKey: 'category.prepared' }, |
| 84 | { id: 'food-009', nameKey: 'food.iceCream', descKey: 'food.iceCream.desc', image: 'https://images.unsplash.com/photo-1563805042-7684c019e1cb?w=400', categoryKey: 'category.frozen' }, | 82 | { id: 'food-009', nameKey: 'food.iceCream', descKey: 'food.iceCream.desc', image: 'https://images.unsplash.com/photo-1563805042-7684c019e1cb?w=400', categoryKey: 'category.frozen' }, |
美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue
| @@ -6,7 +6,8 @@ | @@ -6,7 +6,8 @@ | ||
| 6 | <AppIcon name="chevronLeft" size="sm" color="white" /> | 6 | <AppIcon name="chevronLeft" size="sm" color="white" /> |
| 7 | </view> | 7 | </view> |
| 8 | <view class="top-center"> | 8 | <view class="top-center"> |
| 9 | - <text class="app-name">{{ t('labels.title') }}</text> | 9 | + <text class="app-name">Labeling</text> |
| 10 | + <LocationPicker /> | ||
| 10 | </view> | 11 | </view> |
| 11 | <view class="top-right" @click="isMenuOpen = true"> | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | <AppIcon name="menu" size="sm" color="white" /> | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| @@ -15,7 +16,7 @@ | @@ -15,7 +16,7 @@ | ||
| 15 | 16 | ||
| 16 | <view class="bt-bar" @click="goBluetoothPage"> | 17 | <view class="bt-bar" @click="goBluetoothPage"> |
| 17 | <view class="bt-left"> | 18 | <view class="bt-left"> |
| 18 | - <AppIcon name="bluetooth" size="sm" :color="btConnected ? 'white' : 'white'" /> | 19 | + <AppIcon name="bluetooth" size="sm" color="white" /> |
| 19 | <text class="bt-text">{{ btConnected ? btDeviceName : 'No printer connected' }}</text> | 20 | <text class="bt-text">{{ btConnected ? btDeviceName : 'No printer connected' }}</text> |
| 20 | </view> | 21 | </view> |
| 21 | <view class="bt-status" :class="{ connected: btConnected }"> | 22 | <view class="bt-status" :class="{ connected: btConnected }"> |
| @@ -29,17 +30,17 @@ | @@ -29,17 +30,17 @@ | ||
| 29 | <view class="body"> | 30 | <view class="body"> |
| 30 | <view class="sidebar"> | 31 | <view class="sidebar"> |
| 31 | <view | 32 | <view |
| 32 | - v-for="type in labelTypes" | ||
| 33 | - :key="type.id" | 33 | + v-for="cat in labelCategories" |
| 34 | + :key="cat.id" | ||
| 34 | class="cat-item" | 35 | class="cat-item" |
| 35 | - :class="{ active: selectedType === type.id }" | ||
| 36 | - @click="selectedType = type.id" | 36 | + :class="{ active: selectedCategory === cat.id }" |
| 37 | + @click="selectedCategory = cat.id" | ||
| 37 | > | 38 | > |
| 38 | - <view v-if="selectedType === type.id" class="active-bar" /> | ||
| 39 | - <view class="cat-icon" :class="type.bgClass"> | ||
| 40 | - <AppIcon :name="type.icon" size="sm" :color="selectedType === type.id ? 'white' : type.iconColor" /> | 39 | + <view v-if="selectedCategory === cat.id" class="active-bar" /> |
| 40 | + <view class="cat-icon" :class="cat.bgClass"> | ||
| 41 | + <AppIcon :name="cat.icon" size="sm" :color="selectedCategory === cat.id ? 'white' : cat.iconColor" /> | ||
| 41 | </view> | 42 | </view> |
| 42 | - <text class="cat-name">{{ t(type.nameKey) }}</text> | 43 | + <text class="cat-name">{{ cat.name }}</text> |
| 43 | </view> | 44 | </view> |
| 44 | </view> | 45 | </view> |
| 45 | 46 | ||
| @@ -52,34 +53,88 @@ | @@ -52,34 +53,88 @@ | ||
| 52 | <input | 53 | <input |
| 53 | v-model="searchTerm" | 54 | v-model="searchTerm" |
| 54 | class="search-input" | 55 | class="search-input" |
| 55 | - :placeholder="t('labels.searchFood')" | 56 | + placeholder="Search products..." |
| 56 | placeholder-class="placeholder" | 57 | placeholder-class="placeholder" |
| 57 | /> | 58 | /> |
| 58 | </view> | 59 | </view> |
| 59 | 60 | ||
| 60 | - <view v-if="filteredFoods.length === 0" class="empty"> | 61 | + <view v-if="filteredProductCategories.length === 0" class="empty"> |
| 61 | <AppIcon name="search" size="lg" color="gray" /> | 62 | <AppIcon name="search" size="lg" color="gray" /> |
| 62 | - <text class="empty-text">{{ t('labels.noFoodFound') }}</text> | 63 | + <text class="empty-text">No products found</text> |
| 63 | </view> | 64 | </view> |
| 64 | 65 | ||
| 65 | - <view v-else class="food-grid"> | 66 | + <view v-else class="category-list"> |
| 66 | <view | 67 | <view |
| 67 | - v-for="food in filteredFoods" | ||
| 68 | - :key="food.id" | ||
| 69 | - class="food-card" | ||
| 70 | - @click="goPreview(food.id)" | 68 | + v-for="pCat in filteredProductCategories" |
| 69 | + :key="pCat.id" | ||
| 70 | + class="cat-section" | ||
| 71 | > | 71 | > |
| 72 | - <view class="food-img-wrap"> | ||
| 73 | - <image :src="food.image" class="food-img" mode="aspectFill" /> | 72 | + <view class="cat-header" @click="toggleCategory(pCat.id)"> |
| 73 | + <view class="cat-header-left"> | ||
| 74 | + <view class="cat-header-icon" :class="pCat.colorClass"> | ||
| 75 | + <AppIcon :name="pCat.icon" size="sm" :color="pCat.iconColor" /> | ||
| 76 | + </view> | ||
| 77 | + <view class="cat-header-info"> | ||
| 78 | + <text class="cat-header-name">{{ pCat.name }}</text> | ||
| 79 | + <text class="cat-header-count">{{ pCat.products.length }} items</text> | ||
| 80 | + </view> | ||
| 81 | + </view> | ||
| 82 | + <AppIcon | ||
| 83 | + :name="expandedCategories.indexOf(pCat.id) >= 0 ? 'chevronUp' : 'chevronDown'" | ||
| 84 | + size="sm" | ||
| 85 | + color="gray" | ||
| 86 | + /> | ||
| 87 | + </view> | ||
| 88 | + | ||
| 89 | + <view v-if="expandedCategories.indexOf(pCat.id) >= 0" class="cat-foods"> | ||
| 90 | + <view class="food-grid"> | ||
| 91 | + <view | ||
| 92 | + v-for="product in pCat.products" | ||
| 93 | + :key="product.id" | ||
| 94 | + class="food-card" | ||
| 95 | + @click="handleProductClick(product)" | ||
| 96 | + > | ||
| 97 | + <view class="food-img-wrap"> | ||
| 98 | + <image :src="product.image" class="food-img" mode="aspectFill" /> | ||
| 99 | + <view class="size-badge"> | ||
| 100 | + <text class="size-badge-text">{{ product.templateSize }}</text> | ||
| 101 | + </view> | ||
| 102 | + <view v-if="product.labelTypes.length > 0" class="type-badge"> | ||
| 103 | + <text class="type-badge-text">{{ product.labelTypes.length }} Types</text> | ||
| 104 | + </view> | ||
| 105 | + </view> | ||
| 106 | + <text class="food-name">{{ product.name }}</text> | ||
| 107 | + <text class="food-desc">{{ product.templateName }}</text> | ||
| 108 | + </view> | ||
| 109 | + </view> | ||
| 74 | </view> | 110 | </view> |
| 75 | - <text class="food-name">{{ t(food.nameKey) }}</text> | ||
| 76 | - <text class="food-desc">{{ t(food.descKey) }}</text> | ||
| 77 | </view> | 111 | </view> |
| 78 | </view> | 112 | </view> |
| 79 | </view> | 113 | </view> |
| 80 | </scroll-view> | 114 | </scroll-view> |
| 81 | </view> | 115 | </view> |
| 82 | 116 | ||
| 117 | + <view v-if="showSubTypeModal" class="modal-mask" @click="showSubTypeModal = false"> | ||
| 118 | + <view class="modal-body" @click.stop> | ||
| 119 | + <text class="modal-title">Select Label Type</text> | ||
| 120 | + <text class="modal-desc">{{ selectedProduct ? selectedProduct.name : '' }}</text> | ||
| 121 | + <view class="subtype-list"> | ||
| 122 | + <view | ||
| 123 | + v-for="lt in currentLabelTypes" | ||
| 124 | + :key="lt.id" | ||
| 125 | + class="subtype-item" | ||
| 126 | + @click="selectLabelType(lt)" | ||
| 127 | + > | ||
| 128 | + <text class="subtype-name">{{ lt.name }}</text> | ||
| 129 | + <AppIcon name="chevronRight" size="sm" color="gray" /> | ||
| 130 | + </view> | ||
| 131 | + </view> | ||
| 132 | + <view class="modal-cancel" @click="showSubTypeModal = false"> | ||
| 133 | + <text class="modal-cancel-text">Cancel</text> | ||
| 134 | + </view> | ||
| 135 | + </view> | ||
| 136 | + </view> | ||
| 137 | + | ||
| 83 | <SideMenu v-model="isMenuOpen" /> | 138 | <SideMenu v-model="isMenuOpen" /> |
| 84 | </view> | 139 | </view> |
| 85 | </template> | 140 | </template> |
| @@ -87,16 +142,19 @@ | @@ -87,16 +142,19 @@ | ||
| 87 | <script setup lang="ts"> | 142 | <script setup lang="ts"> |
| 88 | import { ref, computed } from 'vue' | 143 | import { ref, computed } from 'vue' |
| 89 | import { onShow } from '@dcloudio/uni-app' | 144 | import { onShow } from '@dcloudio/uni-app' |
| 90 | -import { useI18n } from 'vue-i18n' | ||
| 91 | import AppIcon from '../../components/AppIcon.vue' | 145 | import AppIcon from '../../components/AppIcon.vue' |
| 92 | import SideMenu from '../../components/SideMenu.vue' | 146 | import SideMenu from '../../components/SideMenu.vue' |
| 147 | +import LocationPicker from '../../components/LocationPicker.vue' | ||
| 93 | import { getStatusBarHeight } from '../../utils/statusBar' | 148 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 94 | 149 | ||
| 95 | -const { t } = useI18n() | ||
| 96 | const statusBarHeight = getStatusBarHeight() | 150 | const statusBarHeight = getStatusBarHeight() |
| 97 | const isMenuOpen = ref(false) | 151 | const isMenuOpen = ref(false) |
| 98 | -const selectedType = ref('nutrition') | 152 | +const selectedCategory = ref('prep') |
| 99 | const searchTerm = ref('') | 153 | const searchTerm = ref('') |
| 154 | +const expandedCategories = ref<string[]>([]) | ||
| 155 | +const showSubTypeModal = ref(false) | ||
| 156 | +const selectedProduct = ref<any>(null) | ||
| 157 | +const currentLabelTypes = ref<any[]>([]) | ||
| 100 | 158 | ||
| 101 | const btConnected = ref(false) | 159 | const btConnected = ref(false) |
| 102 | const btDeviceName = ref('') | 160 | const btDeviceName = ref('') |
| @@ -107,33 +165,279 @@ onShow(() => { | @@ -107,33 +165,279 @@ onShow(() => { | ||
| 107 | btDeviceName.value = name || '' | 165 | btDeviceName.value = name || '' |
| 108 | }) | 166 | }) |
| 109 | 167 | ||
| 110 | -const labelTypes = computed(() => [ | ||
| 111 | - { id: 'nutrition', nameKey: 'labelType.nutrition.name', icon: 'food', iconColor: 'blue' as const, bgClass: 'blue' }, | ||
| 112 | - { id: 'allergen', nameKey: 'labelType.allergen.name', icon: 'alert', iconColor: 'orange' as const, bgClass: 'red' }, | ||
| 113 | - { id: 'storage', nameKey: 'labelType.storage.name', icon: 'snowflake', iconColor: 'blue' as const, bgClass: 'cyan' }, | ||
| 114 | - { id: 'expiry', nameKey: 'labelType.expiry.name', icon: 'calendar', iconColor: 'orange' as const, bgClass: 'orange' }, | ||
| 115 | - { id: 'batch', nameKey: 'labelType.batch.name', icon: 'package', iconColor: 'purple' as const, bgClass: 'purple' }, | ||
| 116 | - { id: 'preparation', nameKey: 'labelType.preparation.name', icon: 'chef', iconColor: 'green' as const, bgClass: 'green' }, | ||
| 117 | -]) | ||
| 118 | - | ||
| 119 | -const allFoods = [ | ||
| 120 | - { id: 'food-001', nameKey: 'food.chickenBreast', descKey: 'food.chickenBreast.desc', image: 'https://images.unsplash.com/photo-1532550907401-a500c9a57435?w=400', categoryKey: 'category.meat' }, | ||
| 121 | - { id: 'food-002', nameKey: 'food.caesarSalad', descKey: 'food.caesarSalad.desc', image: 'https://images.unsplash.com/photo-1546793665-c74683f339c1?w=400', categoryKey: 'category.salads' }, | ||
| 122 | - { id: 'food-003', nameKey: 'food.salmonFillet', descKey: 'food.salmonFillet.desc', image: 'https://images.unsplash.com/photo-1519708227418-c8fd9a32b7a2?w=400', categoryKey: 'category.seafood' }, | ||
| 123 | - { id: 'food-004', nameKey: 'food.beefPatties', descKey: 'food.beefPatties.desc', image: 'https://images.unsplash.com/photo-1607623488235-e2e6794c30b5?w=400', categoryKey: 'category.meat' }, | ||
| 124 | - { id: 'food-005', nameKey: 'food.marinaraSauce', descKey: 'food.marinaraSauce.desc', image: 'https://images.unsplash.com/photo-1621996346565-e3dbc646d9a9?w=400', categoryKey: 'category.sauces' }, | ||
| 125 | - { id: 'food-006', nameKey: 'food.vegetables', descKey: 'food.vegetables.desc', image: 'https://images.unsplash.com/photo-1540420773420-3366772f4999?w=400', categoryKey: 'category.vegetables' }, | ||
| 126 | - { id: 'food-007', nameKey: 'food.brownie', descKey: 'food.brownie.desc', image: 'https://images.unsplash.com/photo-1606313564200-e75d5e30476c?w=400', categoryKey: 'category.desserts' }, | ||
| 127 | - { id: 'food-008', nameKey: 'food.shrimpPasta', descKey: 'food.shrimpPasta.desc', image: 'https://images.unsplash.com/photo-1563379926898-05f4575a45d8?w=400', categoryKey: 'category.prepared' }, | ||
| 128 | - { id: 'food-009', nameKey: 'food.iceCream', descKey: 'food.iceCream.desc', image: 'https://images.unsplash.com/photo-1563805042-7684c019e1cb?w=400', categoryKey: 'category.frozen' }, | 168 | +const labelCategories = [ |
| 169 | + { id: 'prep', name: 'Prep', icon: 'chef', iconColor: 'green' as const, bgClass: 'green' }, | ||
| 170 | + { id: 'grabngo', name: "Grab'n'Go", icon: 'package', iconColor: 'orange' as const, bgClass: 'orange' }, | ||
| 171 | + { id: 'expiry', name: 'Expiry', icon: 'calendar', iconColor: 'red' as const, bgClass: 'red' }, | ||
| 129 | ] | 172 | ] |
| 130 | 173 | ||
| 131 | -const filteredFoods = computed(() => { | 174 | +interface LabelType { |
| 175 | + id: string | ||
| 176 | + name: string | ||
| 177 | + desc: string | ||
| 178 | +} | ||
| 179 | + | ||
| 180 | +interface Product { | ||
| 181 | + id: string | ||
| 182 | + name: string | ||
| 183 | + image: string | ||
| 184 | + templateSize: string | ||
| 185 | + templateName: string | ||
| 186 | + lastEdited: string | ||
| 187 | + labelTypes: LabelType[] | ||
| 188 | +} | ||
| 189 | + | ||
| 190 | +interface ProductCategory { | ||
| 191 | + id: string | ||
| 192 | + name: string | ||
| 193 | + icon: string | ||
| 194 | + iconColor: 'blue' | 'orange' | 'green' | 'purple' | 'red' | 'gray' | ||
| 195 | + colorClass: string | ||
| 196 | + products: Product[] | ||
| 197 | +} | ||
| 198 | + | ||
| 199 | +const productCategoriesByLabel: Record<string, ProductCategory[]> = { | ||
| 200 | + prep: [ | ||
| 201 | + { | ||
| 202 | + id: 'meat', | ||
| 203 | + name: 'Meat', | ||
| 204 | + icon: 'food', | ||
| 205 | + iconColor: 'red', | ||
| 206 | + colorClass: 'bg-red', | ||
| 207 | + products: [ | ||
| 208 | + { | ||
| 209 | + id: 'chicken', | ||
| 210 | + name: 'Chicken', | ||
| 211 | + image: 'https://images.unsplash.com/photo-1532550907401-a500c9a57435?w=400', | ||
| 212 | + templateSize: '2"x2"', | ||
| 213 | + templateName: 'Basic', | ||
| 214 | + lastEdited: '2025.12.03 11:45', | ||
| 215 | + labelTypes: [ | ||
| 216 | + { id: 'defrost', name: 'Defrost', desc: 'For defrosted items, tracks thaw date' }, | ||
| 217 | + { id: 'opened', name: 'Opened/Preped', desc: 'For opened or prepared items' }, | ||
| 218 | + { id: 'heated', name: 'Heated', desc: 'For heated/cooked items' }, | ||
| 219 | + ], | ||
| 220 | + }, | ||
| 221 | + { | ||
| 222 | + id: 'beef', | ||
| 223 | + name: 'Beef', | ||
| 224 | + image: 'https://images.unsplash.com/photo-1607623488235-e2e6794c30b5?w=400', | ||
| 225 | + templateSize: '2"x2"', | ||
| 226 | + templateName: 'Basic', | ||
| 227 | + lastEdited: '2025.12.04 09:20', | ||
| 228 | + labelTypes: [ | ||
| 229 | + { id: 'defrost', name: 'Defrost', desc: 'For defrosted items' }, | ||
| 230 | + { id: 'opened', name: 'Opened/Preped', desc: 'For opened or prepared items' }, | ||
| 231 | + { id: 'heated', name: 'Heated', desc: 'For heated/cooked items' }, | ||
| 232 | + ], | ||
| 233 | + }, | ||
| 234 | + { | ||
| 235 | + id: 'bacon', | ||
| 236 | + name: 'Bacon', | ||
| 237 | + image: 'https://images.unsplash.com/photo-1528607929212-2636ec44253e?w=400', | ||
| 238 | + templateSize: '2"x2"', | ||
| 239 | + templateName: 'Basic', | ||
| 240 | + lastEdited: '2025.12.03 14:30', | ||
| 241 | + labelTypes: [ | ||
| 242 | + { id: 'raw', name: 'Raw Bacon', desc: 'Uncooked, requires refrigeration' }, | ||
| 243 | + { id: 'cooked', name: 'Cooked Bacon', desc: 'Fully cooked, ready to eat' }, | ||
| 244 | + ], | ||
| 245 | + }, | ||
| 246 | + ], | ||
| 247 | + }, | ||
| 248 | + ], | ||
| 249 | + grabngo: [ | ||
| 250 | + { | ||
| 251 | + id: 'sandwich', | ||
| 252 | + name: 'Sandwich', | ||
| 253 | + icon: 'food', | ||
| 254 | + iconColor: 'orange', | ||
| 255 | + colorClass: 'bg-orange', | ||
| 256 | + products: [ | ||
| 257 | + { | ||
| 258 | + id: 'chicken-sandwich', | ||
| 259 | + name: 'Chicken Sandwich', | ||
| 260 | + image: 'https://images.unsplash.com/photo-1553909489-cd47e0907980?w=400', | ||
| 261 | + templateSize: '2"x6"', | ||
| 262 | + templateName: "G'n'G", | ||
| 263 | + lastEdited: '2025.12.03 11:45', | ||
| 264 | + labelTypes: [], | ||
| 265 | + }, | ||
| 266 | + { | ||
| 267 | + id: 'turkey-club', | ||
| 268 | + name: 'Turkey Club', | ||
| 269 | + image: 'https://images.unsplash.com/photo-1534422298391-e4f8c172dddb?w=400', | ||
| 270 | + templateSize: '2"x6"', | ||
| 271 | + templateName: "G'n'G", | ||
| 272 | + lastEdited: '2025.12.04 09:30', | ||
| 273 | + labelTypes: [], | ||
| 274 | + }, | ||
| 275 | + { | ||
| 276 | + id: 'cheese-burger', | ||
| 277 | + name: 'Cheese Burger', | ||
| 278 | + image: 'https://images.unsplash.com/photo-1568901346375-23c9450c58cd?w=400', | ||
| 279 | + templateSize: '2"x6"', | ||
| 280 | + templateName: "G'n'G", | ||
| 281 | + lastEdited: '2025.12.03 15:20', | ||
| 282 | + labelTypes: [], | ||
| 283 | + }, | ||
| 284 | + ], | ||
| 285 | + }, | ||
| 286 | + { | ||
| 287 | + id: 'salads', | ||
| 288 | + name: 'Salads', | ||
| 289 | + icon: 'food', | ||
| 290 | + iconColor: 'green', | ||
| 291 | + colorClass: 'bg-green', | ||
| 292 | + products: [ | ||
| 293 | + { | ||
| 294 | + id: 'caesar-salad', | ||
| 295 | + name: 'Caesar Salad', | ||
| 296 | + image: 'https://images.unsplash.com/photo-1546793665-c74683f339c1?w=400', | ||
| 297 | + templateSize: '2"x4"', | ||
| 298 | + templateName: "G'n'G", | ||
| 299 | + lastEdited: '2025.12.04 09:00', | ||
| 300 | + labelTypes: [], | ||
| 301 | + }, | ||
| 302 | + { | ||
| 303 | + id: 'greek-salad', | ||
| 304 | + name: 'Greek Salad', | ||
| 305 | + image: 'https://images.unsplash.com/photo-1540189543-6e6f32e2c1f0?w=400', | ||
| 306 | + templateSize: '2"x4"', | ||
| 307 | + templateName: "G'n'G", | ||
| 308 | + lastEdited: '2025.12.03 12:30', | ||
| 309 | + labelTypes: [], | ||
| 310 | + }, | ||
| 311 | + ], | ||
| 312 | + }, | ||
| 313 | + { | ||
| 314 | + id: 'beverages', | ||
| 315 | + name: 'Beverages', | ||
| 316 | + icon: 'food', | ||
| 317 | + iconColor: 'blue', | ||
| 318 | + colorClass: 'bg-blue', | ||
| 319 | + products: [ | ||
| 320 | + { | ||
| 321 | + id: 'fresh-juice', | ||
| 322 | + name: 'Fresh Juice', | ||
| 323 | + image: 'https://images.unsplash.com/photo-1621506289937-a8e4df240d0b?w=400', | ||
| 324 | + templateSize: '2"x2"', | ||
| 325 | + templateName: "G'n'G", | ||
| 326 | + lastEdited: '2025.12.04 07:45', | ||
| 327 | + labelTypes: [], | ||
| 328 | + }, | ||
| 329 | + { | ||
| 330 | + id: 'smoothie', | ||
| 331 | + name: 'Smoothie', | ||
| 332 | + image: 'https://images.unsplash.com/photo-1553530666-ba11a7da3888?w=400', | ||
| 333 | + templateSize: '2"x2"', | ||
| 334 | + templateName: "G'n'G", | ||
| 335 | + lastEdited: '2025.12.03 11:00', | ||
| 336 | + labelTypes: [], | ||
| 337 | + }, | ||
| 338 | + ], | ||
| 339 | + }, | ||
| 340 | + ], | ||
| 341 | + expiry: [ | ||
| 342 | + { | ||
| 343 | + id: 'dairy', | ||
| 344 | + name: 'Dairy', | ||
| 345 | + icon: 'food', | ||
| 346 | + iconColor: 'blue', | ||
| 347 | + colorClass: 'bg-blue', | ||
| 348 | + products: [ | ||
| 349 | + { | ||
| 350 | + id: 'milk', | ||
| 351 | + name: 'Milk', | ||
| 352 | + image: 'https://images.unsplash.com/photo-1563636619-e9143da7973b?w=400', | ||
| 353 | + templateSize: '2"x2"', | ||
| 354 | + templateName: 'Basic', | ||
| 355 | + lastEdited: '2025.12.04 08:30', | ||
| 356 | + labelTypes: [], | ||
| 357 | + }, | ||
| 358 | + { | ||
| 359 | + id: 'cheese', | ||
| 360 | + name: 'Cheese', | ||
| 361 | + image: 'https://images.unsplash.com/photo-1486297678162-eb2a19b0a32d?w=400', | ||
| 362 | + templateSize: '2"x2"', | ||
| 363 | + templateName: 'Basic', | ||
| 364 | + lastEdited: '2025.12.03 11:15', | ||
| 365 | + labelTypes: [], | ||
| 366 | + }, | ||
| 367 | + ], | ||
| 368 | + }, | ||
| 369 | + { | ||
| 370 | + id: 'deli', | ||
| 371 | + name: 'Deli', | ||
| 372 | + icon: 'food', | ||
| 373 | + iconColor: 'red', | ||
| 374 | + colorClass: 'bg-red', | ||
| 375 | + products: [ | ||
| 376 | + { | ||
| 377 | + id: 'sliced-ham', | ||
| 378 | + name: 'Sliced Ham', | ||
| 379 | + image: 'https://images.unsplash.com/photo-1529692236671-f1f6cf9683ba?w=400', | ||
| 380 | + templateSize: '2"x2"', | ||
| 381 | + templateName: 'Basic', | ||
| 382 | + lastEdited: '2025.12.04 10:00', | ||
| 383 | + labelTypes: [], | ||
| 384 | + }, | ||
| 385 | + { | ||
| 386 | + id: 'turkey-deli', | ||
| 387 | + name: 'Turkey Deli', | ||
| 388 | + image: 'https://images.unsplash.com/photo-1607623488235-e2e6794c30b5?w=400', | ||
| 389 | + templateSize: '2"x2"', | ||
| 390 | + templateName: 'Basic', | ||
| 391 | + lastEdited: '2025.12.03 14:20', | ||
| 392 | + labelTypes: [], | ||
| 393 | + }, | ||
| 394 | + ], | ||
| 395 | + }, | ||
| 396 | + ], | ||
| 397 | +} | ||
| 398 | + | ||
| 399 | +const filteredProductCategories = computed(() => { | ||
| 400 | + const cats = productCategoriesByLabel[selectedCategory.value] || [] | ||
| 132 | const s = searchTerm.value.toLowerCase() | 401 | const s = searchTerm.value.toLowerCase() |
| 133 | - if (!s) return allFoods | ||
| 134 | - return allFoods.filter((f) => t(f.nameKey).toLowerCase().includes(s)) | 402 | + if (!s) return cats |
| 403 | + return cats | ||
| 404 | + .map(function (cat) { | ||
| 405 | + return { | ||
| 406 | + ...cat, | ||
| 407 | + products: cat.products.filter(function (p) { | ||
| 408 | + return p.name.toLowerCase().indexOf(s) >= 0 | ||
| 409 | + }), | ||
| 410 | + } | ||
| 411 | + }) | ||
| 412 | + .filter(function (cat) { return cat.products.length > 0 }) | ||
| 135 | }) | 413 | }) |
| 136 | 414 | ||
| 415 | +const toggleCategory = (catId: string) => { | ||
| 416 | + if (expandedCategories.value.indexOf(catId) >= 0) { | ||
| 417 | + expandedCategories.value = [] | ||
| 418 | + } else { | ||
| 419 | + expandedCategories.value = [catId] | ||
| 420 | + } | ||
| 421 | +} | ||
| 422 | + | ||
| 423 | +const handleProductClick = (product: Product) => { | ||
| 424 | + if (product.labelTypes.length > 0) { | ||
| 425 | + selectedProduct.value = product | ||
| 426 | + currentLabelTypes.value = product.labelTypes | ||
| 427 | + showSubTypeModal.value = true | ||
| 428 | + } else { | ||
| 429 | + goPreview(product.id, '') | ||
| 430 | + } | ||
| 431 | +} | ||
| 432 | + | ||
| 433 | +const selectLabelType = (lt: LabelType) => { | ||
| 434 | + const product = selectedProduct.value | ||
| 435 | + if (product) { | ||
| 436 | + showSubTypeModal.value = false | ||
| 437 | + goPreview(product.id, lt.id) | ||
| 438 | + } | ||
| 439 | +} | ||
| 440 | + | ||
| 137 | const goBack = () => { | 441 | const goBack = () => { |
| 138 | const pages = getCurrentPages() | 442 | const pages = getCurrentPages() |
| 139 | if (pages.length > 1) { | 443 | if (pages.length > 1) { |
| @@ -147,10 +451,12 @@ const goBluetoothPage = () => { | @@ -147,10 +451,12 @@ const goBluetoothPage = () => { | ||
| 147 | uni.navigateTo({ url: '/pages/labels/bluetooth' }) | 451 | uni.navigateTo({ url: '/pages/labels/bluetooth' }) |
| 148 | } | 452 | } |
| 149 | 453 | ||
| 150 | -const goPreview = (foodId: string) => { | ||
| 151 | - uni.navigateTo({ | ||
| 152 | - url: '/pages/labels/preview?labelType=' + selectedType.value + '&foodId=' + foodId, | ||
| 153 | - }) | 454 | +const goPreview = (productId: string, subType: string) => { |
| 455 | + let url = '/pages/labels/preview?productId=' + productId | ||
| 456 | + if (subType) { | ||
| 457 | + url += '&subType=' + subType | ||
| 458 | + } | ||
| 459 | + uni.navigateTo({ url }) | ||
| 154 | } | 460 | } |
| 155 | </script> | 461 | </script> |
| 156 | 462 | ||
| @@ -176,7 +482,8 @@ const goPreview = (foodId: string) => { | @@ -176,7 +482,8 @@ const goPreview = (foodId: string) => { | ||
| 176 | justify-content: space-between; | 482 | justify-content: space-between; |
| 177 | } | 483 | } |
| 178 | 484 | ||
| 179 | -.top-left, .top-right { | 485 | +.top-left, |
| 486 | +.top-right { | ||
| 180 | width: 64rpx; | 487 | width: 64rpx; |
| 181 | height: 64rpx; | 488 | height: 64rpx; |
| 182 | border-radius: 999rpx; | 489 | border-radius: 999rpx; |
| @@ -188,7 +495,9 @@ const goPreview = (foodId: string) => { | @@ -188,7 +495,9 @@ const goPreview = (foodId: string) => { | ||
| 188 | 495 | ||
| 189 | .top-center { | 496 | .top-center { |
| 190 | flex: 1; | 497 | flex: 1; |
| 191 | - text-align: center; | 498 | + display: flex; |
| 499 | + flex-direction: column; | ||
| 500 | + align-items: center; | ||
| 192 | } | 501 | } |
| 193 | 502 | ||
| 194 | .app-name { | 503 | .app-name { |
| @@ -197,7 +506,6 @@ const goPreview = (foodId: string) => { | @@ -197,7 +506,6 @@ const goPreview = (foodId: string) => { | ||
| 197 | color: #ffffff; | 506 | color: #ffffff; |
| 198 | } | 507 | } |
| 199 | 508 | ||
| 200 | -/* Bluetooth status bar */ | ||
| 201 | .bt-bar { | 509 | .bt-bar { |
| 202 | display: flex; | 510 | display: flex; |
| 203 | align-items: center; | 511 | align-items: center; |
| @@ -207,7 +515,6 @@ const goPreview = (foodId: string) => { | @@ -207,7 +515,6 @@ const goPreview = (foodId: string) => { | ||
| 207 | margin-bottom: 16rpx; | 515 | margin-bottom: 16rpx; |
| 208 | background: rgba(255, 255, 255, 0.12); | 516 | background: rgba(255, 255, 255, 0.12); |
| 209 | border-radius: 16rpx; | 517 | border-radius: 16rpx; |
| 210 | - cursor: pointer; | ||
| 211 | } | 518 | } |
| 212 | 519 | ||
| 213 | .bt-left { | 520 | .bt-left { |
| @@ -252,7 +559,6 @@ const goPreview = (foodId: string) => { | @@ -252,7 +559,6 @@ const goPreview = (foodId: string) => { | ||
| 252 | color: rgba(255, 255, 255, 0.85); | 559 | color: rgba(255, 255, 255, 0.85); |
| 253 | } | 560 | } |
| 254 | 561 | ||
| 255 | -/* Body */ | ||
| 256 | .body { | 562 | .body { |
| 257 | flex: 1; | 563 | flex: 1; |
| 258 | display: flex; | 564 | display: flex; |
| @@ -272,7 +578,6 @@ const goPreview = (foodId: string) => { | @@ -272,7 +578,6 @@ const goPreview = (foodId: string) => { | ||
| 272 | flex-direction: column; | 578 | flex-direction: column; |
| 273 | align-items: center; | 579 | align-items: center; |
| 274 | padding: 20rpx 8rpx; | 580 | padding: 20rpx 8rpx; |
| 275 | - cursor: pointer; | ||
| 276 | position: relative; | 581 | position: relative; |
| 277 | } | 582 | } |
| 278 | 583 | ||
| @@ -300,19 +605,31 @@ const goPreview = (foodId: string) => { | @@ -300,19 +605,31 @@ const goPreview = (foodId: string) => { | ||
| 300 | margin-bottom: 8rpx; | 605 | margin-bottom: 8rpx; |
| 301 | } | 606 | } |
| 302 | 607 | ||
| 303 | -.cat-icon.blue { background: #eff6ff; } | ||
| 304 | -.cat-icon.red { background: #fef2f2; } | ||
| 305 | -.cat-icon.cyan { background: #ecfeff; } | ||
| 306 | -.cat-icon.orange { background: #fff7ed; } | ||
| 307 | -.cat-icon.purple { background: #faf5ff; } | ||
| 308 | -.cat-icon.green { background: #f0fdf4; } | 608 | +.cat-icon.green { |
| 609 | + background: #f0fdf4; | ||
| 610 | +} | ||
| 309 | 611 | ||
| 310 | -.cat-item.active .cat-icon.blue, | ||
| 311 | -.cat-item.active .cat-icon.red, | ||
| 312 | -.cat-item.active .cat-icon.cyan, | 612 | +.cat-icon.orange { |
| 613 | + background: #fff7ed; | ||
| 614 | +} | ||
| 615 | + | ||
| 616 | +.cat-icon.red { | ||
| 617 | + background: #fef2f2; | ||
| 618 | +} | ||
| 619 | + | ||
| 620 | +.cat-icon.blue { | ||
| 621 | + background: #eff6ff; | ||
| 622 | +} | ||
| 623 | + | ||
| 624 | +.cat-icon.cyan { | ||
| 625 | + background: #ecfeff; | ||
| 626 | +} | ||
| 627 | + | ||
| 628 | +.cat-item.active .cat-icon.green, | ||
| 313 | .cat-item.active .cat-icon.orange, | 629 | .cat-item.active .cat-icon.orange, |
| 314 | -.cat-item.active .cat-icon.purple, | ||
| 315 | -.cat-item.active .cat-icon.green { | 630 | +.cat-item.active .cat-icon.red, |
| 631 | +.cat-item.active .cat-icon.blue, | ||
| 632 | +.cat-item.active .cat-icon.cyan { | ||
| 316 | background: var(--theme-primary); | 633 | background: var(--theme-primary); |
| 317 | } | 634 | } |
| 318 | 635 | ||
| @@ -374,23 +691,104 @@ const goPreview = (foodId: string) => { | @@ -374,23 +691,104 @@ const goPreview = (foodId: string) => { | ||
| 374 | margin-top: 24rpx; | 691 | margin-top: 24rpx; |
| 375 | } | 692 | } |
| 376 | 693 | ||
| 694 | +.category-list { | ||
| 695 | + display: flex; | ||
| 696 | + flex-direction: column; | ||
| 697 | + gap: 16rpx; | ||
| 698 | +} | ||
| 699 | + | ||
| 700 | +.cat-section { | ||
| 701 | + background: #fff; | ||
| 702 | + border-radius: 16rpx; | ||
| 703 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); | ||
| 704 | + overflow: hidden; | ||
| 705 | +} | ||
| 706 | + | ||
| 707 | +.cat-header { | ||
| 708 | + display: flex; | ||
| 709 | + align-items: center; | ||
| 710 | + justify-content: space-between; | ||
| 711 | + padding: 24rpx; | ||
| 712 | +} | ||
| 713 | + | ||
| 714 | +.cat-header-left { | ||
| 715 | + display: flex; | ||
| 716 | + align-items: center; | ||
| 717 | + gap: 16rpx; | ||
| 718 | + flex: 1; | ||
| 719 | + min-width: 0; | ||
| 720 | +} | ||
| 721 | + | ||
| 722 | +.cat-header-icon { | ||
| 723 | + width: 56rpx; | ||
| 724 | + height: 56rpx; | ||
| 725 | + border-radius: 14rpx; | ||
| 726 | + display: flex; | ||
| 727 | + align-items: center; | ||
| 728 | + justify-content: center; | ||
| 729 | + flex-shrink: 0; | ||
| 730 | +} | ||
| 731 | + | ||
| 732 | +.cat-header-icon.bg-red { | ||
| 733 | + background: #fef2f2; | ||
| 734 | +} | ||
| 735 | + | ||
| 736 | +.cat-header-icon.bg-blue { | ||
| 737 | + background: #eff6ff; | ||
| 738 | +} | ||
| 739 | + | ||
| 740 | +.cat-header-icon.bg-green { | ||
| 741 | + background: #f0fdf4; | ||
| 742 | +} | ||
| 743 | + | ||
| 744 | +.cat-header-icon.bg-orange { | ||
| 745 | + background: #fff7ed; | ||
| 746 | +} | ||
| 747 | + | ||
| 748 | +.cat-header-icon.bg-purple { | ||
| 749 | + background: #faf5ff; | ||
| 750 | +} | ||
| 751 | + | ||
| 752 | +.cat-header-info { | ||
| 753 | + flex: 1; | ||
| 754 | + min-width: 0; | ||
| 755 | +} | ||
| 756 | + | ||
| 757 | +.cat-header-name { | ||
| 758 | + font-size: 28rpx; | ||
| 759 | + font-weight: 600; | ||
| 760 | + color: #111827; | ||
| 761 | + display: block; | ||
| 762 | +} | ||
| 763 | + | ||
| 764 | +.cat-header-count { | ||
| 765 | + font-size: 22rpx; | ||
| 766 | + color: #9ca3af; | ||
| 767 | + display: block; | ||
| 768 | + margin-top: 2rpx; | ||
| 769 | +} | ||
| 770 | + | ||
| 771 | +.cat-foods { | ||
| 772 | + padding: 0 16rpx 16rpx; | ||
| 773 | + border-top: 1rpx solid #f3f4f6; | ||
| 774 | +} | ||
| 775 | + | ||
| 377 | .food-grid { | 776 | .food-grid { |
| 378 | display: grid; | 777 | display: grid; |
| 379 | grid-template-columns: repeat(2, minmax(0, 1fr)); | 778 | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| 380 | - column-gap: 16rpx; | ||
| 381 | - row-gap: 16rpx; | 779 | + column-gap: 12rpx; |
| 780 | + row-gap: 12rpx; | ||
| 781 | + padding-top: 16rpx; | ||
| 382 | } | 782 | } |
| 383 | 783 | ||
| 384 | .food-card { | 784 | .food-card { |
| 385 | - background: #fff; | ||
| 386 | - padding: 12rpx; | ||
| 387 | - border-radius: 16rpx; | ||
| 388 | - box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); | ||
| 389 | - cursor: pointer; | 785 | + background: #f9fafb; |
| 786 | + padding: 10rpx; | ||
| 787 | + border-radius: 14rpx; | ||
| 390 | } | 788 | } |
| 391 | 789 | ||
| 392 | .food-card:active { | 790 | .food-card:active { |
| 393 | - background: #fafafa; | 791 | + background: #f3f4f6; |
| 394 | } | 792 | } |
| 395 | 793 | ||
| 396 | .food-img-wrap { | 794 | .food-img-wrap { |
| @@ -398,8 +796,8 @@ const goPreview = (foodId: string) => { | @@ -398,8 +796,8 @@ const goPreview = (foodId: string) => { | ||
| 398 | position: relative; | 796 | position: relative; |
| 399 | padding-top: 75%; | 797 | padding-top: 75%; |
| 400 | border-radius: 10rpx; | 798 | border-radius: 10rpx; |
| 401 | - background: #f3f4f6; | ||
| 402 | - margin-bottom: 12rpx; | 799 | + background: #e5e7eb; |
| 800 | + margin-bottom: 10rpx; | ||
| 403 | overflow: hidden; | 801 | overflow: hidden; |
| 404 | } | 802 | } |
| 405 | 803 | ||
| @@ -411,12 +809,42 @@ const goPreview = (foodId: string) => { | @@ -411,12 +809,42 @@ const goPreview = (foodId: string) => { | ||
| 411 | height: 100%; | 809 | height: 100%; |
| 412 | } | 810 | } |
| 413 | 811 | ||
| 812 | +.size-badge { | ||
| 813 | + position: absolute; | ||
| 814 | + left: 8rpx; | ||
| 815 | + top: 8rpx; | ||
| 816 | + background: rgba(0, 0, 0, 0.25); | ||
| 817 | + padding: 4rpx 12rpx; | ||
| 818 | + border-radius: 8rpx; | ||
| 819 | +} | ||
| 820 | + | ||
| 821 | +.size-badge-text { | ||
| 822 | + font-size: 18rpx; | ||
| 823 | + color: #fff; | ||
| 824 | + font-weight: 500; | ||
| 825 | +} | ||
| 826 | + | ||
| 827 | +.type-badge { | ||
| 828 | + position: absolute; | ||
| 829 | + right: 8rpx; | ||
| 830 | + bottom: 8rpx; | ||
| 831 | + background: rgba(0, 0, 0, 0.25); | ||
| 832 | + padding: 4rpx 12rpx; | ||
| 833 | + border-radius: 8rpx; | ||
| 834 | +} | ||
| 835 | + | ||
| 836 | +.type-badge-text { | ||
| 837 | + font-size: 18rpx; | ||
| 838 | + color: #fff; | ||
| 839 | + font-weight: 500; | ||
| 840 | +} | ||
| 841 | + | ||
| 414 | .food-name { | 842 | .food-name { |
| 415 | font-size: 24rpx; | 843 | font-size: 24rpx; |
| 416 | font-weight: 600; | 844 | font-weight: 600; |
| 417 | color: #111827; | 845 | color: #111827; |
| 418 | display: block; | 846 | display: block; |
| 419 | - margin-bottom: 4rpx; | 847 | + margin-bottom: 2rpx; |
| 420 | overflow: hidden; | 848 | overflow: hidden; |
| 421 | text-overflow: ellipsis; | 849 | text-overflow: ellipsis; |
| 422 | white-space: nowrap; | 850 | white-space: nowrap; |
| @@ -425,11 +853,94 @@ const goPreview = (foodId: string) => { | @@ -425,11 +853,94 @@ const goPreview = (foodId: string) => { | ||
| 425 | .food-desc { | 853 | .food-desc { |
| 426 | font-size: 20rpx; | 854 | font-size: 20rpx; |
| 427 | color: #6b7280; | 855 | color: #6b7280; |
| 428 | - display: -webkit-box; | ||
| 429 | - -webkit-line-clamp: 2; | ||
| 430 | - -webkit-box-orient: vertical; | 856 | + display: block; |
| 431 | overflow: hidden; | 857 | overflow: hidden; |
| 432 | - line-height: 1.4; | 858 | + text-overflow: ellipsis; |
| 859 | + white-space: nowrap; | ||
| 860 | +} | ||
| 861 | + | ||
| 862 | +.modal-mask { | ||
| 863 | + position: fixed; | ||
| 864 | + top: 0; | ||
| 865 | + right: 0; | ||
| 866 | + bottom: 0; | ||
| 867 | + left: 0; | ||
| 868 | + background: rgba(0, 0, 0, 0.5); | ||
| 869 | + z-index: 1000; | ||
| 870 | + display: flex; | ||
| 871 | + align-items: flex-end; | ||
| 872 | + justify-content: center; | ||
| 873 | +} | ||
| 874 | + | ||
| 875 | +.modal-body { | ||
| 876 | + width: 100%; | ||
| 877 | + background: #fff; | ||
| 878 | + border-radius: 32rpx 32rpx 0 0; | ||
| 879 | + padding: 40rpx 32rpx; | ||
| 880 | + max-height: 70vh; | ||
| 881 | + overflow-y: auto; | ||
| 882 | +} | ||
| 883 | + | ||
| 884 | +.modal-title { | ||
| 885 | + font-size: 36rpx; | ||
| 886 | + font-weight: 700; | ||
| 887 | + color: #111827; | ||
| 888 | + display: block; | ||
| 889 | + margin-bottom: 8rpx; | ||
| 890 | +} | ||
| 891 | + | ||
| 892 | +.modal-desc { | ||
| 893 | + font-size: 28rpx; | ||
| 894 | + color: #6b7280; | ||
| 895 | + display: block; | ||
| 896 | + margin-bottom: 32rpx; | ||
| 897 | +} | ||
| 898 | + | ||
| 899 | +.subtype-list { | ||
| 900 | + display: flex; | ||
| 901 | + flex-direction: column; | ||
| 902 | + gap: 16rpx; | ||
| 903 | + margin-bottom: 24rpx; | ||
| 904 | +} | ||
| 905 | + | ||
| 906 | +.subtype-item { | ||
| 907 | + display: flex; | ||
| 908 | + align-items: center; | ||
| 909 | + gap: 20rpx; | ||
| 910 | + padding: 28rpx 24rpx; | ||
| 911 | + background: #f9fafb; | ||
| 912 | + border-radius: 16rpx; | ||
| 913 | + border: 2rpx solid #e5e7eb; | ||
| 914 | +} | ||
| 915 | + | ||
| 916 | +.subtype-item:active { | ||
| 917 | + background: var(--theme-primary-light); | ||
| 918 | + border-color: var(--theme-primary); | ||
| 919 | +} | ||
| 920 | + | ||
| 921 | +.subtype-name { | ||
| 922 | + flex: 1; | ||
| 923 | + min-width: 0; | ||
| 924 | + font-size: 30rpx; | ||
| 925 | + font-weight: 600; | ||
| 926 | + color: #111827; | ||
| 927 | +} | ||
| 928 | + | ||
| 929 | +.modal-cancel { | ||
| 930 | + width: 100%; | ||
| 931 | + height: 88rpx; | ||
| 932 | + background: #f3f4f6; | ||
| 933 | + border-radius: 16rpx; | ||
| 934 | + display: flex; | ||
| 935 | + align-items: center; | ||
| 936 | + justify-content: center; | ||
| 937 | +} | ||
| 938 | + | ||
| 939 | +.modal-cancel-text { | ||
| 940 | + font-size: 30rpx; | ||
| 941 | + font-weight: 500; | ||
| 942 | + color: #374151; | ||
| 943 | + line-height: 1; | ||
| 433 | } | 944 | } |
| 434 | 945 | ||
| 435 | @media (min-width: 768px) { | 946 | @media (min-width: 768px) { |
| @@ -439,8 +950,8 @@ const goPreview = (foodId: string) => { | @@ -439,8 +950,8 @@ const goPreview = (foodId: string) => { | ||
| 439 | 950 | ||
| 440 | .food-grid { | 951 | .food-grid { |
| 441 | grid-template-columns: repeat(3, minmax(0, 1fr)); | 952 | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| 442 | - column-gap: 20rpx; | ||
| 443 | - row-gap: 20rpx; | 953 | + column-gap: 16rpx; |
| 954 | + row-gap: 16rpx; | ||
| 444 | } | 955 | } |
| 445 | } | 956 | } |
| 446 | </style> | 957 | </style> |
美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue
| @@ -6,8 +6,8 @@ | @@ -6,8 +6,8 @@ | ||
| 6 | <AppIcon name="chevronLeft" size="sm" color="white" /> | 6 | <AppIcon name="chevronLeft" size="sm" color="white" /> |
| 7 | </view> | 7 | </view> |
| 8 | <view class="top-center"> | 8 | <view class="top-center"> |
| 9 | - <text class="page-title">{{ t('labels.preview.title') }}</text> | ||
| 10 | - <text class="page-sub">{{ t('labels.preview.subtitle') }}</text> | 9 | + <text class="page-title">Label Preview</text> |
| 10 | + <LocationPicker /> | ||
| 11 | </view> | 11 | </view> |
| 12 | <view class="top-right" @click="isMenuOpen = true"> | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 13 | <AppIcon name="menu" size="sm" color="white" /> | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| @@ -16,20 +16,21 @@ | @@ -16,20 +16,21 @@ | ||
| 16 | </view> | 16 | </view> |
| 17 | 17 | ||
| 18 | <view class="content"> | 18 | <view class="content"> |
| 19 | - <!-- Food info --> | ||
| 20 | <view class="food-card"> | 19 | <view class="food-card"> |
| 21 | - <image | ||
| 22 | - :src="food && food.image ? food.image : ''" | ||
| 23 | - class="food-img" | ||
| 24 | - mode="aspectFill" | ||
| 25 | - /> | ||
| 26 | <view class="food-info"> | 20 | <view class="food-info"> |
| 27 | - <text class="food-name">{{ food ? t(food.nameKey) : '' }}</text> | ||
| 28 | - <text class="food-cat">{{ food ? t(food.categoryKey) : '' }}</text> | 21 | + <text class="food-name">{{ displayProductName }}</text> |
| 22 | + <text class="food-cat">{{ productCategory }}</text> | ||
| 23 | + <view v-if="labelTypeName" class="food-label-type"> | ||
| 24 | + <AppIcon name="tag" size="sm" color="primary" /> | ||
| 25 | + <text class="food-label-type-text">{{ labelTypeName }}</text> | ||
| 26 | + </view> | ||
| 27 | + </view> | ||
| 28 | + <view class="food-template"> | ||
| 29 | + <text class="template-size">{{ templateSize }}</text> | ||
| 30 | + <text class="template-name">{{ templateName }}</text> | ||
| 29 | </view> | 31 | </view> |
| 30 | </view> | 32 | </view> |
| 31 | 33 | ||
| 32 | - <!-- Print quantity --> | ||
| 33 | <view class="qty-card"> | 34 | <view class="qty-card"> |
| 34 | <text class="qty-label">Print Quantity</text> | 35 | <text class="qty-label">Print Quantity</text> |
| 35 | <view class="qty-control"> | 36 | <view class="qty-control"> |
| @@ -43,46 +44,32 @@ | @@ -43,46 +44,32 @@ | ||
| 43 | </view> | 44 | </view> |
| 44 | </view> | 45 | </view> |
| 45 | 46 | ||
| 46 | - <!-- Label preview --> | ||
| 47 | - <text class="section-title">{{ t('labels.preview.labelPreview') }}</text> | 47 | + <text class="section-title">Label Preview</text> |
| 48 | 48 | ||
| 49 | <view class="label-card"> | 49 | <view class="label-card"> |
| 50 | - <view class="label-border"> | ||
| 51 | - <view class="label-header"> | ||
| 52 | - <view class="label-type-icon-wrap"> | ||
| 53 | - <AppIcon :name="typeIconName" size="lg" color="white" /> | ||
| 54 | - </view> | ||
| 55 | - <text class="label-title">{{ t(labelData.titleKey) }}</text> | ||
| 56 | - </view> | ||
| 57 | - <view class="label-food-name"> | ||
| 58 | - <text>{{ food ? t(food.nameKey) : '' }}</text> | ||
| 59 | - </view> | ||
| 60 | - <view class="label-fields"> | ||
| 61 | - <view | ||
| 62 | - v-for="(field, i) in labelData.fields" | ||
| 63 | - :key="i" | ||
| 64 | - class="field-row" | ||
| 65 | - :class="{ indent: field.indent, 'field-row-last': i === labelData.fields.length - 1 }" | ||
| 66 | - > | ||
| 67 | - <text class="field-label" :class="{ bold: field.bold, warning: field.warning }">{{ t(field.labelKey) }}</text> | ||
| 68 | - <text class="field-value" :class="{ bold: field.bold, warning: field.warning }">{{ field.value }}</text> | ||
| 69 | - </view> | ||
| 70 | - </view> | ||
| 71 | - <view class="label-footer"> | ||
| 72 | - <text>{{ t('labels.preview.printedBy') }}: {{ userName }}</text> | ||
| 73 | - <text>{{ t('labels.preview.printDate') }}: {{ printDate }}</text> | ||
| 74 | - </view> | 50 | + <view class="label-img-wrap"> |
| 51 | + <image :src="labelImage" class="label-img" mode="widthFix" /> | ||
| 52 | + </view> | ||
| 53 | + </view> | ||
| 54 | + | ||
| 55 | + <view class="info-row"> | ||
| 56 | + <view class="info-item"> | ||
| 57 | + <text class="info-label">Last Edited</text> | ||
| 58 | + <text class="info-value">{{ lastEdited }}</text> | ||
| 59 | + </view> | ||
| 60 | + <view class="info-item"> | ||
| 61 | + <text class="info-label">Location</text> | ||
| 62 | + <text class="info-value">{{ locationName }}</text> | ||
| 75 | </view> | 63 | </view> |
| 76 | </view> | 64 | </view> |
| 77 | 65 | ||
| 78 | <view class="note-card"> | 66 | <view class="note-card"> |
| 79 | <AppIcon name="alert" size="sm" color="blue" /> | 67 | <AppIcon name="alert" size="sm" color="blue" /> |
| 80 | - <text class="note-text">{{ t('labels.preview.note') }}</text> | 68 | + <text class="note-text">This is a preview of the label. Actual printed labels may vary slightly in appearance.</text> |
| 81 | </view> | 69 | </view> |
| 82 | </view> | 70 | </view> |
| 83 | 71 | ||
| 84 | - <!-- Bottom bar --> | ||
| 85 | - <view class="bottom-bar" :style="{ paddingBottom: (bottomSafeArea + 20) + 'px' }"> | 72 | + <view class="bottom-bar"> |
| 86 | <view class="bottom-info"> | 73 | <view class="bottom-info"> |
| 87 | <text class="bottom-qty">{{ printQty }} label{{ printQty > 1 ? 's' : '' }}</text> | 74 | <text class="bottom-qty">{{ printQty }} label{{ printQty > 1 ? 's' : '' }}</text> |
| 88 | <view class="bt-indicator" :class="{ connected: btConnected }"> | 75 | <view class="bt-indicator" :class="{ connected: btConnected }"> |
| @@ -96,47 +83,18 @@ | @@ -96,47 +83,18 @@ | ||
| 96 | </view> | 83 | </view> |
| 97 | <view class="print-btn" :class="{ disabled: isPrinting }" @click="handlePrint"> | 84 | <view class="print-btn" :class="{ disabled: isPrinting }" @click="handlePrint"> |
| 98 | <AppIcon name="printer" size="sm" color="white" /> | 85 | <AppIcon name="printer" size="sm" color="white" /> |
| 99 | - <text class="print-btn-text">{{ isPrinting ? t('labels.print.printing') : t('labels.print.button') }}</text> | 86 | + <text class="print-btn-text">{{ isPrinting ? 'Printing...' : 'Print' }}</text> |
| 100 | </view> | 87 | </view> |
| 101 | </view> | 88 | </view> |
| 102 | </view> | 89 | </view> |
| 103 | 90 | ||
| 104 | - <!-- Preview modal --> | ||
| 105 | <view v-if="showPreviewModal" class="modal-mask" @click="showPreviewModal = false"> | 91 | <view v-if="showPreviewModal" class="modal-mask" @click="showPreviewModal = false"> |
| 106 | - <view class="modal-body" @click.stop> | ||
| 107 | - <view class="modal-top"> | ||
| 108 | - <text class="modal-title">Label Preview</text> | ||
| 109 | - <view class="modal-close" @click="showPreviewModal = false"> | ||
| 110 | - <AppIcon name="plus" size="sm" color="gray" /> | 92 | + <view class="modal-body modal-body-label-only" @click.stop> |
| 93 | + <view class="modal-label-wrap"> | ||
| 94 | + <view class="modal-label-inner"> | ||
| 95 | + <image :src="labelImage" class="modal-label-img" mode="widthFix" /> | ||
| 111 | </view> | 96 | </view> |
| 112 | </view> | 97 | </view> |
| 113 | - <scroll-view class="modal-scroll" scroll-y> | ||
| 114 | - <view class="pv-card"> | ||
| 115 | - <view class="pv-border"> | ||
| 116 | - <view class="pv-header"> | ||
| 117 | - <text class="pv-type">{{ t(labelData.titleKey) }}</text> | ||
| 118 | - </view> | ||
| 119 | - <view class="pv-food"> | ||
| 120 | - <text>{{ food ? t(food.nameKey) : '' }}</text> | ||
| 121 | - </view> | ||
| 122 | - <view class="pv-fields"> | ||
| 123 | - <view | ||
| 124 | - v-for="(field, i) in labelData.fields" | ||
| 125 | - :key="i" | ||
| 126 | - class="pv-row" | ||
| 127 | - :class="{ 'pv-row-last': i === labelData.fields.length - 1 }" | ||
| 128 | - > | ||
| 129 | - <text class="pv-label">{{ t(field.labelKey) }}</text> | ||
| 130 | - <text class="pv-value" :class="{ bold: field.bold, warning: field.warning }">{{ field.value }}</text> | ||
| 131 | - </view> | ||
| 132 | - </view> | ||
| 133 | - <view class="pv-footer"> | ||
| 134 | - <text>{{ userName }} | {{ printDate }}</text> | ||
| 135 | - </view> | ||
| 136 | - </view> | ||
| 137 | - </view> | ||
| 138 | - <text class="pv-note">Actual print size: {{ printQty }} {{ printQty > 1 ? 'copies' : 'copy' }}</text> | ||
| 139 | - </scroll-view> | ||
| 140 | </view> | 98 | </view> |
| 141 | </view> | 99 | </view> |
| 142 | 100 | ||
| @@ -146,17 +104,13 @@ | @@ -146,17 +104,13 @@ | ||
| 146 | 104 | ||
| 147 | <script setup lang="ts"> | 105 | <script setup lang="ts"> |
| 148 | import { ref, computed } from 'vue' | 106 | import { ref, computed } from 'vue' |
| 149 | -import { useI18n } from 'vue-i18n' | ||
| 150 | import { onLoad, onShow } from '@dcloudio/uni-app' | 107 | import { onLoad, onShow } from '@dcloudio/uni-app' |
| 151 | import AppIcon from '../../components/AppIcon.vue' | 108 | import AppIcon from '../../components/AppIcon.vue' |
| 152 | import SideMenu from '../../components/SideMenu.vue' | 109 | import SideMenu from '../../components/SideMenu.vue' |
| 153 | -import { getStatusBarHeight, getBottomSafeArea } from '../../utils/statusBar' | 110 | +import LocationPicker from '../../components/LocationPicker.vue' |
| 111 | +import { getStatusBarHeight } from '../../utils/statusBar' | ||
| 154 | 112 | ||
| 155 | -const { t } = useI18n() | ||
| 156 | const statusBarHeight = getStatusBarHeight() | 113 | const statusBarHeight = getStatusBarHeight() |
| 157 | -const bottomSafeArea = getBottomSafeArea() | ||
| 158 | -const labelType = ref('nutrition') | ||
| 159 | -const foodId = ref('food-001') | ||
| 160 | const isPrinting = ref(false) | 114 | const isPrinting = ref(false) |
| 161 | const isMenuOpen = ref(false) | 115 | const isMenuOpen = ref(false) |
| 162 | const printQty = ref(1) | 116 | const printQty = ref(1) |
| @@ -165,98 +119,85 @@ const showPreviewModal = ref(false) | @@ -165,98 +119,85 @@ const showPreviewModal = ref(false) | ||
| 165 | const btConnected = ref(false) | 119 | const btConnected = ref(false) |
| 166 | const btDeviceName = ref('') | 120 | const btDeviceName = ref('') |
| 167 | 121 | ||
| 122 | +const productName = ref('Chicken') | ||
| 123 | +const productCategory = ref('Meat') | ||
| 124 | +const labelTypeName = ref('') | ||
| 125 | +const templateSize = ref('2"x2"') | ||
| 126 | +const templateName = ref('Basic') | ||
| 127 | +const lastEdited = ref('2025.12.03 11:45') | ||
| 128 | +const locationName = ref('Location A') | ||
| 129 | + | ||
| 130 | +const labelImage = computed(() => { | ||
| 131 | + const size = templateSize.value | ||
| 132 | + if (size.indexOf('2"x6"') >= 0 || size.indexOf('2"x4"') >= 0) { | ||
| 133 | + return '/static/lable2.png' | ||
| 134 | + } | ||
| 135 | + return '/static/lable1.png' | ||
| 136 | +}) | ||
| 137 | + | ||
| 138 | +// 产品名称按标签模板固定:lable1 → Syrup,lable2 → Cheese Burger Deluxe | ||
| 139 | +const displayProductName = computed(() => | ||
| 140 | + labelImage.value.includes('lable1') ? 'Syrup' : 'Cheese Burger Deluxe' | ||
| 141 | +) | ||
| 142 | + | ||
| 168 | onShow(() => { | 143 | onShow(() => { |
| 169 | const name = uni.getStorageSync('btDeviceName') | 144 | const name = uni.getStorageSync('btDeviceName') |
| 170 | btConnected.value = !!name | 145 | btConnected.value = !!name |
| 171 | btDeviceName.value = name || '' | 146 | btDeviceName.value = name || '' |
| 172 | }) | 147 | }) |
| 173 | 148 | ||
| 174 | -const foodData: Record<string, any> = { | ||
| 175 | - 'food-001': { nameKey: 'food.chickenBreast', image: 'https://images.unsplash.com/photo-1532550907401-a500c9a57435?w=600', categoryKey: 'category.meat' }, | ||
| 176 | - 'food-002': { nameKey: 'food.caesarSalad', image: 'https://images.unsplash.com/photo-1546793665-c74683f339c1?w=600', categoryKey: 'category.salads' }, | ||
| 177 | - 'food-003': { nameKey: 'food.salmonFillet', image: 'https://images.unsplash.com/photo-1519708227418-c8fd9a32b7a2?w=600', categoryKey: 'category.seafood' }, | ||
| 178 | - 'food-004': { nameKey: 'food.beefPatties', image: 'https://images.unsplash.com/photo-1607623488235-e2e6794c30b5?w=600', categoryKey: 'category.meat' }, | ||
| 179 | - 'food-005': { nameKey: 'food.marinaraSauce', image: 'https://images.unsplash.com/photo-1621996346565-e3dbc646d9a9?w=600', categoryKey: 'category.sauces' }, | ||
| 180 | - 'food-006': { nameKey: 'food.vegetables', image: 'https://images.unsplash.com/photo-1540420773420-3366772f4999?w=600', categoryKey: 'category.vegetables' }, | ||
| 181 | - 'food-007': { nameKey: 'food.brownie', image: 'https://images.unsplash.com/photo-1606313564200-e75d5e30476c?w=600', categoryKey: 'category.desserts' }, | ||
| 182 | -} | ||
| 183 | - | ||
| 184 | -const typeIconMap: Record<string, string> = { | ||
| 185 | - nutrition: 'food', allergen: 'alert', storage: 'snowflake', expiry: 'calendar', batch: 'package', preparation: 'chef', | 149 | +interface ProductData { |
| 150 | + name: string | ||
| 151 | + category: string | ||
| 152 | + templateSize: string | ||
| 153 | + templateName: string | ||
| 154 | + lastEdited: string | ||
| 155 | + labelTypes: { id: string; name: string }[] | ||
| 156 | +} | ||
| 157 | + | ||
| 158 | +const productMap: Record<string, ProductData> = { | ||
| 159 | + 'chicken': { name: 'Chicken', category: 'Meat', templateSize: '2"x2"', templateName: 'Basic', lastEdited: '2025.12.03 11:45', labelTypes: [{ id: 'defrost', name: 'Defrost' }, { id: 'opened', name: 'Opened/Preped' }, { id: 'heated', name: 'Heated' }] }, | ||
| 160 | + 'beef': { name: 'Beef', category: 'Meat', templateSize: '2"x2"', templateName: 'Basic', lastEdited: '2025.12.04 09:20', labelTypes: [{ id: 'defrost', name: 'Defrost' }, { id: 'opened', name: 'Opened/Preped' }, { id: 'heated', name: 'Heated' }] }, | ||
| 161 | + 'bacon': { name: 'Bacon', category: 'Meat', templateSize: '2"x2"', templateName: 'Basic', lastEdited: '2025.12.03 14:30', labelTypes: [{ id: 'raw', name: 'Raw Bacon' }, { id: 'cooked', name: 'Cooked Bacon' }] }, | ||
| 162 | + 'chicken-sandwich': { name: 'Chicken Sandwich', category: 'Sandwich', templateSize: '2"x6"', templateName: "G'n'G", lastEdited: '2025.12.03 11:45', labelTypes: [] }, | ||
| 163 | + 'turkey-club': { name: 'Turkey Club', category: 'Sandwich', templateSize: '2"x6"', templateName: "G'n'G", lastEdited: '2025.12.04 09:30', labelTypes: [] }, | ||
| 164 | + 'cheese-burger': { name: 'Cheese Burger', category: 'Sandwich', templateSize: '2"x6"', templateName: "G'n'G", lastEdited: '2025.12.03 15:20', labelTypes: [] }, | ||
| 165 | + 'caesar-salad': { name: 'Caesar Salad', category: 'Salads', templateSize: '2"x4"', templateName: "G'n'G", lastEdited: '2025.12.04 09:00', labelTypes: [] }, | ||
| 166 | + 'greek-salad': { name: 'Greek Salad', category: 'Salads', templateSize: '2"x4"', templateName: "G'n'G", lastEdited: '2025.12.03 12:30', labelTypes: [] }, | ||
| 167 | + 'fresh-juice': { name: 'Fresh Juice', category: 'Beverages', templateSize: '2"x2"', templateName: "G'n'G", lastEdited: '2025.12.04 07:45', labelTypes: [] }, | ||
| 168 | + 'smoothie': { name: 'Smoothie', category: 'Beverages', templateSize: '2"x2"', templateName: "G'n'G", lastEdited: '2025.12.03 11:00', labelTypes: [] }, | ||
| 169 | + 'milk': { name: 'Milk', category: 'Dairy', templateSize: '2"x2"', templateName: 'Basic', lastEdited: '2025.12.04 08:30', labelTypes: [] }, | ||
| 170 | + 'cheese': { name: 'Cheese', category: 'Dairy', templateSize: '2"x2"', templateName: 'Basic', lastEdited: '2025.12.03 11:15', labelTypes: [] }, | ||
| 171 | + 'sliced-ham': { name: 'Sliced Ham', category: 'Deli', templateSize: '2"x2"', templateName: 'Basic', lastEdited: '2025.12.04 10:00', labelTypes: [] }, | ||
| 172 | + 'turkey-deli': { name: 'Turkey Deli', category: 'Deli', templateSize: '2"x2"', templateName: 'Basic', lastEdited: '2025.12.03 14:20', labelTypes: [] }, | ||
| 186 | } | 173 | } |
| 187 | 174 | ||
| 188 | onLoad((opts: any) => { | 175 | onLoad((opts: any) => { |
| 189 | - labelType.value = (opts && opts.labelType) || 'nutrition' | ||
| 190 | - foodId.value = (opts && opts.foodId) || 'food-001' | ||
| 191 | -}) | 176 | + const pid = (opts && opts.productId) || 'chicken' |
| 177 | + const subType = (opts && opts.subType) || '' | ||
| 178 | + | ||
| 179 | + const product = productMap[pid] | ||
| 180 | + if (product) { | ||
| 181 | + productName.value = product.name | ||
| 182 | + productCategory.value = product.category | ||
| 183 | + templateSize.value = product.templateSize | ||
| 184 | + templateName.value = product.templateName | ||
| 185 | + lastEdited.value = product.lastEdited | ||
| 186 | + | ||
| 187 | + if (subType) { | ||
| 188 | + const found = product.labelTypes.filter(function (lt) { return lt.id === subType }) | ||
| 189 | + if (found.length > 0) { | ||
| 190 | + labelTypeName.value = found[0].name | ||
| 191 | + } | ||
| 192 | + } | ||
| 193 | + } | ||
| 192 | 194 | ||
| 193 | -const food = computed(() => foodData[foodId.value] || foodData['food-001']) | ||
| 194 | -const typeIconName = computed(() => typeIconMap[labelType.value] || 'tag') | ||
| 195 | -const userName = uni.getStorageSync('userName') || 'Staff' | ||
| 196 | -const printDate = new Date().toLocaleString() | 195 | + const storeId = uni.getStorageSync('storeId') || '001' |
| 196 | + locationName.value = 'Location A' | ||
| 197 | +}) | ||
| 197 | 198 | ||
| 198 | const increment = () => { if (printQty.value < 99) printQty.value++ } | 199 | const increment = () => { if (printQty.value < 99) printQty.value++ } |
| 199 | const decrement = () => { if (printQty.value > 1) printQty.value-- } | 200 | const decrement = () => { if (printQty.value > 1) printQty.value-- } |
| 200 | - | ||
| 201 | -const getLabelPreviewData = () => { | ||
| 202 | - const today = new Date() | ||
| 203 | - const expiry = new Date(today) | ||
| 204 | - expiry.setDate(expiry.getDate() + 5) | ||
| 205 | - const maps: Record<string, any> = { | ||
| 206 | - nutrition: { | ||
| 207 | - titleKey: 'labelPreview.nutrition', | ||
| 208 | - fields: [ | ||
| 209 | - { labelKey: 'nutrition.servingSize', value: '150g' }, | ||
| 210 | - { labelKey: 'nutrition.calories', value: '165 kcal', bold: true }, | ||
| 211 | - { labelKey: 'nutrition.protein', value: '31g', bold: true }, | ||
| 212 | - ], | ||
| 213 | - }, | ||
| 214 | - allergen: { | ||
| 215 | - titleKey: 'labelPreview.allergen', | ||
| 216 | - fields: [ | ||
| 217 | - { labelKey: 'allergen.contains', value: 'Tree Nuts, Dairy, Eggs', warning: true }, | ||
| 218 | - { labelKey: 'allergen.mayContain', value: 'Sesame, Soy' }, | ||
| 219 | - ], | ||
| 220 | - }, | ||
| 221 | - storage: { | ||
| 222 | - titleKey: 'labelPreview.storage', | ||
| 223 | - fields: [ | ||
| 224 | - { labelKey: 'storage.temperature', value: `32-40${t('storage.tempRange')}`, bold: true }, | ||
| 225 | - { labelKey: 'storage.location', value: 'Walk-in Cooler - Section B' }, | ||
| 226 | - { labelKey: 'storage.shelfLife', value: `5 ${t('storage.daysFromPrep')}` }, | ||
| 227 | - ], | ||
| 228 | - }, | ||
| 229 | - expiry: { | ||
| 230 | - titleKey: 'labelPreview.expiry', | ||
| 231 | - fields: [ | ||
| 232 | - { labelKey: 'expiry.prepDate', value: today.toLocaleDateString() }, | ||
| 233 | - { labelKey: 'expiry.expiryDate', value: expiry.toLocaleDateString(), bold: true }, | ||
| 234 | - { labelKey: 'expiry.preparedBy', value: userName }, | ||
| 235 | - ], | ||
| 236 | - }, | ||
| 237 | - batch: { | ||
| 238 | - titleKey: 'labelPreview.batch', | ||
| 239 | - fields: [ | ||
| 240 | - { labelKey: 'batch.batchNumber', value: `BATCH-${Date.now().toString().slice(-8)}`, bold: true }, | ||
| 241 | - { labelKey: 'batch.productionDate', value: today.toLocaleDateString() }, | ||
| 242 | - { labelKey: 'batch.supplier', value: t('batch.supplierName') }, | ||
| 243 | - ], | ||
| 244 | - }, | ||
| 245 | - preparation: { | ||
| 246 | - titleKey: 'labelPreview.preparation', | ||
| 247 | - fields: [ | ||
| 248 | - { labelKey: 'prep.prepDate', value: today.toLocaleDateString() }, | ||
| 249 | - { labelKey: 'prep.prepTime', value: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }, | ||
| 250 | - { labelKey: 'prep.preparedBy', value: userName, bold: true }, | ||
| 251 | - { labelKey: 'prep.location', value: uni.getStorageSync('storeName') || 'Kitchen' }, | ||
| 252 | - { labelKey: 'prep.useBy', value: expiry.toLocaleDateString() }, | ||
| 253 | - ], | ||
| 254 | - }, | ||
| 255 | - } | ||
| 256 | - return maps[labelType.value] || maps.nutrition | ||
| 257 | -} | ||
| 258 | - | ||
| 259 | -const labelData = computed(() => getLabelPreviewData()) | ||
| 260 | const goBack = () => uni.navigateBack() | 201 | const goBack = () => uni.navigateBack() |
| 261 | 202 | ||
| 262 | const handlePrint = () => { | 203 | const handlePrint = () => { |
| @@ -276,7 +217,7 @@ const handlePrint = () => { | @@ -276,7 +217,7 @@ const handlePrint = () => { | ||
| 276 | isPrinting.value = true | 217 | isPrinting.value = true |
| 277 | setTimeout(() => { | 218 | setTimeout(() => { |
| 278 | isPrinting.value = false | 219 | isPrinting.value = false |
| 279 | - uni.showToast({ title: `${printQty.value} label${printQty.value > 1 ? 's' : ''} printed!`, icon: 'success' }) | 220 | + uni.showToast({ title: printQty.value + ' label' + (printQty.value > 1 ? 's' : '') + ' printed!', icon: 'success' }) |
| 280 | }, 2000) | 221 | }, 2000) |
| 281 | } | 222 | } |
| 282 | </script> | 223 | </script> |
| @@ -285,31 +226,61 @@ const handlePrint = () => { | @@ -285,31 +226,61 @@ const handlePrint = () => { | ||
| 285 | .page { | 226 | .page { |
| 286 | min-height: 100vh; | 227 | min-height: 100vh; |
| 287 | background: #f9fafb; | 228 | background: #f9fafb; |
| 229 | + overflow-x: hidden; | ||
| 230 | + overflow-x: clip; | ||
| 231 | + width: 100%; | ||
| 232 | + max-width: 100vw; | ||
| 233 | + box-sizing: border-box; | ||
| 288 | } | 234 | } |
| 289 | 235 | ||
| 290 | -/* ---- Header ---- */ | ||
| 291 | .header-hero { | 236 | .header-hero { |
| 292 | background: linear-gradient(135deg, var(--theme-primary), var(--theme-primary-dark)); | 237 | background: linear-gradient(135deg, var(--theme-primary), var(--theme-primary-dark)); |
| 293 | padding: 16rpx 32rpx 24rpx; | 238 | padding: 16rpx 32rpx 24rpx; |
| 294 | } | 239 | } |
| 295 | -.top-bar { height: 96rpx; display: flex; align-items: center; justify-content: space-between; } | ||
| 296 | -.top-left, .top-right { | ||
| 297 | - width: 64rpx; height: 64rpx; border-radius: 999rpx; | ||
| 298 | - background: rgba(255,255,255,0.15); | ||
| 299 | - display: flex; align-items: center; justify-content: center; | 240 | + |
| 241 | +.top-bar { | ||
| 242 | + height: 96rpx; | ||
| 243 | + display: flex; | ||
| 244 | + align-items: center; | ||
| 245 | + justify-content: space-between; | ||
| 246 | +} | ||
| 247 | + | ||
| 248 | +.top-left, | ||
| 249 | +.top-right { | ||
| 250 | + width: 64rpx; | ||
| 251 | + height: 64rpx; | ||
| 252 | + border-radius: 999rpx; | ||
| 253 | + background: rgba(255, 255, 255, 0.15); | ||
| 254 | + display: flex; | ||
| 255 | + align-items: center; | ||
| 256 | + justify-content: center; | ||
| 257 | +} | ||
| 258 | + | ||
| 259 | +.top-center { | ||
| 260 | + flex: 1; | ||
| 261 | + display: flex; | ||
| 262 | + flex-direction: column; | ||
| 263 | + align-items: center; | ||
| 264 | +} | ||
| 265 | + | ||
| 266 | +.page-title { | ||
| 267 | + font-size: 34rpx; | ||
| 268 | + font-weight: 600; | ||
| 269 | + color: #fff; | ||
| 270 | + display: block; | ||
| 300 | } | 271 | } |
| 301 | -.top-center { flex: 1; text-align: center; } | ||
| 302 | -.page-title { font-size: 34rpx; font-weight: 600; color: #fff; display: block; } | ||
| 303 | -.page-sub { font-size: 24rpx; color: rgba(255,255,255,0.85); } | ||
| 304 | 272 | ||
| 305 | -/* ---- Content ---- */ | ||
| 306 | .content { | 273 | .content { |
| 307 | padding: 32rpx; | 274 | padding: 32rpx; |
| 308 | padding-bottom: 300rpx; | 275 | padding-bottom: 300rpx; |
| 309 | box-sizing: border-box; | 276 | box-sizing: border-box; |
| 277 | + overflow-x: hidden; | ||
| 278 | + overflow-x: clip; | ||
| 279 | + width: 100%; | ||
| 280 | + max-width: 100%; | ||
| 281 | + min-width: 0; | ||
| 310 | } | 282 | } |
| 311 | 283 | ||
| 312 | -/* Food card */ | ||
| 313 | .food-card { | 284 | .food-card { |
| 314 | background: #fff; | 285 | background: #fff; |
| 315 | padding: 28rpx; | 286 | padding: 28rpx; |
| @@ -317,204 +288,428 @@ const handlePrint = () => { | @@ -317,204 +288,428 @@ const handlePrint = () => { | ||
| 317 | margin-bottom: 24rpx; | 288 | margin-bottom: 24rpx; |
| 318 | display: flex; | 289 | display: flex; |
| 319 | align-items: center; | 290 | align-items: center; |
| 291 | + justify-content: space-between; | ||
| 320 | gap: 24rpx; | 292 | gap: 24rpx; |
| 321 | - box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); | 293 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); |
| 322 | } | 294 | } |
| 323 | -.food-img { | ||
| 324 | - width: 120rpx; height: 120rpx; | ||
| 325 | - border-radius: 16rpx; background: #f3f4f6; flex-shrink: 0; | 295 | + |
| 296 | +.food-info { | ||
| 297 | + flex: 1; | ||
| 298 | + min-width: 0; | ||
| 326 | } | 299 | } |
| 327 | -.food-info { flex: 1; min-width: 0; } | 300 | + |
| 328 | .food-name { | 301 | .food-name { |
| 329 | - font-size: 32rpx; font-weight: 600; color: #111827; | ||
| 330 | - display: block; margin-bottom: 4rpx; | ||
| 331 | - overflow: hidden; text-overflow: ellipsis; white-space: nowrap; | 302 | + font-size: 32rpx; |
| 303 | + font-weight: 600; | ||
| 304 | + color: #111827; | ||
| 305 | + display: block; | ||
| 306 | + margin-bottom: 4rpx; | ||
| 307 | +} | ||
| 308 | + | ||
| 309 | +.food-cat { | ||
| 310 | + font-size: 26rpx; | ||
| 311 | + color: #6b7280; | ||
| 312 | + display: block; | ||
| 313 | +} | ||
| 314 | + | ||
| 315 | +.food-label-type { | ||
| 316 | + display: flex; | ||
| 317 | + align-items: center; | ||
| 318 | + gap: 8rpx; | ||
| 319 | + margin-top: 12rpx; | ||
| 320 | +} | ||
| 321 | + | ||
| 322 | +.food-label-type-text { | ||
| 323 | + font-size: 24rpx; | ||
| 324 | + color: var(--theme-primary); | ||
| 325 | + font-weight: 500; | ||
| 326 | +} | ||
| 327 | + | ||
| 328 | +.food-template { | ||
| 329 | + flex-shrink: 0; | ||
| 330 | + text-align: center; | ||
| 331 | + background: #f3f4f6; | ||
| 332 | + padding: 16rpx 20rpx; | ||
| 333 | + border-radius: 14rpx; | ||
| 334 | +} | ||
| 335 | + | ||
| 336 | +.template-size { | ||
| 337 | + font-size: 28rpx; | ||
| 338 | + font-weight: 700; | ||
| 339 | + color: #111827; | ||
| 340 | + display: block; | ||
| 341 | +} | ||
| 342 | + | ||
| 343 | +.template-name { | ||
| 344 | + font-size: 22rpx; | ||
| 345 | + color: #6b7280; | ||
| 346 | + display: block; | ||
| 347 | + margin-top: 4rpx; | ||
| 332 | } | 348 | } |
| 333 | -.food-cat { font-size: 26rpx; color: #6b7280; } | ||
| 334 | 349 | ||
| 335 | -/* Quantity card */ | ||
| 336 | .qty-card { | 350 | .qty-card { |
| 337 | - display: flex; align-items: center; justify-content: space-between; | ||
| 338 | - background: #fff; padding: 24rpx 28rpx; border-radius: 20rpx; | ||
| 339 | - margin-bottom: 32rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); | 351 | + display: flex; |
| 352 | + align-items: center; | ||
| 353 | + justify-content: space-between; | ||
| 354 | + background: #fff; | ||
| 355 | + padding: 24rpx 28rpx; | ||
| 356 | + border-radius: 20rpx; | ||
| 357 | + margin-bottom: 32rpx; | ||
| 358 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); | ||
| 359 | +} | ||
| 360 | + | ||
| 361 | +.qty-label { | ||
| 362 | + font-size: 30rpx; | ||
| 363 | + font-weight: 600; | ||
| 364 | + color: #111827; | ||
| 365 | +} | ||
| 366 | + | ||
| 367 | +.qty-control { | ||
| 368 | + display: flex; | ||
| 369 | + align-items: center; | ||
| 340 | } | 370 | } |
| 341 | -.qty-label { font-size: 30rpx; font-weight: 600; color: #111827; } | ||
| 342 | -.qty-control { display: flex; align-items: center; } | 371 | + |
| 343 | .qty-btn { | 372 | .qty-btn { |
| 344 | - width: 64rpx; height: 64rpx; border-radius: 14rpx; background: #f3f4f6; | ||
| 345 | - display: flex; align-items: center; justify-content: center; | ||
| 346 | - cursor: pointer; transition: background 0.15s; | 373 | + width: 64rpx; |
| 374 | + height: 64rpx; | ||
| 375 | + border-radius: 14rpx; | ||
| 376 | + background: #f3f4f6; | ||
| 377 | + display: flex; | ||
| 378 | + align-items: center; | ||
| 379 | + justify-content: center; | ||
| 380 | +} | ||
| 381 | + | ||
| 382 | +.qty-btn:active { | ||
| 383 | + background: #e5e7eb; | ||
| 384 | +} | ||
| 385 | + | ||
| 386 | +.qty-btn.disabled { | ||
| 387 | + opacity: 0.35; | ||
| 347 | } | 388 | } |
| 348 | -.qty-btn:active { background: #e5e7eb; } | ||
| 349 | -.qty-btn.disabled { opacity: 0.35; } | 389 | + |
| 350 | .qty-value { | 390 | .qty-value { |
| 351 | - width: 88rpx; text-align: center; | ||
| 352 | - font-size: 36rpx; font-weight: 700; color: #111827; | 391 | + width: 88rpx; |
| 392 | + text-align: center; | ||
| 393 | + font-size: 36rpx; | ||
| 394 | + font-weight: 700; | ||
| 395 | + color: #111827; | ||
| 353 | } | 396 | } |
| 354 | 397 | ||
| 355 | -/* Section title */ | ||
| 356 | .section-title { | 398 | .section-title { |
| 357 | - font-size: 30rpx; font-weight: 600; color: #111827; | ||
| 358 | - display: block; margin-bottom: 20rpx; | 399 | + font-size: 30rpx; |
| 400 | + font-weight: 600; | ||
| 401 | + color: #111827; | ||
| 402 | + display: block; | ||
| 403 | + margin-bottom: 20rpx; | ||
| 359 | } | 404 | } |
| 360 | 405 | ||
| 361 | -/* Label card */ | 406 | +/* 使用 block 布局避免移动端 Safari flex+图片溢出问题 */ |
| 362 | .label-card { | 407 | .label-card { |
| 363 | - background: #fff; border-radius: 20rpx; overflow: hidden; | ||
| 364 | - margin-bottom: 24rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); | ||
| 365 | - box-sizing: border-box; width: 100%; | ||
| 366 | -} | ||
| 367 | -.label-border { border: 6rpx solid #1f2937; } | ||
| 368 | -.label-header { | ||
| 369 | - background: #1f2937; color: #fff; | ||
| 370 | - padding: 28rpx; text-align: center; | 408 | + background: #fff; |
| 409 | + border-radius: 20rpx; | ||
| 410 | + overflow: hidden; | ||
| 411 | + overflow-x: clip; | ||
| 412 | + margin-bottom: 24rpx; | ||
| 413 | + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06); | ||
| 414 | + padding: 24rpx 0; | ||
| 415 | + display: block; | ||
| 416 | + width: 100%; | ||
| 417 | + max-width: 100%; | ||
| 418 | + position: relative; | ||
| 419 | + contain: layout; | ||
| 420 | +} | ||
| 421 | + | ||
| 422 | +.label-img-wrap { | ||
| 423 | + width: 100%; | ||
| 424 | + max-width: 100%; | ||
| 425 | + overflow: hidden; | ||
| 426 | + overflow-x: clip; | ||
| 427 | + box-sizing: border-box; | ||
| 428 | + position: relative; | ||
| 429 | +} | ||
| 430 | + | ||
| 431 | +/* 移动端:width:auto + max-width:100% 比 width:100% 更可靠 */ | ||
| 432 | +.label-img { | ||
| 433 | + width: 100%; | ||
| 434 | + max-width: 100%; | ||
| 435 | + height: auto; | ||
| 436 | + display: block; | ||
| 437 | + vertical-align: top; | ||
| 438 | + object-fit: contain; | ||
| 439 | + border-radius: 12rpx; | ||
| 440 | + box-sizing: border-box; | ||
| 371 | } | 441 | } |
| 372 | -.label-type-icon-wrap { display: flex; justify-content: center; margin-bottom: 12rpx; } | ||
| 373 | -.label-title { font-size: 36rpx; font-weight: 700; } | ||
| 374 | 442 | ||
| 375 | -.label-food-name { | ||
| 376 | - border-bottom: 6rpx solid #1f2937; | ||
| 377 | - background: #f9fafb; padding: 24rpx; | ||
| 378 | -} | ||
| 379 | -.label-food-name text { | ||
| 380 | - font-size: 40rpx; font-weight: 700; | ||
| 381 | - text-align: center; display: block; | ||
| 382 | - word-break: break-word; | 443 | +.info-row { |
| 444 | + display: flex; | ||
| 445 | + gap: 16rpx; | ||
| 446 | + margin-bottom: 24rpx; | ||
| 383 | } | 447 | } |
| 384 | 448 | ||
| 385 | -.label-fields { padding: 32rpx; } | ||
| 386 | -.field-row { | ||
| 387 | - display: flex; justify-content: space-between; align-items: center; | ||
| 388 | - padding: 16rpx 0; border-bottom: 1rpx solid #e5e7eb; | ||
| 389 | -} | ||
| 390 | -.field-row-last { | ||
| 391 | - border-bottom: none; | 449 | +.info-item { |
| 450 | + flex: 1; | ||
| 451 | + background: #fff; | ||
| 452 | + padding: 20rpx 24rpx; | ||
| 453 | + border-radius: 16rpx; | ||
| 454 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); | ||
| 392 | } | 455 | } |
| 393 | -.field-row.indent { padding-left: 24rpx; } | ||
| 394 | -.field-label { font-size: 26rpx; color: #4b5563; flex-shrink: 0; margin-right: 16rpx; } | ||
| 395 | -.field-value { font-size: 26rpx; color: #111827; text-align: right; word-break: break-word; } | ||
| 396 | -.field-label.bold, .field-value.bold { font-weight: 700; } | ||
| 397 | -.field-label.warning, .field-value.warning { color: #dc2626; } | ||
| 398 | 456 | ||
| 399 | -.label-footer { | ||
| 400 | - border-top: 6rpx solid #1f2937; | ||
| 401 | - background: #f9fafb; padding: 20rpx 28rpx; text-align: center; | 457 | +.info-label { |
| 458 | + font-size: 22rpx; | ||
| 459 | + color: #9ca3af; | ||
| 460 | + display: block; | ||
| 461 | + margin-bottom: 6rpx; | ||
| 402 | } | 462 | } |
| 403 | -.label-footer text { | ||
| 404 | - display: block; font-size: 24rpx; color: #6b7280; line-height: 1.6; | 463 | + |
| 464 | +.info-value { | ||
| 465 | + font-size: 26rpx; | ||
| 466 | + font-weight: 600; | ||
| 467 | + color: #111827; | ||
| 468 | + display: block; | ||
| 405 | } | 469 | } |
| 406 | 470 | ||
| 407 | -/* Note */ | ||
| 408 | .note-card { | 471 | .note-card { |
| 409 | - display: flex; align-items: flex-start; gap: 16rpx; | ||
| 410 | - padding: 24rpx 28rpx; background: #eff6ff; | ||
| 411 | - border: 1rpx solid #bfdbfe; border-radius: 16rpx; | 472 | + display: flex; |
| 473 | + align-items: flex-start; | ||
| 474 | + gap: 16rpx; | ||
| 475 | + padding: 24rpx 28rpx; | ||
| 476 | + background: #eff6ff; | ||
| 477 | + border: 1rpx solid #bfdbfe; | ||
| 478 | + border-radius: 16rpx; | ||
| 479 | +} | ||
| 480 | + | ||
| 481 | +.note-text { | ||
| 482 | + flex: 1; | ||
| 483 | + font-size: 26rpx; | ||
| 484 | + color: #1e40af; | ||
| 485 | + line-height: 1.5; | ||
| 412 | } | 486 | } |
| 413 | -.note-text { flex: 1; font-size: 26rpx; color: #1e40af; line-height: 1.5; } | ||
| 414 | 487 | ||
| 415 | -/* ---- Bottom bar ---- */ | ||
| 416 | .bottom-bar { | 488 | .bottom-bar { |
| 417 | - position: fixed; bottom: 0; left: 0; right: 0; | ||
| 418 | - padding: 20rpx 32rpx; padding-bottom: 48rpx; | ||
| 419 | - background: #fff; border-top: 1rpx solid #e5e7eb; | ||
| 420 | - box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.04); | 489 | + position: fixed; |
| 490 | + bottom: 0; | ||
| 491 | + left: 0; | ||
| 492 | + right: 0; | ||
| 493 | + padding: 20rpx 32rpx; | ||
| 494 | + /* 兼容浏览器:env(safe-area-inset-bottom) + 最小 36px,避免打印按钮偏上/遮挡 */ | ||
| 495 | + padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 36px); | ||
| 496 | + background: #fff; | ||
| 497 | + border-top: 1rpx solid #e5e7eb; | ||
| 498 | + box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04); | ||
| 421 | } | 499 | } |
| 500 | + | ||
| 422 | .bottom-info { | 501 | .bottom-info { |
| 423 | - display: flex; justify-content: space-between; align-items: center; | 502 | + display: flex; |
| 503 | + justify-content: space-between; | ||
| 504 | + align-items: center; | ||
| 424 | margin-bottom: 16rpx; | 505 | margin-bottom: 16rpx; |
| 425 | } | 506 | } |
| 426 | -.bottom-qty { font-size: 26rpx; font-weight: 600; color: #111827; } | 507 | + |
| 508 | +.bottom-qty { | ||
| 509 | + font-size: 26rpx; | ||
| 510 | + font-weight: 600; | ||
| 511 | + color: #111827; | ||
| 512 | +} | ||
| 513 | + | ||
| 427 | .bt-indicator { | 514 | .bt-indicator { |
| 428 | - display: flex; align-items: center; gap: 8rpx; | 515 | + display: flex; |
| 516 | + align-items: center; | ||
| 517 | + gap: 8rpx; | ||
| 429 | } | 518 | } |
| 519 | + | ||
| 430 | .bt-dot { | 520 | .bt-dot { |
| 431 | - width: 12rpx; height: 12rpx; border-radius: 50%; | 521 | + width: 12rpx; |
| 522 | + height: 12rpx; | ||
| 523 | + border-radius: 50%; | ||
| 432 | background: #d1d5db; | 524 | background: #d1d5db; |
| 433 | } | 525 | } |
| 434 | -.bt-indicator.connected .bt-dot { background: #4ade80; } | ||
| 435 | -.bt-name { font-size: 24rpx; color: #9ca3af; } | ||
| 436 | -.bt-indicator.connected .bt-name { color: #16a34a; } | 526 | + |
| 527 | +.bt-indicator.connected .bt-dot { | ||
| 528 | + background: #4ade80; | ||
| 529 | +} | ||
| 530 | + | ||
| 531 | +.bt-name { | ||
| 532 | + font-size: 24rpx; | ||
| 533 | + color: #9ca3af; | ||
| 534 | +} | ||
| 535 | + | ||
| 536 | +.bt-indicator.connected .bt-name { | ||
| 537 | + color: #16a34a; | ||
| 538 | +} | ||
| 437 | 539 | ||
| 438 | .bottom-actions { | 540 | .bottom-actions { |
| 439 | - display: flex; align-items: stretch; gap: 24rpx; | 541 | + display: flex; |
| 542 | + align-items: stretch; | ||
| 543 | + gap: 24rpx; | ||
| 440 | } | 544 | } |
| 441 | 545 | ||
| 442 | .btn-preview-sq { | 546 | .btn-preview-sq { |
| 443 | - width: 100rpx; height: 100rpx; flex-shrink: 0; | ||
| 444 | - background: #fff; border: 2rpx solid #d1d5db; border-radius: 16rpx; | ||
| 445 | - display: flex; align-items: center; justify-content: center; | ||
| 446 | - cursor: pointer; transition: background 0.15s; | 547 | + width: 100rpx; |
| 548 | + height: 100rpx; | ||
| 549 | + flex-shrink: 0; | ||
| 550 | + background: #fff; | ||
| 551 | + border: 2rpx solid #d1d5db; | ||
| 552 | + border-radius: 16rpx; | ||
| 553 | + display: flex; | ||
| 554 | + align-items: center; | ||
| 555 | + justify-content: center; | ||
| 556 | +} | ||
| 557 | + | ||
| 558 | +.btn-preview-sq:active { | ||
| 559 | + background: #f3f4f6; | ||
| 447 | } | 560 | } |
| 448 | -.btn-preview-sq:active { background: #f3f4f6; } | ||
| 449 | 561 | ||
| 450 | .print-btn { | 562 | .print-btn { |
| 451 | - flex: 1; min-width: 0; height: 100rpx; | ||
| 452 | - background: var(--theme-primary); border-radius: 16rpx; | ||
| 453 | - display: flex; align-items: center; justify-content: center; gap: 12rpx; | ||
| 454 | - cursor: pointer; transition: opacity 0.15s; | 563 | + flex: 1; |
| 564 | + min-width: 0; | ||
| 565 | + height: 100rpx; | ||
| 566 | + background: var(--theme-primary); | ||
| 567 | + border-radius: 16rpx; | ||
| 568 | + display: flex; | ||
| 569 | + align-items: center; | ||
| 570 | + justify-content: center; | ||
| 571 | + gap: 12rpx; | ||
| 572 | +} | ||
| 573 | + | ||
| 574 | +.print-btn.disabled { | ||
| 575 | + opacity: 0.6; | ||
| 455 | } | 576 | } |
| 456 | -.print-btn.disabled { opacity: 0.6; } | 577 | + |
| 457 | .print-btn-text { | 578 | .print-btn-text { |
| 458 | - font-size: 30rpx; font-weight: 600; color: #fff; line-height: 1; | 579 | + font-size: 30rpx; |
| 580 | + font-weight: 600; | ||
| 581 | + color: #fff; | ||
| 582 | + line-height: 1; | ||
| 459 | } | 583 | } |
| 460 | 584 | ||
| 461 | -/* ---- Modal ---- */ | ||
| 462 | .modal-mask { | 585 | .modal-mask { |
| 463 | - position: fixed; top: 0; left: 0; right: 0; bottom: 0; | ||
| 464 | - background: rgba(0,0,0,0.5); | ||
| 465 | - display: flex; align-items: center; justify-content: center; | ||
| 466 | - z-index: 999; padding: 48rpx; | 586 | + position: fixed; |
| 587 | + top: 0; | ||
| 588 | + left: 0; | ||
| 589 | + right: 0; | ||
| 590 | + bottom: 0; | ||
| 591 | + background: rgba(0, 0, 0, 0.5); | ||
| 592 | + display: flex; | ||
| 593 | + align-items: center; | ||
| 594 | + justify-content: center; | ||
| 595 | + z-index: 999; | ||
| 596 | + padding: 24rpx; | ||
| 467 | } | 597 | } |
| 598 | + | ||
| 468 | .modal-body { | 599 | .modal-body { |
| 469 | - width: 100%; max-width: 640rpx; max-height: 80vh; | ||
| 470 | - background: #fff; border-radius: 24rpx; overflow: hidden; | ||
| 471 | - display: flex; flex-direction: column; | 600 | + width: 100%; |
| 601 | + max-width: 700rpx; | ||
| 602 | + max-height: 85vh; | ||
| 603 | + background: #fff; | ||
| 604 | + border-radius: 24rpx; | ||
| 605 | + overflow-x: hidden; | ||
| 606 | + overflow-y: hidden; | ||
| 607 | + display: flex; | ||
| 608 | + flex-direction: column; | ||
| 609 | + min-width: 0; | ||
| 472 | } | 610 | } |
| 611 | + | ||
| 612 | +.modal-body-label-only .modal-label-wrap { | ||
| 613 | + margin-bottom: 0; | ||
| 614 | + padding: 24rpx; | ||
| 615 | +} | ||
| 616 | + | ||
| 473 | .modal-top { | 617 | .modal-top { |
| 474 | - display: flex; justify-content: space-between; align-items: center; | ||
| 475 | - padding: 28rpx 32rpx; border-bottom: 1rpx solid #e5e7eb; flex-shrink: 0; | 618 | + display: flex; |
| 619 | + justify-content: space-between; | ||
| 620 | + align-items: center; | ||
| 621 | + padding: 28rpx 32rpx; | ||
| 622 | + border-bottom: 1rpx solid #e5e7eb; | ||
| 623 | + flex-shrink: 0; | ||
| 476 | } | 624 | } |
| 477 | -.modal-title { font-size: 30rpx; font-weight: 600; color: #111827; } | 625 | + |
| 626 | +.modal-title { | ||
| 627 | + font-size: 30rpx; | ||
| 628 | + font-weight: 600; | ||
| 629 | + color: #111827; | ||
| 630 | +} | ||
| 631 | + | ||
| 478 | .modal-close { | 632 | .modal-close { |
| 479 | - width: 52rpx; height: 52rpx; | ||
| 480 | - display: flex; align-items: center; justify-content: center; | ||
| 481 | - border-radius: 50%; background: #f3f4f6; cursor: pointer; | 633 | + width: 52rpx; |
| 634 | + height: 52rpx; | ||
| 635 | + display: flex; | ||
| 636 | + align-items: center; | ||
| 637 | + justify-content: center; | ||
| 638 | + border-radius: 50%; | ||
| 639 | + background: #f3f4f6; | ||
| 482 | transform: rotate(45deg); | 640 | transform: rotate(45deg); |
| 483 | } | 641 | } |
| 484 | 642 | ||
| 485 | -.modal-scroll { flex: 1; padding: 28rpx; overflow-y: auto; } | ||
| 486 | - | ||
| 487 | -.pv-card { border-radius: 12rpx; overflow: hidden; margin-bottom: 20rpx; } | ||
| 488 | -.pv-border { border: 4rpx solid #1f2937; } | ||
| 489 | -.pv-header { | ||
| 490 | - background: #1f2937; color: #fff; | ||
| 491 | - padding: 16rpx; text-align: center; | ||
| 492 | -} | ||
| 493 | -.pv-type { font-size: 26rpx; font-weight: 700; } | ||
| 494 | -.pv-food { | ||
| 495 | - padding: 14rpx 16rpx; border-bottom: 4rpx solid #1f2937; | ||
| 496 | - background: #f9fafb; text-align: center; | ||
| 497 | -} | ||
| 498 | -.pv-food text { font-size: 30rpx; font-weight: 700; } | ||
| 499 | -.pv-fields { padding: 16rpx; } | ||
| 500 | -.pv-row { | ||
| 501 | - display: flex; justify-content: space-between; | ||
| 502 | - padding: 8rpx 0; border-bottom: 1rpx solid #e5e7eb; | ||
| 503 | -} | ||
| 504 | -.pv-row-last { | ||
| 505 | - border-bottom: none; | ||
| 506 | -} | ||
| 507 | -.pv-label { font-size: 22rpx; color: #4b5563; } | ||
| 508 | -.pv-value { font-size: 22rpx; color: #111827; } | ||
| 509 | -.pv-value.bold { font-weight: 700; } | ||
| 510 | -.pv-value.warning { color: #dc2626; } | ||
| 511 | -.pv-footer { | ||
| 512 | - border-top: 4rpx solid #1f2937; | ||
| 513 | - padding: 10rpx; text-align: center; background: #f9fafb; | ||
| 514 | -} | ||
| 515 | -.pv-footer text { font-size: 20rpx; color: #6b7280; } | ||
| 516 | -.pv-note { | ||
| 517 | - font-size: 24rpx; color: #6b7280; | ||
| 518 | - text-align: center; display: block; | 643 | +.modal-scroll { |
| 644 | + flex: 1; | ||
| 645 | + min-width: 0; | ||
| 646 | + padding: 28rpx 40rpx; | ||
| 647 | + overflow-y: auto; | ||
| 648 | + overflow-x: hidden; | ||
| 649 | + overflow-x: clip; | ||
| 650 | + box-sizing: border-box; | ||
| 651 | +} | ||
| 652 | + | ||
| 653 | +.modal-label-wrap { | ||
| 654 | + width: 100%; | ||
| 655 | + max-width: 100%; | ||
| 656 | + margin-bottom: 24rpx; | ||
| 657 | + padding: 0; | ||
| 658 | + box-sizing: border-box; | ||
| 659 | + overflow: hidden; | ||
| 660 | + overflow-x: clip; | ||
| 661 | +} | ||
| 662 | + | ||
| 663 | +.modal-label-inner { | ||
| 664 | + width: 100%; | ||
| 665 | + max-width: 100%; | ||
| 666 | + min-width: 0; | ||
| 667 | + overflow: hidden; | ||
| 668 | + overflow-x: clip; | ||
| 669 | + box-sizing: border-box; | ||
| 670 | +} | ||
| 671 | + | ||
| 672 | +.modal-label-img { | ||
| 673 | + width: 100%; | ||
| 674 | + max-width: 100%; | ||
| 675 | + height: auto; | ||
| 676 | + display: block; | ||
| 677 | + vertical-align: top; | ||
| 678 | + object-fit: contain; | ||
| 679 | + border-radius: 12rpx; | ||
| 680 | + box-sizing: border-box; | ||
| 681 | +} | ||
| 682 | + | ||
| 683 | +.modal-info { | ||
| 684 | + text-align: center; | ||
| 685 | +} | ||
| 686 | + | ||
| 687 | +.modal-info-text { | ||
| 688 | + font-size: 30rpx; | ||
| 689 | + font-weight: 600; | ||
| 690 | + color: #111827; | ||
| 691 | + display: block; | ||
| 692 | + margin-bottom: 8rpx; | ||
| 693 | +} | ||
| 694 | + | ||
| 695 | +.modal-info-sub { | ||
| 696 | + font-size: 24rpx; | ||
| 697 | + color: #6b7280; | ||
| 698 | + display: block; | ||
| 699 | + margin-bottom: 4rpx; | ||
| 700 | +} | ||
| 701 | + | ||
| 702 | +/* 移动端额外约束,防止部分浏览器仍出现溢出 */ | ||
| 703 | +@media screen and (max-width: 768px) { | ||
| 704 | + .page, | ||
| 705 | + .content, | ||
| 706 | + .label-card, | ||
| 707 | + .label-img-wrap { | ||
| 708 | + max-width: 100vw; | ||
| 709 | + } | ||
| 710 | + .label-img, | ||
| 711 | + .modal-label-img { | ||
| 712 | + max-width: 100% !important; | ||
| 713 | + } | ||
| 519 | } | 714 | } |
| 520 | </style> | 715 | </style> |
美国版/Food Labeling Management App UniApp/src/pages/login/login.vue
| @@ -31,7 +31,7 @@ | @@ -31,7 +31,7 @@ | ||
| 31 | </view> | 31 | </view> |
| 32 | <view class="form-row"> | 32 | <view class="form-row"> |
| 33 | <view class="remember-row"> | 33 | <view class="remember-row"> |
| 34 | - <switch :checked="rememberMe" @change="rememberMe = $event.detail.value" :color="'#4278bd'" /> | 34 | + <switch :checked="rememberMe" @change="rememberMe = $event.detail.value" :color="'#1F3A8A'" /> |
| 35 | <text class="remember-text">{{ t('login.rememberMe') }}</text> | 35 | <text class="remember-text">{{ t('login.rememberMe') }}</text> |
| 36 | </view> | 36 | </view> |
| 37 | <text class="forgot-link">{{ t('login.forgotPassword') }}</text> | 37 | <text class="forgot-link">{{ t('login.forgotPassword') }}</text> |
美国版/Food Labeling Management App UniApp/src/pages/more/change-password.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="page"> | ||
| 3 | + <view class="header-hero" :style="{ paddingTop: statusBarHeight + 'px' }"> | ||
| 4 | + <view class="top-bar"> | ||
| 5 | + <view class="top-left" @click="goBack"> | ||
| 6 | + <AppIcon name="chevronLeft" size="sm" color="white" /> | ||
| 7 | + </view> | ||
| 8 | + <view class="top-center"> | ||
| 9 | + <text class="page-title">Change Password</text> | ||
| 10 | + <LocationPicker /> | ||
| 11 | + </view> | ||
| 12 | + <view class="top-right" @click="isMenuOpen = true"> | ||
| 13 | + <AppIcon name="menu" size="sm" color="white" /> | ||
| 14 | + </view> | ||
| 15 | + </view> | ||
| 16 | + </view> | ||
| 17 | + | ||
| 18 | + <view class="content"> | ||
| 19 | + <view class="lock-icon-wrap"> | ||
| 20 | + <view class="lock-circle"> | ||
| 21 | + <AppIcon name="lock" size="lg" color="white" /> | ||
| 22 | + </view> | ||
| 23 | + </view> | ||
| 24 | + | ||
| 25 | + <view class="form-card"> | ||
| 26 | + <view class="form-group"> | ||
| 27 | + <text class="label">Current Password</text> | ||
| 28 | + <view class="input-wrap"> | ||
| 29 | + <input | ||
| 30 | + v-model="currentPassword" | ||
| 31 | + class="input" | ||
| 32 | + :type="showCurrent ? 'text' : 'password'" | ||
| 33 | + placeholder="Enter current password" | ||
| 34 | + placeholder-class="placeholder-cls" | ||
| 35 | + /> | ||
| 36 | + <view class="eye-btn" @click="showCurrent = !showCurrent"> | ||
| 37 | + <AppIcon name="eye" size="sm" :color="showCurrent ? 'primary' : 'gray'" /> | ||
| 38 | + </view> | ||
| 39 | + </view> | ||
| 40 | + </view> | ||
| 41 | + | ||
| 42 | + <view class="form-group"> | ||
| 43 | + <text class="label">New Password</text> | ||
| 44 | + <view class="input-wrap"> | ||
| 45 | + <input | ||
| 46 | + v-model="newPassword" | ||
| 47 | + class="input" | ||
| 48 | + :type="showNew ? 'text' : 'password'" | ||
| 49 | + placeholder="Enter new password" | ||
| 50 | + placeholder-cls="placeholder-cls" | ||
| 51 | + /> | ||
| 52 | + <view class="eye-btn" @click="showNew = !showNew"> | ||
| 53 | + <AppIcon name="eye" size="sm" :color="showNew ? 'primary' : 'gray'" /> | ||
| 54 | + </view> | ||
| 55 | + </view> | ||
| 56 | + </view> | ||
| 57 | + | ||
| 58 | + <view class="form-group form-group-last"> | ||
| 59 | + <text class="label">Confirm New Password</text> | ||
| 60 | + <view class="input-wrap"> | ||
| 61 | + <input | ||
| 62 | + v-model="confirmPassword" | ||
| 63 | + class="input" | ||
| 64 | + :type="showConfirm ? 'text' : 'password'" | ||
| 65 | + placeholder="Confirm new password" | ||
| 66 | + placeholder-cls="placeholder-cls" | ||
| 67 | + /> | ||
| 68 | + <view class="eye-btn" @click="showConfirm = !showConfirm"> | ||
| 69 | + <AppIcon name="eye" size="sm" :color="showConfirm ? 'primary' : 'gray'" /> | ||
| 70 | + </view> | ||
| 71 | + </view> | ||
| 72 | + </view> | ||
| 73 | + </view> | ||
| 74 | + | ||
| 75 | + <view class="tips-card"> | ||
| 76 | + <text class="tips-title">Password Requirements</text> | ||
| 77 | + <text class="tips-item">At least 8 characters long</text> | ||
| 78 | + <text class="tips-item">Contains uppercase and lowercase letters</text> | ||
| 79 | + <text class="tips-item">Contains at least one number</text> | ||
| 80 | + <text class="tips-item tips-last">Contains at least one special character</text> | ||
| 81 | + </view> | ||
| 82 | + | ||
| 83 | + <view | ||
| 84 | + class="submit-btn" | ||
| 85 | + :class="{ disabled: !canSubmit }" | ||
| 86 | + @click="handleSubmit" | ||
| 87 | + > | ||
| 88 | + <text class="submit-btn-text">Update Password</text> | ||
| 89 | + </view> | ||
| 90 | + </view> | ||
| 91 | + | ||
| 92 | + <SideMenu v-model="isMenuOpen" /> | ||
| 93 | + </view> | ||
| 94 | +</template> | ||
| 95 | + | ||
| 96 | +<script setup lang="ts"> | ||
| 97 | +import { ref, computed } from 'vue' | ||
| 98 | +import AppIcon from '../../components/AppIcon.vue' | ||
| 99 | +import SideMenu from '../../components/SideMenu.vue' | ||
| 100 | +import LocationPicker from '../../components/LocationPicker.vue' | ||
| 101 | +import { getStatusBarHeight } from '../../utils/statusBar' | ||
| 102 | + | ||
| 103 | +const statusBarHeight = getStatusBarHeight() | ||
| 104 | +const isMenuOpen = ref(false) | ||
| 105 | + | ||
| 106 | +const currentPassword = ref('') | ||
| 107 | +const newPassword = ref('') | ||
| 108 | +const confirmPassword = ref('') | ||
| 109 | +const showCurrent = ref(false) | ||
| 110 | +const showNew = ref(false) | ||
| 111 | +const showConfirm = ref(false) | ||
| 112 | + | ||
| 113 | +const canSubmit = computed(() => { | ||
| 114 | + return currentPassword.value.length > 0 | ||
| 115 | + && newPassword.value.length >= 8 | ||
| 116 | + && confirmPassword.value === newPassword.value | ||
| 117 | +}) | ||
| 118 | + | ||
| 119 | +const goBack = () => { | ||
| 120 | + const pages = getCurrentPages() | ||
| 121 | + if (pages.length > 1) { | ||
| 122 | + uni.navigateBack() | ||
| 123 | + } else { | ||
| 124 | + uni.redirectTo({ url: '/pages/more/profile' }) | ||
| 125 | + } | ||
| 126 | +} | ||
| 127 | + | ||
| 128 | +const handleSubmit = () => { | ||
| 129 | + if (!canSubmit.value) return | ||
| 130 | + | ||
| 131 | + if (newPassword.value !== confirmPassword.value) { | ||
| 132 | + uni.showToast({ title: 'Passwords do not match', icon: 'none' }) | ||
| 133 | + return | ||
| 134 | + } | ||
| 135 | + | ||
| 136 | + uni.showLoading({ title: 'Updating...' }) | ||
| 137 | + setTimeout(() => { | ||
| 138 | + uni.hideLoading() | ||
| 139 | + uni.showToast({ title: 'Password updated!', icon: 'success' }) | ||
| 140 | + setTimeout(() => { | ||
| 141 | + uni.navigateBack() | ||
| 142 | + }, 1500) | ||
| 143 | + }, 1000) | ||
| 144 | +} | ||
| 145 | +</script> | ||
| 146 | + | ||
| 147 | +<style scoped> | ||
| 148 | +.page { | ||
| 149 | + min-height: 100vh; | ||
| 150 | + background: #f9fafb; | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +.header-hero { | ||
| 154 | + background: linear-gradient(135deg, var(--theme-primary), var(--theme-primary-dark)); | ||
| 155 | + padding: 16rpx 32rpx 24rpx; | ||
| 156 | +} | ||
| 157 | + | ||
| 158 | +.top-bar { | ||
| 159 | + height: 96rpx; | ||
| 160 | + display: flex; | ||
| 161 | + align-items: center; | ||
| 162 | + justify-content: space-between; | ||
| 163 | +} | ||
| 164 | + | ||
| 165 | +.top-left, | ||
| 166 | +.top-right { | ||
| 167 | + width: 64rpx; | ||
| 168 | + height: 64rpx; | ||
| 169 | + border-radius: 999rpx; | ||
| 170 | + background: rgba(255, 255, 255, 0.15); | ||
| 171 | + display: flex; | ||
| 172 | + align-items: center; | ||
| 173 | + justify-content: center; | ||
| 174 | +} | ||
| 175 | + | ||
| 176 | +.top-center { | ||
| 177 | + flex: 1; | ||
| 178 | + display: flex; | ||
| 179 | + flex-direction: column; | ||
| 180 | + align-items: center; | ||
| 181 | +} | ||
| 182 | + | ||
| 183 | +.page-title { | ||
| 184 | + font-size: 34rpx; | ||
| 185 | + font-weight: 600; | ||
| 186 | + color: #fff; | ||
| 187 | +} | ||
| 188 | + | ||
| 189 | +.content { | ||
| 190 | + padding: 48rpx; | ||
| 191 | +} | ||
| 192 | + | ||
| 193 | +.lock-icon-wrap { | ||
| 194 | + display: flex; | ||
| 195 | + justify-content: center; | ||
| 196 | + margin-bottom: 40rpx; | ||
| 197 | +} | ||
| 198 | + | ||
| 199 | +.lock-circle { | ||
| 200 | + width: 120rpx; | ||
| 201 | + height: 120rpx; | ||
| 202 | + background: var(--theme-primary); | ||
| 203 | + border-radius: 50%; | ||
| 204 | + display: flex; | ||
| 205 | + align-items: center; | ||
| 206 | + justify-content: center; | ||
| 207 | +} | ||
| 208 | + | ||
| 209 | +.form-card { | ||
| 210 | + background: #fff; | ||
| 211 | + padding: 32rpx; | ||
| 212 | + border-radius: 20rpx; | ||
| 213 | + margin-bottom: 32rpx; | ||
| 214 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); | ||
| 215 | +} | ||
| 216 | + | ||
| 217 | +.form-group { | ||
| 218 | + margin-bottom: 32rpx; | ||
| 219 | +} | ||
| 220 | + | ||
| 221 | +.form-group-last { | ||
| 222 | + margin-bottom: 0; | ||
| 223 | +} | ||
| 224 | + | ||
| 225 | +.label { | ||
| 226 | + font-size: 28rpx; | ||
| 227 | + font-weight: 500; | ||
| 228 | + color: #374151; | ||
| 229 | + display: block; | ||
| 230 | + margin-bottom: 12rpx; | ||
| 231 | +} | ||
| 232 | + | ||
| 233 | +.input-wrap { | ||
| 234 | + display: flex; | ||
| 235 | + align-items: center; | ||
| 236 | + background: #f3f4f6; | ||
| 237 | + border-radius: 16rpx; | ||
| 238 | + padding-right: 16rpx; | ||
| 239 | +} | ||
| 240 | + | ||
| 241 | +.input { | ||
| 242 | + flex: 1; | ||
| 243 | + height: 96rpx; | ||
| 244 | + padding: 0 24rpx; | ||
| 245 | + font-size: 30rpx; | ||
| 246 | + background: transparent; | ||
| 247 | +} | ||
| 248 | + | ||
| 249 | +.placeholder-cls { | ||
| 250 | + color: #9ca3af; | ||
| 251 | +} | ||
| 252 | + | ||
| 253 | +.eye-btn { | ||
| 254 | + width: 64rpx; | ||
| 255 | + height: 64rpx; | ||
| 256 | + display: flex; | ||
| 257 | + align-items: center; | ||
| 258 | + justify-content: center; | ||
| 259 | + flex-shrink: 0; | ||
| 260 | +} | ||
| 261 | + | ||
| 262 | +.tips-card { | ||
| 263 | + background: #eff6ff; | ||
| 264 | + padding: 28rpx 32rpx; | ||
| 265 | + border-radius: 16rpx; | ||
| 266 | + margin-bottom: 40rpx; | ||
| 267 | +} | ||
| 268 | + | ||
| 269 | +.tips-title { | ||
| 270 | + font-size: 26rpx; | ||
| 271 | + font-weight: 600; | ||
| 272 | + color: var(--theme-primary); | ||
| 273 | + display: block; | ||
| 274 | + margin-bottom: 16rpx; | ||
| 275 | +} | ||
| 276 | + | ||
| 277 | +.tips-item { | ||
| 278 | + font-size: 24rpx; | ||
| 279 | + color: #4b5563; | ||
| 280 | + display: block; | ||
| 281 | + margin-bottom: 8rpx; | ||
| 282 | + padding-left: 20rpx; | ||
| 283 | +} | ||
| 284 | + | ||
| 285 | +.tips-last { | ||
| 286 | + margin-bottom: 0; | ||
| 287 | +} | ||
| 288 | + | ||
| 289 | +.submit-btn { | ||
| 290 | + width: 100%; | ||
| 291 | + height: 96rpx; | ||
| 292 | + background: var(--theme-primary); | ||
| 293 | + border-radius: 16rpx; | ||
| 294 | + display: flex; | ||
| 295 | + align-items: center; | ||
| 296 | + justify-content: center; | ||
| 297 | +} | ||
| 298 | + | ||
| 299 | +.submit-btn.disabled { | ||
| 300 | + opacity: 0.5; | ||
| 301 | +} | ||
| 302 | + | ||
| 303 | +.submit-btn-text { | ||
| 304 | + font-size: 32rpx; | ||
| 305 | + font-weight: 600; | ||
| 306 | + color: #ffffff; | ||
| 307 | + line-height: 1; | ||
| 308 | +} | ||
| 309 | +</style> |
美国版/Food Labeling Management App UniApp/src/pages/more/label-report.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="page"> | ||
| 3 | + <view class="header-hero" :style="{ paddingTop: statusBarHeight + 'px' }"> | ||
| 4 | + <view class="top-bar"> | ||
| 5 | + <view class="top-left" @click="goBack"> | ||
| 6 | + <AppIcon name="chevronLeft" size="sm" color="white" /> | ||
| 7 | + </view> | ||
| 8 | + <view class="top-center"> | ||
| 9 | + <text class="page-title">Label Report</text> | ||
| 10 | + <LocationPicker /> | ||
| 11 | + </view> | ||
| 12 | + <view class="top-right" @click="isMenuOpen = true"> | ||
| 13 | + <AppIcon name="menu" size="sm" color="white" /> | ||
| 14 | + </view> | ||
| 15 | + </view> | ||
| 16 | + </view> | ||
| 17 | + | ||
| 18 | + <scroll-view class="content" scroll-y> | ||
| 19 | + <!-- 指标卡 --> | ||
| 20 | + <view class="metrics-grid"> | ||
| 21 | + <view class="metric-card"> | ||
| 22 | + <text class="metric-label">Total Labels Printed</text> | ||
| 23 | + <text class="metric-value">2,543</text> | ||
| 24 | + <text class="metric-trend up">+20.1% from last month</text> | ||
| 25 | + </view> | ||
| 26 | + <view class="metric-card"> | ||
| 27 | + <text class="metric-label">Most Printed Category</text> | ||
| 28 | + <text class="metric-value">Dairy</text> | ||
| 29 | + <text class="metric-trend">450 labels generated</text> | ||
| 30 | + </view> | ||
| 31 | + <view class="metric-card"> | ||
| 32 | + <text class="metric-label">Top Product</text> | ||
| 33 | + <text class="metric-value">Whole Milk</text> | ||
| 34 | + <text class="metric-trend">182 labels generated</text> | ||
| 35 | + </view> | ||
| 36 | + <view class="metric-card"> | ||
| 37 | + <text class="metric-label">Avg. Daily Prints</text> | ||
| 38 | + <text class="metric-value">85</text> | ||
| 39 | + <text class="metric-trend up">+12% from last week</text> | ||
| 40 | + </view> | ||
| 41 | + </view> | ||
| 42 | + | ||
| 43 | + <!-- 柱状图 --> | ||
| 44 | + <view class="section"> | ||
| 45 | + <text class="section-title">Labels by Category</text> | ||
| 46 | + <text class="section-desc">Distribution of printed labels across product categories.</text> | ||
| 47 | + <view class="bar-chart"> | ||
| 48 | + <view v-for="item in categoryData" :key="item.name" class="bar-row"> | ||
| 49 | + <text class="bar-label">{{ item.name }}</text> | ||
| 50 | + <view class="bar-track"> | ||
| 51 | + <view class="bar-fill" :style="{ width: item.percent + '%' }" /> | ||
| 52 | + </view> | ||
| 53 | + <text class="bar-value">{{ item.value }}</text> | ||
| 54 | + </view> | ||
| 55 | + </view> | ||
| 56 | + </view> | ||
| 57 | + | ||
| 58 | + <!-- 走势图 --> | ||
| 59 | + <view class="section"> | ||
| 60 | + <text class="section-title">Print Volume Trends</text> | ||
| 61 | + <text class="section-desc">Daily label printing volume for the last 7 days.</text> | ||
| 62 | + <view class="line-chart"> | ||
| 63 | + <view class="chart-bars"> | ||
| 64 | + <view v-for="(pct, i) in trendData" :key="i" class="day-bar-wrap"> | ||
| 65 | + <view class="day-bar" :style="{ height: pct + '%' }" /> | ||
| 66 | + </view> | ||
| 67 | + </view> | ||
| 68 | + <view class="chart-labels"> | ||
| 69 | + <text v-for="d in dayLabels" :key="d" class="day-label">{{ d }}</text> | ||
| 70 | + </view> | ||
| 71 | + </view> | ||
| 72 | + </view> | ||
| 73 | + | ||
| 74 | + <!-- 最常用产品列表 --> | ||
| 75 | + <view class="section"> | ||
| 76 | + <text class="section-title">Most Used Products</text> | ||
| 77 | + <view class="product-table"> | ||
| 78 | + <view class="product-row header"> | ||
| 79 | + <text class="col-name">Product Name</text> | ||
| 80 | + <text class="col-cat">Category</text> | ||
| 81 | + <text class="col-total">Total Printed</text> | ||
| 82 | + <text class="col-pct">Usage %</text> | ||
| 83 | + </view> | ||
| 84 | + <view v-for="p in topProducts" :key="p.name" class="product-row"> | ||
| 85 | + <text class="col-name">{{ p.name }}</text> | ||
| 86 | + <text class="col-cat">{{ p.category }}</text> | ||
| 87 | + <text class="col-total">{{ p.total }}</text> | ||
| 88 | + <text class="col-pct">{{ p.usage }}%</text> | ||
| 89 | + </view> | ||
| 90 | + </view> | ||
| 91 | + </view> | ||
| 92 | + </scroll-view> | ||
| 93 | + | ||
| 94 | + <SideMenu v-model="isMenuOpen" /> | ||
| 95 | + </view> | ||
| 96 | +</template> | ||
| 97 | + | ||
| 98 | +<script setup lang="ts"> | ||
| 99 | +import { ref, computed } from 'vue' | ||
| 100 | +import AppIcon from '../../components/AppIcon.vue' | ||
| 101 | +import SideMenu from '../../components/SideMenu.vue' | ||
| 102 | +import LocationPicker from '../../components/LocationPicker.vue' | ||
| 103 | +import { getStatusBarHeight } from '../../utils/statusBar' | ||
| 104 | + | ||
| 105 | +const statusBarHeight = getStatusBarHeight() | ||
| 106 | +const isMenuOpen = ref(false) | ||
| 107 | + | ||
| 108 | +const categoryDataRaw = [ | ||
| 109 | + { name: 'Dairy', value: 450 }, | ||
| 110 | + { name: 'Meat', value: 380 }, | ||
| 111 | + { name: 'Bakery', value: 320 }, | ||
| 112 | + { name: 'Deli', value: 280 }, | ||
| 113 | + { name: 'Produce', value: 220 }, | ||
| 114 | + { name: 'Beverage', value: 180 }, | ||
| 115 | +] | ||
| 116 | + | ||
| 117 | +const categoryData = computed(() => { | ||
| 118 | + const max = Math.max(...categoryDataRaw.map(d => d.value)) | ||
| 119 | + return categoryDataRaw.map(d => ({ ...d, percent: (d.value / max) * 100 })) | ||
| 120 | +}) | ||
| 121 | + | ||
| 122 | +const trendDataRaw = [72, 85, 78, 92, 88, 95, 90] | ||
| 123 | +const trendData = computed(() => { | ||
| 124 | + const max = Math.max(...trendDataRaw) | ||
| 125 | + return trendDataRaw.map(v => (v / max) * 100) | ||
| 126 | +}) | ||
| 127 | + | ||
| 128 | +const dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] | ||
| 129 | + | ||
| 130 | +const topProducts = ref([ | ||
| 131 | + { name: 'Whole Milk', category: 'Dairy', total: 182, usage: '7.2' }, | ||
| 132 | + { name: 'Ground Beef 80/20', category: 'Meat', total: 145, usage: '5.7' }, | ||
| 133 | + { name: 'Chicken Breast', category: 'Meat', total: 132, usage: '5.2' }, | ||
| 134 | + { name: 'Sliced Ham', category: 'Deli', total: 98, usage: '3.8' }, | ||
| 135 | +]) | ||
| 136 | + | ||
| 137 | +const goBack = () => { | ||
| 138 | + const pages = getCurrentPages() | ||
| 139 | + if (pages.length > 1) { | ||
| 140 | + uni.navigateBack() | ||
| 141 | + } else { | ||
| 142 | + uni.redirectTo({ url: '/pages/index/index' }) | ||
| 143 | + } | ||
| 144 | +} | ||
| 145 | +</script> | ||
| 146 | + | ||
| 147 | +<style scoped> | ||
| 148 | +.page { | ||
| 149 | + min-height: 100vh; | ||
| 150 | + background: #f3f4f6; | ||
| 151 | + display: flex; | ||
| 152 | + flex-direction: column; | ||
| 153 | +} | ||
| 154 | + | ||
| 155 | +.header-hero { | ||
| 156 | + background: linear-gradient(135deg, #1F3A8A, #142a6c); | ||
| 157 | + padding: 16rpx 32rpx 24rpx; | ||
| 158 | +} | ||
| 159 | + | ||
| 160 | +.top-bar { | ||
| 161 | + height: 96rpx; | ||
| 162 | + display: flex; | ||
| 163 | + align-items: center; | ||
| 164 | + justify-content: space-between; | ||
| 165 | +} | ||
| 166 | + | ||
| 167 | +.top-left, .top-right { | ||
| 168 | + width: 64rpx; | ||
| 169 | + height: 64rpx; | ||
| 170 | + border-radius: 999rpx; | ||
| 171 | + background: rgba(255, 255, 255, 0.15); | ||
| 172 | + display: flex; | ||
| 173 | + align-items: center; | ||
| 174 | + justify-content: center; | ||
| 175 | +} | ||
| 176 | + | ||
| 177 | +.top-center { | ||
| 178 | + flex: 1; | ||
| 179 | + display: flex; | ||
| 180 | + flex-direction: column; | ||
| 181 | + align-items: center; | ||
| 182 | +} | ||
| 183 | + | ||
| 184 | +.page-title { | ||
| 185 | + font-size: 34rpx; | ||
| 186 | + font-weight: 600; | ||
| 187 | + color: #fff; | ||
| 188 | +} | ||
| 189 | + | ||
| 190 | +.content { | ||
| 191 | + flex: 1; | ||
| 192 | + padding: 24rpx 28rpx 40rpx; | ||
| 193 | + box-sizing: border-box; | ||
| 194 | +} | ||
| 195 | + | ||
| 196 | +.metrics-grid { | ||
| 197 | + display: grid; | ||
| 198 | + grid-template-columns: 1fr 1fr; | ||
| 199 | + gap: 20rpx; | ||
| 200 | + margin-bottom: 32rpx; | ||
| 201 | +} | ||
| 202 | + | ||
| 203 | +.metric-card { | ||
| 204 | + background: #fff; | ||
| 205 | + padding: 28rpx; | ||
| 206 | + border-radius: 20rpx; | ||
| 207 | + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); | ||
| 208 | + border: 1rpx solid #e5e7eb; | ||
| 209 | +} | ||
| 210 | + | ||
| 211 | +.metric-label { | ||
| 212 | + font-size: 24rpx; | ||
| 213 | + color: #6b7280; | ||
| 214 | + display: block; | ||
| 215 | + margin-bottom: 12rpx; | ||
| 216 | + line-height: 1.5; | ||
| 217 | +} | ||
| 218 | + | ||
| 219 | +.metric-value { | ||
| 220 | + font-size: 38rpx; | ||
| 221 | + font-weight: 700; | ||
| 222 | + color: #111827; | ||
| 223 | + display: block; | ||
| 224 | + margin-bottom: 8rpx; | ||
| 225 | + letter-spacing: -0.02em; | ||
| 226 | +} | ||
| 227 | + | ||
| 228 | +.metric-trend { | ||
| 229 | + font-size: 24rpx; | ||
| 230 | + color: #6b7280; | ||
| 231 | +} | ||
| 232 | + | ||
| 233 | +.metric-trend.up { | ||
| 234 | + color: #16a34a; | ||
| 235 | + font-weight: 500; | ||
| 236 | +} | ||
| 237 | + | ||
| 238 | +.section { | ||
| 239 | + margin-bottom: 32rpx; | ||
| 240 | +} | ||
| 241 | + | ||
| 242 | +.section-title { | ||
| 243 | + font-size: 32rpx; | ||
| 244 | + font-weight: 600; | ||
| 245 | + color: #111827; | ||
| 246 | + display: block; | ||
| 247 | + margin-bottom: 8rpx; | ||
| 248 | +} | ||
| 249 | + | ||
| 250 | +.section-desc { | ||
| 251 | + font-size: 26rpx; | ||
| 252 | + color: #6b7280; | ||
| 253 | + display: block; | ||
| 254 | + margin-bottom: 24rpx; | ||
| 255 | + line-height: 1.5; | ||
| 256 | +} | ||
| 257 | + | ||
| 258 | +.bar-chart { | ||
| 259 | + background: #fff; | ||
| 260 | + padding: 28rpx; | ||
| 261 | + border-radius: 20rpx; | ||
| 262 | + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); | ||
| 263 | + border: 1rpx solid #e5e7eb; | ||
| 264 | +} | ||
| 265 | + | ||
| 266 | +.bar-row { | ||
| 267 | + display: flex; | ||
| 268 | + align-items: center; | ||
| 269 | + gap: 16rpx; | ||
| 270 | + margin-bottom: 24rpx; | ||
| 271 | +} | ||
| 272 | + | ||
| 273 | +.bar-row:last-child { | ||
| 274 | + margin-bottom: 0; | ||
| 275 | +} | ||
| 276 | + | ||
| 277 | +.bar-label { | ||
| 278 | + width: 120rpx; | ||
| 279 | + font-size: 26rpx; | ||
| 280 | + color: #374151; | ||
| 281 | + flex-shrink: 0; | ||
| 282 | + font-weight: 500; | ||
| 283 | +} | ||
| 284 | + | ||
| 285 | +.bar-track { | ||
| 286 | + flex: 1; | ||
| 287 | + height: 36rpx; | ||
| 288 | + background: #f3f4f6; | ||
| 289 | + border-radius: 10rpx; | ||
| 290 | + overflow: hidden; | ||
| 291 | +} | ||
| 292 | + | ||
| 293 | +.bar-fill { | ||
| 294 | + height: 100%; | ||
| 295 | + background: linear-gradient(90deg, #1F3A8A, #1447E6); | ||
| 296 | + border-radius: 10rpx; | ||
| 297 | + transition: width 0.3s ease; | ||
| 298 | +} | ||
| 299 | + | ||
| 300 | +.bar-value { | ||
| 301 | + width: 88rpx; | ||
| 302 | + text-align: right; | ||
| 303 | + font-size: 26rpx; | ||
| 304 | + font-weight: 600; | ||
| 305 | + color: #111827; | ||
| 306 | +} | ||
| 307 | + | ||
| 308 | +.line-chart { | ||
| 309 | + background: #fff; | ||
| 310 | + padding: 28rpx; | ||
| 311 | + border-radius: 20rpx; | ||
| 312 | + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); | ||
| 313 | + border: 1rpx solid #e5e7eb; | ||
| 314 | +} | ||
| 315 | + | ||
| 316 | +.chart-bars { | ||
| 317 | + display: flex; | ||
| 318 | + align-items: flex-end; | ||
| 319 | + justify-content: space-between; | ||
| 320 | + gap: 12rpx; | ||
| 321 | + height: 220rpx; | ||
| 322 | + margin-bottom: 20rpx; | ||
| 323 | +} | ||
| 324 | + | ||
| 325 | +.day-bar-wrap { | ||
| 326 | + flex: 1; | ||
| 327 | + display: flex; | ||
| 328 | + align-items: flex-end; | ||
| 329 | + justify-content: center; | ||
| 330 | +} | ||
| 331 | + | ||
| 332 | +.day-bar { | ||
| 333 | + width: 100%; | ||
| 334 | + max-width: 52rpx; | ||
| 335 | + min-height: 12rpx; | ||
| 336 | + background: linear-gradient(180deg, #1F3A8A, #1447E6); | ||
| 337 | + border-radius: 10rpx 10rpx 0 0; | ||
| 338 | + transition: height 0.3s ease; | ||
| 339 | +} | ||
| 340 | + | ||
| 341 | +.chart-labels { | ||
| 342 | + display: flex; | ||
| 343 | + justify-content: space-between; | ||
| 344 | + gap: 12rpx; | ||
| 345 | +} | ||
| 346 | + | ||
| 347 | +.day-label { | ||
| 348 | + flex: 1; | ||
| 349 | + padding: 0 4rpx; | ||
| 350 | + font-size: 24rpx; | ||
| 351 | + color: #6b7280; | ||
| 352 | + text-align: center; | ||
| 353 | +} | ||
| 354 | + | ||
| 355 | +.product-table { | ||
| 356 | + background: #fff; | ||
| 357 | + border-radius: 20rpx; | ||
| 358 | + overflow: hidden; | ||
| 359 | + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); | ||
| 360 | + border: 1rpx solid #e5e7eb; | ||
| 361 | +} | ||
| 362 | + | ||
| 363 | +.product-row { | ||
| 364 | + display: flex; | ||
| 365 | + padding: 24rpx 28rpx; | ||
| 366 | + border-bottom: 1rpx solid #e5e7eb; | ||
| 367 | + font-size: 28rpx; | ||
| 368 | +} | ||
| 369 | + | ||
| 370 | +.product-row:last-child { | ||
| 371 | + border-bottom: none; | ||
| 372 | +} | ||
| 373 | + | ||
| 374 | +.product-row.header { | ||
| 375 | + background: #fafbfc; | ||
| 376 | + font-weight: 600; | ||
| 377 | + color: #6b7280; | ||
| 378 | + font-size: 24rpx; | ||
| 379 | +} | ||
| 380 | + | ||
| 381 | +.col-name { flex: 1; min-width: 0; } | ||
| 382 | +.col-cat { flex: 0 0 140rpx; } | ||
| 383 | +.col-total { flex: 0 0 120rpx; text-align: right; } | ||
| 384 | +.col-pct { flex: 0 0 88rpx; text-align: right; } | ||
| 385 | + | ||
| 386 | +.product-row:not(.header) .col-name { color: #111827; font-weight: 500; } | ||
| 387 | +.product-row:not(.header) .col-cat { color: #6b7280; } | ||
| 388 | +.product-row:not(.header) .col-total { color: #111827; } | ||
| 389 | +.product-row:not(.header) .col-pct { color: #1F3A8A; font-weight: 600; } | ||
| 390 | +</style> |
美国版/Food Labeling Management App UniApp/src/pages/more/language.vue
| @@ -7,6 +7,7 @@ | @@ -7,6 +7,7 @@ | ||
| 7 | </view> | 7 | </view> |
| 8 | <view class="top-center"> | 8 | <view class="top-center"> |
| 9 | <text class="page-title">{{ t('language.title') }}</text> | 9 | <text class="page-title">{{ t('language.title') }}</text> |
| 10 | + <LocationPicker /> | ||
| 10 | </view> | 11 | </view> |
| 11 | <view class="top-right" @click="isMenuOpen = true"> | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | <AppIcon name="menu" size="sm" color="white" /> | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| @@ -44,6 +45,7 @@ import { useI18n } from 'vue-i18n' | @@ -44,6 +45,7 @@ import { useI18n } from 'vue-i18n' | ||
| 44 | import { setLocale } from '../../utils/i18n' | 45 | import { setLocale } from '../../utils/i18n' |
| 45 | import AppIcon from '../../components/AppIcon.vue' | 46 | import AppIcon from '../../components/AppIcon.vue' |
| 46 | import SideMenu from '../../components/SideMenu.vue' | 47 | import SideMenu from '../../components/SideMenu.vue' |
| 48 | +import LocationPicker from '../../components/LocationPicker.vue' | ||
| 47 | import { getStatusBarHeight } from '../../utils/statusBar' | 49 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 48 | 50 | ||
| 49 | const { t, locale } = useI18n() | 51 | const { t, locale } = useI18n() |
| @@ -100,7 +102,9 @@ const handleChange = (code: 'en' | 'zh') => { | @@ -100,7 +102,9 @@ const handleChange = (code: 'en' | 'zh') => { | ||
| 100 | 102 | ||
| 101 | .top-center { | 103 | .top-center { |
| 102 | flex: 1; | 104 | flex: 1; |
| 103 | - text-align: center; | 105 | + display: flex; |
| 106 | + flex-direction: column; | ||
| 107 | + align-items: center; | ||
| 104 | } | 108 | } |
| 105 | 109 | ||
| 106 | .page-title { | 110 | .page-title { |
美国版/Food Labeling Management App UniApp/src/pages/more/location.vue
| @@ -7,6 +7,7 @@ | @@ -7,6 +7,7 @@ | ||
| 7 | </view> | 7 | </view> |
| 8 | <view class="top-center"> | 8 | <view class="top-center"> |
| 9 | <text class="page-title">{{ t('location.title') }}</text> | 9 | <text class="page-title">{{ t('location.title') }}</text> |
| 10 | + <LocationPicker /> | ||
| 10 | </view> | 11 | </view> |
| 11 | <view class="top-right" @click="isMenuOpen = true"> | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | <AppIcon name="menu" size="sm" color="white" /> | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| @@ -131,6 +132,7 @@ import { ref, computed } from 'vue' | @@ -131,6 +132,7 @@ import { ref, computed } from 'vue' | ||
| 131 | import { useI18n } from 'vue-i18n' | 132 | import { useI18n } from 'vue-i18n' |
| 132 | import AppIcon from '../../components/AppIcon.vue' | 133 | import AppIcon from '../../components/AppIcon.vue' |
| 133 | import SideMenu from '../../components/SideMenu.vue' | 134 | import SideMenu from '../../components/SideMenu.vue' |
| 135 | +import LocationPicker from '../../components/LocationPicker.vue' | ||
| 134 | import { getStatusBarHeight } from '../../utils/statusBar' | 136 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 135 | 137 | ||
| 136 | const { t } = useI18n() | 138 | const { t } = useI18n() |
| @@ -200,7 +202,9 @@ const handleSwitch = () => { | @@ -200,7 +202,9 @@ const handleSwitch = () => { | ||
| 200 | 202 | ||
| 201 | .top-center { | 203 | .top-center { |
| 202 | flex: 1; | 204 | flex: 1; |
| 203 | - text-align: center; | 205 | + display: flex; |
| 206 | + flex-direction: column; | ||
| 207 | + align-items: center; | ||
| 204 | } | 208 | } |
| 205 | 209 | ||
| 206 | .page-title { | 210 | .page-title { |
美国版/Food Labeling Management App UniApp/src/pages/more/print-detail.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="page"> | ||
| 3 | + <view class="header-hero" :style="{ paddingTop: statusBarHeight + 'px' }"> | ||
| 4 | + <view class="top-bar"> | ||
| 5 | + <view class="top-left" @click="goBack"> | ||
| 6 | + <AppIcon name="chevronLeft" size="sm" color="white" /> | ||
| 7 | + </view> | ||
| 8 | + <view class="top-center"> | ||
| 9 | + <text class="page-title">Print Detail</text> | ||
| 10 | + <LocationPicker /> | ||
| 11 | + </view> | ||
| 12 | + <view class="top-right" @click="isMenuOpen = true"> | ||
| 13 | + <AppIcon name="menu" size="sm" color="white" /> | ||
| 14 | + </view> | ||
| 15 | + </view> | ||
| 16 | + </view> | ||
| 17 | + | ||
| 18 | + <scroll-view v-if="record" class="content" scroll-y> | ||
| 19 | + <view class="detail-label-section"> | ||
| 20 | + <text class="detail-section-title">Label Content</text> | ||
| 21 | + <view class="detail-label-wrap"> | ||
| 22 | + <image :src="labelImage" class="detail-label-img" mode="widthFix" /> | ||
| 23 | + </view> | ||
| 24 | + </view> | ||
| 25 | + | ||
| 26 | + <view class="detail-info-section"> | ||
| 27 | + <text class="detail-section-title">Print Info</text> | ||
| 28 | + <view class="info-card"> | ||
| 29 | + <view class="info-row"> | ||
| 30 | + <text class="info-label">Print Time</text> | ||
| 31 | + <text class="info-value">{{ record.dateFull }} {{ record.time }}</text> | ||
| 32 | + </view> | ||
| 33 | + <view class="info-row"> | ||
| 34 | + <text class="info-label">Product</text> | ||
| 35 | + <text class="info-value">{{ displayProductName }}</text> | ||
| 36 | + </view> | ||
| 37 | + <view class="info-row"> | ||
| 38 | + <text class="info-label">Quantity</text> | ||
| 39 | + <text class="info-value">{{ record.qty }} label{{ record.qty > 1 ? 's' : '' }}</text> | ||
| 40 | + </view> | ||
| 41 | + <view v-if="record.labelType" class="info-row"> | ||
| 42 | + <text class="info-label">Label Type</text> | ||
| 43 | + <text class="info-value">{{ record.labelType }}</text> | ||
| 44 | + </view> | ||
| 45 | + <view class="info-row"> | ||
| 46 | + <text class="info-label">Template</text> | ||
| 47 | + <text class="info-value">{{ record.templateSize }} {{ record.templateName }}</text> | ||
| 48 | + </view> | ||
| 49 | + <view class="info-row"> | ||
| 50 | + <text class="info-label">Printed By</text> | ||
| 51 | + <text class="info-value">{{ record.userName }}</text> | ||
| 52 | + </view> | ||
| 53 | + <view class="info-row"> | ||
| 54 | + <text class="info-label">Printer</text> | ||
| 55 | + <text class="info-value">{{ record.printer }}</text> | ||
| 56 | + </view> | ||
| 57 | + </view> | ||
| 58 | + </view> | ||
| 59 | + </scroll-view> | ||
| 60 | + | ||
| 61 | + <view v-else class="empty-wrap"> | ||
| 62 | + <AppIcon name="alert" size="lg" color="gray" /> | ||
| 63 | + <text class="empty-text">Record not found</text> | ||
| 64 | + </view> | ||
| 65 | + | ||
| 66 | + <SideMenu v-model="isMenuOpen" /> | ||
| 67 | + </view> | ||
| 68 | +</template> | ||
| 69 | + | ||
| 70 | +<script setup lang="ts"> | ||
| 71 | +import { ref, computed } from 'vue' | ||
| 72 | +import { onLoad } from '@dcloudio/uni-app' | ||
| 73 | +import AppIcon from '../../components/AppIcon.vue' | ||
| 74 | +import SideMenu from '../../components/SideMenu.vue' | ||
| 75 | +import LocationPicker from '../../components/LocationPicker.vue' | ||
| 76 | +import { getStatusBarHeight } from '../../utils/statusBar' | ||
| 77 | +import { getRecordById } from '../../utils/printRecords' | ||
| 78 | + | ||
| 79 | +const statusBarHeight = getStatusBarHeight() | ||
| 80 | +const isMenuOpen = ref(false) | ||
| 81 | +const record = ref<any>(null) | ||
| 82 | + | ||
| 83 | +const labelImage = computed(() => { | ||
| 84 | + const r = record.value | ||
| 85 | + if (!r) return '/static/lable1.png' | ||
| 86 | + const size = r.templateSize || '' | ||
| 87 | + if (size.indexOf('2"x6"') >= 0 || size.indexOf('2"x4"') >= 0) { | ||
| 88 | + return '/static/lable2.png' | ||
| 89 | + } | ||
| 90 | + return '/static/lable1.png' | ||
| 91 | +}) | ||
| 92 | + | ||
| 93 | +// 产品名称按标签模板固定:lable1 → Syrup,lable2 → Cheese Burger Deluxe | ||
| 94 | +const displayProductName = computed(() => | ||
| 95 | + labelImage.value.includes('lable1') ? 'Syrup' : 'Cheese Burger Deluxe' | ||
| 96 | +) | ||
| 97 | + | ||
| 98 | +onLoad((opts: any) => { | ||
| 99 | + const id = opts && opts.id | ||
| 100 | + if (id) { | ||
| 101 | + record.value = getRecordById(id) | ||
| 102 | + } | ||
| 103 | +}) | ||
| 104 | + | ||
| 105 | +const goBack = () => uni.navigateBack() | ||
| 106 | +</script> | ||
| 107 | + | ||
| 108 | +<style scoped> | ||
| 109 | +.page { | ||
| 110 | + min-height: 100vh; | ||
| 111 | + background: #f9fafb; | ||
| 112 | + display: flex; | ||
| 113 | + flex-direction: column; | ||
| 114 | + overflow-x: hidden; | ||
| 115 | +} | ||
| 116 | + | ||
| 117 | +.header-hero { | ||
| 118 | + background: linear-gradient(135deg, var(--theme-primary), var(--theme-primary-dark)); | ||
| 119 | + padding: 16rpx 32rpx 24rpx; | ||
| 120 | +} | ||
| 121 | + | ||
| 122 | +.top-bar { | ||
| 123 | + height: 96rpx; | ||
| 124 | + display: flex; | ||
| 125 | + align-items: center; | ||
| 126 | + justify-content: space-between; | ||
| 127 | +} | ||
| 128 | + | ||
| 129 | +.top-left, | ||
| 130 | +.top-right { | ||
| 131 | + width: 64rpx; | ||
| 132 | + height: 64rpx; | ||
| 133 | + border-radius: 999rpx; | ||
| 134 | + background: rgba(255, 255, 255, 0.15); | ||
| 135 | + display: flex; | ||
| 136 | + align-items: center; | ||
| 137 | + justify-content: center; | ||
| 138 | +} | ||
| 139 | + | ||
| 140 | +.top-center { | ||
| 141 | + flex: 1; | ||
| 142 | + display: flex; | ||
| 143 | + flex-direction: column; | ||
| 144 | + align-items: center; | ||
| 145 | +} | ||
| 146 | + | ||
| 147 | +.page-title { | ||
| 148 | + font-size: 34rpx; | ||
| 149 | + font-weight: 600; | ||
| 150 | + color: #fff; | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +.content { | ||
| 154 | + flex: 1; | ||
| 155 | + padding: 32rpx; | ||
| 156 | + box-sizing: border-box; | ||
| 157 | + overflow-x: hidden; | ||
| 158 | +} | ||
| 159 | + | ||
| 160 | +.detail-label-section { | ||
| 161 | + margin-bottom: 32rpx; | ||
| 162 | +} | ||
| 163 | + | ||
| 164 | +.detail-section-title { | ||
| 165 | + font-size: 28rpx; | ||
| 166 | + font-weight: 600; | ||
| 167 | + color: #111827; | ||
| 168 | + display: block; | ||
| 169 | + margin-bottom: 20rpx; | ||
| 170 | +} | ||
| 171 | + | ||
| 172 | +.detail-label-wrap { | ||
| 173 | + width: 100%; | ||
| 174 | + padding: 24rpx 0; | ||
| 175 | + background: #fff; | ||
| 176 | + border-radius: 16rpx; | ||
| 177 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); | ||
| 178 | + overflow: hidden; | ||
| 179 | + box-sizing: border-box; | ||
| 180 | +} | ||
| 181 | + | ||
| 182 | +.detail-label-img { | ||
| 183 | + width: 100%; | ||
| 184 | + max-width: 100%; | ||
| 185 | + display: block; | ||
| 186 | + border-radius: 12rpx; | ||
| 187 | + box-sizing: border-box; | ||
| 188 | +} | ||
| 189 | + | ||
| 190 | +.detail-info-section { | ||
| 191 | + padding-top: 0; | ||
| 192 | +} | ||
| 193 | + | ||
| 194 | +.info-card { | ||
| 195 | + background: #fff; | ||
| 196 | + border-radius: 16rpx; | ||
| 197 | + padding: 28rpx 32rpx; | ||
| 198 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); | ||
| 199 | +} | ||
| 200 | + | ||
| 201 | +.info-row { | ||
| 202 | + padding: 20rpx 0; | ||
| 203 | + border-bottom: 1rpx solid #f3f4f6; | ||
| 204 | +} | ||
| 205 | + | ||
| 206 | +.info-row:last-child { | ||
| 207 | + border-bottom: none; | ||
| 208 | +} | ||
| 209 | + | ||
| 210 | +.info-label { | ||
| 211 | + font-size: 24rpx; | ||
| 212 | + color: #6b7280; | ||
| 213 | + display: block; | ||
| 214 | + margin-bottom: 8rpx; | ||
| 215 | +} | ||
| 216 | + | ||
| 217 | +.info-value { | ||
| 218 | + font-size: 30rpx; | ||
| 219 | + font-weight: 500; | ||
| 220 | + color: #111827; | ||
| 221 | + display: block; | ||
| 222 | +} | ||
| 223 | + | ||
| 224 | +.empty-wrap { | ||
| 225 | + flex: 1; | ||
| 226 | + display: flex; | ||
| 227 | + flex-direction: column; | ||
| 228 | + align-items: center; | ||
| 229 | + justify-content: center; | ||
| 230 | + padding: 48rpx; | ||
| 231 | +} | ||
| 232 | + | ||
| 233 | +.empty-text { | ||
| 234 | + font-size: 28rpx; | ||
| 235 | + color: #6b7280; | ||
| 236 | + margin-top: 24rpx; | ||
| 237 | +} | ||
| 238 | +</style> |
美国版/Food Labeling Management App UniApp/src/pages/more/print-log.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <view class="page"> | ||
| 3 | + <view class="header-hero" :style="{ paddingTop: statusBarHeight + 'px' }"> | ||
| 4 | + <view class="top-bar"> | ||
| 5 | + <view class="top-left" @click="goBack"> | ||
| 6 | + <AppIcon name="chevronLeft" size="sm" color="white" /> | ||
| 7 | + </view> | ||
| 8 | + <view class="top-center"> | ||
| 9 | + <text class="page-title">Print Log</text> | ||
| 10 | + <LocationPicker /> | ||
| 11 | + </view> | ||
| 12 | + <view class="top-right" @click="isMenuOpen = true"> | ||
| 13 | + <AppIcon name="menu" size="sm" color="white" /> | ||
| 14 | + </view> | ||
| 15 | + </view> | ||
| 16 | + </view> | ||
| 17 | + | ||
| 18 | + <scroll-view class="content" scroll-y> | ||
| 19 | + <view class="log-list"> | ||
| 20 | + <view | ||
| 21 | + v-for="row in printLogData" | ||
| 22 | + :key="row.labelId" | ||
| 23 | + class="log-card" | ||
| 24 | + > | ||
| 25 | + <view class="card-header"> | ||
| 26 | + <text class="product-name">{{ row.productName }}</text> | ||
| 27 | + <text class="label-id">{{ row.labelId }}</text> | ||
| 28 | + </view> | ||
| 29 | + <view class="card-tags"> | ||
| 30 | + <text class="tag">{{ row.category }}</text> | ||
| 31 | + <text class="tag">{{ row.template }}</text> | ||
| 32 | + </view> | ||
| 33 | + <view class="card-details"> | ||
| 34 | + <view class="detail-row"> | ||
| 35 | + <AppIcon name="clock" size="sm" color="gray" /> | ||
| 36 | + <text class="detail-text">{{ row.printedAt }}</text> | ||
| 37 | + </view> | ||
| 38 | + <view class="detail-row"> | ||
| 39 | + <AppIcon name="user" size="sm" color="gray" /> | ||
| 40 | + <text class="detail-text">{{ row.printedBy }}</text> | ||
| 41 | + </view> | ||
| 42 | + <view class="detail-row"> | ||
| 43 | + <AppIcon name="mapPin" size="sm" color="gray" /> | ||
| 44 | + <text class="detail-text">{{ row.location }}</text> | ||
| 45 | + </view> | ||
| 46 | + <view class="detail-row"> | ||
| 47 | + <AppIcon name="calendar" size="sm" color="gray" /> | ||
| 48 | + <text class="detail-text">Expires {{ row.expiryDate }}</text> | ||
| 49 | + </view> | ||
| 50 | + </view> | ||
| 51 | + <view class="card-footer"> | ||
| 52 | + <view class="reprint-btn" @click="handleReprint(row)"> | ||
| 53 | + <AppIcon name="printer" size="sm" color="white" /> | ||
| 54 | + <text class="reprint-text">Reprint</text> | ||
| 55 | + </view> | ||
| 56 | + </view> | ||
| 57 | + </view> | ||
| 58 | + </view> | ||
| 59 | + </scroll-view> | ||
| 60 | + | ||
| 61 | + <SideMenu v-model="isMenuOpen" /> | ||
| 62 | + </view> | ||
| 63 | +</template> | ||
| 64 | + | ||
| 65 | +<script setup lang="ts"> | ||
| 66 | +import { ref } from 'vue' | ||
| 67 | +import AppIcon from '../../components/AppIcon.vue' | ||
| 68 | +import SideMenu from '../../components/SideMenu.vue' | ||
| 69 | +import LocationPicker from '../../components/LocationPicker.vue' | ||
| 70 | +import { getStatusBarHeight } from '../../utils/statusBar' | ||
| 71 | + | ||
| 72 | +const statusBarHeight = getStatusBarHeight() | ||
| 73 | +const isMenuOpen = ref(false) | ||
| 74 | + | ||
| 75 | +const printLogData = ref([ | ||
| 76 | + { labelId: '1-251201', productName: 'Whole Milk', category: 'Dairy', template: '2"x2" Basic', printedAt: '2024-03-20 09:30 AM', printedBy: 'Alice Johnson', location: 'Downtown Store (101)', expiryDate: '2024-03-27' }, | ||
| 77 | + { labelId: '2-251201', productName: 'Ground Beef', category: 'Meat', template: '2"x2" Basic', printedAt: '2024-03-20 10:15 AM', printedBy: 'Bob Smith', location: 'Uptown Store (102)', expiryDate: '2024-03-23' }, | ||
| 78 | + { labelId: '3-251201', productName: 'Croissant', category: 'Bakery', template: '2"x2" Basic', printedAt: '2024-03-19 14:00 PM', printedBy: 'Charlie Brown', location: 'Downtown Store (101)', expiryDate: '2024-03-20' }, | ||
| 79 | + { labelId: '4-251201', productName: 'Caesar Salad', category: 'Deli', template: '2"x6" G\'n\'G !!!', printedAt: '2024-03-18 11:45 AM', printedBy: 'Alice Johnson', location: 'Downtown Store (101)', expiryDate: '2024-03-21' }, | ||
| 80 | + { labelId: '5-251201', productName: 'Orange Juice', category: 'Beverage', template: '2"x2" Basic', printedAt: '2024-03-18 08:20 AM', printedBy: 'Bob Smith', location: 'Airport Kiosk (201)', expiryDate: '2024-03-25' }, | ||
| 81 | +]) | ||
| 82 | + | ||
| 83 | +const handleReprint = (row: any) => { | ||
| 84 | + uni.showToast({ title: 'Reprint: ' + row.productName, icon: 'none' }) | ||
| 85 | +} | ||
| 86 | + | ||
| 87 | +const goBack = () => { | ||
| 88 | + const pages = getCurrentPages() | ||
| 89 | + if (pages.length > 1) { | ||
| 90 | + uni.navigateBack() | ||
| 91 | + } else { | ||
| 92 | + uni.redirectTo({ url: '/pages/index/index' }) | ||
| 93 | + } | ||
| 94 | +} | ||
| 95 | +</script> | ||
| 96 | + | ||
| 97 | +<style scoped> | ||
| 98 | +.page { | ||
| 99 | + min-height: 100vh; | ||
| 100 | + background: #f3f4f6; | ||
| 101 | + display: flex; | ||
| 102 | + flex-direction: column; | ||
| 103 | +} | ||
| 104 | + | ||
| 105 | +.header-hero { | ||
| 106 | + background: linear-gradient(135deg, #1F3A8A, #142a6c); | ||
| 107 | + padding: 16rpx 32rpx 24rpx; | ||
| 108 | +} | ||
| 109 | + | ||
| 110 | +.top-bar { | ||
| 111 | + height: 96rpx; | ||
| 112 | + display: flex; | ||
| 113 | + align-items: center; | ||
| 114 | + justify-content: space-between; | ||
| 115 | +} | ||
| 116 | + | ||
| 117 | +.top-left, | ||
| 118 | +.top-right { | ||
| 119 | + width: 64rpx; | ||
| 120 | + height: 64rpx; | ||
| 121 | + border-radius: 999rpx; | ||
| 122 | + background: rgba(255, 255, 255, 0.15); | ||
| 123 | + display: flex; | ||
| 124 | + align-items: center; | ||
| 125 | + justify-content: center; | ||
| 126 | +} | ||
| 127 | + | ||
| 128 | +.top-center { | ||
| 129 | + flex: 1; | ||
| 130 | + display: flex; | ||
| 131 | + flex-direction: column; | ||
| 132 | + align-items: center; | ||
| 133 | +} | ||
| 134 | + | ||
| 135 | +.page-title { | ||
| 136 | + font-size: 34rpx; | ||
| 137 | + font-weight: 600; | ||
| 138 | + color: #fff; | ||
| 139 | +} | ||
| 140 | + | ||
| 141 | +.content { | ||
| 142 | + flex: 1; | ||
| 143 | + padding: 24rpx 28rpx 40rpx; | ||
| 144 | + box-sizing: border-box; | ||
| 145 | +} | ||
| 146 | + | ||
| 147 | +.log-list { | ||
| 148 | + display: flex; | ||
| 149 | + flex-direction: column; | ||
| 150 | + gap: 20rpx; | ||
| 151 | +} | ||
| 152 | + | ||
| 153 | +.log-card { | ||
| 154 | + background: #fff; | ||
| 155 | + border-radius: 24rpx; | ||
| 156 | + padding: 0; | ||
| 157 | + box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04); | ||
| 158 | + overflow: hidden; | ||
| 159 | + border: 1rpx solid #e5e7eb; | ||
| 160 | +} | ||
| 161 | + | ||
| 162 | +.card-header { | ||
| 163 | + display: flex; | ||
| 164 | + justify-content: space-between; | ||
| 165 | + align-items: center; | ||
| 166 | + padding: 28rpx 28rpx 20rpx; | ||
| 167 | + background: linear-gradient(180deg, #fafbfc 0%, #fff 100%); | ||
| 168 | +} | ||
| 169 | + | ||
| 170 | +.product-name { | ||
| 171 | + font-size: 34rpx; | ||
| 172 | + font-weight: 700; | ||
| 173 | + color: #111827; | ||
| 174 | + flex: 1; | ||
| 175 | + line-height: 1.3; | ||
| 176 | + letter-spacing: -0.02em; | ||
| 177 | +} | ||
| 178 | + | ||
| 179 | +.label-id { | ||
| 180 | + font-size: 22rpx; | ||
| 181 | + color: #9ca3af; | ||
| 182 | + flex-shrink: 0; | ||
| 183 | + margin-left: 16rpx; | ||
| 184 | + padding: 6rpx 12rpx; | ||
| 185 | + background: #f3f4f6; | ||
| 186 | + border-radius: 8rpx; | ||
| 187 | +} | ||
| 188 | + | ||
| 189 | +.card-tags { | ||
| 190 | + display: flex; | ||
| 191 | + flex-wrap: wrap; | ||
| 192 | + gap: 12rpx; | ||
| 193 | + padding: 0 28rpx 20rpx; | ||
| 194 | +} | ||
| 195 | + | ||
| 196 | +.tag { | ||
| 197 | + font-size: 24rpx; | ||
| 198 | + color: #1F3A8A; | ||
| 199 | + background: #e8ecf5; | ||
| 200 | + padding: 8rpx 18rpx; | ||
| 201 | + border-radius: 10rpx; | ||
| 202 | + font-weight: 500; | ||
| 203 | +} | ||
| 204 | + | ||
| 205 | +.card-details { | ||
| 206 | + display: flex; | ||
| 207 | + flex-direction: column; | ||
| 208 | + gap: 16rpx; | ||
| 209 | + padding: 24rpx 28rpx; | ||
| 210 | + background: #fafbfc; | ||
| 211 | + border-top: 1rpx solid #e5e7eb; | ||
| 212 | +} | ||
| 213 | + | ||
| 214 | +.detail-row { | ||
| 215 | + display: flex; | ||
| 216 | + align-items: center; | ||
| 217 | + gap: 12rpx; | ||
| 218 | +} | ||
| 219 | + | ||
| 220 | +.detail-text { | ||
| 221 | + font-size: 28rpx; | ||
| 222 | + color: #4b5563; | ||
| 223 | + line-height: 1.4; | ||
| 224 | +} | ||
| 225 | + | ||
| 226 | +.card-footer { | ||
| 227 | + padding: 20rpx 28rpx 24rpx; | ||
| 228 | + background: #fff; | ||
| 229 | + border-top: 1rpx solid #e5e7eb; | ||
| 230 | +} | ||
| 231 | + | ||
| 232 | +.reprint-btn { | ||
| 233 | + display: inline-flex; | ||
| 234 | + align-items: center; | ||
| 235 | + justify-content: center; | ||
| 236 | + gap: 10rpx; | ||
| 237 | + padding: 16rpx 32rpx; | ||
| 238 | + background: linear-gradient(135deg, #1F3A8A, #1447E6); | ||
| 239 | + border-radius: 14rpx; | ||
| 240 | + box-shadow: 0 4rpx 12rpx rgba(31, 58, 138, 0.25); | ||
| 241 | +} | ||
| 242 | + | ||
| 243 | +.reprint-btn:active { | ||
| 244 | + opacity: 0.9; | ||
| 245 | + transform: scale(0.98); | ||
| 246 | +} | ||
| 247 | + | ||
| 248 | +.reprint-text { | ||
| 249 | + font-size: 28rpx; | ||
| 250 | + font-weight: 600; | ||
| 251 | + color: #fff; | ||
| 252 | +} | ||
| 253 | +</style> |
美国版/Food Labeling Management App UniApp/src/pages/more/profile.vue
| @@ -7,6 +7,7 @@ | @@ -7,6 +7,7 @@ | ||
| 7 | </view> | 7 | </view> |
| 8 | <view class="top-center"> | 8 | <view class="top-center"> |
| 9 | <text class="page-title">{{ t('profile.title') }}</text> | 9 | <text class="page-title">{{ t('profile.title') }}</text> |
| 10 | + <LocationPicker /> | ||
| 10 | </view> | 11 | </view> |
| 11 | <view class="top-right" @click="isMenuOpen = true"> | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | <AppIcon name="menu" size="sm" color="white" /> | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| @@ -17,35 +18,56 @@ | @@ -17,35 +18,56 @@ | ||
| 17 | <view class="content"> | 18 | <view class="content"> |
| 18 | <view class="avatar-wrap"> | 19 | <view class="avatar-wrap"> |
| 19 | <view class="avatar"><AppIcon name="user" size="lg" color="white" /></view> | 20 | <view class="avatar"><AppIcon name="user" size="lg" color="white" /></view> |
| 21 | + <text class="avatar-name">{{ name }}</text> | ||
| 22 | + <text class="avatar-role">Employee</text> | ||
| 20 | </view> | 23 | </view> |
| 21 | - <view class="form-card"> | ||
| 22 | - <view class="form-group"> | ||
| 23 | - <text class="label">{{ t('profile.name') }}</text> | ||
| 24 | - <input v-model="name" class="input" :disabled="!isEditing" /> | 24 | + |
| 25 | + <view class="info-card"> | ||
| 26 | + <view class="info-row"> | ||
| 27 | + <view class="info-icon-box blue"> | ||
| 28 | + <AppIcon name="user" size="sm" color="blue" /> | ||
| 29 | + </view> | ||
| 30 | + <view class="info-detail"> | ||
| 31 | + <text class="info-label">{{ t('profile.name') }}</text> | ||
| 32 | + <text class="info-value">{{ name }}</text> | ||
| 33 | + </view> | ||
| 25 | </view> | 34 | </view> |
| 26 | - <view class="form-group"> | ||
| 27 | - <text class="label">{{ t('profile.email') }}</text> | ||
| 28 | - <input v-model="email" type="text" class="input" :disabled="!isEditing" /> | 35 | + <view class="info-divider" /> |
| 36 | + <view class="info-row"> | ||
| 37 | + <view class="info-icon-box green"> | ||
| 38 | + <AppIcon name="mail" size="sm" color="green" /> | ||
| 39 | + </view> | ||
| 40 | + <view class="info-detail"> | ||
| 41 | + <text class="info-label">{{ t('profile.email') }}</text> | ||
| 42 | + <text class="info-value">{{ email }}</text> | ||
| 43 | + </view> | ||
| 29 | </view> | 44 | </view> |
| 30 | - <view class="form-group"> | ||
| 31 | - <text class="label">{{ t('profile.phone') }}</text> | ||
| 32 | - <input v-model="phone" type="text" class="input" :disabled="!isEditing" /> | 45 | + <view class="info-divider" /> |
| 46 | + <view class="info-row"> | ||
| 47 | + <view class="info-icon-box orange"> | ||
| 48 | + <AppIcon name="phone" size="sm" color="orange" /> | ||
| 49 | + </view> | ||
| 50 | + <view class="info-detail"> | ||
| 51 | + <text class="info-label">{{ t('profile.phone') }}</text> | ||
| 52 | + <text class="info-value">{{ phone }}</text> | ||
| 53 | + </view> | ||
| 33 | </view> | 54 | </view> |
| 34 | - <view class="form-group"> | ||
| 35 | - <text class="label">{{ t('profile.employeeId') }}</text> | ||
| 36 | - <input v-model="employeeId" class="input disabled" disabled /> | 55 | + <view class="info-divider" /> |
| 56 | + <view class="info-row"> | ||
| 57 | + <view class="info-icon-box purple"> | ||
| 58 | + <AppIcon name="tag" size="sm" color="purple" /> | ||
| 59 | + </view> | ||
| 60 | + <view class="info-detail"> | ||
| 61 | + <text class="info-label">{{ t('profile.employeeId') }}</text> | ||
| 62 | + <text class="info-value">{{ employeeId }}</text> | ||
| 63 | + </view> | ||
| 37 | </view> | 64 | </view> |
| 38 | </view> | 65 | </view> |
| 39 | - <view v-if="isEditing" class="actions"> | ||
| 40 | - <view class="btn-outline" @click="isEditing = false"> | ||
| 41 | - <text class="btn-outline-text">{{ t('common.cancel') }}</text> | ||
| 42 | - </view> | ||
| 43 | - <view class="btn-primary" @click="handleSave"> | ||
| 44 | - <text class="btn-primary-text">{{ t('profile.saveChanges') }}</text> | ||
| 45 | - </view> | ||
| 46 | - </view> | ||
| 47 | - <view v-else class="btn-primary full" @click="isEditing = true"> | ||
| 48 | - <text class="btn-primary-text">{{ t('profile.editProfile') }}</text> | 66 | + |
| 67 | + <view class="btn-password" @click="goChangePassword"> | ||
| 68 | + <AppIcon name="settings" size="sm" color="primary" /> | ||
| 69 | + <text class="btn-password-text">Change Password</text> | ||
| 70 | + <AppIcon name="chevronRight" size="sm" color="gray" /> | ||
| 49 | </view> | 71 | </view> |
| 50 | </view> | 72 | </view> |
| 51 | 73 | ||
| @@ -58,11 +80,11 @@ import { ref } from 'vue' | @@ -58,11 +80,11 @@ import { ref } from 'vue' | ||
| 58 | import { useI18n } from 'vue-i18n' | 80 | import { useI18n } from 'vue-i18n' |
| 59 | import AppIcon from '../../components/AppIcon.vue' | 81 | import AppIcon from '../../components/AppIcon.vue' |
| 60 | import SideMenu from '../../components/SideMenu.vue' | 82 | import SideMenu from '../../components/SideMenu.vue' |
| 83 | +import LocationPicker from '../../components/LocationPicker.vue' | ||
| 61 | import { getStatusBarHeight } from '../../utils/statusBar' | 84 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 62 | 85 | ||
| 63 | const { t } = useI18n() | 86 | const { t } = useI18n() |
| 64 | const statusBarHeight = getStatusBarHeight() | 87 | const statusBarHeight = getStatusBarHeight() |
| 65 | -const isEditing = ref(false) | ||
| 66 | const isMenuOpen = ref(false) | 88 | const isMenuOpen = ref(false) |
| 67 | const name = ref(uni.getStorageSync('userName') || 'John Smith') | 89 | const name = ref(uni.getStorageSync('userName') || 'John Smith') |
| 68 | const email = ref('john.smith@company.com') | 90 | const email = ref('john.smith@company.com') |
| @@ -78,10 +100,8 @@ const goBack = () => { | @@ -78,10 +100,8 @@ const goBack = () => { | ||
| 78 | } | 100 | } |
| 79 | } | 101 | } |
| 80 | 102 | ||
| 81 | -const handleSave = () => { | ||
| 82 | - uni.setStorageSync('userName', name.value) | ||
| 83 | - isEditing.value = false | ||
| 84 | - uni.showToast({ title: 'Profile updated successfully!', icon: 'success' }) | 103 | +const goChangePassword = () => { |
| 104 | + uni.navigateTo({ url: '/pages/more/change-password' }) | ||
| 85 | } | 105 | } |
| 86 | </script> | 106 | </script> |
| 87 | 107 | ||
| @@ -116,7 +136,9 @@ const handleSave = () => { | @@ -116,7 +136,9 @@ const handleSave = () => { | ||
| 116 | 136 | ||
| 117 | .top-center { | 137 | .top-center { |
| 118 | flex: 1; | 138 | flex: 1; |
| 119 | - text-align: center; | 139 | + display: flex; |
| 140 | + flex-direction: column; | ||
| 141 | + align-items: center; | ||
| 120 | } | 142 | } |
| 121 | 143 | ||
| 122 | .page-title { | 144 | .page-title { |
| @@ -131,93 +153,114 @@ const handleSave = () => { | @@ -131,93 +153,114 @@ const handleSave = () => { | ||
| 131 | 153 | ||
| 132 | .avatar-wrap { | 154 | .avatar-wrap { |
| 133 | display: flex; | 155 | display: flex; |
| 134 | - justify-content: center; | ||
| 135 | - margin-bottom: 48rpx; | 156 | + flex-direction: column; |
| 157 | + align-items: center; | ||
| 158 | + margin-bottom: 40rpx; | ||
| 136 | } | 159 | } |
| 137 | 160 | ||
| 138 | .avatar { | 161 | .avatar { |
| 139 | - width: 160rpx; | ||
| 140 | - height: 160rpx; | 162 | + width: 140rpx; |
| 163 | + height: 140rpx; | ||
| 141 | background: var(--theme-primary); | 164 | background: var(--theme-primary); |
| 142 | border-radius: 50%; | 165 | border-radius: 50%; |
| 143 | display: flex; | 166 | display: flex; |
| 144 | align-items: center; | 167 | align-items: center; |
| 145 | justify-content: center; | 168 | justify-content: center; |
| 169 | + margin-bottom: 16rpx; | ||
| 146 | } | 170 | } |
| 147 | 171 | ||
| 148 | -.form-card { | ||
| 149 | - background: #fff; | ||
| 150 | - padding: 48rpx; | ||
| 151 | - border-radius: 20rpx; | ||
| 152 | - margin-bottom: 48rpx; | ||
| 153 | - box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); | ||
| 154 | -} | ||
| 155 | - | ||
| 156 | -.form-group { | ||
| 157 | - margin-bottom: 32rpx; | ||
| 158 | -} | ||
| 159 | - | ||
| 160 | -.label { | ||
| 161 | - font-size: 30rpx; | ||
| 162 | - font-weight: 500; | ||
| 163 | - color: #374151; | 172 | +.avatar-name { |
| 173 | + font-size: 36rpx; | ||
| 174 | + font-weight: 700; | ||
| 175 | + color: #111827; | ||
| 164 | display: block; | 176 | display: block; |
| 165 | - margin-bottom: 16rpx; | 177 | + margin-bottom: 4rpx; |
| 166 | } | 178 | } |
| 167 | 179 | ||
| 168 | -.input { | ||
| 169 | - height: 96rpx; | ||
| 170 | - padding: 0 24rpx; | ||
| 171 | - background: #f3f4f6; | ||
| 172 | - border-radius: 16rpx; | ||
| 173 | - font-size: 32rpx; | 180 | +.avatar-role { |
| 181 | + font-size: 26rpx; | ||
| 182 | + color: #6b7280; | ||
| 174 | } | 183 | } |
| 175 | 184 | ||
| 176 | -.input.disabled { | ||
| 177 | - background: #f9fafb; | ||
| 178 | - color: #9ca3af; | 185 | +.info-card { |
| 186 | + background: #fff; | ||
| 187 | + padding: 12rpx 32rpx; | ||
| 188 | + border-radius: 20rpx; | ||
| 189 | + margin-bottom: 32rpx; | ||
| 190 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); | ||
| 179 | } | 191 | } |
| 180 | 192 | ||
| 181 | -.actions { | 193 | +.info-row { |
| 182 | display: flex; | 194 | display: flex; |
| 195 | + align-items: center; | ||
| 183 | gap: 24rpx; | 196 | gap: 24rpx; |
| 197 | + padding: 28rpx 0; | ||
| 184 | } | 198 | } |
| 185 | 199 | ||
| 186 | -.btn-outline { | ||
| 187 | - flex: 1; | ||
| 188 | - height: 96rpx; | 200 | +.info-divider { |
| 201 | + height: 1rpx; | ||
| 189 | background: #f3f4f6; | 202 | background: #f3f4f6; |
| 203 | +} | ||
| 204 | + | ||
| 205 | +.info-icon-box { | ||
| 206 | + width: 72rpx; | ||
| 207 | + height: 72rpx; | ||
| 190 | border-radius: 16rpx; | 208 | border-radius: 16rpx; |
| 191 | display: flex; | 209 | display: flex; |
| 192 | align-items: center; | 210 | align-items: center; |
| 193 | justify-content: center; | 211 | justify-content: center; |
| 212 | + flex-shrink: 0; | ||
| 194 | } | 213 | } |
| 195 | 214 | ||
| 196 | -.btn-outline-text { | ||
| 197 | - font-size: 32rpx; | ||
| 198 | - font-weight: 600; | ||
| 199 | - color: #374151; | ||
| 200 | - line-height: 1; | 215 | +.info-icon-box.blue { |
| 216 | + background: #eff6ff; | ||
| 201 | } | 217 | } |
| 202 | 218 | ||
| 203 | -.btn-primary { | 219 | +.info-icon-box.green { |
| 220 | + background: #f0fdf4; | ||
| 221 | +} | ||
| 222 | + | ||
| 223 | +.info-icon-box.orange { | ||
| 224 | + background: #fff7ed; | ||
| 225 | +} | ||
| 226 | + | ||
| 227 | +.info-icon-box.purple { | ||
| 228 | + background: #faf5ff; | ||
| 229 | +} | ||
| 230 | + | ||
| 231 | +.info-detail { | ||
| 204 | flex: 1; | 232 | flex: 1; |
| 205 | - height: 96rpx; | ||
| 206 | - background: var(--theme-primary); | ||
| 207 | - border-radius: 16rpx; | ||
| 208 | - display: flex; | ||
| 209 | - align-items: center; | ||
| 210 | - justify-content: center; | 233 | + min-width: 0; |
| 211 | } | 234 | } |
| 212 | 235 | ||
| 213 | -.btn-primary.full { | ||
| 214 | - width: 100%; | 236 | +.info-label { |
| 237 | + font-size: 24rpx; | ||
| 238 | + color: #9ca3af; | ||
| 239 | + display: block; | ||
| 240 | + margin-bottom: 4rpx; | ||
| 241 | +} | ||
| 242 | + | ||
| 243 | +.info-value { | ||
| 244 | + font-size: 30rpx; | ||
| 245 | + font-weight: 500; | ||
| 246 | + color: #111827; | ||
| 247 | + display: block; | ||
| 248 | +} | ||
| 249 | + | ||
| 250 | +.btn-password { | ||
| 251 | + background: #fff; | ||
| 252 | + padding: 32rpx; | ||
| 253 | + border-radius: 20rpx; | ||
| 254 | + display: flex; | ||
| 255 | + align-items: center; | ||
| 256 | + gap: 20rpx; | ||
| 257 | + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04); | ||
| 215 | } | 258 | } |
| 216 | 259 | ||
| 217 | -.btn-primary-text { | ||
| 218 | - font-size: 32rpx; | 260 | +.btn-password-text { |
| 261 | + flex: 1; | ||
| 262 | + font-size: 30rpx; | ||
| 219 | font-weight: 600; | 263 | font-weight: 600; |
| 220 | - color: #fff; | ||
| 221 | - line-height: 1; | 264 | + color: #111827; |
| 222 | } | 265 | } |
| 223 | </style> | 266 | </style> |
美国版/Food Labeling Management App UniApp/src/pages/more/support.vue
| @@ -7,6 +7,7 @@ | @@ -7,6 +7,7 @@ | ||
| 7 | </view> | 7 | </view> |
| 8 | <view class="top-center"> | 8 | <view class="top-center"> |
| 9 | <text class="page-title">{{ t('support.title') }}</text> | 9 | <text class="page-title">{{ t('support.title') }}</text> |
| 10 | + <LocationPicker /> | ||
| 10 | </view> | 11 | </view> |
| 11 | <view class="top-right" @click="isMenuOpen = true"> | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | <AppIcon name="menu" size="sm" color="white" /> | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| @@ -120,6 +121,7 @@ import { ref } from 'vue' | @@ -120,6 +121,7 @@ import { ref } from 'vue' | ||
| 120 | import { useI18n } from 'vue-i18n' | 121 | import { useI18n } from 'vue-i18n' |
| 121 | import AppIcon from '../../components/AppIcon.vue' | 122 | import AppIcon from '../../components/AppIcon.vue' |
| 122 | import SideMenu from '../../components/SideMenu.vue' | 123 | import SideMenu from '../../components/SideMenu.vue' |
| 124 | +import LocationPicker from '../../components/LocationPicker.vue' | ||
| 123 | import { getStatusBarHeight } from '../../utils/statusBar' | 125 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 124 | 126 | ||
| 125 | const { t } = useI18n() | 127 | const { t } = useI18n() |
| @@ -175,7 +177,9 @@ const callEmergency = () => { | @@ -175,7 +177,9 @@ const callEmergency = () => { | ||
| 175 | 177 | ||
| 176 | .top-center { | 178 | .top-center { |
| 177 | flex: 1; | 179 | flex: 1; |
| 178 | - text-align: center; | 180 | + display: flex; |
| 181 | + flex-direction: column; | ||
| 182 | + align-items: center; | ||
| 179 | } | 183 | } |
| 180 | 184 | ||
| 181 | .page-title { | 185 | .page-title { |
美国版/Food Labeling Management App UniApp/src/pages/more/sync.vue
| @@ -7,6 +7,7 @@ | @@ -7,6 +7,7 @@ | ||
| 7 | </view> | 7 | </view> |
| 8 | <view class="top-center"> | 8 | <view class="top-center"> |
| 9 | <text class="page-title">{{ t('sync.title') }}</text> | 9 | <text class="page-title">{{ t('sync.title') }}</text> |
| 10 | + <LocationPicker /> | ||
| 10 | </view> | 11 | </view> |
| 11 | <view class="top-right" @click="isMenuOpen = true"> | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | <AppIcon name="menu" size="sm" color="white" /> | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| @@ -71,6 +72,7 @@ import { ref } from 'vue' | @@ -71,6 +72,7 @@ import { ref } from 'vue' | ||
| 71 | import { useI18n } from 'vue-i18n' | 72 | import { useI18n } from 'vue-i18n' |
| 72 | import AppIcon from '../../components/AppIcon.vue' | 73 | import AppIcon from '../../components/AppIcon.vue' |
| 73 | import SideMenu from '../../components/SideMenu.vue' | 74 | import SideMenu from '../../components/SideMenu.vue' |
| 75 | +import LocationPicker from '../../components/LocationPicker.vue' | ||
| 74 | import { getStatusBarHeight } from '../../utils/statusBar' | 76 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 75 | 77 | ||
| 76 | const { t } = useI18n() | 78 | const { t } = useI18n() |
| @@ -130,7 +132,9 @@ const handleSync = () => { | @@ -130,7 +132,9 @@ const handleSync = () => { | ||
| 130 | 132 | ||
| 131 | .top-center { | 133 | .top-center { |
| 132 | flex: 1; | 134 | flex: 1; |
| 133 | - text-align: center; | 135 | + display: flex; |
| 136 | + flex-direction: column; | ||
| 137 | + align-items: center; | ||
| 134 | } | 138 | } |
| 135 | 139 | ||
| 136 | .page-title { | 140 | .page-title { |
美国版/Food Labeling Management App UniApp/src/static/lable1.png
0 → 100644
26 KB
美国版/Food Labeling Management App UniApp/src/static/lable2.png
0 → 100644
67.5 KB
美国版/Food Labeling Management App UniApp/src/uni.scss
| @@ -15,7 +15,7 @@ | @@ -15,7 +15,7 @@ | ||
| 15 | /* 颜色变量 */ | 15 | /* 颜色变量 */ |
| 16 | 16 | ||
| 17 | /* 行为相关颜色 */ | 17 | /* 行为相关颜色 */ |
| 18 | -$uni-color-primary: #2563eb; /* 欧美简洁风格主色 */ | 18 | +$uni-color-primary: #1F3A8A; /* 欧美简洁风格主色 */ |
| 19 | $uni-color-success: #4cd964; | 19 | $uni-color-success: #4cd964; |
| 20 | $uni-color-warning: #f0ad4e; | 20 | $uni-color-warning: #f0ad4e; |
| 21 | $uni-color-error: #dd524d; | 21 | $uni-color-error: #dd524d; |
| @@ -38,9 +38,9 @@ $uni-border-color: #c8c7cc; | @@ -38,9 +38,9 @@ $uni-border-color: #c8c7cc; | ||
| 38 | $uni-border-color-light: #e5e7eb; | 38 | $uni-border-color-light: #e5e7eb; |
| 39 | 39 | ||
| 40 | /* 主题色 —— 修改此处可全局换色(SCSS 编译时使用) */ | 40 | /* 主题色 —— 修改此处可全局换色(SCSS 编译时使用) */ |
| 41 | -$theme-primary: #4278bd; | ||
| 42 | -$theme-primary-dark: #2a288f; | ||
| 43 | -$theme-primary-light: #eef4fb; | 41 | +$theme-primary: #1F3A8A; |
| 42 | +$theme-primary-dark: #142a6c; | ||
| 43 | +$theme-primary-light: #e8ecf5; | ||
| 44 | 44 | ||
| 45 | /* 欧美版:卡片/区块 */ | 45 | /* 欧美版:卡片/区块 */ |
| 46 | $app-card-radius: 20rpx; | 46 | $app-card-radius: 20rpx; |
美国版/Food Labeling Management App UniApp/src/utils/printRecords.ts
0 → 100644
| 1 | +export interface PrintRecord { | ||
| 2 | + id: string | ||
| 3 | + productName: string | ||
| 4 | + category: string | ||
| 5 | + qty: number | ||
| 6 | + userName: string | ||
| 7 | + date: string | ||
| 8 | + dateFull: string | ||
| 9 | + time: string | ||
| 10 | + labelType?: string | ||
| 11 | + templateSize: string | ||
| 12 | + templateName: string | ||
| 13 | + printer: string | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +export const printRecordsList: PrintRecord[] = [ | ||
| 17 | + { id: '1', productName: 'Chicken Sandwich', category: 'Sandwich', qty: 2, userName: 'John Smith', date: '12/04', dateFull: 'Dec 4, 2025', time: '10:45 AM', templateSize: '2"x6"', templateName: "G'n'G", printer: 'Zebra ZD421' }, | ||
| 18 | + { id: '2', productName: 'Chicken', category: 'Meat', qty: 1, userName: 'John Smith', date: '12/04', dateFull: 'Dec 4, 2025', time: '10:32 AM', labelType: 'Defrost', templateSize: '2"x2"', templateName: 'Basic', printer: 'Zebra ZD421' }, | ||
| 19 | + { id: '3', productName: 'Caesar Salad', category: 'Salads', qty: 3, userName: 'Jane Doe', date: '12/04', dateFull: 'Dec 4, 2025', time: '09:15 AM', templateSize: '2"x4"', templateName: "G'n'G", printer: 'Brother QL-820NWB' }, | ||
| 20 | + { id: '4', productName: 'Beef', category: 'Meat', qty: 1, userName: 'John Smith', date: '12/03', dateFull: 'Dec 3, 2025', time: '4:20 PM', labelType: 'Heated', templateSize: '2"x2"', templateName: 'Basic', printer: 'Zebra ZD421' }, | ||
| 21 | + { id: '5', productName: 'Cheese Burger', category: 'Sandwich', qty: 2, userName: 'Jane Doe', date: '12/03', dateFull: 'Dec 3, 2025', time: '3:45 PM', templateSize: '2"x6"', templateName: "G'n'G", printer: 'Brother QL-820NWB' }, | ||
| 22 | + { id: '6', productName: 'Ice Cream', category: 'Frozen', qty: 1, userName: 'John Smith', date: '12/03', dateFull: 'Dec 3, 2025', time: '2:30 PM', labelType: 'Vanilla', templateSize: '2"x2"', templateName: 'Storage', printer: 'Epson TM-T88VI' }, | ||
| 23 | + { id: '7', productName: 'Milk', category: 'Dairy', qty: 1, userName: 'Jane Doe', date: '12/03', dateFull: 'Dec 3, 2025', time: '11:00 AM', templateSize: '2"x2"', templateName: 'Basic', printer: 'Zebra ZD421' }, | ||
| 24 | + { id: '8', productName: 'Turkey Club', category: 'Sandwich', qty: 1, userName: 'John Smith', date: '12/02', dateFull: 'Dec 2, 2025', time: '1:20 PM', templateSize: '2"x6"', templateName: "G'n'G", printer: 'Brother QL-820NWB' }, | ||
| 25 | +] | ||
| 26 | + | ||
| 27 | +export function getRecordById(id: string): PrintRecord | undefined { | ||
| 28 | + return printRecordsList.find(function (r) { return r.id === id }) | ||
| 29 | +} |
美国版/Food Labeling Management App UniApp/src/utils/stores.ts
0 → 100644
| 1 | +export interface StoreInfo { | ||
| 2 | + id: string | ||
| 3 | + nameKey: string | ||
| 4 | + address: string | ||
| 5 | + city: string | ||
| 6 | +} | ||
| 7 | + | ||
| 8 | +export const storeList: StoreInfo[] = [ | ||
| 9 | + { id: '1', nameKey: 'login.store1', address: '123 Main St', city: 'New York, NY 10001' }, | ||
| 10 | + { id: '2', nameKey: 'login.store2', address: '456 Oak Ave', city: 'Brooklyn, NY 11201' }, | ||
| 11 | + { id: '3', nameKey: 'login.store3', address: '789 Pine Rd', city: 'Queens, NY 11354' }, | ||
| 12 | + { id: '4', nameKey: 'login.store4', address: '321 Elm St', city: 'Manhattan, NY 10002' }, | ||
| 13 | +] | ||
| 14 | + | ||
| 15 | +export function getCurrentStoreId(): string { | ||
| 16 | + return uni.getStorageSync('storeId') || '1' | ||
| 17 | +} | ||
| 18 | + | ||
| 19 | +export function switchStore(id: string, storeName: string) { | ||
| 20 | + uni.setStorageSync('storeId', id) | ||
| 21 | + uni.setStorageSync('storeName', storeName) | ||
| 22 | +} |
美国版/Food Labeling Management App UniApp/vite.config.ts
| @@ -3,5 +3,25 @@ import uni from "@dcloudio/vite-plugin-uni"; | @@ -3,5 +3,25 @@ import uni from "@dcloudio/vite-plugin-uni"; | ||
| 3 | 3 | ||
| 4 | // https://vitejs.dev/config/ | 4 | // https://vitejs.dev/config/ |
| 5 | export default defineConfig({ | 5 | export default defineConfig({ |
| 6 | - plugins: [uni()], | 6 | + base: "/app/", |
| 7 | + plugins: [ | ||
| 8 | + uni(), | ||
| 9 | + { | ||
| 10 | + name: "redirect-root-to-app", | ||
| 11 | + configureServer(server) { | ||
| 12 | + server.middlewares.use((req, res, next) => { | ||
| 13 | + const url = (req.url || "/").split("?")[0]; | ||
| 14 | + if (url === "/" || url === "/index.html") { | ||
| 15 | + res.writeHead(302, { Location: "/app/" }); | ||
| 16 | + res.end(); | ||
| 17 | + return; | ||
| 18 | + } | ||
| 19 | + next(); | ||
| 20 | + }); | ||
| 21 | + }, | ||
| 22 | + }, | ||
| 23 | + ], | ||
| 24 | + server: { | ||
| 25 | + open: "/app/", | ||
| 26 | + }, | ||
| 7 | }); | 27 | }); |