Commit 940fb6ea83ce997c7907738bfb0837a7312a50ad

Authored by “wangming”
1 parent b165f94a

又改了一个版本,这泰额太纠结了,一个设计,还要看示例数据对不对。

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

26 KB

美国版/Food Labeling Management App UniApp/src/static/lable2.png 0 → 100644

67.5 KB

美国版/Food Labeling Management App UniApp/src/uni.scss
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 /* 颜色变量 */ 15 /* 颜色变量 */
16 16
17 /* 行为相关颜色 */ 17 /* 行为相关颜色 */
18 -$uni-color-primary: #2563eb; /* 欧美简洁风格主色 */ 18 +$uni-color-primary: #1F3A8A; /* 欧美简洁风格主色 */
19 $uni-color-success: #4cd964; 19 $uni-color-success: #4cd964;
20 $uni-color-warning: #f0ad4e; 20 $uni-color-warning: #f0ad4e;
21 $uni-color-error: #dd524d; 21 $uni-color-error: #dd524d;
@@ -38,9 +38,9 @@ $uni-border-color: #c8c7cc; @@ -38,9 +38,9 @@ $uni-border-color: #c8c7cc;
38 $uni-border-color-light: #e5e7eb; 38 $uni-border-color-light: #e5e7eb;
39 39
40 /* 主题色 —— 修改此处可全局换色(SCSS 编译时使用) */ 40 /* 主题色 —— 修改此处可全局换色(SCSS 编译时使用) */
41 -$theme-primary: #4278bd;  
42 -$theme-primary-dark: #2a288f;  
43 -$theme-primary-light: #eef4fb; 41 +$theme-primary: #1F3A8A;
  42 +$theme-primary-dark: #142a6c;
  43 +$theme-primary-light: #e8ecf5;
44 44
45 /* 欧美版:卡片/区块 */ 45 /* 欧美版:卡片/区块 */
46 $app-card-radius: 20rpx; 46 $app-card-radius: 20rpx;
美国版/Food Labeling Management App UniApp/src/utils/printRecords.ts 0 → 100644
  1 +export interface PrintRecord {
  2 + id: string
  3 + productName: string
  4 + category: string
  5 + qty: number
  6 + userName: string
  7 + date: string
  8 + dateFull: string
  9 + time: string
  10 + labelType?: string
  11 + templateSize: string
  12 + templateName: string
  13 + printer: string
  14 +}
  15 +
  16 +export const printRecordsList: PrintRecord[] = [
  17 + { id: '1', productName: 'Chicken Sandwich', category: 'Sandwich', qty: 2, userName: 'John Smith', date: '12/04', dateFull: 'Dec 4, 2025', time: '10:45 AM', templateSize: '2"x6"', templateName: "G'n'G", printer: 'Zebra ZD421' },
  18 + { id: '2', productName: 'Chicken', category: 'Meat', qty: 1, userName: 'John Smith', date: '12/04', dateFull: 'Dec 4, 2025', time: '10:32 AM', labelType: 'Defrost', templateSize: '2"x2"', templateName: 'Basic', printer: 'Zebra ZD421' },
  19 + { id: '3', productName: 'Caesar Salad', category: 'Salads', qty: 3, userName: 'Jane Doe', date: '12/04', dateFull: 'Dec 4, 2025', time: '09:15 AM', templateSize: '2"x4"', templateName: "G'n'G", printer: 'Brother QL-820NWB' },
  20 + { id: '4', productName: 'Beef', category: 'Meat', qty: 1, userName: 'John Smith', date: '12/03', dateFull: 'Dec 3, 2025', time: '4:20 PM', labelType: 'Heated', templateSize: '2"x2"', templateName: 'Basic', printer: 'Zebra ZD421' },
  21 + { id: '5', productName: 'Cheese Burger', category: 'Sandwich', qty: 2, userName: 'Jane Doe', date: '12/03', dateFull: 'Dec 3, 2025', time: '3:45 PM', templateSize: '2"x6"', templateName: "G'n'G", printer: 'Brother QL-820NWB' },
  22 + { id: '6', productName: 'Ice Cream', category: 'Frozen', qty: 1, userName: 'John Smith', date: '12/03', dateFull: 'Dec 3, 2025', time: '2:30 PM', labelType: 'Vanilla', templateSize: '2"x2"', templateName: 'Storage', printer: 'Epson TM-T88VI' },
  23 + { id: '7', productName: 'Milk', category: 'Dairy', qty: 1, userName: 'Jane Doe', date: '12/03', dateFull: 'Dec 3, 2025', time: '11:00 AM', templateSize: '2"x2"', templateName: 'Basic', printer: 'Zebra ZD421' },
  24 + { id: '8', productName: 'Turkey Club', category: 'Sandwich', qty: 1, userName: 'John Smith', date: '12/02', dateFull: 'Dec 2, 2025', time: '1:20 PM', templateSize: '2"x6"', templateName: "G'n'G", printer: 'Brother QL-820NWB' },
  25 +]
  26 +
  27 +export function getRecordById(id: string): PrintRecord | undefined {
  28 + return printRecordsList.find(function (r) { return r.id === id })
  29 +}
美国版/Food Labeling Management App UniApp/src/utils/stores.ts 0 → 100644
  1 +export interface StoreInfo {
  2 + id: string
  3 + nameKey: string
  4 + address: string
  5 + city: string
  6 +}
  7 +
  8 +export const storeList: StoreInfo[] = [
  9 + { id: '1', nameKey: 'login.store1', address: '123 Main St', city: 'New York, NY 10001' },
  10 + { id: '2', nameKey: 'login.store2', address: '456 Oak Ave', city: 'Brooklyn, NY 11201' },
  11 + { id: '3', nameKey: 'login.store3', address: '789 Pine Rd', city: 'Queens, NY 11354' },
  12 + { id: '4', nameKey: 'login.store4', address: '321 Elm St', city: 'Manhattan, NY 10002' },
  13 +]
  14 +
  15 +export function getCurrentStoreId(): string {
  16 + return uni.getStorageSync('storeId') || '1'
  17 +}
  18 +
  19 +export function switchStore(id: string, storeName: string) {
  20 + uni.setStorageSync('storeId', id)
  21 + uni.setStorageSync('storeName', storeName)
  22 +}
美国版/Food Labeling Management App UniApp/vite.config.ts
@@ -3,5 +3,25 @@ import uni from &quot;@dcloudio/vite-plugin-uni&quot;; @@ -3,5 +3,25 @@ import uni from &quot;@dcloudio/vite-plugin-uni&quot;;
3 3
4 // https://vitejs.dev/config/ 4 // https://vitejs.dev/config/
5 export default defineConfig({ 5 export default defineConfig({
6 - plugins: [uni()], 6 + base: "/app/",
  7 + plugins: [
  8 + uni(),
  9 + {
  10 + name: "redirect-root-to-app",
  11 + configureServer(server) {
  12 + server.middlewares.use((req, res, next) => {
  13 + const url = (req.url || "/").split("?")[0];
  14 + if (url === "/" || url === "/index.html") {
  15 + res.writeHead(302, { Location: "/app/" });
  16 + res.end();
  17 + return;
  18 + }
  19 + next();
  20 + });
  21 + },
  22 + },
  23 + ],
  24 + server: {
  25 + open: "/app/",
  26 + },
7 }); 27 });