已添加5个文件
已修改18个文件
1026 ■■■■■ 文件已修改
YZFX.bat 451 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/leave-type-config/index.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/company-config/index.ts 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/storage/index.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bootstrap.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/auth.vue 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/@core/ui-kit/shadcn-ui/src/components/segmented/segmented.vue 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/effects/layouts/src/widgets/preferences/blocks/company/company.vue 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/effects/layouts/src/widgets/preferences/blocks/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/locales/src/langs/en-US/preferences.json 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/locales/src/langs/zh-CN/preferences.json 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/company-config.ts 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/company-config.ts 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/leave-type-config/data.ts 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/leave-type-config/index.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/leave-type-config/modules/form.vue 59 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/data.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/route/modules/process-form.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/process-form.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/data.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/modules/process-form.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
YZFX.bat
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,451 @@
@echo off
setlocal EnableExtensions EnableDelayedExpansion
REM ============================================================
REM Vue Frontend Deploy Script
REM ============================================================
REM =========================
REM Configuration
REM =========================
set "COMPANY=YZFX"
set "REMOTE_USER=root"
set "REMOTE_HOST=42.63.70.231"
set "REMOTE_PORT=22"
set "PASSWORD=Nxyzfxxcl1@"
set "ZIP_FILE=dist.zip"
set "REMOTE_DIR=/home/vue/dist"
set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3"
set "RETRY_COUNT=3"
set "SSHPASS_DIR=%~dp0bin"
REM ============================================================
REM Start
REM ============================================================
echo.
echo ============================================================
echo              Vue Dist Auto Deploy
echo ============================================================
echo.
echo Company    : %COMPANY%
echo Server     : %REMOTE_USER%@%REMOTE_HOST%
echo Port       : %REMOTE_PORT%
echo Remote Dir : %REMOTE_DIR%
echo Package    : %ZIP_FILE%
echo.
REM ============================================================
REM Check sshpass
REM ============================================================
echo [1/7] Checking sshpass...
if exist "%SSHPASS_DIR%\sshpass.exe" (
    set "PATH=%SSHPASS_DIR%;%PATH%"
)
where sshpass >nul 2>&1
if not errorlevel 1 (
    echo [OK] sshpass found.
    goto CHECK_PNPM
)
echo [WARN] sshpass not found.
if not exist "%SSHPASS_DIR%" (
    mkdir "%SSHPASS_DIR%" >nul 2>&1
)
if exist "%SSHPASS_DIR%\sshpass.exe" (
    set "PATH=%SSHPASS_DIR%;%PATH%"
    echo [OK] sshpass found.
    goto CHECK_PNPM
)
echo [INFO] Downloading sshpass...
powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out"
if errorlevel 1 (
    echo [ERROR] Failed to download sshpass.
    echo [ERROR] Please install sshpass manually.
    goto ERROR_EXIT
)
set "PATH=%SSHPASS_DIR%;%PATH%"
echo [OK] sshpass installed.
REM ============================================================
REM Check pnpm
REM ============================================================
:CHECK_PNPM
echo.
echo [2/7] Checking pnpm...
where pnpm >nul 2>&1
if errorlevel 1 (
    echo [ERROR] pnpm not found.
    echo [HINT] npm install -g pnpm
    goto ERROR_EXIT
)
call pnpm --version
if errorlevel 1 (
    echo [ERROR] pnpm execution failed.
    goto ERROR_EXIT
)
REM ============================================================
REM Check node
REM ============================================================
echo.
echo [3/7] Checking Node.js...
where node >nul 2>&1
if errorlevel 1 (
    echo [ERROR] Node.js not found.
    goto ERROR_EXIT
)
call node --version
if errorlevel 1 (
    echo [ERROR] Node.js execution failed.
    goto ERROR_EXIT
)
REM ============================================================
REM Check node_modules
REM ============================================================
if not exist "node_modules" (
    echo.
    echo [INFO] node_modules not found.
    echo [INFO] Running pnpm install...
    call pnpm install
    if errorlevel 1 (
        echo [ERROR] pnpm install failed.
        goto ERROR_EXIT
    )
    echo [OK] pnpm install completed.
) else (
    echo [OK] node_modules exists.
)
REM ============================================================
REM Build
REM ============================================================
echo.
echo [4/7] Building project...
if exist "%ZIP_FILE%" (
    echo.
    echo [WARN] %ZIP_FILE% already exists.
    set "REBUILD="
    set /p "REBUILD=Rebuild? Y/N (default Y): "
    if /I "!REBUILD!"=="N" (
        echo [INFO] Skip build.
        goto CHECK_ZIP
    )
)
echo [INFO] Company: %COMPANY%
echo [INFO] Command: pnpm run build
echo.
set "VITE_COMPANY=%COMPANY%"
call pnpm run build
if errorlevel 1 (
    echo.
    echo [ERROR] Build failed.
    goto ERROR_EXIT
)
echo.
echo [OK] Build completed.
REM ============================================================
REM Check ZIP
REM ============================================================
:CHECK_ZIP
echo.
echo [5/7] Checking package...
if not exist "%ZIP_FILE%" (
    echo [ERROR] %ZIP_FILE% not found.
    echo [HINT] Please check whether your build generates dist.zip.
    goto ERROR_EXIT
)
for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF"
set /a ZIP_MB=ZIP_BYTES/1024/1024
echo [OK] Package found.
echo [INFO] File size: !ZIP_BYTES! bytes
echo [INFO] File size: !ZIP_MB! MB
REM ============================================================
REM Set sshpass password
REM ============================================================
set "SSHPASS=%PASSWORD%"
REM ============================================================
REM Test SSH
REM ============================================================
echo.
echo [6/7] Testing SSH connection...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK"
if errorlevel 1 (
    echo [ERROR] SSH connection failed.
    goto ERROR_EXIT
)
echo [OK] SSH connection successful.
REM ============================================================
REM Create remote directory
REM ============================================================
echo.
echo [INFO] Creating remote directory...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'"
if errorlevel 1 (
    echo [ERROR] Failed to create remote directory.
    goto ERROR_EXIT
)
echo [OK] Remote directory ready.
REM ============================================================
REM Upload temporary file
REM ============================================================
echo.
echo [INFO] Uploading package...
set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading"
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'"
sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%"
if errorlevel 1 (
    echo [ERROR] Upload failed.
    goto ERROR_EXIT
)
echo [OK] Upload completed.
REM ============================================================
REM Check remote file
REM ============================================================
echo.
echo [INFO] Checking remote package...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'"
if errorlevel 1 (
    echo [ERROR] Remote package does not exist.
    goto ERROR_EXIT
)
echo [OK] Remote package exists.
REM ============================================================
REM Compare file size
REM ============================================================
echo.
echo [INFO] Checking package size...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'"
echo.
REM ============================================================
REM Rename temporary file
REM ============================================================
echo [INFO] Activating package...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'"
if errorlevel 1 (
    echo [ERROR] Failed to activate package.
    goto ERROR_EXIT
)
echo [OK] Package activated.
REM ============================================================
REM Check unzip
REM ============================================================
echo.
echo [INFO] Checking unzip...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip"
if errorlevel 1 (
    echo [ERROR] unzip is not installed on server.
    echo.
    echo CentOS/RHEL:
    echo yum install -y unzip
    echo.
    echo Ubuntu/Debian:
    echo apt install -y unzip
    goto ERROR_EXIT
)
echo [OK] unzip found.
REM ============================================================
REM Test ZIP
REM ============================================================
echo.
echo [INFO] Testing remote ZIP...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'"
if errorlevel 1 (
    echo.
    echo [ERROR] ZIP test failed.
    echo [INFO] Remote ZIP has been kept.
    goto ERROR_EXIT
)
echo [OK] ZIP test passed.
REM ============================================================
REM Extract
REM ============================================================
echo.
echo [INFO] Extracting package...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'"
if errorlevel 1 (
    echo.
    echo [ERROR] Remote unzip failed.
    echo [INFO] Remote ZIP has been kept.
    goto ERROR_EXIT
)
echo.
echo [OK] Extract completed.
REM ============================================================
REM Remove ZIP
REM ============================================================
echo.
echo [INFO] Removing remote ZIP...
sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'"
if errorlevel 1 (
    echo [WARN] Failed to remove remote ZIP.
) else (
    echo [OK] Remote ZIP removed.
)
REM ============================================================
REM Success
REM ============================================================
echo.
echo ============================================================
echo                 DEPLOY SUCCESS
echo ============================================================
echo.
echo Company    : %COMPANY%
echo Server     : %REMOTE_HOST%:%REMOTE_PORT%
echo Remote Dir : %REMOTE_DIR%
echo Package    : %ZIP_FILE%
echo Size       : !ZIP_MB! MB
echo.
echo Deployment completed successfully.
echo.
goto END
REM ============================================================
REM Error
REM ============================================================
:ERROR_EXIT
echo.
echo ============================================================
echo                  DEPLOY FAILED
echo ============================================================
echo.
echo Please check the error message above.
echo.
goto END
REM ============================================================
REM End
REM ============================================================
:END
pause
endlocal
src/api/hrm/leave-type-config/index.ts
@@ -22,7 +22,17 @@
  );
}
/** æ–°å¢žè¯·å‡ç±»åž‹é…ç½® */
export function createLeaveTypeConfig(data: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
  return requestClient.post<number>('/hrm/leave-type-config/create', data);
}
/** æ›´æ–°è¯·å‡ç±»åž‹é…ç½® */
export function updateLeaveTypeConfig(data: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
  return requestClient.put('/hrm/leave-type-config/update', data);
}
/** åˆ é™¤è¯·å‡ç±»åž‹é…ç½® */
export function deleteLeaveTypeConfig(id: number) {
  return requestClient.delete(`/hrm/leave-type-config/delete?id=${id}`);
}
src/api/system/company-config/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
import { requestClient } from '#/api/request';
export namespace SystemCompanyConfigApi {
  /** å…¬å¸å“ç‰Œä¿¡æ¯ */
  export interface CompanyConfig {
    id?: number;
    companyName?: string;
    logoUrl?: string;
    logoBlobId?: number;
    loginTitle?: string;
    loginDescription?: string;
  }
}
/** èŽ·å¾—å…¬å¸å“ç‰Œä¿¡æ¯ï¼ˆå…ç™»å½•å…¬å¼€ï¼Œç™»å½•é¡µ / åº”用启动时使用) */
export async function getCompanyConfig() {
  return requestClient.get<SystemCompanyConfigApi.CompanyConfig | null>(
    '/system/company-config/get',
  );
}
/** ä¿å­˜å…¬å¸å“ç‰Œä¿¡æ¯ */
export async function saveCompanyConfig(
  data: SystemCompanyConfigApi.CompanyConfig,
) {
  return requestClient.post<number>('/system/company-config/save', data);
}
src/api/system/storage/index.ts
@@ -45,6 +45,17 @@
  );
}
/** å…¬å…±æ–‡ä»¶ä¸Šä¼ ï¼ˆæ°¸ä¹…有效,返回永久公开 previewURL,用于登录页等免登录展示的场景) */
export function publicUploadFile(files: globalThis.File[]) {
  const formData = new FormData();
  files.forEach((file) => formData.append('files', file));
  return requestClient.post<SystemStorageApi.StorageBlob[]>(
    '/system/storage-blob/public-upload',
    formData,
    { headers: { 'Content-Type': 'multipart/form-data' } },
  );
}
/** æŸ¥è¯¢é™„件列表 */
export function listAttachments(params: SystemStorageApi.AttachmentListParams) {
  return requestClient.get<SystemStorageApi.StorageBlob[]>(
src/bootstrap.ts
@@ -18,6 +18,9 @@
import App from './app.vue';
import { router } from './router';
import { getCompanyConfig } from '#/api/system/company-config';
import { applyCompanyConfig } from '#/utils/company-config';
async function bootstrap(namespace: string) {
  // åˆå§‹åŒ–组件适配器
  await initComponentAdapter();
@@ -49,6 +52,17 @@
  // é…ç½® pinia-store
  await initStores(app, { namespace });
  // åŠ è½½å…¬å¸å“ç‰Œä¿¡æ¯ï¼ˆä»ŽåŽç«¯ï¼Œå…ç™»å½•æŽ¥å£ï¼‰ï¼Œè”åŠ¨ç™»å½•é¡µ/菜单栏/网页标题。
  // æ³¨ï¼šæ­¤æ—¶ pinia å°šæœªåœ¨ç»„件外激活,这里用纯函数,不依赖 store。
  try {
    const companyConfig = await getCompanyConfig();
    if (companyConfig) {
      applyCompanyConfig(companyConfig);
    }
  } catch {
    // åŽç«¯ä¸å¯ç”¨æˆ–未配置,保持编译期默认
  }
  // å®‰è£…权限指令
  registerAccessDirective(app);
src/layouts/auth.vue
@@ -1,16 +1,31 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, onMounted } from 'vue';
import { AuthPageLayout } from '@vben/layouts';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
import { useCompanyConfigStore } from '#/store/company-config';
declare const __APP_LOGO__: string;
const companyConfigStore = useCompanyConfigStore();
onMounted(() => {
  // ç™»å½•页展示 loginTitle:启动加载走的是纯函数(不填 store),进入登录页时拉取一次
  companyConfigStore.loadFromServer();
});
const appName = computed(() => preferences.copyright.companyName);
const logo = computed(() => __APP_LOGO__ || preferences.logo.source);
const logoDark = computed(() => preferences.logo.sourceDark);
// ç™»å½•页标题/描述优先取后端配置,未配置时回退到 i18n é»˜è®¤
const pageTitle = computed(
  () => companyConfigStore.loginTitle || $t('authentication.pageTitle'),
);
const pageDescription = computed(
  () => companyConfigStore.loginDescription || $t('authentication.pageDesc'),
);
</script>
<template>
@@ -18,8 +33,8 @@
    :app-name="appName"
    :logo="logo"
    :logo-dark="logoDark"
    :page-description="$t('authentication.pageDesc')"
    :page-title="$t('authentication.pageTitle')"
    :page-description="pageDescription"
    :page-title="pageTitle"
  >
    <!-- è‡ªå®šä¹‰å·¥å…·æ  -->
    <!-- <template #toolbar></template> -->
src/packages/@core/ui-kit/shadcn-ui/src/components/segmented/segmented.vue
@@ -5,17 +5,22 @@
import { TabsTrigger } from 'reka-ui';
import { cn } from '../../../../../base/shared/src/utils';
import { Tabs, TabsContent, TabsList } from '../../ui';
import TabsIndicator from './tabs-indicator.vue';
interface Props {
  defaultValue?: string;
  tabs?: SegmentedItem[];
  /** æ˜¯å¦å…è®¸æ¨ªå‘滚动:tab è¾ƒå¤šæ—¶å¯ç”¨ï¼Œå„ tab æŒ‰å†…容宽度排列,超出部分可滑动 */
  scrollable?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
  defaultValue: '',
  tabs: () => [],
  scrollable: false,
});
const activeTab = defineModel<string>();
@@ -25,12 +30,18 @@
});
const tabsStyle = computed(() => {
  if (props.scrollable) {
    return undefined;
  }
  return {
    'grid-template-columns': `repeat(${props.tabs.length}, minmax(0, 1fr))`,
  };
});
const tabsIndicatorStyle = computed(() => {
  if (props.scrollable) {
    return undefined;
  }
  return {
    width: `${(100 / props.tabs.length).toFixed(0)}%`,
  };
@@ -45,14 +56,22 @@
  <Tabs v-model="activeTab" :default-value="getDefaultValue">
    <TabsList
      :style="tabsStyle"
      class="bg-accent outline-heavy! relative grid w-full outline-2!"
      :class="
        cn(
          'bg-accent outline-heavy! relative outline-2!',
          // å¯æ»šåŠ¨æ¨¡å¼ï¼šå·¦å¯¹é½ï¼ˆè¦†ç›– reka-ui çš„ justify-center,否则内容超宽时左侧被挤出且无法滚动)、全宽、横向滚动
          scrollable
            ? 'flex! w-full! overflow-x-auto justify-start!'
            : 'grid w-full',
        )
      "
    >
      <TabsIndicator :style="tabsIndicatorStyle" />
      <TabsIndicator v-if="!scrollable" :style="tabsIndicatorStyle" />
      <template v-for="tab in tabs" :key="tab.value">
        <TabsTrigger
          :value="tab.value"
          :class="activeClass(tab.value)"
          class="hover:text-primary z-20 inline-flex items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap disabled:pointer-events-none disabled:opacity-50"
          class="hover:text-primary z-20 inline-flex shrink-0 items-center justify-center rounded-md px-2 py-1 text-sm font-medium whitespace-nowrap disabled:pointer-events-none disabled:opacity-50"
        >
          {{ tab.label }}
        </TabsTrigger>
src/packages/effects/layouts/src/widgets/preferences/blocks/company/company.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,151 @@
<script setup lang="ts">
import { message } from 'ant-design-vue';
import { Button, Input, Upload } from 'ant-design-vue';
import { onMounted, ref } from 'vue';
// æ³¨æ„ï¼šæœ¬æ–‡ä»¶ä½äºŽ @vben çš„ packages å†…部,无法使用应用层别名 #/,需用相对路径(与同目录 custom.vue ä¿æŒä¸€è‡´ï¼‰
import { publicUploadFile } from '../../../../../../../../api/system/storage';
import { $t } from '../../../../../../../locales/src';
import { useCompanyConfigStore } from '../../../../../../../../store/company-config';
defineOptions({
  name: 'PreferenceCompanyConfig',
});
const companyConfigStore = useCompanyConfigStore();
const companyName = ref(companyConfigStore.companyName);
const loginTitle = ref(companyConfigStore.loginTitle);
const loginDescription = ref(companyConfigStore.loginDescription);
const logoUrl = ref(companyConfigStore.logoUrl);
const uploading = ref(false);
// æ‰“开区块时从后端拉取最新公司配置并回填表单(store åˆå€¼å¯èƒ½ä¸ºç©ºï¼Œå› å¯åŠ¨åŠ è½½èµ°çš„æ˜¯çº¯å‡½æ•°ï¼‰
onMounted(async () => {
  await companyConfigStore.loadFromServer();
  companyName.value = companyConfigStore.companyName;
  loginTitle.value = companyConfigStore.loginTitle;
  loginDescription.value = companyConfigStore.loginDescription;
  logoUrl.value = companyConfigStore.logoUrl;
});
/** ä¸Šä¼  logo(永久公开,供登录页免登录展示) */
async function handleBeforeUpload(file: File) {
  uploading.value = true;
  try {
    const blobs = await publicUploadFile([file]);
    const blob = blobs?.[0];
    if (blob?.previewURL || blob?.url) {
      logoUrl.value = blob.previewURL || blob.url || '';
      message.success($t('preferences.company.uploadSuccess'));
    } else {
      message.error($t('preferences.company.uploadFailed'));
    }
  } catch {
    message.error($t('preferences.company.uploadFailed'));
  } finally {
    uploading.value = false;
  }
  // è¿”回 false,阻止组件默认上传(已通过接口上传)
  return false;
}
async function handleSave() {
  if (!companyName.value.trim()) {
    message.warning($t('preferences.company.companyNameRequired'));
    return;
  }
  try {
    await companyConfigStore.save({
      companyName: companyName.value.trim(),
      logoUrl: logoUrl.value || undefined,
      loginTitle: loginTitle.value,
      loginDescription: loginDescription.value,
    });
    message.success($t('preferences.company.saveSuccess'));
  } catch {
    message.error($t('preferences.company.saveFailed'));
  }
}
</script>
<template>
  <div class="flex flex-col">
    <!-- å…¬å¸åç§° -->
    <div class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1">
      <span class="flex items-center text-sm">
        {{ $t('preferences.company.companyName') }}
      </span>
      <Input
        v-model:value="companyName"
        :placeholder="$t('preferences.company.companyNamePlaceholder')"
        class="w-41.25"
        allow-clear
      />
    </div>
    <!-- ç™»å½•页标题 -->
    <div class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1">
      <span class="flex items-center text-sm">
        {{ $t('preferences.company.loginTitle') }}
      </span>
      <Input
        v-model:value="loginTitle"
        :placeholder="$t('preferences.company.loginTitlePlaceholder')"
        class="w-41.25"
        allow-clear
      />
    </div>
    <!-- ç™»å½•页描述 -->
    <div class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1">
      <span class="flex items-center text-sm">
        {{ $t('preferences.company.loginDescription') }}
      </span>
      <Input
        v-model:value="loginDescription"
        :placeholder="$t('preferences.company.loginDescriptionPlaceholder')"
        class="w-41.25"
        allow-clear
      />
    </div>
    <!-- å…¬å¸ Logo -->
    <div class="my-1 flex w-full items-center justify-between rounded-md px-2 py-1">
      <span class="flex items-center text-sm">
        {{ $t('preferences.company.logo') }}
      </span>
      <div class="flex items-center gap-2">
        <Upload
          :show-upload-list="false"
          accept="image/*"
          :before-upload="handleBeforeUpload"
        >
          <Button size="small" :loading="uploading">
            {{ $t('preferences.company.uploadLogo') }}
          </Button>
        </Upload>
        <img
          v-if="logoUrl"
          :src="logoUrl"
          alt="company-logo"
          class="h-9 w-9 rounded border border-border object-contain"
        />
        <span v-else class="text-xs text-muted-foreground">
          {{ $t('preferences.company.logoEmpty') }}
        </span>
      </div>
    </div>
    <div v-if="uploading" class="px-2 pb-1 text-xs text-muted-foreground">
      {{ $t('preferences.company.uploading') }}
    </div>
    <!-- ä¿å­˜ -->
    <div class="mt-3 px-2">
      <Button type="primary" class="w-full" @click="handleSave">
        {{ $t('preferences.company.save') }}
      </Button>
    </div>
  </div>
</template>
src/packages/effects/layouts/src/widgets/preferences/blocks/index.ts
@@ -1,4 +1,5 @@
export { default as Block } from './block.vue';
export { default as Company } from './company/company.vue';
export { default as Custom } from './custom/custom.vue';
export { default as Animation } from './general/animation.vue';
export { default as General } from './general/general.vue';
src/packages/effects/layouts/src/widgets/preferences/preferences-drawer.vue
@@ -43,6 +43,7 @@
  Breadcrumb,
  BuiltinTheme,
  ColorMode,
  Company,
  Content,
  Copyright,
  Custom,
@@ -256,6 +257,10 @@
      label: $t('preferences.general'),
      value: 'general',
    },
    {
      label: $t('preferences.company.title'),
      value: 'company',
    },
  ];
  if (showCustomTab.value) {
@@ -352,6 +357,7 @@
        <VbenSegmented
          v-model="activeTab"
          :tabs="tabs"
          scrollable
          :class="{
            'sticky-tabs-header': appEnableStickyPreferencesNavigationBar,
          }"
@@ -526,6 +532,11 @@
              />
            </Block>
          </template>
          <template #company>
            <Block :title="$t('preferences.company.title')">
              <Company />
            </Block>
          </template>
          <template #custom>
            <Block :title="customTabTitle">
              <Custom
src/packages/locales/src/langs/en-US/preferences.json
@@ -229,5 +229,24 @@
        "placeholder": "Please enter the default report title"
      }
    }
  },
  "company": {
    "title": "Company Info",
    "companyName": "Company Name",
    "companyNamePlaceholder": "Please enter company name",
    "companyNameRequired": "Company name is required",
    "loginTitle": "Login Page Title",
    "loginTitlePlaceholder": "Enter login page title (optional)",
    "loginDescription": "Login Page Description",
    "loginDescriptionPlaceholder": "Enter login page description (optional)",
    "logo": "Company Logo",
    "uploadLogo": "Upload Logo",
    "uploadSuccess": "Logo uploaded successfully",
    "uploadFailed": "Logo upload failed",
    "uploading": "Uploading...",
    "logoEmpty": "No logo set",
    "save": "Save",
    "saveSuccess": "Company info saved successfully",
    "saveFailed": "Failed to save company info"
  }
}
src/packages/locales/src/langs/zh-CN/preferences.json
@@ -229,5 +229,24 @@
        "placeholder": "请输入报表默认标题"
      }
    }
  },
  "company": {
    "title": "公司信息",
    "companyName": "公司名称",
    "companyNamePlaceholder": "请输入公司名称",
    "companyNameRequired": "公司名称不能为空",
    "loginTitle": "登录页标题",
    "loginTitlePlaceholder": "请输入登录页标题(可选)",
    "loginDescription": "登录页描述",
    "loginDescriptionPlaceholder": "请输入登录页描述(可选)",
    "logo": "公司 Logo",
    "uploadLogo": "上传 Logo",
    "uploadSuccess": "Logo ä¸Šä¼ æˆåŠŸ",
    "uploadFailed": "Logo ä¸Šä¼ å¤±è´¥",
    "uploading": "上传中...",
    "logoEmpty": "未设置 Logo",
    "save": "保存",
    "saveSuccess": "公司信息保存成功",
    "saveFailed": "公司信息保存失败"
  }
}
src/store/company-config.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
import {
  getCompanyConfig,
  saveCompanyConfig as saveCompanyConfigApi,
} from '#/api/system/company-config';
import { applyCompanyConfig } from '#/utils/company-config';
/**
 * å…¬å¸å“ç‰Œä¿¡æ¯å…¨å±€é…ç½®
 *
 * å¯åŠ¨æ—¶ä»ŽåŽç«¯åŠ è½½ï¼Œä¿å­˜æ—¶å†™å›žåŽç«¯ï¼›å¹¶å°†å…¬å¸å/logo åŒæ­¥åˆ° preferences,
 * ä»Žè€Œè”动登录页、菜单栏、网页标题等所有消费点。
 */
export const useCompanyConfigStore = defineStore('companyConfig', () => {
  const companyName = ref('');
  const logoUrl = ref('');
  const loginTitle = ref('');
  const loginDescription = ref('');
  /** å°†å…¬å¸ä¿¡æ¯åº”用到 preferences,联动登录页/菜单栏/标题 */
  function applyToPreferences() {
    applyCompanyConfig({
      companyName: companyName.value,
      logoUrl: logoUrl.value,
    });
  }
  /** ä»ŽåŽç«¯åŠ è½½å…¬å¸é…ç½®å¹¶åº”ç”¨ */
  async function loadFromServer() {
    try {
      const config = await getCompanyConfig();
      if (config) {
        companyName.value = config.companyName ?? '';
        logoUrl.value = config.logoUrl ?? '';
        loginTitle.value = config.loginTitle ?? '';
        loginDescription.value = config.loginDescription ?? '';
        applyToPreferences();
      }
    } catch {
      // åŽç«¯ä¸å¯ç”¨æˆ–未配置,保持编译期默认
    }
  }
  /** ä¿å­˜å…¬å¸é…ç½®åˆ°åŽç«¯å¹¶åº”用 */
  async function save(data: {
    companyName: string;
    logoUrl?: string;
    logoBlobId?: number;
    loginTitle?: string;
    loginDescription?: string;
  }) {
    await saveCompanyConfigApi(data);
    companyName.value = data.companyName;
    logoUrl.value = data.logoUrl ?? '';
    loginTitle.value = data.loginTitle ?? '';
    loginDescription.value = data.loginDescription ?? '';
    applyToPreferences();
  }
  return {
    companyName,
    logoUrl,
    loginTitle,
    loginDescription,
    applyToPreferences,
    loadFromServer,
    save,
  };
});
src/store/index.ts
@@ -1 +1,2 @@
export * from './auth';
export * from './company-config';
src/utils/company-config.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
import { preferencesManager } from '@vben/preferences';
/** å…¬å¸å“ç‰Œä¿¡æ¯ï¼ˆä¸ŽåŽç«¯ system_company_config è¡¨å­—段对应) */
export interface CompanyConfigData {
  companyName?: string;
  logoUrl?: string;
  loginTitle?: string;
  loginDescription?: string;
}
/**
 * å°†å…¬å¸å“ç‰Œä¿¡æ¯åº”用到全局:
 * - å…¬å¸å â†’ preferences.app.name / copyright.companyName(联动菜单栏、登录页、网页标题、版权)
 * - logo â†’ preferences.logo.source(联动登录页、侧边栏 logo)
 * - æœ‰å…¬å¸åæ—¶ï¼Œè®¾ç½® document.title(基础标题,dynamicTitle å¼€å¯æ—¶è·¯ç”±æ ‡é¢˜åœ¨æ­¤åŸºç¡€ä¸Šæ‹¼æŽ¥ï¼‰
 */
export function applyCompanyConfig(config: CompanyConfigData) {
  const updates: Record<string, any> = {};
  if (config.logoUrl) {
    updates.logo = { source: config.logoUrl };
  }
  if (config.companyName) {
    updates.app = { name: config.companyName };
    updates.copyright = { companyName: config.companyName };
  }
  if (Object.keys(updates).length > 0) {
    preferencesManager.updatePreferences(updates);
  }
  if (config.companyName) {
    document.title = config.companyName;
  }
}
src/views/hrm/salary/leave-type-config/data.ts
@@ -3,7 +3,7 @@
import type { HrmLeaveTypeConfigApi } from '#/api/hrm/leave-type-config';
/** è¡¨å•类型 */
export type FormType = 'update';
export type FormType = 'create' | 'update';
/** æ–°å¢ž/修改请假扣款配置的表单 */
export function useFormSchema(): VbenFormSchema[] {
@@ -17,13 +17,29 @@
      },
    },
    {
      fieldName: 'name',
      fieldName: 'leaveType',
      label: '请假类型',
      component: 'Input',
      component: 'Select',
      componentProps: {
        disabled: true,
        placeholder: '',
        options: [
          { label: '事假', value: 1 },
          { label: '病假', value: 2 },
          { label: '年假', value: 3 },
          { label: '婚假', value: 4 },
          { label: '产假', value: 5 },
          { label: '丧假', value: 6 },
        ],
        placeholder: '请选择请假类型',
        style: { width: '100%' },
      },
      dependencies: {
        triggerFields: ['id'],
        // ç¼–辑时不允许修改请假类型
        componentProps: (values: Record<string, unknown>) => ({
          disabled: Boolean(values.id),
        }),
      },
      rules: 'required',
      formItemClass: 'col-span-2',
    },
    {
@@ -47,6 +63,7 @@
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      defaultValue: 0,
      componentProps: {
        options: [
          { label: '启用', value: 0 },
src/views/hrm/salary/leave-type-config/index.vue
@@ -7,7 +7,10 @@
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getLeaveTypeConfigList } from '#/api/hrm/leave-type-config';
import {
  deleteLeaveTypeConfig,
  getLeaveTypeConfigList,
} from '#/api/hrm/leave-type-config';
import { $t } from '#/locales';
import { useGridColumns } from './data';
@@ -23,9 +26,21 @@
  gridApi.query();
}
/** æ–°å¢žé…ç½® */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** ç¼–辑配置 */
function handleEdit(row: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
  formModalApi.setData({ id: row.id, row }).open();
  formModalApi.setData({ formType: 'update', row }).open();
}
/** åˆ é™¤é…ç½® */
async function handleDelete(row: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
  await deleteLeaveTypeConfig(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', ['请假扣款配置']));
  handleRefresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -56,6 +71,19 @@
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="请假扣款配置列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['请假扣款配置']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:leave-type-config:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #deductionRatio="{ row }">
        <Tag :color="row.deductionRatio === 0 ? 'success' : row.deductionRatio === 1 ? 'error' : 'warning'">
          {{ row.deductionRatio === 0 ? '不扣款' : `${(row.deductionRatio * 100).toFixed(0)}%` }}
@@ -76,6 +104,17 @@
              auth: ['hrm:leave-type-config:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:leave-type-config:delete'],
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', ['请假扣款配置']),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
src/views/hrm/salary/leave-type-config/modules/form.vue
@@ -1,19 +1,28 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { HrmLeaveTypeConfigApi } from '#/api/hrm/leave-type-config';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { updateLeaveTypeConfig } from '#/api/hrm/leave-type-config';
import {
  createLeaveTypeConfig,
  updateLeaveTypeConfig,
} from '#/api/hrm/leave-type-config';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const getTitle = computed(() =>
  formType.value === 'update' ? '编辑扣款比例' : '新增请假扣款配置',
);
const [Form, formApi] = useVbenForm({
  commonConfig: {
@@ -36,11 +45,35 @@
      return;
    }
    modalApi.lock();
    const data = await formApi.getValues();
    const data = (await formApi.getValues()) as HrmLeaveTypeConfigApi.LeaveTypeConfig;
    // è½¬æ¢æ¯”例为小数
    data.deductionRatio = data.deductionRatio / 100;
    const deductionRatio = data.deductionRatio! / 100;
    try {
      await updateLeaveTypeConfig(data);
      if (formType.value === 'update') {
        await updateLeaveTypeConfig({
          id: data.id,
          deductionRatio,
          remark: data.remark,
          status: data.status,
        });
      } else {
        // è¯·å‡ç±»åž‹åç§°ç”±æ‰€é€‰ç±»åž‹è‡ªåŠ¨å¸¦å‡ºï¼Œæ— éœ€æ‰‹åŠ¨å¡«å†™
        const NAME_MAP: Record<number, string> = {
          1: '事假',
          2: '病假',
          3: '年假',
          4: '婚假',
          5: '产假',
          6: '丧假',
        };
        await createLeaveTypeConfig({
          leaveType: data.leaveType,
          name: NAME_MAP[data.leaveType!] ?? '',
          deductionRatio,
          remark: data.remark,
          status: data.status,
        });
      }
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
@@ -52,16 +85,24 @@
    if (!isOpen) {
      return;
    }
    const data = modalApi.getData<{ id: number; row: HrmLeaveTypeConfigApi.LeaveTypeConfig }>();
    const data = modalApi.getData<{ formType: FormType; row: HrmLeaveTypeConfigApi.LeaveTypeConfig }>();
    formType.value = data.formType;
    if (data.formType === 'create' || !data.row) {
      await formApi.resetForm();
      return;
    }
    // è½¬æ¢æ¯”例为百分比显示
    const row = { ...data.row, deductionRatio: (data.row.deductionRatio || 0) * 100 };
    const row = {
      ...data.row,
      deductionRatio: (data.row.deductionRatio || 0) * 100,
    };
    await formApi.setValues(row);
  },
});
</script>
<template>
  <Modal title="编辑扣款比例" class="w-[600px]">
  <Modal :title="getTitle" class="w-[600px]">
    <Form class="mx-4" />
  </Modal>
</template>
</template>
src/views/mes/pro/route/data.ts
@@ -219,6 +219,15 @@
      fieldName: 'linkType',
      label: '与下道工序关系',
      component: 'Select',
      help: () =>
        h(
          'div',
          {
            style:
              'white-space: pre-line; max-width: 320px; line-height: 1.6;',
          },
          '0=开始-开始(SS):本工序开始后,下道工序才能开始(可重叠并行开工)\n1=结束-结束(FF):下道工序必须等到本工序结束才能结束(并行但同步完工)\n2=开始-结束(SF):本工序开始后,下道工序才能结束\n3=结束-开始(FS):本工序结束后,下道工序才能开始(默认值)',
        ),
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, 'number'),
src/views/mes/pro/route/modules/process-form.vue
@@ -31,7 +31,7 @@
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-1',
    labelWidth: 130,
    labelWidth: 150,
  },
  layout: 'horizontal',
  schema: [],
src/views/mes/pro/workorder/modules/process-form.vue
@@ -2,7 +2,7 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
import { computed, ref } from 'vue';
import { computed, h, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
@@ -91,6 +91,15 @@
      fieldName: 'linkType',
      label: '工序关系',
      component: 'Select',
      help: () =>
        h(
          'div',
          {
            style:
              'white-space: pre-line; max-width: 320px; line-height: 1.6;',
          },
          '0=开始-开始(SS):本工序开始后,下道工序才能开始(可重叠并行开工)\n1=结束-结束(FF):下道工序必须等到本工序结束才能结束(并行但同步完工)\n2=开始-结束(SF):本工序开始后,下道工序才能结束\n3=结束-开始(FS):本工序结束后,下道工序才能开始(默认值)',
        ),
      componentProps: {
        allowClear: true,
        options: [
src/views/mes/process-design/route/data.ts
@@ -205,6 +205,15 @@
      fieldName: "linkType",
      label: "与下道工序关系",
      component: "Select",
      help: () =>
        h(
          "div",
          {
            style:
              "white-space: pre-line; max-width: 320px; line-height: 1.6;",
          },
          "0=开始-开始(SS):本工序开始后,下道工序才能开始(可重叠并行开工)\n1=结束-结束(FF):下道工序必须等到本工序结束才能结束(并行但同步完工)\n2=开始-结束(SF):本工序开始后,下道工序才能结束\n3=结束-开始(FS):本工序结束后,下道工序才能开始(默认值)",
        ),
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, "number"),
src/views/mes/process-design/route/modules/process-form.vue
@@ -31,7 +31,7 @@
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-1',
    labelWidth: 130,
    labelWidth: 150,
  },
  layout: 'horizontal',
  schema: [],