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

26 KB

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

67.5 KB

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