Parcourir la source

系统配置基本完成

Huanyi il y a 2 semaines
Parent
commit
f829df3665
37 fichiers modifiés avec 1651 ajouts et 187 suppressions
  1. 1 1
      index.html
  2. 5 1
      src/App.vue
  3. 26 0
      src/api/order/setting/index.ts
  4. 19 0
      src/api/order/setting/types.ts
  5. 26 0
      src/api/system/appSetting/index.ts
  6. 27 0
      src/api/system/appSetting/types.ts
  7. 26 0
      src/api/system/customerServiceSetting/index.ts
  8. 41 0
      src/api/system/customerServiceSetting/types.ts
  9. 26 0
      src/api/system/customerSetting/index.ts
  10. 41 0
      src/api/system/customerSetting/types.ts
  11. 26 0
      src/api/system/mapSetting/index.ts
  12. 23 0
      src/api/system/mapSetting/types.ts
  13. 29 0
      src/api/system/websiteSetting/index.ts
  14. 19 0
      src/api/system/websiteSetting/types.ts
  15. 1 1
      src/components/CustomerDetailDrawer/index.vue
  16. 1 1
      src/components/PetDetailDrawer/index.vue
  17. 0 0
      src/json/archieves.json
  18. 1 1
      src/json/fulfiller.json
  19. 7 7
      src/layout/components/Sidebar/Logo.vue
  20. 5 0
      src/permission.ts
  21. 56 2
      src/store/modules/settings.ts
  22. 3 2
      src/utils/dynamicTitle.ts
  23. 8 3
      src/views/archieves/customer/index.vue
  24. 1 1
      src/views/fulfiller/pool/index.vue
  25. 7 2
      src/views/login.vue
  26. 137 69
      src/views/order/dispatch/index.vue
  27. 1 1
      src/views/order/orderList/components/RewardDialog.vue
  28. 1 1
      src/views/order/orderList/index.vue
  29. 114 75
      src/views/system/store/index.vue
  30. 177 0
      src/views/systemConfig/app/index.vue
  31. 12 17
      src/views/systemConfig/index.vue
  32. 183 0
      src/views/systemConfig/platform/components/CustomerConfig.vue
  33. 131 0
      src/views/systemConfig/platform/components/MapConfig.vue
  34. 120 0
      src/views/systemConfig/platform/components/OrderConfig.vue
  35. 103 0
      src/views/systemConfig/platform/index.vue
  36. 245 0
      src/views/systemConfig/website/index.vue
  37. 2 2
      vite.config.ts

+ 1 - 1
index.html

@@ -7,7 +7,7 @@
     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
 <!--    <link rel="icon" href="/favicon.ico" />-->
     <link rel="icon" href="/?" />
-    <title>%VITE_APP_TITLE%</title>
+    <title></title>
     <!--[if lt IE 11
       ]><script>
         window.location.href = '/html/ie.html';

+ 5 - 1
src/App.vue

@@ -10,11 +10,15 @@ import { handleThemeStyle } from '@/utils/theme';
 import { useAppStore } from '@/store/modules/app';
 
 const appStore = useAppStore();
+const settingsStore = useSettingsStore();
+
+// 提前加载网站设置,确保护理小结、标签等渲染时不显示旧版标题
+settingsStore.fetchWebsiteSetting();
 
 onMounted(() => {
   nextTick(() => {
     // 初始化主题样式
-    handleThemeStyle(useSettingsStore().theme);
+    handleThemeStyle(settingsStore.theme);
   });
 });
 </script>

+ 26 - 0
src/api/order/setting/index.ts

@@ -0,0 +1,26 @@
+import request from '@/utils/request';
+import { OrderSettingVO, OrderSettingForm } from './types';
+import { AxiosPromise } from 'axios';
+
+/**
+ * 获取订单设置详情
+ * @param id 设置ID
+ */
+export function getOrderSetting(id: number | string): AxiosPromise<OrderSettingVO> {
+  return request({
+    url: `/order/setting/${id}`,
+    method: 'get'
+  });
+}
+
+/**
+ * 修改订单设置
+ * @param data 设置数据
+ */
+export function updateOrderSetting(data: OrderSettingForm): AxiosPromise<void> {
+  return request({
+    url: '/order/setting/edit',
+    method: 'put',
+    data: data
+  });
+}

+ 19 - 0
src/api/order/setting/types.ts

@@ -0,0 +1,19 @@
+/**
+ * 订单设置对象类型
+ */
+export interface OrderSettingVO {
+  /** 主键ID */
+  id: number | string;
+  /** 超时取消时间 (如:分钟) */
+  timeoutCancelTime: number;
+}
+
+/**
+ * 订单设置表单类型
+ */
+export interface OrderSettingForm {
+  /** 主键ID */
+  id: number | string;
+  /** 超时取消时间 (如:分钟) */
+  timeoutCancelTime: number;
+}

+ 26 - 0
src/api/system/appSetting/index.ts

@@ -0,0 +1,26 @@
+import request from '@/utils/request';
+import { AppSettingVO, AppSettingForm } from './types';
+import { AxiosPromise } from 'axios';
+
+/**
+ * 获取APP配置详情
+ * @param id 配置ID
+ */
+export function getAppSetting(id: number | string): AxiosPromise<AppSettingVO> {
+  return request({
+    url: `/system/appSetting/${id}`,
+    method: 'get'
+  });
+}
+
+/**
+ * 修改APP配置
+ * @param data 配置数据
+ */
+export function updateAppSetting(data: AppSettingForm): AxiosPromise<void> {
+  return request({
+    url: '/system/appSetting/edit',
+    method: 'put',
+    data: data
+  });
+}

+ 27 - 0
src/api/system/appSetting/types.ts

@@ -0,0 +1,27 @@
+/**
+ * APP配置对象类型
+ */
+export interface AppSettingVO {
+  /** 主键ID */
+  id: number | string;
+  /** 登录页图标ID */
+  loginIcon: number | string;
+  /** 登录页图标URL */
+  loginIconUrl?: string;
+  /** 登录页背景ID */
+  loginBackground: number | string;
+  /** 登录页背景URL */
+  loginBackgroundUrl?: string;
+}
+
+/**
+ * APP配置更新参数类型
+ */
+export interface AppSettingForm {
+  /** 主键ID */
+  id: number | string;
+  /** 登录页图标ID */
+  loginIcon: number | string;
+  /** 登录页背景ID */
+  loginBackground: number | string;
+}

+ 26 - 0
src/api/system/customerServiceSetting/index.ts

@@ -0,0 +1,26 @@
+import request from '@/utils/request';
+import { CustomerServiceSettingVO, CustomerServiceSettingForm } from './types';
+import { AxiosPromise } from 'axios';
+
+/**
+ * 获取客服配置详情
+ * @param id 配置ID
+ */
+export function getCustomerServiceSetting(id: number | string): AxiosPromise<CustomerServiceSettingVO> {
+  return request({
+    url: `/system/customerServiceSetting/${id}`,
+    method: 'get'
+  });
+}
+
+/**
+ * 修改客服配置
+ * @param data 配置数据
+ */
+export function updateCustomerServiceSetting(data: CustomerServiceSettingForm): AxiosPromise<void> {
+  return request({
+    url: '/system/customerServiceSetting/edit',
+    method: 'put',
+    data: data
+  });
+}

+ 41 - 0
src/api/system/customerServiceSetting/types.ts

@@ -0,0 +1,41 @@
+/**
+ * 客服设置对象类型
+ */
+export interface CustomerServiceSettingVO {
+  /** 主键ID */
+  id: number | string;
+  /** 微信账号 */
+  wechatAccount: string;
+  /** 电话号码 */
+  phoneNumber: string;
+  /** 客服开始时间 */
+  startServiceTime: string;
+  /** 客服结束时间 */
+  endServiceTime: string;
+  /** 二维码ID */
+  qrCode: number | string;
+  /** 二维码URL */
+  qrCodeUrl?: string;
+  /** 企业微信链接 */
+  enterpriseWechatLink: string;
+}
+
+/**
+ * 客服设置表单类型
+ */
+export interface CustomerServiceSettingForm {
+  /** 主键ID */
+  id: number | string;
+  /** 微信账号 */
+  wechatAccount: string;
+  /** 电话号码 */
+  phoneNumber: string;
+  /** 客服开始时间 */
+  startServiceTime: string;
+  /** 客服结束时间 */
+  endServiceTime: string;
+  /** 二维码ID */
+  qrCode: number | string;
+  /** 企业微信链接 */
+  enterpriseWechatLink: string;
+}

+ 26 - 0
src/api/system/customerSetting/index.ts

@@ -0,0 +1,26 @@
+import request from '@/utils/request';
+import { CustomerSettingVO, CustomerSettingForm } from './types';
+import { AxiosPromise } from 'axios';
+
+/**
+ * 获取客服配置详情
+ * @param id 配置ID
+ */
+export function getCustomerSetting(id: number | string): AxiosPromise<CustomerSettingVO> {
+  return request({
+    url: `/system/customerSetting/${id}`,
+    method: 'get'
+  });
+}
+
+/**
+ * 修改客服配置
+ * @param data 配置数据
+ */
+export function updateCustomerSetting(data: CustomerSettingForm): AxiosPromise<void> {
+  return request({
+    url: '/system/customerSetting/edit',
+    method: 'put',
+    data: data
+  });
+}

+ 41 - 0
src/api/system/customerSetting/types.ts

@@ -0,0 +1,41 @@
+/**
+ * 客服设置对象类型
+ */
+export interface CustomerSettingVO {
+  /** 主键ID */
+  id: number | string;
+  /** 微信账号 */
+  wechatAccount: string;
+  /** 电话号码 */
+  phoneNumber: string;
+  /** 客服开始时间 */
+  startServiceTime: string;
+  /** 客服结束时间 */
+  endServiceTime: string;
+  /** 二维码ID */
+  qrCode: number | string;
+  /** 二维码URL */
+  qrCodeUrl?: string;
+  /** 企业微信链接 */
+  enterpriseWechatLink: string;
+}
+
+/**
+ * 客服设置表单类型
+ */
+export interface CustomerSettingForm {
+  /** 主键ID */
+  id: number | string;
+  /** 微信账号 */
+  wechatAccount: string;
+  /** 电话号码 */
+  phoneNumber: string;
+  /** 客服开始时间 */
+  startServiceTime: string;
+  /** 客服结束时间 */
+  endServiceTime: string;
+  /** 二维码ID */
+  qrCode: number | string;
+  /** 企业微信链接 */
+  enterpriseWechatLink: string;
+}

+ 26 - 0
src/api/system/mapSetting/index.ts

@@ -0,0 +1,26 @@
+import request from '@/utils/request';
+import { MapSettingVO, MapSettingForm } from './types';
+import { AxiosPromise } from 'axios';
+
+/**
+ * 获取地图配置详情
+ * @param id 配置ID
+ */
+export function getMapSetting(id: number | string): AxiosPromise<MapSettingVO> {
+  return request({
+    url: `/system/mapSetting/${id}`,
+    method: 'get'
+  });
+}
+
+/**
+ * 修改地图配置
+ * @param data 配置数据
+ */
+export function updateMapSetting(data: MapSettingForm): AxiosPromise<void> {
+  return request({
+    url: '/system/mapSetting/edit',
+    method: 'put',
+    data: data
+  });
+}

+ 23 - 0
src/api/system/mapSetting/types.ts

@@ -0,0 +1,23 @@
+/**
+ * 地图Keys设置对象类型
+ */
+export interface MapSettingVO {
+  /** 主键ID */
+  id: number | string;
+  /** 地图Key */
+  apiKey: string;
+  /** 地图Secret */
+  apiSecret: string;
+}
+
+/**
+ * 地图Keys设置表单类型
+ */
+export interface MapSettingForm {
+  /** 主键ID */
+  id: number | string;
+  /** 地图Key */
+  apiKey: string;
+  /** 地图Secret */
+  apiSecret: string;
+}

+ 29 - 0
src/api/system/websiteSetting/index.ts

@@ -0,0 +1,29 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { WebsiteSettingVO, WebsiteSettingEditForm } from './types';
+
+/**
+ * 获取网站设置
+ */
+export const getWebsiteSetting = (id: number | string = 1): AxiosPromise<WebsiteSettingVO> => {
+  return request({
+    url: '/system/websiteSetting/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 修改网站设置
+ */
+export const editWebsiteSetting = (data: WebsiteSettingEditForm) => {
+  return request({
+    url: '/system/websiteSetting/edit',
+    method: 'put',
+    data: data
+  });
+};
+
+export default {
+    getWebsiteSetting,
+    editWebsiteSetting
+};

+ 19 - 0
src/api/system/websiteSetting/types.ts

@@ -0,0 +1,19 @@
+export interface WebsiteSettingVO {
+  id: number | string;
+  icon: number | string;
+  iconUrl: string;
+  title: string;
+  loginTitle: string;
+  loginBackground: number | string;
+  loginBackgroundUrl: string;
+  menuTitle: string;
+}
+
+export interface WebsiteSettingEditForm {
+  id: number | string;
+  icon: number | string;
+  title: string;
+  loginTitle: string;
+  loginBackground: number | string;
+  menuTitle: string;
+}

+ 1 - 1
src/components/CustomerDetailDrawer/index.vue

@@ -126,7 +126,7 @@ import { listAllChangeLog } from '@/api/archieves/changeLog';
 import { listOnStore } from '@/api/system/areaStation';
 import { listSubOrderOnCustomer } from '@/api/order/subOrder/index';
 import { listAllService } from '@/api/service/list/index';
-import archievesEnum from '@/enums/archieves.json';
+import archievesEnum from '@/json/archieves.json';
 
 const { changeLogType } = archievesEnum;
 

+ 1 - 1
src/components/PetDetailDrawer/index.vue

@@ -121,7 +121,7 @@ import { getPet, updatePet, updatePetRemark } from '@/api/archieves/pet'
 import { listAllChangeLog } from '@/api/archieves/changeLog'
 import { listSubOrderOnPet } from '@/api/order/subOrder/index'
 import { ElMessage } from 'element-plus'
-import archievesEnum from '@/enums/archieves.json'
+import archievesEnum from '@/json/archieves.json'
 
 const { changeLogType } = archievesEnum
 

+ 0 - 0
src/enums/archieves.json → src/json/archieves.json


+ 1 - 1
src/enums/fulfiller.json → src/json/fulfiller.json

@@ -89,4 +89,4 @@
       "tagType": "info"
     }
   }
-}
+}

+ 7 - 7
src/layout/components/Sidebar/Logo.vue

@@ -6,15 +6,13 @@
   >
     <transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
       <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
-<!--        <img v-if="logo" :src="logo" class="sidebar-logo" />-->
-<!--        <h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">-->
-<!--          {{ title }}-->
-<!--        </h1>-->
+        <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
+          {{ websiteSetting.menuTitle }}
+        </h1>
       </router-link>
       <router-link v-else key="expand" class="sidebar-logo-link" to="/">
-<!--        <img v-if="logo" :src="logo" class="sidebar-logo" />-->
         <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
-          {{ title }}
+          {{ websiteSetting.menuTitle }}
         </h1>
       </router-link>
     </transition>
@@ -25,6 +23,7 @@
 import variables from '@/assets/styles/variables.module.scss';
 import logo from '@/assets/logo/logo.png';
 import { useSettingsStore } from '@/store/modules/settings';
+import { storeToRefs } from 'pinia';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 defineProps({
@@ -34,8 +33,9 @@ defineProps({
   }
 });
 
-const title = import.meta.env.VITE_APP_LOGO_TITLE;
 const settingsStore = useSettingsStore();
+const { websiteSetting } = storeToRefs(settingsStore);
+const title = import.meta.env.VITE_APP_LOGO_TITLE;
 const sideTheme = computed(() => settingsStore.sideTheme);
 </script>
 

+ 5 - 0
src/permission.ts

@@ -19,6 +19,11 @@ const isWhiteList = (path: string) => {
 
 router.beforeEach(async (to, from, next) => {
   NProgress.start();
+  // 加载网站设置
+  const settingsStore = useSettingsStore();
+  if (!settingsStore.isFetchedWebsiteSetting) {
+    await settingsStore.fetchWebsiteSetting();
+  }
   if (getToken()) {
     to.meta.title && useSettingsStore().setTitle(to.meta.title as string);
     /* has token*/

+ 56 - 2
src/store/modules/settings.ts

@@ -3,6 +3,8 @@ import defaultSettings from '@/settings';
 import { useDynamicTitle } from '@/utils/dynamicTitle';
 import { useStorage } from '@vueuse/core';
 import { ref } from 'vue';
+import { getWebsiteSetting } from '@/api/system/websiteSetting';
+import { WebsiteSettingVO } from '@/api/system/websiteSetting/types';
 
 export const useSettingsStore = defineStore('setting', () => {
   const storageSetting = useStorage<LayoutSetting>('layout-setting', {
@@ -15,7 +17,7 @@ export const useSettingsStore = defineStore('setting', () => {
     sideTheme: defaultSettings.sideTheme,
     theme: defaultSettings.theme
   });
-  const title = ref<string>(defaultSettings.title);
+  const title = ref<string>('');
   const theme = ref<string>(storageSetting.value.theme);
   const sideTheme = ref<string>(storageSetting.value.sideTheme);
   const showSettings = ref<boolean>(defaultSettings.showSettings);
@@ -28,10 +30,59 @@ export const useSettingsStore = defineStore('setting', () => {
   const animationEnable = ref<boolean>(defaultSettings.animationEnable);
   const dark = ref<boolean>(defaultSettings.dark);
 
+  // 网站设置数据
+  const websiteSetting = ref<WebsiteSettingVO>({
+    id: 0,
+    icon: 0,
+    iconUrl: '',
+    title: '',
+    loginTitle: '',
+    loginBackground: 0,
+    loginBackgroundUrl: '',
+    menuTitle: ''
+  });
+  const isFetchedWebsiteSetting = ref(false);
+
   const setTitle = (value: string) => {
     title.value = value;
     useDynamicTitle();
   };
+
+  /**
+   * 获取网站设置并应用
+   */
+  const fetchWebsiteSetting = async () => {
+    if (isFetchedWebsiteSetting.value) return;
+    const res = await getWebsiteSetting();
+    if (res.code === 200) {
+      websiteSetting.value = res.data;
+      // 同步更新网页标题
+      if (res.data.title) {
+        setTitle(res.data.title);
+      }
+      // 更新 Favicon
+      if (res.data.iconUrl) {
+        updateFavicon(res.data.iconUrl);
+      }
+      isFetchedWebsiteSetting.value = true;
+    }
+  };
+
+  /**
+   * 更新 Favicon
+   */
+  const updateFavicon = (url: string) => {
+    let link: HTMLLinkElement | null = document.querySelector("link[rel*='icon']");
+    if (link) {
+      link.href = url;
+    } else {
+      link = document.createElement('link');
+      link.rel = 'shortcut icon';
+      link.href = url;
+      document.getElementsByTagName('head')[0].appendChild(link);
+    }
+  };
+
   return {
     title,
     theme,
@@ -45,6 +96,9 @@ export const useSettingsStore = defineStore('setting', () => {
     dynamicTitle,
     animationEnable,
     dark,
-    setTitle
+    websiteSetting,
+    isFetchedWebsiteSetting,
+    setTitle,
+    fetchWebsiteSetting
   };
 });

+ 3 - 2
src/utils/dynamicTitle.ts

@@ -6,9 +6,10 @@ import { useSettingsStore } from '@/store/modules/settings';
  */
 export const useDynamicTitle = () => {
   const settingsStore = useSettingsStore();
+  const baseTitle = settingsStore.websiteSetting.title || import.meta.env.VITE_APP_TITLE;
   if (settingsStore.dynamicTitle) {
-    document.title = settingsStore.title + ' - ' + import.meta.env.VITE_APP_TITLE;
+    document.title = settingsStore.title + ' - ' + baseTitle;
   } else {
-    document.title = defaultSettings.title as string;
+    document.title = baseTitle;
   }
 };

+ 8 - 3
src/views/archieves/customer/index.vue

@@ -8,9 +8,9 @@
             <el-cascader
               v-model="searchAreaValue"
               :options="areaTreeOptions"
-              :props="{ checkStrictly: true, value: 'id', label: 'name' }"
+              :props="{ value: 'id', label: 'name' }"
               placeholder="所属站点"
-              style="width: 350px; margin-right: 10px"
+              style="width: 240px; margin-right: 10px"
               clearable
               @change="onSearchAreaChange"
             />
@@ -558,7 +558,12 @@ const areaTreeOptions = computed(() => {
       .filter(item => String(item.parentId) === String(parentId))
       .map(item => {
         const children = buildTree(data, item.id)
-        const node = { id: item.id, name: item.name }
+        const node = { 
+          id: item.id, 
+          name: item.name,
+          // 如果不是站点且没有下级了,则不可选(防止误选区域叶子点)
+          disabled: Number(item.type) !== 2 && (!children || children.length === 0)
+        }
         if (children.length > 0) node.children = children
         return node
       })

+ 1 - 1
src/views/fulfiller/pool/index.vue

@@ -701,7 +701,7 @@ import { listAllTag } from '@/api/fulfiller/tag'
 import type { FlfTagVO } from '@/api/fulfiller/tag/types'
 import { listAreaStation as listOnStore } from '@/api/system/areaStation'
 import type { AreaStationVO as SysAreaStationOnStoreVo } from '@/api/system/areaStation/types'
-import fulfillerEnums from '@/enums/fulfiller.json'
+import fulfillerEnums from '@/json/fulfiller.json'
 import ImageUpload from '@/components/ImageUpload/index.vue'
 import { listAllLevelRights, addLevelRights, updateLevelRights, delLevelRights, changeLevelRightsStatus } from '@/api/fulfiller/levelRights';
 import { listAllLevelConfig, addLevelConfig, updateLevelConfig, delLevelConfig } from '@/api/fulfiller/levelConfig';

+ 7 - 2
src/views/login.vue

@@ -1,8 +1,8 @@
 <template>
-  <div class="login">
+    <div class="login" :style="{ backgroundImage: 'url(' + (websiteSetting.loginBackgroundUrl || defaultBg) + ')' }">
     <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
       <div class="title-box">
-        <h3 class="title">{{ title }}</h3>
+        <h3 class="title">{{ websiteSetting.loginTitle }}</h3>
         <lang-select />
       </div>
       <el-form-item v-if="tenantEnabled" prop="tenantId">
@@ -82,12 +82,17 @@
 import { getCodeImg, getTenantList } from '@/api/login';
 import { authRouterUrl } from '@/api/system/social/auth';
 import { useUserStore } from '@/store/modules/user';
+import { useSettingsStore } from '@/store/modules/settings';
 import { LoginData, TenantVO } from '@/api/types';
 import { to } from 'await-to-js';
 import { HttpStatus } from '@/enums/RespEnum';
 import { useI18n } from 'vue-i18n';
+import defaultBg from '@/assets/images/login-background.jpg';
+import { storeToRefs } from 'pinia';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const settingsStore = useSettingsStore();
+const { websiteSetting } = storeToRefs(settingsStore);
 
 const title = import.meta.env.VITE_APP_TITLE;
 const userStore = useUserStore();

+ 137 - 69
src/views/order/dispatch/index.vue

@@ -9,23 +9,34 @@
           }}</el-radio-button>
         </el-radio-group>
       </div>
-      <div class="filter-right">
-        <el-cascader v-model="regionValue" :options="areaOptions" placeholder="省市区域" clearable
-          style="width: 170px; margin-right: 10px" @change="handleAreaChange" />
-        <el-select v-model="filters.station" placeholder="站点" clearable style="width: 170px">
-          <el-option v-for="item in siteOptions" :key="item.value" :label="item.label" :value="item.value" />
-        </el-select>
-      </div>
+        <el-cascader
+          v-model="regionValue"
+          :options="areaOptions"
+          placeholder="所属站点"
+          clearable
+          style="width: 240px"
+          @change="handleAreaChange"
+        />
     </div>
 
     <div class="main-content">
       <!-- Left: Real Gaode Map Area -->
       <div class="map-wrapper">
         <template v-if="checkPermi(['order:dispatch:map'])">
-          <div id="amap-container" class="map-view"></div>
+          <div v-if="!mapLoadError" id="amap-container" class="map-view"></div>
+          
+          <!-- 地图加载失败提示 -->
+          <div v-else class="map-error-state">
+            <div class="error-content">
+              <el-icon class="error-icon"><CircleCloseFilled /></el-icon>
+              <h3 class="error-title">地图加载失败</h3>
+              <p class="error-desc">请检查系统设置中的高德地图配置或联系管理员处理</p>
+              <el-button type="primary" plain size="small" @click="retryLoadMap">重试加载</el-button>
+            </div>
+          </div>
 
           <!-- Bottom Left: Map Controls & Stats -->
-          <div class="map-controls-panel">
+          <div v-if="!mapLoadError" class="map-controls-panel">
             <div class="control-group">
               <div class="c-btn" :class="{ active: activeMapFilter === 'all' }" @click="setMapFilter('all')">全部</div>
               <div class="c-btn red" :class="{ active: activeMapFilter === 'merchants' }"
@@ -75,6 +86,8 @@ import { checkPermi } from "@/utils/permission";
 import { ElMessage } from 'element-plus';
 import { listAllService } from '@/api/service/list/index'
 import { listAreaStation } from '@/api/system/areaStation'
+import { getMapSetting } from '@/api/system/mapSetting';
+import { CircleCloseFilled } from '@element-plus/icons-vue';
 import { listStoreOnDispatch } from '@/api/system/store/index'
 import { listSubOrderOnDispatch, dispatchSubOrder } from '@/api/order/subOrder/index';
 import { listFulfillerOnDispatch } from '@/api/fulfiller/pool/index'
@@ -235,28 +248,31 @@ watch([() => filters.orderType, () => filters.station], () => {
 
 const areaStationList = ref([])
 const areaOptions = ref([])
-const siteOptions = ref([])
 const regionValue = ref([])
 
 const buildTree = (data, parentId) => {
   return (data || [])
     .filter(item => String(item.parentId) === String(parentId))
-    .map(item => ({
-      value: item.id,
-      label: item.name,
-      children: buildTree(data, item.id)
-    }))
+    .map(item => {
+      const children = buildTree(data, item.id);
+      const node: any = {
+        value: item.id,
+        label: item.name,
+        // 如果不是站点且没有下级了,则不可选(防止误选区域叶子点)
+        disabled: Number(item.type) !== 2 && (!children || children.length === 0)
+      };
+      if (children && children.length > 0) {
+        node.children = children;
+      }
+      return node;
+    })
 }
 
 const handleAreaChange = (value) => {
-  filters.station = undefined
   if (value && value.length > 0) {
-    const areaId = value[value.length - 1]
-    siteOptions.value = areaStationList.value
-      .filter(item => Number(item.type) === 2 && String(item.parentId) === String(areaId))
-      .map(item => ({ value: item.id, label: item.name }))
+    filters.station = value[value.length - 1];
   } else {
-    siteOptions.value = []
+    filters.station = undefined;
   }
 }
 
@@ -265,37 +281,30 @@ const getAreaStationList = async () => {
     const res = await listAreaStation()
     const data = res?.data || res
     areaStationList.value = Array.isArray(data) ? data : []
-    const areaData = areaStationList.value.filter(item => Number(item.type) === 0 || Number(item.type) === 1)
-    areaOptions.value = buildTree(areaData, 0)
-    siteOptions.value = []
+    areaOptions.value = buildTree(areaStationList.value, 0)
 
+    // 默认选中第一个站点(如果存在)
     const allStations = areaStationList.value.filter(item => Number(item.type) === 2)
     if (allStations.length > 0) {
       const randomStation = allStations[Math.floor(Math.random() * allStations.length)]
-      const areaId = randomStation.parentId
-
       const path = []
-      let currentId = areaId
+      let currentId = randomStation.id
       while (currentId && String(currentId) !== '0') {
         path.unshift(currentId)
-        const currentArea = areaStationList.value.find(item => String(item.id) === String(currentId))
-        if (currentArea) {
-          currentId = currentArea.parentId
+        const node = areaStationList.value.find(item => String(item.id) === String(currentId))
+        if (node) {
+          currentId = node.parentId
         } else {
           break
         }
       }
-
       regionValue.value = path
-      siteOptions.value = areaStationList.value
-        .filter(item => Number(item.type) === 2 && String(item.parentId) === String(areaId))
-        .map(item => ({ value: item.id, label: item.name }))
       filters.station = randomStation.id
     }
-  } catch {
+  } catch (error) {
+    console.error('getAreaStationList error:', error);
     areaStationList.value = []
     areaOptions.value = []
-    siteOptions.value = []
   }
 }
 
@@ -318,37 +327,65 @@ const handleViewRiderOrders = (rider) => {
 
 // --- Map Logic ---
 let map = null;
-const amapKey = 'a30e76f457c14b6570925522be37565d';
-const securityJsCode = '531ae14ec1dff87e552e1ea51e848582';
+const mapLoadError = ref(false);
 
-const loadAMapScript = () => {
-  // 设置安全密钥
-  window._AMapSecurityConfig = {
-    securityJsCode: securityJsCode,
-  };
+/** 动态加载高德地图脚本 */
+const loadAMapScript = async (): Promise<any> => {
+  try {
+    const res = await getMapSetting(1);
+    if (res.code !== 200) throw new Error(res.msg);
+    const { apiKey, apiSecret } = res.data;
 
-  return new Promise((resolve, reject) => {
-    if (window.AMap) {
-      resolve(window.AMap);
-      return;
+    if (!apiKey) {
+      throw new Error('No Map Key Configured');
     }
-    const script = document.createElement('script');
-    script.src = `https://webapi.amap.com/maps?v=2.0&key=${amapKey}`;
-    script.onload = () => resolve(window.AMap);
-    script.onerror = reject;
-    document.head.appendChild(script);
-  });
+
+    // 设置安全密钥
+    (window as any)._AMapSecurityConfig = {
+      securityJsCode: apiSecret,
+    };
+
+    return new Promise((resolve, reject) => {
+      if ((window as any).AMap) {
+        resolve((window as any).AMap);
+        return;
+      }
+      const script = document.createElement('script');
+      script.src = `https://webapi.amap.com/maps?v=2.0&key=${apiKey}`;
+      script.onload = () => resolve((window as any).AMap);
+      script.onerror = () => reject(new Error('Script Load Error'));
+      document.head.appendChild(script);
+    });
+  } catch (error) {
+    console.error('Map config fetch error:', error);
+    return Promise.reject(error);
+  }
 };
 
-const initMap = () => {
-  if (!window.AMap) return;
-  map = new AMap.Map('amap-container', {
-    zoom: 14,
-    center: [116.4551, 39.9255], // Chaoyang center
-    mapStyle: 'amap://styles/normal' // 可以更换其他样式
-  });
+const initMap = async () => {
+  try {
+    mapLoadError.value = false;
+    await loadAMapScript();
+    
+    const AMap = (window as any).AMap;
+    if (!AMap) throw new Error('AMap Init Fail');
+
+    map = new AMap.Map('amap-container', {
+      zoom: 14,
+      center: [116.4551, 39.9255], // Chaoyang center
+      mapStyle: 'amap://styles/normal'
+    });
 
-  refreshMarkers();
+    refreshMarkers();
+  } catch (err) {
+    console.error('initMap failed', err);
+    mapLoadError.value = true;
+  }
+};
+
+/** 重试加载地图 */
+const retryLoadMap = () => {
+  initMap();
 };
 
 const refreshMarkers = () => {
@@ -465,14 +502,7 @@ onMounted(async () => {
   getServiceList()
   await getAreaStationList()
   if (checkPermi(['order:dispatch:map'])) {
-    loadAMapScript()
-      .then(() => {
-        initMap();
-      })
-      .catch((err) => {
-        console.error('Map loading failed', err);
-        ElMessage.error('地图加载失败,请检查网络');
-      });
+    initMap();
   }
 });
 
@@ -718,6 +748,44 @@ const filteredRiders = computed(() => {
   font-family: monospace;
 }
 
+.map-error-state {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #fefefe;
+}
+
+.error-content {
+  text-align: center;
+  padding: 40px;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.05);
+  border: 1px solid #fde2e2;
+  max-width: 320px;
+
+  .error-icon {
+    font-size: 48px;
+    color: #F56C6C;
+    margin-bottom: 20px;
+  }
+
+  .error-title {
+    margin: 0 0 10px 0;
+    color: #303133;
+    font-size: 18px;
+  }
+
+  .error-desc {
+    color: #909399;
+    font-size: 14px;
+    line-height: 1.6;
+    margin-bottom: 24px;
+  }
+}
+
 /* Right Panel */
 .right-panel {
   width: 440px;

+ 1 - 1
src/views/order/orderList/components/RewardDialog.vue

@@ -50,7 +50,7 @@
 <script setup>
 import { reactive, computed, watch } from 'vue';
 import { ElMessage } from 'element-plus';
-import fulfillerEnums from '@/enums/fulfiller.json';
+import fulfillerEnums from '@/json/fulfiller.json';
 
 const props = defineProps({
   visible: Boolean,

+ 1 - 1
src/views/order/orderList/index.vue

@@ -186,7 +186,7 @@
 import { ref, reactive, onMounted, nextTick } from 'vue';
 import { useRouter } from 'vue-router';
 import { ElMessage, ElMessageBox } from 'element-plus';
-import fulfillerEnums from '@/enums/fulfiller.json';
+import fulfillerEnums from '@/json/fulfiller.json';
 import OrderDetailDrawer from './components/OrderDetailDrawer.vue';
 import DispatchDialog from './components/DispatchDialog.vue';
 import CareSummaryDrawer from './components/CareSummaryDrawer.vue';

+ 114 - 75
src/views/system/store/index.vue

@@ -18,10 +18,10 @@
             <el-cascader
               v-model="searchRegionValue"
               :options="areaOptions"
-              :props="{ checkStrictly: true, value: 'id', label: 'name' }"
+              :props="{ value: 'id', label: 'name' }"
               placeholder="所属站点"
               class="station-select"
-              style="width: 350px"
+              style="width: 240px"
               clearable
               @change="handleSearchAreaChange"
             />
@@ -221,7 +221,8 @@
           <el-input v-model="form.detailAddress" type="textarea" placeholder="输入详细地址" rows="3" style="width: 100%" />
         </el-form-item>
         <el-form-item>
-          <el-button type="primary" style="width: 100%" @click="getGeolocation">获取经纬度</el-button>
+          <el-button type="primary" style="width: 100%" @click="getGeolocation" :loading="geoLoading">获取经纬度</el-button>
+          <div v-if="geoErrorMsg" class="geo-error-tip">{{ geoErrorMsg }}</div>
         </el-form-item>
         <el-row :gutter="10">
           <el-col :span="12">
@@ -327,6 +328,7 @@ import { listOnStore } from '@/api/system/tenant';
 import { listOnStore as listTenantCategoriesOnStore } from '@/api/system/tenantCategories';
 import { listAllService } from '@/api/service/list';
 import { listAreaStation } from '@/api/system/areaStation';
+import { getMapSetting } from '@/api/system/mapSetting';
 import { AreaStationVO } from '@/api/system/areaStation/types';
 import { regionData, codeToText, textToCode } from 'element-china-area-data';
 import PageSelect from '@/components/PageSelect/index.vue';
@@ -343,27 +345,8 @@ const storeFormRef = ref<ElFormInstance>();
 
 const searchRegionValue = ref<any[]>([]); // 搜索的区域值
 const searchSiteOptions = ref<any[]>([]); // 搜索的站点选项
+const regionValue = ref<any[]>([]); // 所在区域/站点的路径值
 
-/** 处理搜索区域选择变化 */
-const handleSearchAreaChange = (value: any[]) => {
-  if (value && value.length > 0) {
-    const lastId = value[value.length - 1];
-    const node = areaStationList.value.find(item => item.id === lastId);
-    if (node && node.type === 2) {
-      queryParams.value.station = lastId;
-      queryParams.value.area = node.parentId;
-    } else {
-      queryParams.value.area = lastId;
-      queryParams.value.station = undefined;
-    }
-  } else {
-    queryParams.value.area = undefined;
-    queryParams.value.station = undefined;
-  }
-  handleQuery();
-};
-
-const regionValue = ref<any[]>([]);
 const province = ref('');
 const city = ref('');
 const district = ref('');
@@ -674,48 +657,71 @@ const handleUpdate = async (row: StoreVO) => {
   dialog.title = "修改门店管理";
 }
 
+const geoLoading = ref(false);
+const geoErrorMsg = ref('');
+
 /** 提交按钮 */
 const submitForm = () => {
   storeFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
       buttonLoading.value = true;
-      if (form.value.id) {
-        await updateStore(form.value).finally(() => buttonLoading.value = false);
-      } else {
-        await addStore(form.value).finally(() => buttonLoading.value = false);
+      try {
+        if (form.value.id) {
+          await updateStore(form.value);
+        } else {
+          await addStore(form.value);
+        }
+        proxy?.$modal.msgSuccess("操作成功");
+        dialog.visible = false;
+        await getList();
+      } finally {
+        buttonLoading.value = false;
       }
-      proxy?.$modal.msgSuccess("操作成功");
-      dialog.visible = false;
-      await getList();
     }
   });
 }
 
-/** 高德地图 Key 配置 */
-const amapKey = 'a30e76f457c14b6570925522be37565d';
-const securityJsCode = '531ae14ec1dff87e552e1ea51e848582';
-
 /** 动态加载高德地图脚本 */
-const loadAMapScript = (): Promise<any> => {
-  // 设置安全密钥
-  (window as any)._AMapSecurityConfig = {
-    securityJsCode: securityJsCode,
-  };
-  return new Promise((resolve, reject) => {
-    if ((window as any).AMap) {
-      resolve((window as any).AMap);
-      return;
+const loadAMapScript = async (): Promise<any> => {
+  try {
+    // 从接口获取配置
+    const res = await getMapSetting(1);
+    if (res.code !== 200) {
+      return Promise.reject(res.msg);
     }
-    const script = document.createElement('script');
-    script.src = `https://webapi.amap.com/maps?v=2.0&key=${amapKey}`;
-    script.onload = () => resolve((window as any).AMap);
-    script.onerror = reject;
-    document.head.appendChild(script);
-  });
+    const { apiKey, apiSecret } = res.data;
+
+    if (!apiKey) {
+      return Promise.reject('No Map Key Configured');
+    }
+
+    // 设置安全密钥
+    (window as any)._AMapSecurityConfig = {
+      securityJsCode: apiSecret,
+    };
+
+    return new Promise((resolve, reject) => {
+      if ((window as any).AMap) {
+        resolve((window as any).AMap);
+        return;
+      }
+      const script = document.createElement('script');
+      script.src = `https://webapi.amap.com/maps?v=2.0&key=${apiKey}`;
+      script.onload = () => resolve((window as any).AMap);
+      script.onerror = () => {
+        reject(new Error('Script load failed'));
+      };
+      document.head.appendChild(script);
+    });
+  } catch (error) {
+    console.error('Map config fetch error:', error);
+    // 此处不重复弹窗,因为接口请求失败通常已有全局拦截器处理显示错误
+    return Promise.reject(error);
+  }
 };
 
 /** 根据详细地址使用高德地图 Geocoder 获取经纬度 */
-const getGeolocation = () => {
+const getGeolocation = async () => {
   // 拼接完整地址(省市区 + 详细地址)
   let areaText = '';
   if (addressCascaderValue.value && addressCascaderValue.value.length > 0) {
@@ -729,38 +735,45 @@ const getGeolocation = () => {
     return;
   }
 
-  // 确保高德地图脚本已加载
-  const doGeocode = () => {
+  geoLoading.value = true;
+  geoErrorMsg.value = '';
+  try {
+    // 确保高德地图脚本已加载
+    await loadAMapScript();
+    
     const AMap = (window as any).AMap;
     if (!AMap) {
-      proxy?.$modal.msgError('高德地图脚本未加载,请稍后重试');
-      return;
+      throw new Error('AMap is not defined');
     }
-    AMap.plugin('AMap.Geocoder', () => {
-      const geocoder = new AMap.Geocoder();
-      geocoder.getLocation(fullAddress, (status: string, result: any) => {
-        if (status === 'complete' && result.info === 'OK') {
-          const location = result.geocodes[0]?.location;
-          if (location) {
-            form.value.longitude = location.lng.toFixed(6);
-            form.value.latitude = location.lat.toFixed(6);
-            proxy?.$modal.msgSuccess('获取经纬度成功');
+
+    const location: any = await new Promise((resolve, reject) => {
+      AMap.plugin('AMap.Geocoder', () => {
+        const geocoder = new AMap.Geocoder();
+        geocoder.getLocation(fullAddress, (status: string, result: any) => {
+          if (status === 'complete' && result.info === 'OK') {
+            resolve(result.geocodes[0]?.location);
           } else {
-            proxy?.$modal.msgError('未能解析到该地址的坐标,请检查地址是否准确');
+            console.error('Geocoder fail:', status, result);
+            reject(new Error(result.info || status || 'fail'));
           }
-        } else {
-          proxy?.$modal.msgError('地理编码失败:' + (result.info || status));
-        }
+        });
       });
+      // 增加 8s 超时
+      setTimeout(() => reject(new Error('timeout')), 8000);
     });
-  };
 
-  if ((window as any).AMap) {
-    doGeocode();
-  } else {
-    loadAMapScript().then(() => doGeocode()).catch(() => {
-      proxy?.$modal.msgError('高德地图加载失败,请检查网络');
-    });
+    if (location) {
+      form.value.longitude = location.lng.toFixed(6);
+      form.value.latitude = location.lat.toFixed(6);
+      proxy?.$modal.msgSuccess('获取经纬度成功');
+    } else {
+      throw new Error('no location');
+    }
+  } catch (err) {
+    console.error('getGeolocation error:', err);
+    geoErrorMsg.value = '经纬度获取失败,请联系管理员处理';
+  } finally {
+    geoLoading.value = false;
   }
 };
 
@@ -806,6 +819,8 @@ const buildTree = (data: any[], parentId: any): any[] => {
       const res: any = {
         id: item.id,
         name: item.name,
+        // 如果不是站点且没有下级了,则不可选(防止误选区域叶子点)
+        disabled: Number(item.type) !== 2 && (!children || children.length === 0)
       };
       if (children && children.length > 0) {
         res.children = children;
@@ -814,7 +829,23 @@ const buildTree = (data: any[], parentId: any): any[] => {
     });
 };
 
-/** 处理所在区域选择变化 */
+/** 处理搜索区域选择变化 */
+const handleSearchAreaChange = (value: any[]) => {
+  if (value && value.length > 0) {
+    const lastId = value[value.length - 1];
+    queryParams.value.station = lastId;
+    const node = areaStationList.value.find(item => item.id === lastId);
+    if (node) {
+      queryParams.value.area = node.parentId;
+    }
+  } else {
+    queryParams.value.station = undefined;
+    queryParams.value.area = undefined;
+  }
+  handleQuery();
+};
+
+/** 处理对话框中的所在区域选择变化 */
 const handleAreaChange = (value: any[]) => {
   if (value && value.length > 0) {
     const lastId = value[value.length - 1];
@@ -1049,6 +1080,14 @@ onMounted(() => {
   gap: 12px;
 }
 
+.geo-error-tip {
+  color: #f56c6c;
+  font-size: 13px;
+  margin-top: 8px;
+  width: 100%;
+  text-align: center;
+}
+
 .delete-item {
   color: #f56c6c !important;
   &:hover {

+ 177 - 0
src/views/systemConfig/app/index.vue

@@ -0,0 +1,177 @@
+<template>
+  <div class="app-config-setting">
+    <!-- 顶部提示信息 -->
+    <div class="setting-hint">
+      <el-alert :closable="false" type="info" class="custom-alert">
+        <template #title>
+          <div class="alert-content">
+            <el-icon class="info-icon"><InfoFilled /></el-icon>
+            <span>在此配置移动端APP的展示信息,包括登录页图标及登录页背景图。</span>
+          </div>
+        </template>
+      </el-alert>
+    </div>
+
+    <!-- 配置表单区 -->
+    <div class="setting-body">
+      <el-form ref="appFormRef" :model="form" :rules="rules" label-width="160px" label-position="right" class="premium-setting-form">
+        <!-- 登录图标 -->
+        <el-form-item label="登录页图标:" prop="loginIcon">
+          <image-upload v-model="form.loginIcon" :limit="1" />
+          <div class="form-tip">建议尺寸:200x200,支持 png/jpg 格式</div>
+        </el-form-item>
+
+        <!-- 登录页背景图 -->
+        <el-form-item label="登录页背景:" prop="loginBackground">
+          <image-upload v-model="form.loginBackground" :limit="1" />
+          <div class="form-tip">建议尺寸:1080x1920(9:16),支持 jpg/png 格式</div>
+        </el-form-item>
+
+        <!-- 保存按钮 -->
+        <el-form-item class="action-item">
+          <el-button type="primary" class="save-btn" :loading="buttonLoading" @click="submitForm">保存修改</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script setup name="AppConfig" lang="ts">
+import { getAppSetting, updateAppSetting } from '@/api/system/appSetting';
+import { AppSettingVO, AppSettingForm } from '@/api/system/appSetting/types';
+import { InfoFilled } from '@element-plus/icons-vue';
+import ImageUpload from '@/components/ImageUpload/index.vue';
+import { ref, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const buttonLoading = ref(false);
+const appFormRef = ref<any>();
+
+const form = ref<any>({
+  id: 1,
+  loginIcon: undefined,
+  loginBackground: undefined
+});
+
+const rules = {
+  loginIcon: [{ required: true, message: "登录页图标不能为空", trigger: "change" }],
+  loginBackground: [{ required: true, message: "登录页背景不能为空", trigger: "change" }],
+};
+
+/** 加载配置 */
+const loadConfig = async () => {
+  try {
+    const res = await getAppSetting(1);
+    if (res.code === 200) {
+      // 接口返回的数据中 loginIcon 和 loginBackground 是 ID,可以直接给 form
+      // 如果 ImageUpload 内部处理字符串 ID,则需要转换一下
+      form.value.id = res.data.id;
+      form.value.loginIcon = res.data.loginIcon ? String(res.data.loginIcon) : undefined;
+      form.value.loginBackground = res.data.loginBackground ? String(res.data.loginBackground) : undefined;
+    }
+  } catch (error) {
+    console.error('加载APP配置失败', error);
+  }
+};
+
+/** 提交保存 */
+const submitForm = () => {
+  appFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      try {
+        const submitData: AppSettingForm = {
+          id: form.value.id,
+          loginIcon: form.value.loginIcon,
+          loginBackground: form.value.loginBackground
+        };
+        await updateAppSetting(submitData);
+        proxy?.$modal.msgSuccess("保存成功");
+        loadConfig();
+      } catch (error) {
+        console.error('保存APP配置失败', error);
+      } finally {
+        buttonLoading.value = false;
+      }
+    }
+  });
+};
+
+onMounted(() => {
+  loadConfig();
+});
+</script>
+
+<style scoped lang="scss">
+.app-config-setting {
+  padding: 8px 0;
+}
+
+.setting-hint {
+  margin-bottom: 32px;
+
+  .custom-alert {
+    background-color: #e8f4ff;
+    border: none;
+    border-radius: 8px;
+    padding: 12px 16px;
+
+    .alert-content {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      color: #1890ff;
+      font-size: 14px;
+      line-height: 1.5;
+
+      .info-icon {
+        font-size: 18px;
+      }
+    }
+  }
+}
+
+.setting-body {
+  padding-left: 20px;
+}
+
+.premium-setting-form {
+  :deep(.el-form-item__label) {
+    font-weight: 500;
+    color: #4e5969;
+    padding-right: 24px;
+  }
+
+  .form-tip {
+    font-size: 13px;
+    color: #86909c;
+    margin-top: 8px;
+    line-height: 1.4;
+    display: block;
+    width: 100%;
+  }
+
+  :deep(.el-input) {
+    max-width: 440px;
+    .el-input__wrapper {
+      box-shadow: 0 0 0 1px #e5e6eb inset;
+      padding: 4px 12px;
+      border-radius: 6px;
+    }
+  }
+
+  .action-item {
+    margin-top: 40px;
+  }
+
+  .save-btn {
+    padding: 12px 32px;
+    height: auto;
+    font-size: 15px;
+    font-weight: 500;
+    border-radius: 6px;
+    box-shadow: 0 4px 10px rgba(64, 158, 255, 0.2);
+  }
+}
+</style>

+ 12 - 17
src/views/systemConfig/index.vue

@@ -21,31 +21,22 @@
         <transition name="fade-transform" mode="out-in">
           <div :key="activeTab" class="content-wrapper">
             <!-- 网站设置 -->
-            <div v-if="activeTab === 'website'" class="empty-state">
-              <div class="empty-content">
-                <el-icon class="empty-icon"><Setting /></el-icon>
-                <div class="empty-text">网站设置</div>
-                <div class="empty-desc">该模块内容正在深度集成中,敬请期待</div>
-              </div>
-            </div>
+            <WebsiteConfig v-if="activeTab === 'website'" />
 
             <!-- 平台配置 -->
-            <div v-if="activeTab === 'platform'" class="empty-state">
-              <div class="empty-content">
-                <el-icon class="empty-icon"><Platform /></el-icon>
-                <div class="empty-text">平台配置</div>
-                <div class="empty-desc">参数调优功能即将上线</div>
-              </div>
-            </div>
+            <PlatformConfig v-if="activeTab === 'platform'" />
 
             <!-- 文件存储配置 -->
             <OssConfig v-if="activeTab === 'storage'" />
 
             <!-- 短信配置 -->
-            <SmsConfig v-if="activeTab === 'sms'" />
+            <!-- <SmsConfig v-if="activeTab === 'sms'" /> -->
 
             <!-- 协议配置 -->
             <ProtocolConfig v-if="activeTab === 'protocol'" />
+
+            <!-- APP配置 -->
+            <AppConfig v-if="activeTab === 'app'" />
           </div>
         </transition>
       </div>
@@ -55,7 +46,10 @@
 
 <script setup name="SystemConfig" lang="ts">
 import { ref, computed } from 'vue';
-import SmsConfig from '@/views/systemConfig/sms/index.vue';
+import WebsiteConfig from './website/index.vue';
+import AppConfig from './app/index.vue';
+import PlatformConfig from './platform/index.vue';
+// import SmsConfig from '@/views/systemConfig/sms/index.vue';
 import ProtocolConfig from '@/views/systemConfig/protocol/index.vue';
 import OssConfig from '@/views/system/oss/config.vue';
 import { Setting, Platform, Document } from '@element-plus/icons-vue';
@@ -64,9 +58,10 @@ const activeTab = ref('website');
 
 const menuList = [
   { label: '网站设置', key: 'website' },
+  { label: 'APP配置', key: 'app' },
   { label: '平台配置', key: 'platform' },
   { label: '存储配置', key: 'storage' },
-  { label: '短信配置', key: 'sms' },
+  // { label: '短信配置', key: 'sms' },
   { label: '协议配置', key: 'protocol' }
 ];
 </script>

+ 183 - 0
src/views/systemConfig/platform/components/CustomerConfig.vue

@@ -0,0 +1,183 @@
+<template>
+  <div class="setting-container">
+    <el-form ref="customerFormRef" :model="form" :rules="rules" label-width="160px" class="premium-setting-form">
+      <el-form-item label="客服微信号:" prop="wechatAccount">
+        <el-input v-model="form.wechatAccount" placeholder="请输入客服微信号" class="config-input" />
+      </el-form-item>
+
+      <el-form-item label="客服电话:" prop="phoneNumber">
+        <el-input v-model="form.phoneNumber" placeholder="请输入客服联系电话" class="config-input" />
+      </el-form-item>
+
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="客服开始时间:" prop="startServiceTime">
+            <el-time-picker
+              v-model="form.startServiceTime"
+              placeholder="选择开始时间"
+              format="HH:mm"
+              value-format="HH:mm"
+              class="config-time-picker"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="客服结束时间:" prop="endServiceTime">
+            <el-time-picker
+              v-model="form.endServiceTime"
+              placeholder="选择结束时间"
+              format="HH:mm"
+              value-format="HH:mm"
+              class="config-time-picker"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-form-item label="企微链接:" prop="enterpriseWechatLink">
+        <el-input v-model="form.enterpriseWechatLink" placeholder="请输入企业微信链接" class="config-input" />
+      </el-form-item>
+
+      <el-form-item label="客服二维码:" prop="qrCode">
+        <image-upload v-model="form.qrCode" :limit="1" />
+        <div class="form-tip">支持 jpg/png 格式</div>
+      </el-form-item>
+
+      <el-form-item class="action-item">
+        <el-button type="primary" class="save-btn" :loading="loading" @click="submitForm">保存修改</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { getCustomerServiceSetting, updateCustomerServiceSetting } from '@/api/system/customerServiceSetting';
+import { CustomerServiceSettingForm } from '@/api/system/customerServiceSetting/types';
+import ImageUpload from '@/components/ImageUpload/index.vue';
+import { ref, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const loading = ref(false);
+const customerFormRef = ref<any>();
+
+const form = ref<any>({
+  id: 1,
+  wechatAccount: '',
+  phoneNumber: '',
+  startServiceTime: '',
+  endServiceTime: '',
+  qrCode: undefined,
+  enterpriseWechatLink: ''
+});
+
+const rules = {
+  wechatAccount: [{ required: true, message: "微信号不能为空", trigger: "blur" }],
+  phoneNumber: [{ required: true, message: "电话不能为空", trigger: "blur" }],
+  startServiceTime: [{ required: true, message: "开始时间不能为空", trigger: "change" }],
+  endServiceTime: [{ required: true, message: "结束时间不能为空", trigger: "change" }],
+  qrCode: [{ required: true, message: "二维码不能为空", trigger: "change" }]
+};
+
+const loadData = async () => {
+  try {
+    const res = await getCustomerServiceSetting(1);
+    if (res.code === 200) {
+      form.value = { ...res.data };
+      // 提取时分:后端返回 1970-01-01 08:00:00,前端需要 HH:mm
+      if (form.value.startServiceTime && form.value.startServiceTime.includes(' ')) {
+        form.value.startServiceTime = form.value.startServiceTime.split(' ')[1].substring(0, 5);
+      }
+      if (form.value.endServiceTime && form.value.endServiceTime.includes(' ')) {
+        form.value.endServiceTime = form.value.endServiceTime.split(' ')[1].substring(0, 5);
+      }
+      if (typeof form.value.qrCode === 'number') form.value.qrCode = String(form.value.qrCode);
+    }
+  } catch (err) {}
+};
+
+const submitForm = () => {
+  customerFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      loading.value = true;
+      try {
+        const submitData: CustomerServiceSettingForm = {
+            id: form.value.id,
+            wechatAccount: form.value.wechatAccount,
+            phoneNumber: form.value.phoneNumber,
+            // 提交时补全日期前缀,以匹配后端的日期时间格式
+            startServiceTime: form.value.startServiceTime ? `1970-01-01 ${form.value.startServiceTime}:00` : '',
+            endServiceTime: form.value.endServiceTime ? `1970-01-01 ${form.value.endServiceTime}:00` : '',
+            qrCode: form.value.qrCode,
+            enterpriseWechatLink: form.value.enterpriseWechatLink
+        };
+        await updateCustomerServiceSetting(submitData);
+        proxy?.$modal.msgSuccess("保存成功");
+        loadData();
+      } finally {
+        loading.value = false;
+      }
+    }
+  });
+};
+
+onMounted(() => {
+  loadData();
+});
+</script>
+
+<style scoped lang="scss">
+.setting-container {
+  padding-top: 10px;
+}
+
+.premium-setting-form {
+  :deep(.el-form-item__label) {
+    font-weight: 500;
+    color: #4e5969;
+    padding-right: 24px;
+  }
+
+  .config-input {
+    max-width: 440px;
+    :deep(.el-input__wrapper) {
+      box-shadow: 0 0 0 1px #e5e6eb inset;
+      padding: 4px 12px;
+      border-radius: 6px;
+
+      &.is-focus {
+        box-shadow: 0 0 0 1px #409eff inset;
+      }
+    }
+  }
+
+  .config-time-picker {
+    max-width: 180px;
+    :deep(.el-input__wrapper) {
+      box-shadow: 0 0 0 1px #e5e6eb inset;
+      border-radius: 6px;
+    }
+  }
+
+  .form-tip {
+    font-size: 13px;
+    color: #86909c;
+    margin-top: 8px;
+    line-height: 1.4;
+    display: block;
+    width: 100%;
+  }
+
+  .action-item {
+    margin-top: 40px;
+  }
+
+  .save-btn {
+    padding: 12px 32px;
+    height: auto;
+    font-size: 15px;
+    font-weight: 500;
+    border-radius: 6px;
+    box-shadow: 0 4px 10px rgba(64, 158, 255, 0.2);
+  }
+}
+</style>

+ 131 - 0
src/views/systemConfig/platform/components/MapConfig.vue

@@ -0,0 +1,131 @@
+<template>
+  <div class="setting-container">
+    <el-form ref="mapFormRef" :model="form" :rules="rules" label-width="170px" class="premium-setting-form">
+      <el-form-item label="高德地图Key:" prop="apiKey">
+        <el-input v-model="form.apiKey" placeholder="请输入高德地图Key" class="config-input" style="width: 800px" />
+        <div class="form-tip">对接高德地图所需的平台Key</div>
+      </el-form-item>
+
+      <el-form-item label="高德地图安全密钥:" prop="apiSecret">
+        <el-input v-model="form.apiSecret" type="password" show-password placeholder="请输入高德地图安全密钥" class="config-input" style="width: 800px" />
+        <div class="form-tip">高德地图对应的安全密钥(Secret)</div>
+      </el-form-item>
+
+      <el-form-item class="action-item">
+        <el-button type="primary" class="save-btn" :loading="loading" @click="submitForm">保存修改</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { getMapSetting, updateMapSetting } from '@/api/system/mapSetting';
+import { MapSettingForm } from '@/api/system/mapSetting/types';
+import { ref, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const loading = ref(false);
+const mapFormRef = ref<any>();
+
+const form = ref<MapSettingForm>({
+  id: 1,
+  apiKey: '',
+  apiSecret: ''
+});
+
+const rules = {
+  apiKey: [{ required: true, message: "Key 不能为空", trigger: "blur" }],
+  apiSecret: [{ required: true, message: "Secret 不能为空", trigger: "blur" }]
+};
+
+const loadData = async () => {
+  try {
+    const res = await getMapSetting(1);
+    if (res.code === 200) {
+      form.value = { ...res.data };
+    }
+  } catch (err) {}
+};
+
+const submitForm = () => {
+  mapFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      loading.value = true;
+      try {
+        await updateMapSetting(form.value);
+        proxy?.$modal.msgSuccess("保存成功");
+        loadData();
+      } finally {
+        loading.value = false;
+      }
+    }
+  });
+};
+
+onMounted(() => {
+  loadData();
+});
+</script>
+
+<style scoped lang="scss">
+.setting-container {
+  padding-top: 10px;
+}
+
+  .premium-setting-form {
+    max-width: 1200px;
+    
+    :deep(.el-form-item__content) {
+      width: 850px;
+    }
+
+    :deep(.el-form-item__label) {
+      font-weight: 500;
+      color: #4e5969;
+      padding-right: 24px;
+      line-height: 32px;
+    }
+
+    .config-input {
+      width: 100% !important;
+      border: 1px solid transparent; // 预留样式确认位
+
+      :deep(.el-input__wrapper) {
+        width: 100%;
+        box-shadow: 0 0 0 1px #e5e6eb inset;
+        padding: 4px 12px;
+        border-radius: 6px;
+
+        &.is-focus {
+          box-shadow: 0 0 0 1px #409eff inset;
+        }
+      }
+      
+      :deep(.el-input__inner) {
+        width: 100%;
+      }
+    }
+
+  .form-tip {
+    font-size: 13px;
+    color: #86909c;
+    margin-top: 8px;
+    line-height: 1.4;
+    display: block;
+    width: 100%;
+  }
+
+  .action-item {
+    margin-top: 40px;
+  }
+
+  .save-btn {
+    padding: 12px 32px;
+    height: auto;
+    font-size: 15px;
+    font-weight: 500;
+    border-radius: 6px;
+    box-shadow: 0 4px 10px rgba(64, 158, 255, 0.2);
+  }
+}
+</style>

+ 120 - 0
src/views/systemConfig/platform/components/OrderConfig.vue

@@ -0,0 +1,120 @@
+<template>
+  <div class="setting-container">
+    <el-form ref="orderFormRef" :model="form" :rules="rules" label-width="160px" class="premium-setting-form">
+      <el-form-item label="超时取消时间:" prop="timeoutCancelTime">
+        <div class="input-with-unit">
+          <el-input-number v-model="form.timeoutCancelTime" :min="1" :precision="0" step-strictly placeholder="请输入" class="config-input-number" />
+          <span class="unit-text">分钟</span>
+        </div>
+        <div class="form-tip">订单派单以后,超时未接单的订单自动取消</div>
+      </el-form-item>
+
+      <el-form-item class="action-item">
+        <el-button type="primary" class="save-btn" :loading="loading" @click="submitForm">保存修改</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { getOrderSetting, updateOrderSetting } from '@/api/order/setting';
+// @ts-ignore
+import { OrderSettingVO, OrderSettingForm } from '@/api/order/setting/types';
+import { ref, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const loading = ref(false);
+const orderFormRef = ref<any>();
+
+const form = ref<OrderSettingForm>({
+  id: 1,
+  timeoutCancelTime: 30
+});
+
+const rules = {
+  timeoutCancelTime: [{ required: true, message: "时长不能为空", trigger: "blur" }]
+};
+
+const loadData = async () => {
+  try {
+    const res = await getOrderSetting(1);
+    if (res.code === 200) {
+      form.value = { ...res.data };
+    }
+  } catch (err) {}
+};
+
+const submitForm = () => {
+  orderFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      loading.value = true;
+      try {
+        await updateOrderSetting(form.value);
+        proxy?.$modal.msgSuccess("保存成功");
+        loadData();
+      } finally {
+        loading.value = false;
+      }
+    }
+  });
+};
+
+onMounted(() => {
+  loadData();
+});
+</script>
+
+<style scoped lang="scss">
+.setting-container {
+  padding-top: 10px;
+}
+
+.premium-setting-form {
+  :deep(.el-form-item__label) {
+    font-weight: 500;
+    color: #4e5969;
+    padding-right: 24px;
+  }
+
+  .input-with-unit {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+  }
+
+  .config-input-number {
+    max-width: 180px;
+    :deep(.el-input__wrapper) {
+      box-shadow: 0 0 0 1px #e5e6eb inset;
+      border-radius: 6px;
+    }
+  }
+
+  .unit-text {
+    color: #4e5969;
+    font-size: 14px;
+  }
+
+  .form-tip {
+    font-size: 13px;
+    color: #86909c;
+    margin-top: 8px;
+    line-height: 1.4;
+    display: block;
+    width: 100%;
+  }
+
+  .action-item {
+    margin-top: 40px;
+  }
+
+  .save-btn {
+    padding: 12px 32px;
+    height: auto;
+    font-size: 15px;
+    font-weight: 500;
+    border-radius: 6px;
+    box-shadow: 0 4px 10px rgba(64, 158, 255, 0.2);
+  }
+}
+</style>

+ 103 - 0
src/views/systemConfig/platform/index.vue

@@ -0,0 +1,103 @@
+<template>
+  <div class="platform-config-container">
+    <div class="sub-tabs-header">
+      <div v-for="item in subTabs" :key="item.key"
+        :class="['sub-tab-item', { active: activeSubTab === item.key }]"
+        @click="activeSubTab = item.key">
+        {{ item.label }}
+      </div>
+    </div>
+
+    <div class="sub-content-body">
+      <transition name="fade-transform" mode="out-in">
+        <div :key="activeSubTab">
+          <OrderConfig v-if="activeSubTab === 'order'" />
+          <MapConfig v-if="activeSubTab === 'map'" />
+          <CustomerConfig v-if="activeSubTab === 'customer'" />
+        </div>
+      </transition>
+    </div>
+  </div>
+</template>
+
+<script setup name="PlatformConfig" lang="ts">
+import { ref } from 'vue';
+import OrderConfig from './components/OrderConfig.vue';
+import MapConfig from './components/MapConfig.vue';
+import CustomerConfig from './components/CustomerConfig.vue';
+
+const activeSubTab = ref('order');
+
+const subTabs = [
+  { label: '订单配置', key: 'order' },
+  { label: '地图key配置', key: 'map' },
+  { label: '客服配置', key: 'customer' }
+];
+</script>
+
+<style scoped lang="scss">
+.platform-config-container {
+  padding: 8px 0;
+}
+
+.sub-tabs-header {
+  display: flex;
+  margin-bottom: 24px;
+  border-bottom: 2px solid #f0f2f5;
+  gap: 32px;
+
+  .sub-tab-item {
+    padding: 12px 4px;
+    font-size: 15px;
+    color: #4e5969;
+    cursor: pointer;
+    position: relative;
+    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    font-weight: 500;
+
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: -2px;
+      left: 0;
+      width: 0;
+      height: 2px;
+      background-color: #409eff;
+      transition: all 0.3s;
+    }
+
+    &:hover {
+      color: #1d2129;
+    }
+
+    &.active {
+      color: #409eff;
+      font-weight: 600;
+
+      &::after {
+        width: 100%;
+      }
+    }
+  }
+}
+
+.sub-content-body {
+  padding-left: 8px;
+}
+
+/* 切换动画 */
+.fade-transform-enter-active,
+.fade-transform-leave-active {
+  transition: all 0.25s;
+}
+
+.fade-transform-enter-from {
+  opacity: 0;
+  transform: translateY(10px);
+}
+
+.fade-transform-leave-to {
+  opacity: 0;
+  transform: translateY(-10px);
+}
+</style>

+ 245 - 0
src/views/systemConfig/website/index.vue

@@ -0,0 +1,245 @@
+<template>
+  <div class="website-config-setting">
+    <!-- 顶部提示信息 -->
+    <div class="setting-hint">
+      <el-alert :closable="false" type="info" class="custom-alert">
+        <template #title>
+          <div class="alert-content">
+            <el-icon class="info-icon"><InfoFilled /></el-icon>
+            <span>在此配置系统各端的展示信息,包括浏览器标题、图标及登录页背景等。</span>
+          </div>
+        </template>
+      </el-alert>
+    </div>
+
+    <!-- 配置表单区 -->
+    <div class="setting-body">
+      <el-form ref="websiteFormRef" :model="form" :rules="rules" label-width="140px" label-position="right" class="premium-setting-form">
+
+        <!-- 客户端选择 -->
+        <el-form-item label="配置端:">
+          <el-radio-group v-model="form.id" @change="handleTypeChange" class="custom-radio-group">
+            <el-radio :label="1" border>平台端</el-radio>
+            <el-radio :label="2" border>商户端</el-radio>
+          </el-radio-group>
+        </el-form-item>
+
+        <!-- 网站标题 -->
+        <el-form-item label="网站标题:" prop="title">
+          <el-input v-model="form.title" placeholder="请输入浏览器显示的网站标题" class="config-input" />
+          <div class="form-tip">用于浏览器标签页显示的标题内容</div>
+        </el-form-item>
+
+        <!-- 登录页标题 -->
+        <el-form-item label="登录页标题:" prop="loginTitle">
+          <el-input v-model="form.loginTitle" placeholder="请输入登录页面显示的标题" class="config-input" />
+        </el-form-item>
+
+        <!-- 侧边栏标题 -->
+        <el-form-item label="菜单标题:" prop="menuTitle">
+          <el-input v-model="form.menuTitle" placeholder="请输入左侧菜单顶部的标题" class="config-input" />
+        </el-form-item>
+
+        <!-- 网站图标 -->
+        <el-form-item label="网站图标:" prop="icon">
+          <image-upload v-model="form.icon" :limit="1" />
+          <div class="form-tip">建议尺寸:32x32,支持 png/ico 格式</div>
+        </el-form-item>
+
+        <!-- 登录背景图 -->
+        <el-form-item label="登录页背景:" prop="loginBackground">
+          <image-upload v-model="form.loginBackground" :limit="1" />
+          <div class="form-tip">建议尺寸:1920x1080,支持 jpg/png 格式</div>
+        </el-form-item>
+
+        <!-- 保存按钮 -->
+        <el-form-item class="action-item">
+          <el-button type="primary" class="save-btn" :loading="buttonLoading" @click="submitForm">保存修改</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script setup name="WebsiteConfig" lang="ts">
+import { getWebsiteSetting, editWebsiteSetting } from '@/api/system/websiteSetting';
+import { WebsiteSettingVO, WebsiteSettingEditForm } from '@/api/system/websiteSetting/types';
+import { InfoFilled } from '@element-plus/icons-vue';
+import ImageUpload from '@/components/ImageUpload/index.vue';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const buttonLoading = ref(false);
+const websiteFormRef = ref<ElFormInstance>();
+
+// 我们在表单中使用 any 处理,因为 ImageUpload 的 v-model 可能在展示时是 ID 字符串,获取时是 number
+const form = ref<any>({
+  id: 1,
+  icon: undefined,
+  title: '',
+  loginTitle: '',
+  loginBackground: undefined,
+  menuTitle: ''
+});
+
+const rules = {
+  title: [{ required: true, message: "网站标题不能为空", trigger: "blur" }],
+  loginTitle: [{ required: true, message: "登录页标题不能为空", trigger: "blur" }],
+  menuTitle: [{ required: true, message: "菜单标题不能为空", trigger: "blur" }],
+};
+
+/** 加载配置 */
+const loadConfig = async (id: number) => {
+  const res = await getWebsiteSetting(id);
+  if (res.code === 200) {
+    // 接口返回的数据中 icon 和 loginBackground 是 ID,可以直接给 form
+    form.value = { ...res.data };
+    // 如果返回的是 number,转成 string 给 ImageUpload (它内部处理字符串 ID)
+    if (typeof form.value.icon === 'number') form.value.icon = String(form.value.icon);
+    if (typeof form.value.loginBackground === 'number') form.value.loginBackground = String(form.value.loginBackground);
+  }
+};
+
+/** 切换配置端 */
+const handleTypeChange = (val: any) => {
+  loadConfig(val);
+};
+
+/** 提交保存 */
+const submitForm = () => {
+  websiteFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      try {
+        const submitData: WebsiteSettingEditForm = {
+            id: form.value.id,
+            icon: form.value.icon,
+            title: form.value.title,
+            loginTitle: form.value.loginTitle,
+            loginBackground: form.value.loginBackground,
+            menuTitle: form.value.menuTitle
+        };
+        await editWebsiteSetting(submitData);
+        proxy?.$modal.msgSuccess("保存成功");
+        loadConfig(form.value.id);
+      } finally {
+        buttonLoading.value = false;
+      }
+    }
+  });
+};
+
+/** 从 URL 或者返回数据中提取 ID 的辅助方法 (根据项目具体逻辑) */
+const extractIdFromUrl = (url: string) => {
+    // 逻辑根据具体的 Oss 上传组件返回而定
+    // 常规做法是 ImageUpload 在上传后 emit 一个对象包含 url 和 ossId
+    return undefined;
+};
+
+// 监听图片组件可能的 ID 返回(如果 ImageUpload 封装了的话)
+// 这里为了简化,我们假设后端 edit 接口可以处理或有相应字段映射
+const handleIconUploadFinish = (ossId: number) => {
+    form.value.icon = ossId;
+};
+const handleBgUploadFinish = (ossId: number) => {
+    form.value.loginBackground = ossId;
+};
+
+onMounted(() => {
+  loadConfig(1);
+});
+</script>
+
+<style scoped lang="scss">
+.website-config-setting {
+  padding: 8px 0;
+}
+
+.setting-hint {
+  margin-bottom: 32px;
+
+  .custom-alert {
+    background-color: #e8f4ff;
+    border: none;
+    border-radius: 8px;
+    padding: 12px 16px;
+
+    .alert-content {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      color: #1890ff;
+      font-size: 14px;
+      line-height: 1.5;
+
+      .info-icon {
+        font-size: 18px;
+      }
+    }
+  }
+}
+
+.setting-body {
+  padding-left: 20px;
+}
+
+.premium-setting-form {
+  :deep(.el-form-item__label) {
+    font-weight: 500;
+    color: #4e5969;
+    padding-right: 24px;
+  }
+
+  .config-input {
+    max-width: 520px;
+
+    :deep(.el-input__wrapper) {
+      box-shadow: 0 0 0 1px #e5e6eb inset;
+      padding: 4px 12px;
+      border-radius: 6px;
+
+      &.is-focus {
+        box-shadow: 0 0 0 1px #409eff inset;
+      }
+    }
+  }
+
+  .form-tip {
+    font-size: 13px;
+    color: #86909c;
+    margin-top: 8px;
+    line-height: 1.4;
+  }
+
+  .custom-radio-group {
+    :deep(.el-radio) {
+      margin-right: 16px;
+      border-radius: 6px;
+      padding: 0 20px;
+      height: 36px;
+
+      &.is-bordered.is-checked {
+        border-color: #409eff;
+        background-color: rgba(64, 158, 255, 0.04);
+      }
+
+      .el-radio__label {
+        font-size: 14px;
+      }
+    }
+  }
+
+  .action-item {
+    margin-top: 40px;
+  }
+
+  .save-btn {
+    padding: 12px 32px;
+    height: auto;
+    font-size: 15px;
+    font-weight: 500;
+    border-radius: 6px;
+    box-shadow: 0 4px 10px rgba(64, 158, 255, 0.2);
+  }
+}
+</style>

+ 2 - 2
vite.config.ts

@@ -25,8 +25,8 @@ export default defineConfig(({ mode, command }) => {
       // open: true,
       proxy: {
         [env.VITE_APP_BASE_API]: {
-          // target: 'http://127.0.0.1:8080',
-          target: 'http://www.hoomeng.pet/api',
+          target: 'http://127.0.0.1:8080',
+          // target: 'http://www.hoomeng.pet/api',
           changeOrigin: true,
           ws: true,
           rewrite: (path) => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')