Commit 940fb6ea83ce997c7907738bfb0837a7312a50ad
1 parent
b165f94a
又改了一个版本,这泰额太纠结了,一个设计,还要看示例数据对不对。
Showing
32 changed files
with
3586 additions
and
777 deletions
美国版/Food Labeling Management App UniApp/README.md
美国版/Food Labeling Management App UniApp/src/App.vue
| ... | ... | @@ -15,17 +15,30 @@ onHide(() => { |
| 15 | 15 | </script> |
| 16 | 16 | |
| 17 | 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 | 28 | background: #f9fafb; |
| 28 | 29 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; |
| 29 | 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 | 44 | </style> | ... | ... |
美国版/Food Labeling Management App UniApp/src/components/AppIcon.vue
| ... | ... | @@ -50,6 +50,9 @@ const icons: Record<string, string> = { |
| 50 | 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 | 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 | 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 | 58 | const svgContent = computed(() => icons[props.name] || icons.food) |
| ... | ... | @@ -80,9 +83,9 @@ const wrapStyle = computed(() => ({})) |
| 80 | 83 | .icon-lg { width: 64rpx; height: 64rpx; } |
| 81 | 84 | |
| 82 | 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 | 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 | 89 | .icon-orange :deep(svg) { stroke: #ea580c; } |
| 87 | 90 | .icon-green :deep(svg) { stroke: #16a34a; } |
| 88 | 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 | 14 | </view> |
| 15 | 15 | |
| 16 | 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 | 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 | 67 | </view> |
| 34 | 68 | |
| 35 | 69 | <view class="drawer-footer" :style="{ paddingBottom: (bottomSafeArea + 24) + 'px' }"> |
| ... | ... | @@ -70,27 +104,43 @@ const storeName = computed(() => { |
| 70 | 104 | return stored || 'MedVantage' |
| 71 | 105 | }) |
| 72 | 106 | |
| 107 | +const expandedKey = ref<string | null>(null) | |
| 108 | + | |
| 73 | 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 | 121 | { key: 'profile', path: '/pages/more/profile', icon: 'user', labelKey: 'more.profile' }, |
| 77 | 122 | { key: 'location', path: '/pages/more/location', icon: 'mapPin', labelKey: 'more.location' }, |
| 78 | 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 | 124 | { key: 'support', path: '/pages/more/support', icon: 'help', labelKey: 'more.support' }, |
| 81 | 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 | 132 | watch( |
| 85 | 133 | () => props.modelValue, |
| 86 | 134 | (val) => { |
| 87 | 135 | if (val) { |
| 88 | 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 | 140 | nextTick(() => { |
| 90 | 141 | animClass.value = 'opening' |
| 91 | 142 | }) |
| 92 | 143 | } else if (isShown.value) { |
| 93 | - // 先播放关闭动画,再卸载 | |
| 94 | 144 | animClass.value = 'closing' |
| 95 | 145 | setTimeout(() => { |
| 96 | 146 | isShown.value = false |
| ... | ... | @@ -112,7 +162,7 @@ const currentPath = computed(() => { |
| 112 | 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 | 167 | const handleLogout = () => { |
| 118 | 168 | uni.removeStorageSync('isLoggedIn') |
| ... | ... | @@ -152,7 +202,7 @@ const handleItemClick = (item: any) => { |
| 152 | 202 | .drawer-panel { |
| 153 | 203 | width: 76%; |
| 154 | 204 | max-width: 640rpx; |
| 155 | - background: #111827; | |
| 205 | + background: #1F3A8A; | |
| 156 | 206 | padding: 16rpx 32rpx 0; |
| 157 | 207 | box-shadow: 4rpx 0 32rpx rgba(0, 0, 0, 0.5); |
| 158 | 208 | display: flex; |
| ... | ... | @@ -253,13 +303,35 @@ const handleItemClick = (item: any) => { |
| 253 | 303 | } |
| 254 | 304 | |
| 255 | 305 | .menu-item.active { |
| 256 | - background: var(--theme-primary); | |
| 306 | + background: #1447E6; | |
| 257 | 307 | } |
| 258 | 308 | |
| 259 | 309 | .menu-item.active .menu-label { |
| 260 | 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 | 335 | .drawer-footer { |
| 264 | 336 | padding: 24rpx 0 48rpx; |
| 265 | 337 | border-top: 1rpx solid rgba(148, 163, 184, 0.2); | ... | ... |
美国版/Food Labeling Management App UniApp/src/components/TabBar.vue
美国版/Food Labeling Management App UniApp/src/locales/en.ts
| ... | ... | @@ -89,6 +89,9 @@ export default { |
| 89 | 89 | more: { |
| 90 | 90 | title: 'More', |
| 91 | 91 | profile: 'My Profile', 'profile.desc': 'View and edit your profile', |
| 92 | + report: 'Report', | |
| 93 | + printLog: 'Print Log', | |
| 94 | + labelReport: 'Label Report', | |
| 92 | 95 | printers: 'Printer Settings', 'printers.desc': 'Manage connected printers', |
| 93 | 96 | location: 'Location', 'location.desc': 'Change your work location', |
| 94 | 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 | 89 | more: { |
| 90 | 90 | title: '更多', |
| 91 | 91 | profile: '我的资料', 'profile.desc': '查看和编辑您的个人资料', |
| 92 | + report: '报表', | |
| 93 | + printLog: '打印日志', | |
| 94 | + labelReport: '标签报表', | |
| 92 | 95 | printers: '打印机设置', 'printers.desc': '管理连接的打印机', |
| 93 | 96 | location: '工作地点', 'location.desc': '更改您的工作地点', |
| 94 | 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 | 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 | 74 | "path": "pages/more/more", |
| 54 | 75 | "style": { |
| 55 | 76 | "navigationBarTitleText": "More", |
| ... | ... | @@ -97,6 +118,13 @@ |
| 97 | 118 | "navigationBarTitleText": "Support", |
| 98 | 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 | 130 | "globalStyle": { | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/index/index.vue
| ... | ... | @@ -5,12 +5,15 @@ |
| 5 | 5 | <view class="top-left-empty" /> |
| 6 | 6 | <view class="top-center"> |
| 7 | 7 | <image class="header-logo" src="/static/logo_us.png" mode="aspectFit" /> |
| 8 | - <text class="app-sub">{{ storeName }}</text> | |
| 9 | 8 | </view> |
| 10 | 9 | <view class="top-right" @click="isMenuOpen = true"> |
| 11 | 10 | <AppIcon name="menu" size="sm" color="white" /> |
| 12 | 11 | </view> |
| 13 | 12 | </view> |
| 13 | + <view class="hero-info"> | |
| 14 | + <text class="app-sub">{{ storeName }}</text> | |
| 15 | + <LocationPicker /> | |
| 16 | + </view> | |
| 14 | 17 | </view> |
| 15 | 18 | |
| 16 | 19 | <view class="main"> |
| ... | ... | @@ -40,6 +43,7 @@ import { ref, computed } from 'vue' |
| 40 | 43 | import { useI18n } from 'vue-i18n' |
| 41 | 44 | import AppIcon from '../../components/AppIcon.vue' |
| 42 | 45 | import SideMenu from '../../components/SideMenu.vue' |
| 46 | +import LocationPicker from '../../components/LocationPicker.vue' | |
| 43 | 47 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 44 | 48 | |
| 45 | 49 | const { t } = useI18n() |
| ... | ... | @@ -50,7 +54,7 @@ const isMenuOpen = ref(false) |
| 50 | 54 | |
| 51 | 55 | const quickActions = computed(() => [ |
| 52 | 56 | { |
| 53 | - label: t('nav.labels'), | |
| 57 | + label: t('Labeling'), | |
| 54 | 58 | icon: 'tag', |
| 55 | 59 | path: '/pages/labels/labels', |
| 56 | 60 | }, |
| ... | ... | @@ -114,9 +118,17 @@ const navTo = (path: string) => { |
| 114 | 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 | 128 | .app-sub { |
| 118 | 129 | font-size: 26rpx; |
| 119 | 130 | color: rgba(255, 255, 255, 0.9); |
| 131 | + margin-bottom: 12rpx; | |
| 120 | 132 | } |
| 121 | 133 | |
| 122 | 134 | .main { | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/labels/bluetooth.vue
| ... | ... | @@ -7,6 +7,7 @@ |
| 7 | 7 | </view> |
| 8 | 8 | <view class="top-center"> |
| 9 | 9 | <text class="page-title">Bluetooth Printer</text> |
| 10 | + <LocationPicker /> | |
| 10 | 11 | </view> |
| 11 | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| ... | ... | @@ -15,13 +16,6 @@ |
| 15 | 16 | </view> |
| 16 | 17 | |
| 17 | 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 | 19 | <view v-if="connectedDevice" class="connected-card"> |
| 26 | 20 | <view class="connected-header"> |
| 27 | 21 | <view class="connected-icon"> |
| ... | ... | @@ -37,45 +31,38 @@ |
| 37 | 31 | </view> |
| 38 | 32 | </view> |
| 39 | 33 | |
| 40 | - <!-- Scan section --> | |
| 41 | 34 | <view class="section-header"> |
| 42 | - <text class="section-title">Available Devices</text> | |
| 35 | + <text class="section-title">Available Printers</text> | |
| 43 | 36 | <view class="btn-scan" :class="{ scanning: isScanning }" @click="handleScan"> |
| 44 | 37 | <AppIcon name="refresh" size="sm" :color="isScanning ? 'white' : 'primary'" /> |
| 45 | 38 | <text class="btn-scan-text">{{ isScanning ? 'Scanning...' : 'Scan' }}</text> |
| 46 | 39 | </view> |
| 47 | 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 | 43 | <view class="scan-anim"> |
| 51 | 44 | <AppIcon name="bluetooth" size="lg" color="blue" /> |
| 52 | 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 | 47 | </view> |
| 61 | 48 | |
| 62 | 49 | <view v-else class="device-list"> |
| 63 | 50 | <view |
| 64 | - v-for="dev in devices" | |
| 51 | + v-for="dev in mockPrinters" | |
| 65 | 52 | :key="dev.deviceId" |
| 66 | 53 | class="device-card" |
| 67 | 54 | :class="{ connecting: connectingId === dev.deviceId }" |
| 68 | 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 | 59 | </view> |
| 73 | 60 | <view class="device-info"> |
| 74 | - <text class="device-name">{{ dev.name || 'Unknown Device' }}</text> | |
| 61 | + <text class="device-name">{{ dev.name }}</text> | |
| 75 | 62 | <text class="device-id">{{ dev.deviceId }}</text> |
| 76 | 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 | 66 | </view> |
| 80 | 67 | </view> |
| 81 | 68 | <view v-if="connectingId === dev.deviceId" class="device-action"> |
| ... | ... | @@ -102,40 +89,24 @@ |
| 102 | 89 | </template> |
| 103 | 90 | |
| 104 | 91 | <script setup lang="ts"> |
| 105 | -import { ref, computed, onMounted, onUnmounted } from 'vue' | |
| 92 | +import { ref, computed } from 'vue' | |
| 106 | 93 | import AppIcon from '../../components/AppIcon.vue' |
| 107 | 94 | import SideMenu from '../../components/SideMenu.vue' |
| 95 | +import LocationPicker from '../../components/LocationPicker.vue' | |
| 108 | 96 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 109 | 97 | |
| 110 | 98 | const statusBarHeight = getStatusBarHeight() |
| 111 | 99 | const isMenuOpen = ref(false) |
| 112 | 100 | const isScanning = ref(false) |
| 101 | +const showDevices = ref(true) | |
| 113 | 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 | 110 | const connectedDevice = computed(() => { |
| 140 | 111 | const name = uni.getStorageSync('btDeviceName') |
| 141 | 112 | const id = uni.getStorageSync('btDeviceId') |
| ... | ... | @@ -145,171 +116,35 @@ const connectedDevice = computed(() => { |
| 145 | 116 | |
| 146 | 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 | 120 | if (isScanning.value) { |
| 216 | - stopDiscovery() | |
| 121 | + isScanning.value = false | |
| 217 | 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 | 133 | if (connectingId.value) return |
| 240 | 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 | 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 | 148 | </script> |
| 314 | 149 | |
| 315 | 150 | <style scoped> |
| ... | ... | @@ -343,7 +178,9 @@ onUnmounted(() => { |
| 343 | 178 | |
| 344 | 179 | .top-center { |
| 345 | 180 | flex: 1; |
| 346 | - text-align: center; | |
| 181 | + display: flex; | |
| 182 | + flex-direction: column; | |
| 183 | + align-items: center; | |
| 347 | 184 | } |
| 348 | 185 | |
| 349 | 186 | .page-title { |
| ... | ... | @@ -356,31 +193,12 @@ onUnmounted(() => { |
| 356 | 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 | 196 | .connected-card { |
| 379 | 197 | background: #fff; |
| 380 | 198 | padding: 32rpx; |
| 381 | 199 | border-radius: 20rpx; |
| 382 | 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 | 202 | border: 2rpx solid #4ade80; |
| 385 | 203 | } |
| 386 | 204 | |
| ... | ... | @@ -424,8 +242,8 @@ onUnmounted(() => { |
| 424 | 242 | display: flex; |
| 425 | 243 | align-items: center; |
| 426 | 244 | justify-content: center; |
| 427 | - cursor: pointer; | |
| 428 | 245 | } |
| 246 | + | |
| 429 | 247 | .btn-disconnect-text { |
| 430 | 248 | font-size: 28rpx; |
| 431 | 249 | font-weight: 600; |
| ... | ... | @@ -433,7 +251,6 @@ onUnmounted(() => { |
| 433 | 251 | line-height: 1; |
| 434 | 252 | } |
| 435 | 253 | |
| 436 | -/* Section header */ | |
| 437 | 254 | .section-header { |
| 438 | 255 | display: flex; |
| 439 | 256 | justify-content: space-between; |
| ... | ... | @@ -456,7 +273,6 @@ onUnmounted(() => { |
| 456 | 273 | background: #fff; |
| 457 | 274 | border: 2rpx solid #e5e7eb; |
| 458 | 275 | border-radius: 14rpx; |
| 459 | - cursor: pointer; | |
| 460 | 276 | } |
| 461 | 277 | |
| 462 | 278 | .btn-scan-text { |
| ... | ... | @@ -475,7 +291,6 @@ onUnmounted(() => { |
| 475 | 291 | color: #fff; |
| 476 | 292 | } |
| 477 | 293 | |
| 478 | -/* Scanning state */ | |
| 479 | 294 | .scanning-wrap { |
| 480 | 295 | display: flex; |
| 481 | 296 | flex-direction: column; |
| ... | ... | @@ -505,30 +320,6 @@ onUnmounted(() => { |
| 505 | 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 | 323 | .device-list { |
| 533 | 324 | margin-bottom: 32rpx; |
| 534 | 325 | } |
| ... | ... | @@ -541,9 +332,7 @@ onUnmounted(() => { |
| 541 | 332 | display: flex; |
| 542 | 333 | align-items: center; |
| 543 | 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 | 338 | .device-card:active { |
| ... | ... | @@ -619,7 +408,6 @@ onUnmounted(() => { |
| 619 | 408 | font-weight: 500; |
| 620 | 409 | } |
| 621 | 410 | |
| 622 | -/* Tips */ | |
| 623 | 411 | .tips-card { |
| 624 | 412 | background: #fffbeb; |
| 625 | 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 | 75 | const allFoods = [ |
| 76 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 6 | <AppIcon name="chevronLeft" size="sm" color="white" /> |
| 7 | 7 | </view> |
| 8 | 8 | <view class="top-center"> |
| 9 | - <text class="app-name">{{ t('labels.title') }}</text> | |
| 9 | + <text class="app-name">Labeling</text> | |
| 10 | + <LocationPicker /> | |
| 10 | 11 | </view> |
| 11 | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| ... | ... | @@ -15,7 +16,7 @@ |
| 15 | 16 | |
| 16 | 17 | <view class="bt-bar" @click="goBluetoothPage"> |
| 17 | 18 | <view class="bt-left"> |
| 18 | - <AppIcon name="bluetooth" size="sm" :color="btConnected ? 'white' : 'white'" /> | |
| 19 | + <AppIcon name="bluetooth" size="sm" color="white" /> | |
| 19 | 20 | <text class="bt-text">{{ btConnected ? btDeviceName : 'No printer connected' }}</text> |
| 20 | 21 | </view> |
| 21 | 22 | <view class="bt-status" :class="{ connected: btConnected }"> |
| ... | ... | @@ -29,17 +30,17 @@ |
| 29 | 30 | <view class="body"> |
| 30 | 31 | <view class="sidebar"> |
| 31 | 32 | <view |
| 32 | - v-for="type in labelTypes" | |
| 33 | - :key="type.id" | |
| 33 | + v-for="cat in labelCategories" | |
| 34 | + :key="cat.id" | |
| 34 | 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 | 42 | </view> |
| 42 | - <text class="cat-name">{{ t(type.nameKey) }}</text> | |
| 43 | + <text class="cat-name">{{ cat.name }}</text> | |
| 43 | 44 | </view> |
| 44 | 45 | </view> |
| 45 | 46 | |
| ... | ... | @@ -52,34 +53,88 @@ |
| 52 | 53 | <input |
| 53 | 54 | v-model="searchTerm" |
| 54 | 55 | class="search-input" |
| 55 | - :placeholder="t('labels.searchFood')" | |
| 56 | + placeholder="Search products..." | |
| 56 | 57 | placeholder-class="placeholder" |
| 57 | 58 | /> |
| 58 | 59 | </view> |
| 59 | 60 | |
| 60 | - <view v-if="filteredFoods.length === 0" class="empty"> | |
| 61 | + <view v-if="filteredProductCategories.length === 0" class="empty"> | |
| 61 | 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 | 64 | </view> |
| 64 | 65 | |
| 65 | - <view v-else class="food-grid"> | |
| 66 | + <view v-else class="category-list"> | |
| 66 | 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 | 110 | </view> |
| 75 | - <text class="food-name">{{ t(food.nameKey) }}</text> | |
| 76 | - <text class="food-desc">{{ t(food.descKey) }}</text> | |
| 77 | 111 | </view> |
| 78 | 112 | </view> |
| 79 | 113 | </view> |
| 80 | 114 | </scroll-view> |
| 81 | 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 | 138 | <SideMenu v-model="isMenuOpen" /> |
| 84 | 139 | </view> |
| 85 | 140 | </template> |
| ... | ... | @@ -87,16 +142,19 @@ |
| 87 | 142 | <script setup lang="ts"> |
| 88 | 143 | import { ref, computed } from 'vue' |
| 89 | 144 | import { onShow } from '@dcloudio/uni-app' |
| 90 | -import { useI18n } from 'vue-i18n' | |
| 91 | 145 | import AppIcon from '../../components/AppIcon.vue' |
| 92 | 146 | import SideMenu from '../../components/SideMenu.vue' |
| 147 | +import LocationPicker from '../../components/LocationPicker.vue' | |
| 93 | 148 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 94 | 149 | |
| 95 | -const { t } = useI18n() | |
| 96 | 150 | const statusBarHeight = getStatusBarHeight() |
| 97 | 151 | const isMenuOpen = ref(false) |
| 98 | -const selectedType = ref('nutrition') | |
| 152 | +const selectedCategory = ref('prep') | |
| 99 | 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 | 159 | const btConnected = ref(false) |
| 102 | 160 | const btDeviceName = ref('') |
| ... | ... | @@ -107,33 +165,279 @@ onShow(() => { |
| 107 | 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 | 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 | 441 | const goBack = () => { |
| 138 | 442 | const pages = getCurrentPages() |
| 139 | 443 | if (pages.length > 1) { |
| ... | ... | @@ -147,10 +451,12 @@ const goBluetoothPage = () => { |
| 147 | 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 | 461 | </script> |
| 156 | 462 | |
| ... | ... | @@ -176,7 +482,8 @@ const goPreview = (foodId: string) => { |
| 176 | 482 | justify-content: space-between; |
| 177 | 483 | } |
| 178 | 484 | |
| 179 | -.top-left, .top-right { | |
| 485 | +.top-left, | |
| 486 | +.top-right { | |
| 180 | 487 | width: 64rpx; |
| 181 | 488 | height: 64rpx; |
| 182 | 489 | border-radius: 999rpx; |
| ... | ... | @@ -188,7 +495,9 @@ const goPreview = (foodId: string) => { |
| 188 | 495 | |
| 189 | 496 | .top-center { |
| 190 | 497 | flex: 1; |
| 191 | - text-align: center; | |
| 498 | + display: flex; | |
| 499 | + flex-direction: column; | |
| 500 | + align-items: center; | |
| 192 | 501 | } |
| 193 | 502 | |
| 194 | 503 | .app-name { |
| ... | ... | @@ -197,7 +506,6 @@ const goPreview = (foodId: string) => { |
| 197 | 506 | color: #ffffff; |
| 198 | 507 | } |
| 199 | 508 | |
| 200 | -/* Bluetooth status bar */ | |
| 201 | 509 | .bt-bar { |
| 202 | 510 | display: flex; |
| 203 | 511 | align-items: center; |
| ... | ... | @@ -207,7 +515,6 @@ const goPreview = (foodId: string) => { |
| 207 | 515 | margin-bottom: 16rpx; |
| 208 | 516 | background: rgba(255, 255, 255, 0.12); |
| 209 | 517 | border-radius: 16rpx; |
| 210 | - cursor: pointer; | |
| 211 | 518 | } |
| 212 | 519 | |
| 213 | 520 | .bt-left { |
| ... | ... | @@ -252,7 +559,6 @@ const goPreview = (foodId: string) => { |
| 252 | 559 | color: rgba(255, 255, 255, 0.85); |
| 253 | 560 | } |
| 254 | 561 | |
| 255 | -/* Body */ | |
| 256 | 562 | .body { |
| 257 | 563 | flex: 1; |
| 258 | 564 | display: flex; |
| ... | ... | @@ -272,7 +578,6 @@ const goPreview = (foodId: string) => { |
| 272 | 578 | flex-direction: column; |
| 273 | 579 | align-items: center; |
| 274 | 580 | padding: 20rpx 8rpx; |
| 275 | - cursor: pointer; | |
| 276 | 581 | position: relative; |
| 277 | 582 | } |
| 278 | 583 | |
| ... | ... | @@ -300,19 +605,31 @@ const goPreview = (foodId: string) => { |
| 300 | 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 | 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 | 633 | background: var(--theme-primary); |
| 317 | 634 | } |
| 318 | 635 | |
| ... | ... | @@ -374,23 +691,104 @@ const goPreview = (foodId: string) => { |
| 374 | 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 | 776 | .food-grid { |
| 378 | 777 | display: grid; |
| 379 | 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 | 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 | 790 | .food-card:active { |
| 393 | - background: #fafafa; | |
| 791 | + background: #f3f4f6; | |
| 394 | 792 | } |
| 395 | 793 | |
| 396 | 794 | .food-img-wrap { |
| ... | ... | @@ -398,8 +796,8 @@ const goPreview = (foodId: string) => { |
| 398 | 796 | position: relative; |
| 399 | 797 | padding-top: 75%; |
| 400 | 798 | border-radius: 10rpx; |
| 401 | - background: #f3f4f6; | |
| 402 | - margin-bottom: 12rpx; | |
| 799 | + background: #e5e7eb; | |
| 800 | + margin-bottom: 10rpx; | |
| 403 | 801 | overflow: hidden; |
| 404 | 802 | } |
| 405 | 803 | |
| ... | ... | @@ -411,12 +809,42 @@ const goPreview = (foodId: string) => { |
| 411 | 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 | 842 | .food-name { |
| 415 | 843 | font-size: 24rpx; |
| 416 | 844 | font-weight: 600; |
| 417 | 845 | color: #111827; |
| 418 | 846 | display: block; |
| 419 | - margin-bottom: 4rpx; | |
| 847 | + margin-bottom: 2rpx; | |
| 420 | 848 | overflow: hidden; |
| 421 | 849 | text-overflow: ellipsis; |
| 422 | 850 | white-space: nowrap; |
| ... | ... | @@ -425,11 +853,94 @@ const goPreview = (foodId: string) => { |
| 425 | 853 | .food-desc { |
| 426 | 854 | font-size: 20rpx; |
| 427 | 855 | color: #6b7280; |
| 428 | - display: -webkit-box; | |
| 429 | - -webkit-line-clamp: 2; | |
| 430 | - -webkit-box-orient: vertical; | |
| 856 | + display: block; | |
| 431 | 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 | 946 | @media (min-width: 768px) { |
| ... | ... | @@ -439,8 +950,8 @@ const goPreview = (foodId: string) => { |
| 439 | 950 | |
| 440 | 951 | .food-grid { |
| 441 | 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 | 957 | </style> | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue
| ... | ... | @@ -6,8 +6,8 @@ |
| 6 | 6 | <AppIcon name="chevronLeft" size="sm" color="white" /> |
| 7 | 7 | </view> |
| 8 | 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 | 11 | </view> |
| 12 | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 13 | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| ... | ... | @@ -16,20 +16,21 @@ |
| 16 | 16 | </view> |
| 17 | 17 | |
| 18 | 18 | <view class="content"> |
| 19 | - <!-- Food info --> | |
| 20 | 19 | <view class="food-card"> |
| 21 | - <image | |
| 22 | - :src="food && food.image ? food.image : ''" | |
| 23 | - class="food-img" | |
| 24 | - mode="aspectFill" | |
| 25 | - /> | |
| 26 | 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 | 31 | </view> |
| 30 | 32 | </view> |
| 31 | 33 | |
| 32 | - <!-- Print quantity --> | |
| 33 | 34 | <view class="qty-card"> |
| 34 | 35 | <text class="qty-label">Print Quantity</text> |
| 35 | 36 | <view class="qty-control"> |
| ... | ... | @@ -43,46 +44,32 @@ |
| 43 | 44 | </view> |
| 44 | 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 | 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 | 63 | </view> |
| 76 | 64 | </view> |
| 77 | 65 | |
| 78 | 66 | <view class="note-card"> |
| 79 | 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 | 69 | </view> |
| 82 | 70 | </view> |
| 83 | 71 | |
| 84 | - <!-- Bottom bar --> | |
| 85 | - <view class="bottom-bar" :style="{ paddingBottom: (bottomSafeArea + 20) + 'px' }"> | |
| 72 | + <view class="bottom-bar"> | |
| 86 | 73 | <view class="bottom-info"> |
| 87 | 74 | <text class="bottom-qty">{{ printQty }} label{{ printQty > 1 ? 's' : '' }}</text> |
| 88 | 75 | <view class="bt-indicator" :class="{ connected: btConnected }"> |
| ... | ... | @@ -96,47 +83,18 @@ |
| 96 | 83 | </view> |
| 97 | 84 | <view class="print-btn" :class="{ disabled: isPrinting }" @click="handlePrint"> |
| 98 | 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 | 87 | </view> |
| 101 | 88 | </view> |
| 102 | 89 | </view> |
| 103 | 90 | |
| 104 | - <!-- Preview modal --> | |
| 105 | 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 | 96 | </view> |
| 112 | 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 | 98 | </view> |
| 141 | 99 | </view> |
| 142 | 100 | |
| ... | ... | @@ -146,17 +104,13 @@ |
| 146 | 104 | |
| 147 | 105 | <script setup lang="ts"> |
| 148 | 106 | import { ref, computed } from 'vue' |
| 149 | -import { useI18n } from 'vue-i18n' | |
| 150 | 107 | import { onLoad, onShow } from '@dcloudio/uni-app' |
| 151 | 108 | import AppIcon from '../../components/AppIcon.vue' |
| 152 | 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 | 113 | const statusBarHeight = getStatusBarHeight() |
| 157 | -const bottomSafeArea = getBottomSafeArea() | |
| 158 | -const labelType = ref('nutrition') | |
| 159 | -const foodId = ref('food-001') | |
| 160 | 114 | const isPrinting = ref(false) |
| 161 | 115 | const isMenuOpen = ref(false) |
| 162 | 116 | const printQty = ref(1) |
| ... | ... | @@ -165,98 +119,85 @@ const showPreviewModal = ref(false) |
| 165 | 119 | const btConnected = ref(false) |
| 166 | 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 | 143 | onShow(() => { |
| 169 | 144 | const name = uni.getStorageSync('btDeviceName') |
| 170 | 145 | btConnected.value = !!name |
| 171 | 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 | 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 | 199 | const increment = () => { if (printQty.value < 99) printQty.value++ } |
| 199 | 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 | 201 | const goBack = () => uni.navigateBack() |
| 261 | 202 | |
| 262 | 203 | const handlePrint = () => { |
| ... | ... | @@ -276,7 +217,7 @@ const handlePrint = () => { |
| 276 | 217 | isPrinting.value = true |
| 277 | 218 | setTimeout(() => { |
| 278 | 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 | 221 | }, 2000) |
| 281 | 222 | } |
| 282 | 223 | </script> |
| ... | ... | @@ -285,31 +226,61 @@ const handlePrint = () => { |
| 285 | 226 | .page { |
| 286 | 227 | min-height: 100vh; |
| 287 | 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 | 236 | .header-hero { |
| 292 | 237 | background: linear-gradient(135deg, var(--theme-primary), var(--theme-primary-dark)); |
| 293 | 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 | 273 | .content { |
| 307 | 274 | padding: 32rpx; |
| 308 | 275 | padding-bottom: 300rpx; |
| 309 | 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 | 284 | .food-card { |
| 314 | 285 | background: #fff; |
| 315 | 286 | padding: 28rpx; |
| ... | ... | @@ -317,204 +288,428 @@ const handlePrint = () => { |
| 317 | 288 | margin-bottom: 24rpx; |
| 318 | 289 | display: flex; |
| 319 | 290 | align-items: center; |
| 291 | + justify-content: space-between; | |
| 320 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 520 | .bt-dot { |
| 431 | - width: 12rpx; height: 12rpx; border-radius: 50%; | |
| 521 | + width: 12rpx; | |
| 522 | + height: 12rpx; | |
| 523 | + border-radius: 50%; | |
| 432 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 715 | </style> | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/login/login.vue
| ... | ... | @@ -31,7 +31,7 @@ |
| 31 | 31 | </view> |
| 32 | 32 | <view class="form-row"> |
| 33 | 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 | 35 | <text class="remember-text">{{ t('login.rememberMe') }}</text> |
| 36 | 36 | </view> |
| 37 | 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 | 7 | </view> |
| 8 | 8 | <view class="top-center"> |
| 9 | 9 | <text class="page-title">{{ t('language.title') }}</text> |
| 10 | + <LocationPicker /> | |
| 10 | 11 | </view> |
| 11 | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| ... | ... | @@ -44,6 +45,7 @@ import { useI18n } from 'vue-i18n' |
| 44 | 45 | import { setLocale } from '../../utils/i18n' |
| 45 | 46 | import AppIcon from '../../components/AppIcon.vue' |
| 46 | 47 | import SideMenu from '../../components/SideMenu.vue' |
| 48 | +import LocationPicker from '../../components/LocationPicker.vue' | |
| 47 | 49 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 48 | 50 | |
| 49 | 51 | const { t, locale } = useI18n() |
| ... | ... | @@ -100,7 +102,9 @@ const handleChange = (code: 'en' | 'zh') => { |
| 100 | 102 | |
| 101 | 103 | .top-center { |
| 102 | 104 | flex: 1; |
| 103 | - text-align: center; | |
| 105 | + display: flex; | |
| 106 | + flex-direction: column; | |
| 107 | + align-items: center; | |
| 104 | 108 | } |
| 105 | 109 | |
| 106 | 110 | .page-title { | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/more/location.vue
| ... | ... | @@ -7,6 +7,7 @@ |
| 7 | 7 | </view> |
| 8 | 8 | <view class="top-center"> |
| 9 | 9 | <text class="page-title">{{ t('location.title') }}</text> |
| 10 | + <LocationPicker /> | |
| 10 | 11 | </view> |
| 11 | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| ... | ... | @@ -131,6 +132,7 @@ import { ref, computed } from 'vue' |
| 131 | 132 | import { useI18n } from 'vue-i18n' |
| 132 | 133 | import AppIcon from '../../components/AppIcon.vue' |
| 133 | 134 | import SideMenu from '../../components/SideMenu.vue' |
| 135 | +import LocationPicker from '../../components/LocationPicker.vue' | |
| 134 | 136 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 135 | 137 | |
| 136 | 138 | const { t } = useI18n() |
| ... | ... | @@ -200,7 +202,9 @@ const handleSwitch = () => { |
| 200 | 202 | |
| 201 | 203 | .top-center { |
| 202 | 204 | flex: 1; |
| 203 | - text-align: center; | |
| 205 | + display: flex; | |
| 206 | + flex-direction: column; | |
| 207 | + align-items: center; | |
| 204 | 208 | } |
| 205 | 209 | |
| 206 | 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 | 7 | </view> |
| 8 | 8 | <view class="top-center"> |
| 9 | 9 | <text class="page-title">{{ t('profile.title') }}</text> |
| 10 | + <LocationPicker /> | |
| 10 | 11 | </view> |
| 11 | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| ... | ... | @@ -17,35 +18,56 @@ |
| 17 | 18 | <view class="content"> |
| 18 | 19 | <view class="avatar-wrap"> |
| 19 | 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 | 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 | 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 | 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 | 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 | 64 | </view> |
| 38 | 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 | 71 | </view> |
| 50 | 72 | </view> |
| 51 | 73 | |
| ... | ... | @@ -58,11 +80,11 @@ import { ref } from 'vue' |
| 58 | 80 | import { useI18n } from 'vue-i18n' |
| 59 | 81 | import AppIcon from '../../components/AppIcon.vue' |
| 60 | 82 | import SideMenu from '../../components/SideMenu.vue' |
| 83 | +import LocationPicker from '../../components/LocationPicker.vue' | |
| 61 | 84 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 62 | 85 | |
| 63 | 86 | const { t } = useI18n() |
| 64 | 87 | const statusBarHeight = getStatusBarHeight() |
| 65 | -const isEditing = ref(false) | |
| 66 | 88 | const isMenuOpen = ref(false) |
| 67 | 89 | const name = ref(uni.getStorageSync('userName') || 'John Smith') |
| 68 | 90 | const email = ref('john.smith@company.com') |
| ... | ... | @@ -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 | 106 | </script> |
| 87 | 107 | |
| ... | ... | @@ -116,7 +136,9 @@ const handleSave = () => { |
| 116 | 136 | |
| 117 | 137 | .top-center { |
| 118 | 138 | flex: 1; |
| 119 | - text-align: center; | |
| 139 | + display: flex; | |
| 140 | + flex-direction: column; | |
| 141 | + align-items: center; | |
| 120 | 142 | } |
| 121 | 143 | |
| 122 | 144 | .page-title { |
| ... | ... | @@ -131,93 +153,114 @@ const handleSave = () => { |
| 131 | 153 | |
| 132 | 154 | .avatar-wrap { |
| 133 | 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 | 161 | .avatar { |
| 139 | - width: 160rpx; | |
| 140 | - height: 160rpx; | |
| 162 | + width: 140rpx; | |
| 163 | + height: 140rpx; | |
| 141 | 164 | background: var(--theme-primary); |
| 142 | 165 | border-radius: 50%; |
| 143 | 166 | display: flex; |
| 144 | 167 | align-items: center; |
| 145 | 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 | 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 | 194 | display: flex; |
| 195 | + align-items: center; | |
| 183 | 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 | 202 | background: #f3f4f6; |
| 203 | +} | |
| 204 | + | |
| 205 | +.info-icon-box { | |
| 206 | + width: 72rpx; | |
| 207 | + height: 72rpx; | |
| 190 | 208 | border-radius: 16rpx; |
| 191 | 209 | display: flex; |
| 192 | 210 | align-items: center; |
| 193 | 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 | 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 | 263 | font-weight: 600; |
| 220 | - color: #fff; | |
| 221 | - line-height: 1; | |
| 264 | + color: #111827; | |
| 222 | 265 | } |
| 223 | 266 | </style> | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/more/support.vue
| ... | ... | @@ -7,6 +7,7 @@ |
| 7 | 7 | </view> |
| 8 | 8 | <view class="top-center"> |
| 9 | 9 | <text class="page-title">{{ t('support.title') }}</text> |
| 10 | + <LocationPicker /> | |
| 10 | 11 | </view> |
| 11 | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| ... | ... | @@ -120,6 +121,7 @@ import { ref } from 'vue' |
| 120 | 121 | import { useI18n } from 'vue-i18n' |
| 121 | 122 | import AppIcon from '../../components/AppIcon.vue' |
| 122 | 123 | import SideMenu from '../../components/SideMenu.vue' |
| 124 | +import LocationPicker from '../../components/LocationPicker.vue' | |
| 123 | 125 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 124 | 126 | |
| 125 | 127 | const { t } = useI18n() |
| ... | ... | @@ -175,7 +177,9 @@ const callEmergency = () => { |
| 175 | 177 | |
| 176 | 178 | .top-center { |
| 177 | 179 | flex: 1; |
| 178 | - text-align: center; | |
| 180 | + display: flex; | |
| 181 | + flex-direction: column; | |
| 182 | + align-items: center; | |
| 179 | 183 | } |
| 180 | 184 | |
| 181 | 185 | .page-title { | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/more/sync.vue
| ... | ... | @@ -7,6 +7,7 @@ |
| 7 | 7 | </view> |
| 8 | 8 | <view class="top-center"> |
| 9 | 9 | <text class="page-title">{{ t('sync.title') }}</text> |
| 10 | + <LocationPicker /> | |
| 10 | 11 | </view> |
| 11 | 12 | <view class="top-right" @click="isMenuOpen = true"> |
| 12 | 13 | <AppIcon name="menu" size="sm" color="white" /> |
| ... | ... | @@ -71,6 +72,7 @@ import { ref } from 'vue' |
| 71 | 72 | import { useI18n } from 'vue-i18n' |
| 72 | 73 | import AppIcon from '../../components/AppIcon.vue' |
| 73 | 74 | import SideMenu from '../../components/SideMenu.vue' |
| 75 | +import LocationPicker from '../../components/LocationPicker.vue' | |
| 74 | 76 | import { getStatusBarHeight } from '../../utils/statusBar' |
| 75 | 77 | |
| 76 | 78 | const { t } = useI18n() |
| ... | ... | @@ -130,7 +132,9 @@ const handleSync = () => { |
| 130 | 132 | |
| 131 | 133 | .top-center { |
| 132 | 134 | flex: 1; |
| 133 | - text-align: center; | |
| 135 | + display: flex; | |
| 136 | + flex-direction: column; | |
| 137 | + align-items: center; | |
| 134 | 138 | } |
| 135 | 139 | |
| 136 | 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 | 15 | /* 颜色变量 */ |
| 16 | 16 | |
| 17 | 17 | /* 行为相关颜色 */ |
| 18 | -$uni-color-primary: #2563eb; /* 欧美简洁风格主色 */ | |
| 18 | +$uni-color-primary: #1F3A8A; /* 欧美简洁风格主色 */ | |
| 19 | 19 | $uni-color-success: #4cd964; |
| 20 | 20 | $uni-color-warning: #f0ad4e; |
| 21 | 21 | $uni-color-error: #dd524d; |
| ... | ... | @@ -38,9 +38,9 @@ $uni-border-color: #c8c7cc; |
| 38 | 38 | $uni-border-color-light: #e5e7eb; |
| 39 | 39 | |
| 40 | 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 | 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 | 3 | |
| 4 | 4 | // https://vitejs.dev/config/ |
| 5 | 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 | }); | ... | ... |