Forráskód Böngészése

智能柜项目提交

skyline 2 hónapja
szülő
commit
02acac0448

+ 1 - 1
haha-admin-web/.env

@@ -1,5 +1,5 @@
 # 平台本地运行端口号
-VITE_PORT = 8848
+VITE_PORT = 8888
 
 # 是否隐藏首页 隐藏 true 不隐藏 false (勿删除,VITE_HIDE_HOME只需在.env文件配置)
 VITE_HIDE_HOME = false

+ 1 - 1
haha-admin-web/.env.development

@@ -1,5 +1,5 @@
 # 平台本地运行端口号
-VITE_PORT = 8848
+VITE_PORT = 8888
 
 # 开发环境读取配置文件路径
 VITE_PUBLIC_PATH = /

+ 17 - 0
haha-admin-web/src/style/dark.scss

@@ -222,6 +222,23 @@ html.dark {
       background-image: initial !important;
     }
 
+    /* 修复暗色模式下消息图标颜色 */
+    &.el-message--success .el-message__content {
+      color: var(--el-color-success) !important;
+    }
+
+    &.el-message--warning .el-message__content {
+      color: var(--el-color-warning) !important;
+    }
+
+    &.el-message--error .el-message__content {
+      color: var(--el-color-error) !important;
+    }
+
+    &.el-message--info .el-message__content {
+      color: var(--el-color-info) !important;
+    }
+
     & .el-message__closeBtn {
       &:hover {
         color: rgb(255 255 255 / 85%);

+ 34 - 1
haha-admin-web/src/style/element-plus.scss

@@ -101,7 +101,7 @@
   }
 }
 
-/* 克隆并自定义 ElMessage 样式,不会影响 ElMessage 原本样式,在 src/utils/message.ts 中调用自定义样式 ElMessage 方法即可,整体暗色风格在 src/style/dark.scss 文件进行了适配 */
+/* 克隆并自定义 ElMessage 样式,不会影响 ElMessage 原本样式,在 src/utils/message.ts中调用自定义样式 ElMessage 方法即可,整体暗色风格在 src/style/dark.scss文件进行了适配 */
 .pure-message {
   background: #fff !important;
   border-width: 0 !important;
@@ -116,6 +116,39 @@
     background-image: initial !important;
   }
 
+  /* 修复消息图标颜色 */
+  &.el-message--success .el-message__content {
+    color: var(--el-color-success) !important;
+  }
+
+  &.el-message--success .el-message__icon {
+    color: var(--el-color-success) !important;
+  }
+
+  &.el-message--warning .el-message__content {
+    color: var(--el-color-warning) !important;
+  }
+
+  &.el-message--warning .el-message__icon {
+    color: var(--el-color-warning) !important;
+  }
+
+  &.el-message--error .el-message__content {
+    color: var(--el-color-error) !important;
+  }
+
+  &.el-message--error .el-message__icon {
+    color: var(--el-color-error) !important;
+  }
+
+  &.el-message--info .el-message__content {
+    color: var(--el-color-info) !important;
+  }
+
+  &.el-message--info .el-message__icon {
+    color: var(--el-color-info) !important;
+  }
+
   & .el-message__closeBtn {
     outline: none;
     border-radius: 4px;

+ 25 - 1
haha-admin-web/src/utils/message.ts

@@ -1,6 +1,12 @@
 import type { VNode } from "vue";
 import { isFunction } from "@pureadmin/utils";
 import { type MessageHandler, ElMessage } from "element-plus";
+import {
+  CircleCheck,
+  CircleClose,
+  WarningFilled,
+  InfoFilled
+} from "@element-plus/icons-vue";
 
 type messageStyle = "el" | "antd";
 type messageTypes = "info" | "success" | "warning" | "error";
@@ -72,9 +78,27 @@ const message = (
       onClose
     } = params;
 
+    // 根据类型自动设置图标(确保图标语义正确)
+    let finalIcon = icon;
+    if (!finalIcon) {
+      if (type === "success") {
+        // success 使用对勾图标
+        finalIcon = CircleCheck;
+      } else if (type === "warning") {
+        // warning 使用感叹号图标
+        finalIcon = WarningFilled;
+      } else if (type === "error") {
+        // error 使用叉号图标
+        finalIcon = CircleClose;
+      } else {
+        // info 使用信息图标
+        finalIcon = InfoFilled;
+      }
+    }
+
     return ElMessage({
       message,
-      icon,
+      icon: finalIcon,
       type,
       plain,
       dangerouslyUseHTMLString,

+ 249 - 84
haha-admin-web/src/views/shop/utils/hook.tsx

@@ -5,6 +5,7 @@ import type { PaginationProps } from "@pureadmin/table";
 import { deviceDetection } from "@pureadmin/utils";
 import { AmapWithSearch } from "@/components/AmapWithSearch";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import { regionData, CodeToText, TextToCode } from "@/utils/chinaArea";
 import {
   getShopList,
   createShop,
@@ -21,7 +22,7 @@ import {
 } from "@/api/shop";
 import { getAllRoles } from "@/api/role";
 import { getAdminList } from "@/api/admin";
-import { type Ref, ref, toRaw, reactive, onMounted } from "vue";
+import { type Ref, ref, toRaw, reactive, onMounted, computed } from "vue";
 import {
   ElForm,
   ElInput,
@@ -239,6 +240,68 @@ export function useShop(tableRef: Ref) {
     status: 1
   });
 
+  // 省市区联动选择器
+  const selectedProvince = ref("");
+  const selectedCity = ref("");
+  const selectedDistrict = ref("");
+
+  // 省份列表
+  const provinceOptions = computed(() => regionData);
+
+  // 城市列表(根据省份联动)
+  const cityOptions = computed(() => {
+    const province = regionData.find((p: any) => p.value === selectedProvince.value);
+    return province?.children || [];
+  });
+
+  // 区县列表(根据城市联动)
+  const districtOptions = computed(() => {
+    const province = regionData.find((p: any) => p.value === selectedProvince.value);
+    const city = province?.children?.find((c: any) => c.value === selectedCity.value);
+    return city?.children || [];
+  });
+
+  // 详细地址的详细部分(不含省市区前缀)
+  const addressDetail = ref("");
+
+  // 更新详细地址前缀
+  function updateAddressPrefix() {
+    const provinceText = shopForm.province || "";
+    const cityText = shopForm.city || "";
+    const districtText = shopForm.district || "";
+    const prefix = provinceText + cityText + districtText;
+    
+    // 更新详细地址:前缀 + 用户输入的详细部分
+    shopForm.address = prefix + addressDetail.value;
+  }
+
+  
+  // 省份变化时,清空城市和区县
+  function handleProvinceChange(val: string) {
+    selectedCity.value = "";
+    selectedDistrict.value = "";
+    shopForm.province = CodeToText[val] || "";
+    shopForm.city = "";
+    shopForm.district = "";
+    addressDetail.value = "";
+    updateAddressPrefix();
+  }
+
+  
+  // 城市变化时,清空区县
+  function handleCityChange(val: string) {
+    selectedDistrict.value = "";
+    shopForm.city = CodeToText[val] || "";
+    shopForm.district = "";
+    updateAddressPrefix();
+  }
+  
+  // 区县变化时
+  function handleDistrictChange(val: string) {
+    shopForm.district = CodeToText[val] || "";
+    updateAddressPrefix();
+  }
+
   // 打开新增/编辑弹窗
   function openDialog(title = "新增", row?: ShopItem) {
     Object.assign(shopForm, {
@@ -255,120 +318,220 @@ export function useShop(tableRef: Ref) {
       status: row?.status ?? 1
     });
 
+    
+    // 初始化省市区选择器
+    if (row?.province) {
+      selectedProvince.value = TextToCode[row.province]?.code || "";
+    } else {
+      selectedProvince.value = "";
+    }
+    if (row?.city && selectedProvince.value) {
+      selectedCity.value = TextToCode[row.province]?.[row.city]?.code || "";
+    } else {
+      selectedCity.value = "";
+    }
+    if (row?.district && selectedCity.value) {
+      selectedDistrict.value = TextToCode[row.province]?.[row.city]?.[row.district]?.code || "";
+    } else {
+      selectedDistrict.value = "";
+    }
+
+    // 从地址中提取详细部分(去除省市区前缀)
+    if (row?.address) {
+      const prefix = (row.province || "") + (row.city || "") + (row.district || "");
+      addressDetail.value = row.address.startsWith(prefix) 
+        ? row.address.substring(prefix.length) 
+        : row.address;
+    } else {
+      addressDetail.value = "";
+    }
+
     addDialog({
       title: `${title}门店`,
-      width: "60%",
+      width: "900px",
       draggable: true,
       fullscreen: deviceDetection(),
       closeOnClickModal: false,
+      center: true,
       contentRenderer: () => (
-        <div>
-          <ElForm ref={ruleFormRef} model={shopForm} label-width="100px">
-            <ElFormItem
-              label="门店名称"
-              prop="name"
-              rules={[{ required: true, message: "请输入门店名称", trigger: "blur" }]}
-            >
-              <ElInput v-model={shopForm.name} placeholder="请输入门店名称" clearable />
-            </ElFormItem>
-            <ElFormItem label="省份" prop="province">
-              <ElInput v-model={shopForm.province} placeholder="请输入省份" clearable />
-            </ElFormItem>
-            <ElFormItem label="城市" prop="city">
-              <ElInput v-model={shopForm.city} placeholder="请输入城市" clearable />
-            </ElFormItem>
-            <ElFormItem label="区县" prop="district">
-              <ElInput v-model={shopForm.district} placeholder="请输入区县" clearable />
-            </ElFormItem>
+        <div class="p-6">
+          <ElForm ref={ruleFormRef} model={shopForm} label-width="90px" size="default">
+            <div class="grid grid-cols-2 gap-x-8 gap-y-2">
+              <ElFormItem
+                label="门店名称"
+                prop="name"
+                rules={[{ required: true, message: "请输入门店名称", trigger: "blur" }]}
+              >
+                <ElInput v-model={shopForm.name} placeholder="请输入门店名称" clearable />
+              </ElFormItem>
+              <ElFormItem label="状态" prop="status">
+                <ElRadioGroup v-model={shopForm.status}>
+                  <ElRadioButton value={1}>营业中</ElRadioButton>
+                  <ElRadioButton value={0}>已停业</ElRadioButton>
+                </ElRadioGroup>
+              </ElFormItem>
+            </div>
+            
+            <div class="grid grid-cols-3 gap-x-4 gap-y-2">
+              <ElFormItem label="省份" prop="province">
+                <ElSelect
+                  v-model={selectedProvince.value}
+                  placeholder="请选择省份"
+                  clearable
+                  filterable
+                  onChange={handleProvinceChange}
+                >
+                  {provinceOptions.value.map((item: any) => (
+                    <ElOption key={item.value} label={item.label} value={item.value} />
+                  ))}
+                </ElSelect>
+              </ElFormItem>
+              <ElFormItem label="城市" prop="city">
+                <ElSelect
+                  v-model={selectedCity.value}
+                  placeholder="请选择城市"
+                  clearable
+                  filterable
+                  disabled={!selectedProvince.value}
+                  onChange={handleCityChange}
+                >
+                  {cityOptions.value.map((item: any) => (
+                    <ElOption key={item.value} label={item.label} value={item.value} />
+                  ))}
+                </ElSelect>
+              </ElFormItem>
+              <ElFormItem label="区县" prop="district">
+                <ElSelect
+                  v-model={selectedDistrict.value}
+                  placeholder="请选择区县"
+                  clearable
+                  filterable
+                  disabled={!selectedCity.value}
+                  onChange={handleDistrictChange}
+                >
+                  {districtOptions.value.map((item: any) => (
+                    <ElOption key={item.value} label={item.label} value={item.value} />
+                  ))}
+                </ElSelect>
+              </ElFormItem>
+            </div>
+            
             <ElFormItem
               label="详细地址"
               prop="address"
               rules={[{ required: true, message: "请输入详细地址", trigger: "blur" }]}
             >
-              <ElInput v-model={shopForm.address} placeholder="请输入详细地址" clearable />
-            </ElFormItem>
-            <ElFormItem label="联系人" prop="contactName">
-              <ElInput v-model={shopForm.contactName} placeholder="请输入联系人" clearable />
-            </ElFormItem>
-            <ElFormItem label="联系电话" prop="contactPhone">
-              <ElInput v-model={shopForm.contactPhone} placeholder="请输入联系电话" clearable />
-            </ElFormItem>
-            <ElFormItem label="营业时间" prop="businessHours">
-              <ElInput v-model={shopForm.businessHours} placeholder="如: 08:00-22:00" clearable />
-            </ElFormItem>
-            <ElFormItem label="状态" prop="status">
-              <ElRadioGroup v-model={shopForm.status}>
-                <ElRadioButton value={1}>营业中</ElRadioButton>
-                <ElRadioButton value={0}>已停业</ElRadioButton>
-              </ElRadioGroup>
-            </ElFormItem>
-          </ElForm>
-          <div class="mt-4">
-            <div class="text-sm font-medium mb-2">门店位置</div>
-            <div class="grid grid-cols-2 gap-4 mb-4">
-              <ElFormItem label="经度" prop="longitude" class="flex-1">
-                <ElInput
-                  v-model={shopForm.longitude}
-                  placeholder="请输入经度"
-                  clearable
-                  onChange={(val: any) => handleLngLatChange("longitude", val)}
+              <div class="flex items-center gap-2 w-full">
+                <ElInput 
+                  value={shopForm.province + shopForm.city + shopForm.district} 
+                  placeholder="省市区" 
+                  readonly 
+                  class="w-[180px]!" 
+                  style="flex-shrink: 0"
                 />
-              </ElFormItem>
-              <ElFormItem label="纬度" prop="latitude" class="flex-1">
-                <ElInput
-                  v-model={shopForm.latitude}
-                  placeholder="请输入纬度"
-                  clearable
-                  onChange={(val: any) => handleLngLatChange("latitude", val)}
+                <ElInput 
+                  v-model={addressDetail.value} 
+                  placeholder="请输入详细地址(如:xx路xx号)" 
+                  clearable 
+                  class="flex-1"
+                  onInput={() => updateAddressPrefix()}
                 />
+              </div>
+            </ElFormItem>
+            
+            <div class="grid grid-cols-3 gap-x-4 gap-y-2">
+              <ElFormItem label="联系人" prop="contactName">
+                <ElInput v-model={shopForm.contactName} placeholder="请输入联系人" clearable />
+              </ElFormItem>
+              <ElFormItem label="联系电话" prop="contactPhone">
+                <ElInput v-model={shopForm.contactPhone} placeholder="请输入联系电话" clearable />
+              </ElFormItem>
+              <ElFormItem label="营业时间" prop="businessHours">
+                <ElInput v-model={shopForm.businessHours} placeholder="如: 08:00-22:00" clearable />
               </ElFormItem>
             </div>
-            <AmapWithSearch
-              longitude={Number(shopForm.longitude) || undefined}
-              latitude={Number(shopForm.latitude) || undefined}
-              height="350px"
-              zoom={15}
-              enableClickMark={true}
-              showSearch={true}
-              securityJsCode="bcfcaa8d8ef22452d4cf6d3892646262"
-              onUpdate:longitude={(val: number) => (shopForm.longitude = String(val))}
-              onUpdate:latitude={(val: number) => (shopForm.latitude = String(val))}
-              onMarkerMoved={(lng: number, lat: number) => {
-                shopForm.longitude = String(lng);
-                shopForm.latitude = String(lat);
-              }}
-            />
-            <div class="text-xs text-gray-500 mt-2">
-              <span>提示:</span>可以通过地址搜索快速定位,也可以点击地图或拖动标记点来精确选择门店位置
+            
+            <div class="border-t border-gray-200 pt-4 mt-4">
+              <div class="flex items-center gap-2 mb-4">
+                <div class="w-1 h-4 bg-primary rounded"></div>
+                <span class="text-sm font-medium text-gray-700">门店位置</span>
+              </div>
+              
+              <div class="grid grid-cols-2 gap-x-8 gap-y-2 mb-4">
+                <ElFormItem label="经度" prop="longitude">
+                  <ElInput
+                    v-model={shopForm.longitude}
+                    placeholder="请输入经度"
+                    clearable
+                    onChange={(val: any) => handleLngLatChange("longitude", val)}
+                  />
+                </ElFormItem>
+                <ElFormItem label="纬度" prop="latitude">
+                  <ElInput
+                    v-model={shopForm.latitude}
+                    placeholder="请输入纬度"
+                    clearable
+                    onChange={(val: any) => handleLngLatChange("latitude", val)}
+                  />
+                </ElFormItem>
+              </div>
+              
+              <AmapWithSearch
+                longitude={Number(shopForm.longitude) || undefined}
+                latitude={Number(shopForm.latitude) || undefined}
+                height="280px"
+                zoom={15}
+                enableClickMark={true}
+                showSearch={true}
+                securityJsCode="bcfcaa8d8ef22452d4cf6d3892646262"
+                onUpdate:longitude={(val: number) => (shopForm.longitude = String(val))}
+                onUpdate:latitude={(val: number) => (shopForm.latitude = String(val))}
+                onMarkerMoved={(lng: number, lat: number) => {
+                  shopForm.longitude = String(lng);
+                  shopForm.latitude = String(lat);
+                }}
+              />
+              <div class="text-xs text-gray-500 mt-2 flex items-center gap-1">
+                <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
+                  <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
+                </svg>
+                <span>可以通过地址搜索快速定位,也可以点击地图或拖动标记点来精确选择门店位置</span>
+              </div>
             </div>
-          </div>
+          </ElForm>
         </div>
       ),
-      beforeSure: async (done) => {
+      beforeSure: async (done, { closeLoading }) => {
         const valid = await ruleFormRef.value.validate().catch(() => false);
-        if (!valid) return;
+        if (!valid) {
+          closeLoading();
+          return;
+        }
 
         try {
           if (title === "新增") {
             const res = await createShop(toRaw(shopForm));
-            if (res.code === 0) {
+            if (res.code === 0 || res.code === 200) {
               message(`新增门店 ${shopForm.name} 成功`, { type: "success" });
               done();
               onSearch();
             } else {
+              closeLoading();
               message(res.message || "新增失败", { type: "error" });
             }
           } else {
             const res = await updateShop(row.id, toRaw(shopForm));
-            if (res.code === 0) {
+            if (res.code === 0 || res.code === 200) {
               message(`修改门店 ${shopForm.name} 成功`, { type: "success" });
               done();
               onSearch();
             } else {
+              closeLoading();
               message(res.message || "修改失败", { type: "error" });
             }
           }
         } catch (error) {
+          closeLoading();
           message("操作失败", { type: "error" });
         }
       }
@@ -379,7 +542,7 @@ export function useShop(tableRef: Ref) {
   async function handleDelete(row: ShopItem) {
     try {
       const res = await deleteShop(row.id);
-      if (res.code === 0) {
+      if (res.code === 0 || res.code === 200) {
         message(`已成功删除门店 ${row.name}`, { type: "success" });
         onSearch();
       } else {
@@ -389,21 +552,21 @@ export function useShop(tableRef: Ref) {
       message("删除失败", { type: "error" });
     }
   }
-
+  
   // 切换门店状态
   async function handleToggleStatus(row: ShopItem) {
     const newStatus = row.status === 1 ? 0 : 1;
     const actionText = newStatus === 1 ? "营业" : "停业";
-
+  
     try {
-      await ElMessageBox.confirm(`确认要${actionText}门店 ${row.name} 吗?`, "系统提示", {
+      await ElMessageBox.confirm(`确认要${actionText}门店 ${row.name} 吗`, "系统提示", {
         confirmButtonText: "确定",
         cancelButtonText: "取消",
         type: "warning"
       });
-
+  
       const res = await toggleShopStatus(row.id, { status: newStatus });
-      if (res.code === 0) {
+      if (res.code === 0 || res.code === 200) {
         row.status = newStatus;
         message(`已${actionText}门店 ${row.name}`, { type: "success" });
       } else {
@@ -443,7 +606,7 @@ export function useShop(tableRef: Ref) {
           />
         </div>
       ),
-      beforeSure: async (done) => {
+      beforeSure: async (done, { closeLoading }) => {
         try {
           // 先解除所有关联
           for (const device of linkedDevices) {
@@ -461,6 +624,7 @@ export function useShop(tableRef: Ref) {
           done();
           onSearch();
         } catch (error) {
+          closeLoading();
           message("操作失败", { type: "error" });
         }
       }
@@ -495,7 +659,7 @@ export function useShop(tableRef: Ref) {
           />
         </div>
       ),
-      beforeSure: async (done) => {
+      beforeSure: async (done, { closeLoading }) => {
         try {
           // 先移除所有补货员
           for (const r of currentReplenishers) {
@@ -509,6 +673,7 @@ export function useShop(tableRef: Ref) {
           done();
           onSearch();
         } catch (error) {
+          closeLoading();
           message("操作失败", { type: "error" });
         }
       }

+ 1 - 1
haha-admin-web/vite.config.ts

@@ -20,7 +20,7 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
     },
     // 服务端渲染
     server: {
-      port: VITE_PORT,
+      port: 8888,
       host: "0.0.0.0",
       proxy: {
         "/login": {