gaoluyang
2026-10-26 64e47529e29ab013b32d3c09e4044b5b192c3c7a
feat: 完善南通苏珩线缆工业智能系统
已添加5个文件
已修改14个文件
1198 ■■■■■ 文件已修改
.env.development 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
company.config.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/regularize/index.ts 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mdm/item/index.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bootstrap.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/auth-scene.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/auth.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/preferences.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/company-config.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/styles/herbal-theme.css 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/_core/authentication/login.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/data.ts 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/mdm/modules/form.vue 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/regularize/data.ts 207 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/regularize/index.vue 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/regularize/modules/audit-modal.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/regularize/modules/form.vue 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.development
@@ -1,6 +1,9 @@
# ç«¯å£å·
VITE_PORT=5666
# å½“前开发环境品牌
VITE_COMPANY=TRT
VITE_BASE=/
# è¯·æ±‚路径
company.config.ts
@@ -78,7 +78,7 @@
    apiUrl: 'http://36.213.150.237:9013/admin-api',
  },
  TRT: {
    name: '北京同仁堂',
    name: '南通苏珩线缆',
    logo: '/logos/YZFXLogo.png',
    favicon: '/logos/YZFXLogo.ico',
    apiUrl: 'http://36.213.150.237:9015/admin-api',
src/api/hrm/regularize/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
import type { PageParam, PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmRegularizeApi {
  /** è½¬æ­£ç”³è¯· */
  export interface RegularizeApplication {
    id?: number;
    no?: string;
    userId?: number;
    userName?: string;
    deptId?: number;
    deptName?: string;
    postId?: number;
    postName?: string;
    employmentType?: number;
    employmentTypeName?: string;
    expectedDate?: string;
    reason?: string;
    status?: number;
    statusName?: string;
    // ä»¥ä¸‹ä¸ºå®¡æ ¸æ´¾ç”Ÿå­—段,由后端在审核时写入,前端只读展示
    reviewerId?: number; // å®¡æ ¸äººID(实际执行审核的用户)
    reviewerName?: string; // å®¡æ ¸äººå§“名
    reviewTime?: string; // å®¡æ ¸æ—¶é—´
    reviewRemark?: string; // å®¡æ ¸æ„è§ï¼ˆå®¡æ ¸é€šè¿‡æ—¶çš„æ„è§ / å®¡æ ¸ä¸é€šè¿‡æ—¶çš„原因)
    approveTime?: string;
    remark?: string;
    createTime?: string;
  }
}
/** æŸ¥è¯¢è½¬æ­£ç”³è¯·åˆ†é¡µåˆ—表 */
export function getRegularizePage(params: PageParam) {
  return requestClient.get<PageResult<HrmRegularizeApi.RegularizeApplication>>(
    '/hrm/regularize/page',
    { params },
  );
}
/** æŸ¥è¯¢è½¬æ­£ç”³è¯·è¯¦æƒ… */
export function getRegularize(id: number) {
  return requestClient.get<HrmRegularizeApi.RegularizeApplication>(
    `/hrm/regularize/get?id=${id}`,
  );
}
/** æ–°å¢žè½¬æ­£ç”³è¯·ï¼ˆè‰ç¨¿ï¼‰ */
export function createRegularize(data: HrmRegularizeApi.RegularizeApplication) {
  return requestClient.post('/hrm/regularize/create', data);
}
/** æ›´æ–°è½¬æ­£ç”³è¯· */
export function updateRegularize(data: HrmRegularizeApi.RegularizeApplication) {
  return requestClient.put('/hrm/regularize/update', data);
}
/** åˆ é™¤è½¬æ­£ç”³è¯· */
export function deleteRegularize(id: number) {
  return requestClient.delete(`/hrm/regularize/delete?id=${id}`);
}
/**
 * æäº¤è½¬æ­£ç”³è¯·å®¡æ‰¹ï¼ˆè‰ç¨¿ / å®¡æ ¸ä¸é€šè¿‡ â†’ å®¡æ‰¹ä¸­ï¼‰
 *
 * å®¡æ‰¹äººç”±ã€Œç³»ç»Ÿç®¡ç† - å®¡æ‰¹é…ç½®ã€æŒ‰ä¸šåŠ¡ç±»åž‹ï¼ˆhrm_regularize_approve)统一配置,提交时无需指定。
 */
export function submitRegularize(id: number) {
  return requestClient.put('/hrm/regularize/submit', null, { params: { id } });
}
/** èŽ·å¾—è½¬æ­£ç”³è¯·çš„å®¡æ‰¹äººç¼–å·é›†åˆï¼Œä¾›åˆ—è¡¨é¡µåˆ¤æ–­æ˜¯å¦å±•ç¤ºå®¡æ ¸æŒ‰é’® */
export function getRegularizeApproverIds() {
  return requestClient.get<number[]>('/hrm/regularize/approver-ids');
}
/**
 * å®¡æ ¸è½¬æ­£ç”³è¯·ï¼ˆå®¡æ‰¹ä¸­ â†’ å®¡æ ¸é€šè¿‡ / å®¡æ ¸ä¸é€šè¿‡ï¼‰
 *
 * ä»…审批配置中的审批人可调用(多人时或签);pass=false æ—¶ reviewRemark(不通过原因)必填。
 */
export function auditRegularize(
  id: number,
  pass: boolean,
  reviewRemark?: string,
) {
  return requestClient.put('/hrm/regularize/audit', { id, pass, reviewRemark });
}
src/api/mdm/item/index.ts
@@ -6,10 +6,11 @@
  /** ç‰©æ–™ä¿¡æ¯ */
  export interface Item {
    id?: number;
    code: string;
    code?: string;
    name: string;
    barCode?: string;
    specification?: string;
    technicalParameters?: string;
    categoryId?: number;
    categoryName?: string;
    unitMeasureId?: number;
@@ -48,6 +49,17 @@
    purchaseOrderCodeFlag?: boolean;
    lotNumberFlag?: boolean;
    qualityStatusFlag?: boolean;
  }
  export interface TechnicalFile {
    id: number;
    itemId: number;
    blobId: number;
    versionNo: number;
    fileName?: string;
    previewURL?: string;
    downloadURL?: string;
    createTime?: string;
  }
  /** ç‰©æ–™åˆ†é¡µæŸ¥è¯¢å‚æ•° */
@@ -129,3 +141,13 @@
    updateSupport,
  });
}
export function createTechnicalFile(itemId: number, blobId: number) {
  return requestClient.post<number>('/mdm/item/technical-file/create', { itemId, blobId });
}
export function getTechnicalFileList(itemId: number) {
  return requestClient.get<MdmItemApi.TechnicalFile[]>('/mdm/item/technical-file/list', {
    params: { itemId },
  });
}
src/bootstrap.ts
@@ -22,6 +22,9 @@
import { getCompanyConfig } from '#/api/system/company-config';
import { applyCompanyConfig } from '#/utils/company-config';
declare const __APP_LOGO__: string;
declare const __APP_NAME__: string;
async function bootstrap(namespace: string) {
  // åˆå§‹åŒ–组件适配器
  await initComponentAdapter();
@@ -64,6 +67,11 @@
    // åŽç«¯ä¸å¯ç”¨æˆ–未配置,保持编译期默认
  }
  // æœ¬åœ°æµ‹è¯•按前端部署品牌展示,避免测试库中的旧公司配置覆盖当前客户品牌。
  if (__APP_NAME__) {
    applyCompanyConfig({ companyName: __APP_NAME__, logoUrl: __APP_LOGO__ });
  }
  // å®‰è£…权限指令
  registerAccessDirective(app);
src/layouts/auth-scene.vue
@@ -1,78 +1,51 @@
<script setup lang="ts">
withDefaults(defineProps<{ appName?: string; logo?: string; platformTitle?: string; platformDescription?: string }>(), {
  appName: '', logo: '', platformTitle: '滋补品行业数字化管理平台', platformDescription: '传承中医药精华 Â· æœåŠ¡å¥åº·ç¾Žå¥½ç”Ÿæ´»',
  appName: '',
  logo: '',
  platformTitle: '工业智能制造系统',
  platformDescription: '数字化管理 Â· æ™ºèƒ½åŒ–生产 Â· ç²¾ç»†åŒ–运营',
});
</script>
<template>
  <div class="herbal-auth">
    <img class="herbal-auth__background" src="/images/auth/herbal-courtyard.png" alt="" fetchpriority="high" decoding="async" aria-hidden="true" />
    <div class="herbal-auth__veil" aria-hidden="true"></div>
    <header class="herbal-auth__header">
      <div class="herbal-brand">
        <img v-if="logo" :src="logo" :alt="appName || '公司标志'" class="herbal-brand__logo" />
        <span v-if="appName" class="herbal-brand__name">{{ appName }}</span>
        <div class="herbal-brand__platform">
          <p class="herbal-brand__title">{{ platformTitle }}</p>
          <p class="herbal-brand__description">{{ platformDescription }}</p>
  <div class="industrial-auth">
    <div class="industrial-auth__toolbar"><slot name="toolbar"></slot></div>
    <main class="industrial-auth__shell">
      <section class="industrial-auth__brand">
        <div class="industrial-auth__brandline">
          <img v-if="logo" :src="logo" :alt="appName || '公司标志'" />
          <div>
            <strong>{{ appName }}</strong>
            <span>{{ platformTitle }}</span>
          </div>
        </div>
      </div>
      <div class="herbal-auth__toolbar"><slot name="toolbar"></slot></div>
    </header>
    <main class="herbal-auth__main">
      <section class="herbal-motto" aria-labelledby="herbal-motto-title">
        <h2 id="herbal-motto-title"><span></span>堂训<span></span></h2>
        <p>品味虽贵,必不敢减物力</p>
        <p>炮制虽繁,必不敢省人工</p>
        <div class="herbal-motto__rule" aria-hidden="true"><span></span><svg viewBox="0 0 60 24"><path d="M3 12h10c0-6 8-9 12-5 0-7 10-7 10 0 5-4 12-1 12 5h10M13 12c0 8 12 11 17 3 5 8 17 5 17-3M24 12c0-7 12-7 12 0s-12 7-12 0Z" /></svg><span></span></div>
      </section>
      <section class="herbal-panel" aria-label="账户登录">
        <div class="herbal-panel__frame" aria-hidden="true">
          <svg v-for="corner in ['tl','tr','bl','br']" :key="corner" :class="'herbal-corner herbal-corner--'+corner" viewBox="0 0 44 44"><path d="M2 42V12A10 10 0 0 1 12 2h30M9 42V9h33M2 21h19V2M9 28h19V9M2 35h33V2" /></svg>
        <div class="industrial-auth__hero">
          <p class="industrial-auth__eyebrow">SMART MANUFACTURING / 01</p>
          <h1>精益制造<br /><em>协同管理</em></h1>
          <p class="industrial-auth__description">让生产、库存、质量与经营数据在同一平台上协同运行。</p>
          <div class="industrial-auth__signal" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
        </div>
        <div class="herbal-panel__content"><slot></slot></div>
        <p class="industrial-auth__tagline">数据驱动 Â· æŒç»­æ”¹å–„</p>
      </section>
      <section class="industrial-auth__card" aria-label="账户登录"><slot></slot></section>
    </main>
    <footer class="industrial-auth__footer">{{ platformDescription }}</footer>
  </div>
</template>
<style scoped>
.herbal-auth{position:relative;isolation:isolate;min-height:100svh;overflow-x:clip;background:#f6eddf;color:#29251f;padding:clamp(24px,3.2vw,52px) clamp(24px,5vw,88px) 42px;font-family:var(--font-family,system-ui,sans-serif)}
.herbal-auth__background,.herbal-auth__veil{position:absolute;inset:0;width:100%;height:100%;z-index:-2;object-fit:cover;object-position:center}
.herbal-auth__veil{z-index:-1;background:linear-gradient(90deg,transparent 48%,rgba(248,239,226,.14))}
.herbal-auth__header{display:flex;align-items:center;justify-content:space-between;gap:24px;position:relative;z-index:2}
.herbal-brand{display:flex;align-items:center;gap:18px;min-width:0}
.herbal-brand__logo{width:70px;height:70px;object-fit:contain;flex-shrink:0}
.herbal-brand__name{font-family:"STKaiti","KaiTi","FangSong",serif;font-weight:700;font-size:clamp(24px,2.3vw,38px);white-space:nowrap}
.herbal-brand__platform{border-left:1px solid #8e7e6a;padding-left:24px;min-width:0}
.herbal-brand__title{font-size:clamp(17px,1.5vw,24px);letter-spacing:.04em;margin:0 0 7px;line-height:1.4}
.herbal-brand__description{font-size:13px;color:#71685b;letter-spacing:.16em;margin:0;line-height:1.5}
.herbal-auth__toolbar{flex-shrink:0;position:relative;min-width:80px}
.herbal-auth__toolbar :deep(>div){position:static!important;background:rgba(255,253,248,.75);border:1px solid #e5d8c5;border-radius:30px;padding:4px 10px}
.herbal-auth__main{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(400px,.85fr);gap:clamp(36px,5vw,88px);align-items:start;min-height:calc(100svh - 176px);padding-top:34px}
.herbal-motto{align-self:start;margin-top:12px;padding:0 0 24px;text-align:center}
.herbal-motto h2{display:flex;align-items:center;justify-content:center;gap:34px;font-family:"STKaiti","KaiTi","FangSong",serif;font-size:clamp(40px,4.3vw,72px);font-weight:700;line-height:1.2;letter-spacing:.2em;color:#8b1419;margin:0 0 12px}
.herbal-motto h2 span{height:1px;background:#ae7650;width:90px}
.herbal-motto p{font-family:"STKaiti","KaiTi","FangSong",serif;font-size:clamp(26px,3.15vw,54px);font-weight:700;line-height:1.65;letter-spacing:.015em;color:#863310;margin:0;white-space:nowrap}
.herbal-motto__rule{display:flex;align-items:center;justify-content:center;gap:22px;margin-top:22px;color:#a46628}
.herbal-motto__rule span{width:110px;height:1px;background:currentColor}
.herbal-motto__rule svg{width:55px;height:26px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.herbal-panel{position:relative;width:100%;max-width:584px;justify-self:end;min-height:690px;border-radius:20px;background:rgba(255,253,249,.96);box-shadow:0 22px 55px rgba(91,49,19,.13);padding:clamp(76px,12.8vh,120px) clamp(34px,4.6vw,78px) 64px;display:flex;align-items:flex-start}
.herbal-panel__frame{position:absolute;inset:16px;border:1px solid #cda878;border-radius:12px;pointer-events:none}
.herbal-corner{position:absolute;width:38px;height:38px;fill:none;stroke:#cda878;stroke-width:1.3;background:#fffdf9}
.herbal-corner--tl{top:-1px;left:-1px;border-top-left-radius:12px}.herbal-corner--tr{top:-1px;right:-1px;transform:rotate(90deg);border-top-left-radius:12px}.herbal-corner--bl{bottom:-1px;left:-1px;transform:rotate(-90deg);border-top-left-radius:12px}.herbal-corner--br{bottom:-1px;right:-1px;transform:rotate(180deg);border-top-left-radius:12px}
.herbal-panel__content{width:100%;position:relative;min-width:0}
:global(.dark .herbal-auth){background:#211b17;color:#f4eadd}
:global(.dark .herbal-auth__background){filter:brightness(.4) saturate(.7)}
:global(.dark .herbal-auth__veil){background:rgba(24,19,15,.28)}
:global(.dark .herbal-brand__description){color:#d4c3ac}
:global(.dark .herbal-motto h2){color:#f0bab3}
:global(.dark .herbal-motto p){color:#edcfa5}
:global(.dark .herbal-panel){background:rgba(39,31,26,.97)}
:global(.dark .herbal-corner){background:#271f1a}
:global(.dark .herbal-auth__toolbar >div){background:#30271f;border-color:#8d7053;color:#f4eadd}
@media(min-width:1800px){.herbal-auth__main{grid-template-columns:minmax(0,1fr) 584px}.herbal-motto{max-width:900px;justify-self:center;width:100%}}
@media(max-width:1199px){.herbal-auth{padding:28px}.herbal-brand{gap:12px}.herbal-brand__logo{width:52px;height:52px}.herbal-brand__platform{padding-left:16px}.herbal-brand__description{letter-spacing:.06em}.herbal-auth__main{gap:26px;grid-template-columns:minmax(0,1fr) minmax(360px,.9fr)}.herbal-motto p{font-size:clamp(25px,3vw,36px)}.herbal-panel{padding:50px 36px;min-height:600px}}
@media(max-width:900px){.herbal-auth{padding:24px;}.herbal-brand__name{font-size:24px}.herbal-brand__description{display:none}.herbal-brand__title{font-size:17px}.herbal-auth__main{grid-template-columns:1fr;min-height:calc(100svh - 112px);padding:32px 0;}.herbal-motto{display:none}.herbal-panel{max-width:520px;min-height:560px;justify-self:center}.herbal-auth__background{object-position:35% center}.herbal-auth__veil{background:rgba(248,239,226,.48)}}
@media(max-width:540px){.herbal-auth{padding:20px 16px 24px}.herbal-auth__header{align-items:flex-start;gap:10px}.herbal-brand{gap:10px;flex-wrap:wrap;max-width:calc(100% - 86px)}.herbal-brand__logo{width:42px;height:42px}.herbal-brand__name{font-size:22px}.herbal-brand__platform{border:0;padding:0;flex-basis:100%}.herbal-brand__title{font-size:14px;letter-spacing:.02em;margin:0}.herbal-auth__toolbar{min-width:76px}.herbal-panel{padding:50px 28px;min-height:510px;border-radius:16px}.herbal-panel__frame{inset:12px}.herbal-auth__main{padding-top:28px}.herbal-corner{width:30px;height:30px}}
@media(max-height:760px) and (min-width:901px){.herbal-panel{min-height:530px;padding-top:48px;padding-bottom:48px}.herbal-auth__main{padding-top:22px}.herbal-motto{margin-top:20px}}
</style>
<style scoped>
.industrial-auth{position:relative;isolation:isolate;min-height:100svh;display:grid;place-items:center;padding:0;background:#f3f6fa;color:#142b43;font-family:var(--font-family,Inter,system-ui,sans-serif);overflow:hidden}
.industrial-auth::before{content:'';position:absolute;width:720px;height:720px;right:-380px;top:-360px;border:1px solid #dbe6ef;border-radius:50%;box-shadow:0 0 0 34px rgba(219,230,239,.28),0 0 0 68px rgba(219,230,239,.18);z-index:-1}
.industrial-auth__toolbar{position:absolute;right:32px;top:24px;z-index:2}
.industrial-auth__toolbar :deep(>div){position:static!important;background:#fff;border:1px solid #e1e8ef;border-radius:10px;padding:4px 10px;box-shadow:0 4px 14px rgba(20,43,67,.06)}
.industrial-auth__shell{width:100%;min-height:100svh;display:grid;grid-template-columns:1.08fr .92fr;background:#fff;border:0;border-radius:0;box-shadow:none;overflow:hidden}
.industrial-auth__brand{position:relative;display:flex;flex-direction:column;padding:72px 82px;background:#102b46;color:#fff;overflow:hidden}
.industrial-auth__brand::before{content:'';position:absolute;width:540px;height:540px;right:-300px;bottom:-300px;border:1px solid rgba(111,179,222,.3);border-radius:50%;box-shadow:0 0 0 26px rgba(111,179,222,.08),0 0 0 52px rgba(111,179,222,.06)}
.industrial-auth__brandline{position:relative;z-index:1;display:flex;align-items:center;gap:16px}.industrial-auth__brandline img{width:54px;height:54px;object-fit:contain;border-radius:10px;background:#fff;padding:5px}.industrial-auth__brandline strong{display:block;font-size:23px;letter-spacing:.06em;line-height:1.35}.industrial-auth__brandline span{display:block;margin-top:5px;color:#9db6c9;font-size:12px;letter-spacing:.12em}
.industrial-auth__hero{position:relative;z-index:1;margin:auto 0}.industrial-auth__eyebrow{margin:0 0 24px;color:#79b9df;font-size:11px;font-weight:700;letter-spacing:.18em}.industrial-auth__hero h1{margin:0;color:#fff;font-size:clamp(42px,4.5vw,62px);font-weight:700;line-height:1.22;letter-spacing:.04em}.industrial-auth__hero h1 em{color:#78bce2;font-style:normal}.industrial-auth__description{max-width:360px;margin:26px 0 0;color:#b5c9d8;font-size:15px;line-height:1.9}.industrial-auth__signal{display:flex;gap:8px;margin-top:40px}.industrial-auth__signal i{display:block;width:40px;height:4px;border-radius:2px;background:#70b8df}.industrial-auth__signal i:nth-child(2){width:72px;background:#3d8fc0}.industrial-auth__signal i:nth-child(3){width:24px;background:#e08946}.industrial-auth__signal i:nth-child(4){width:12px;background:#fff}.industrial-auth__tagline{position:relative;z-index:1;margin:0;color:#8faabd;font-size:12px;letter-spacing:.14em}
.industrial-auth__card{display:flex;align-items:center;padding:80px 96px;background:#fff}.industrial-auth__card :deep(.herbal-login){width:100%;max-width:420px;margin:0 auto}
.industrial-auth__footer{position:absolute;bottom:18px;color:#8a9bab;font-size:11px;letter-spacing:.08em;z-index:2}
:global(.dark .industrial-auth){background:#0c1c2b;color:#e8f1f7}.industrial-auth :global(.dark .industrial-auth__card){background:#122b40}.industrial-auth :global(.dark .industrial-auth__toolbar >div){background:#17374f;border-color:#315873;color:#e8f1f7}
@media(max-width:860px){.industrial-auth__shell{grid-template-columns:1fr;min-height:100svh}.industrial-auth__brand{padding:38px 40px}.industrial-auth__hero{margin:54px 0}.industrial-auth__hero h1{font-size:42px}.industrial-auth__card{padding:48px 40px}.industrial-auth__footer{display:none}}
@media(max-width:520px){.industrial-auth__toolbar{right:16px;top:16px}.industrial-auth__brand{padding:30px 26px}.industrial-auth__brandline strong{font-size:19px}.industrial-auth__hero{margin:42px 0}.industrial-auth__hero h1{font-size:36px}.industrial-auth__card{padding:40px 24px}}
</style>
src/layouts/auth.vue
@@ -24,8 +24,8 @@
  // æœªé…ç½®å…¬å¸æ ‡å¿—时不展示框架演示 Logo。
  return source?.includes('@vbenjs/static-source') ? '' : source;
});
const pageTitle = computed(() => companyConfigStore.loginTitle || '滋补品行业数字化管理平台');
const pageDescription = computed(() => companyConfigStore.loginDescription || '传承中医药精华 Â· æœåŠ¡å¥åº·ç¾Žå¥½ç”Ÿæ´»');
const pageTitle = computed(() => companyConfigStore.loginTitle || `${appName.value}智能制造系统`);
const pageDescription = computed(() => companyConfigStore.loginDescription || `${appName.value}数字化管理平台`);
</script>
<template>
src/main.ts
@@ -27,23 +27,23 @@
  // é¦–次升级同步品牌主色,避免旧的蓝色偏好遮盖本次主题更新。
  // å‡çº§åŽç”¨æˆ·åœ¨åå¥½è®¾ç½®ä¸­é€‰æ‹©çš„颜色和明暗模式仍会保留。
  document.documentElement.dataset.brandTheme = 'herbal';
  const brandThemeKey = `${namespace}-herbal-theme-v1`;
  document.documentElement.dataset.brandTheme = 'industrial';
  const brandThemeKey = `${namespace}-industrial-theme-v1`;
  try {
    if (!localStorage.getItem(brandThemeKey)) {
      preferencesManager.updatePreferences({
        theme: { builtinType: 'custom', colorPrimary: '#a51d24' },
        theme: { builtinType: 'custom', colorPrimary: '#2563eb' },
      });
      localStorage.setItem(brandThemeKey, '1');
    }
  } catch {
    preferencesManager.updatePreferences({
      theme: { builtinType: 'custom', colorPrimary: '#a51d24' },
      theme: { builtinType: 'custom', colorPrimary: '#2563eb' },
    });
  }
  watch(
    () => preferences.theme.colorPrimary,
    (color) => { document.documentElement.dataset.herbalPalette = String(color.toLowerCase() === '#a51d24'); },
    (color) => { document.documentElement.dataset.industrialPalette = String(color.toLowerCase() === '#2563eb'); },
    { immediate: true, flush: 'sync' },
  );
src/preferences.ts
@@ -22,7 +22,7 @@
  // overrides
  theme: {
    builtinType: 'custom',
    colorPrimary: '#a51d24',
    colorPrimary: '#2563eb',
  },
  app: {
    /** åŽç«¯è·¯ç”±æ¨¡å¼ */
src/store/company-config.ts
@@ -7,6 +7,9 @@
} from '#/api/system/company-config';
import { applyCompanyConfig } from '#/utils/company-config';
declare const __APP_LOGO__: string;
declare const __APP_NAME__: string;
/**
 * å…¬å¸å“ç‰Œä¿¡æ¯å…¨å±€é…ç½®
 *
@@ -41,6 +44,15 @@
    } catch {
      // åŽç«¯ä¸å¯ç”¨æˆ–未配置,保持编译期默认
    }
    // æœ¬åœ°æµ‹è¯•按前端部署品牌展示,避免测试库中的旧公司配置覆盖当前客户品牌。
    if (__APP_NAME__) {
      companyName.value = __APP_NAME__;
      logoUrl.value = __APP_LOGO__;
      loginTitle.value = `${__APP_NAME__}智能制造系统`;
      loginDescription.value = `${__APP_NAME__}数字化管理平台`;
      applyToPreferences();
    }
  }
  /** ä¿å­˜å…¬å¸é…ç½®åˆ°åŽç«¯å¹¶åº”用 */
src/styles/herbal-theme.css
@@ -1,59 +1,15 @@
/* å“ç‰Œä¸­æ€§è‰²ä¸Žä¸»è‰²åˆ†ç¦»ï¼šä¸»è‰²ä»é€šè¿‡ Vben preferences ç”Ÿæˆå…¨éƒ¨è‰²é˜¶ã€‚ */
html[data-brand-theme='herbal']:not(.dark) {
  --background: 40 50% 99%;
  --background-deep: 37 33% 95%;
  --foreground: 27 16% 17%;
  --card: 40 50% 99%;
  --card-foreground: 27 16% 17%;
  --popover: 40 50% 99%;
  --popover-foreground: 27 16% 17%;
  --muted: 35 25% 94%;
  --muted-foreground: 28 10% 40%;
  --secondary: 35 25% 94%;
  --secondary-foreground: 27 16% 17%;
  --accent: 33 35% 93%;
  --accent-dark: 33 28% 90%;
  --accent-darker: 33 24% 87%;
  --accent-lighter: 38 45% 97%;
  --accent-hover: 33 28% 90%;
  --accent-foreground: 27 16% 17%;
  --border: 33 25% 86%;
  --input: 33 25% 86%;
  --sidebar: 40 50% 99%;
  --sidebar-deep: 37 35% 97%;
  --menu: var(--sidebar);
  --header: 40 50% 99%;
/* å—通苏珩线缆工业智能系统:蓝灰色企业后台基调 */
html[data-brand-theme='industrial']:not(.dark) {
  --background: 210 40% 98%; --background-deep: 210 32% 95%; --foreground: 215 35% 16%;
  --card: 0 0% 100%; --card-foreground: 215 35% 16%; --popover: 0 0% 100%; --popover-foreground: 215 35% 16%;
  --muted: 210 30% 94%; --muted-foreground: 215 16% 42%; --secondary: 210 30% 94%; --secondary-foreground: 215 35% 16%;
  --accent: 210 60% 94%; --accent-dark: 210 45% 88%; --accent-darker: 210 40% 82%; --accent-lighter: 210 50% 98%; --accent-hover: 210 50% 90%; --accent-foreground: 215 35% 16%;
  --border: 210 25% 86%; --input: 210 25% 86%; --sidebar: 215 38% 16%; --sidebar-deep: 215 42% 12%; --menu: var(--sidebar); --header: 0 0% 100%;
}
html[data-brand-theme='herbal'].dark {
  --background: 26 16% 12%;
  --background-deep: 25 18% 9%;
  --foreground: 35 30% 90%;
  --card: 26 15% 14%;
  --card-foreground: 35 30% 90%;
  --popover: 26 15% 14%;
  --popover-foreground: 35 30% 90%;
  --muted: 26 13% 19%;
  --muted-foreground: 33 15% 68%;
  --border: 27 15% 26%;
  --input: 27 15% 26%;
  --sidebar: 26 16% 12%;
  --sidebar-deep: 25 18% 10%;
  --menu: var(--sidebar);
  --header: 26 16% 12%;
html[data-brand-theme='industrial'].dark {
  --background: 215 40% 10%; --background-deep: 215 42% 8%; --foreground: 210 30% 94%; --card: 215 35% 14%; --card-foreground: 210 30% 94%;
  --popover: 215 35% 14%; --popover-foreground: 210 30% 94%; --muted: 215 28% 20%; --muted-foreground: 210 18% 70%; --border: 215 24% 28%; --input: 215 24% 28%;
  --sidebar: 215 42% 9%; --sidebar-deep: 215 45% 7%; --menu: var(--sidebar); --header: 215 38% 12%;
}
/* æš—色下使用明亮朱砂色,保证链接与选中态可读;按钮搭配深色文字。 */
html[data-brand-theme='herbal'][data-herbal-palette='true'].dark {
  --primary: 357 64% 73% !important;
  --primary-500: 357 64% 73% !important;
  --primary-600: 357 69% 77% !important;
  --primary-700: 357 70% 83% !important;
  --primary-foreground: 27 25% 12%;
}
/* æ”¯æŒåŠæ·±è‰²å¯¼èˆªåå¥½ï¼Œä¸è®©æµ…色品牌底覆盖该偏好。 */
html[data-brand-theme='herbal']:not(.dark) .dark {
  --sidebar: 26 16% 12%;
  --sidebar-deep: 25 18% 10%;
  --menu: var(--sidebar);
  --header: 26 16% 12%;
}
html[data-brand-theme='industrial'][data-industrial-palette='true'].dark { --primary: 217 91% 72% !important; --primary-500: 217 91% 72% !important; --primary-600: 217 91% 76% !important; --primary-700: 217 91% 82% !important; --primary-foreground: 215 45% 10%; }
src/views/_core/authentication/login.vue
@@ -163,14 +163,14 @@
.herbal-login__heading{margin-bottom:32px}
.herbal-login__heading h1{font-size:clamp(28px,2.4vw,36px);font-weight:700;line-height:1.4;letter-spacing:-.02em;margin:0 0 12px;color:#29251f;display:flex;align-items:center;gap:14px}
.herbal-login__heading p{font-size:15px;line-height:1.7;color:#756d63;margin:0}
.herbal-login__seal{width:20px;height:30px;flex-shrink:0;color:#a51d24;fill:none;stroke:currentColor;stroke-width:1.2}
.herbal-login :deep(input:not([type='checkbox'])){min-height:56px;border-radius:9px;border:1px solid #ddd6cc;background:rgba(255,255,255,.82);padding:12px 16px;font-size:16px;color:#30281f}
.herbal-login :deep(input::placeholder){color:#7a7167;opacity:1}
.herbal-login :deep(input:focus-visible){outline:2px solid #a51d24;outline-offset:3px}
.herbal-login__seal{width:20px;height:30px;flex-shrink:0;color:#2563eb;fill:none;stroke:currentColor;stroke-width:1.2}
.herbal-login :deep(input:not([type='checkbox'])){min-height:52px;border-radius:8px;border:1px solid #d7e1ea;background:#fff;padding:12px 16px;font-size:16px;color:#1e293b}
.herbal-login :deep(input::placeholder){color:#64748b;opacity:1}
.herbal-login :deep(input:focus-visible){outline:2px solid #2563eb;outline-offset:3px}
.herbal-login :deep(input[autocomplete='current-password']){padding-right:44px}
.herbal-login :deep(button[aria-label='login']){min-height:58px;margin-top:8px;border-radius:9px;background:#a51d24;color:#fff;font-size:19px;font-weight:600;letter-spacing:.12em;box-shadow:0 5px 12px rgba(137,24,30,.12)}
.herbal-login :deep(button[aria-label='login']:hover){background:#87171d}
.herbal-login :deep(button[aria-label='login']:focus-visible){outline:2px solid #a51d24;outline-offset:4px}
.herbal-login :deep(button[aria-label='login']){min-height:52px;margin-top:8px;border-radius:8px;background:#2563eb;color:#fff;font-size:16px;font-weight:600;letter-spacing:.12em;box-shadow:0 6px 16px rgba(37,99,235,.22)}
.herbal-login :deep(button[aria-label='login']:hover){background:#1d4ed8}
.herbal-login :deep(button[aria-label='login']:focus-visible){outline:2px solid #2563eb;outline-offset:4px}
.herbal-login :deep(button[aria-label='login']:disabled){opacity:.65;cursor:wait}
:global(.dark .herbal-login__heading h1){color:#f5eee4}
:global(.dark .herbal-login__heading p){color:#d1c1ae}
src/views/basicData/mdm/data.ts
@@ -1,19 +1,14 @@
import type { VbenFormApi, VbenFormSchema } from "#/adapter/form";
import type { VxeTableGridOptions } from "#/adapter/vxe-table";
import { h } from 'vue';
import { CommonStatusEnum, DICT_TYPE } from "#/packages/constants/src";
import { getDictOptions } from "#/packages/effects/hooks/src";
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { MesAutoCodeRuleCode } from '@vben/constants';
import { getItemTypeSimpleList } from '#/api/mes/md/item/type';
import { getUnitPage } from '#/api/mdm/unit';
import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
import { Button } from 'ant-design-vue';
// ç¼“存单位列表
let unitListCache: any[] | null = null;
@@ -95,26 +90,13 @@
    {
      component: "Input",
      fieldName: "code",
      label: "物料编码",
      label: "产品编号",
      componentProps: {
        maxLength: 64,
        placeholder: "请输入物料编码",
        placeholder: "选择产品类型并保存后自动生成",
        disabled: true,
      },
      rules: z.string().min(1, '物料编码不能为空').max(64),
      suffix: () =>
        h(
          Button,
          {
            type: 'default',
            onClick: async () => {
              const code = await generateAutoCode(
                MesAutoCodeRuleCode.MD_ITEM_CODE,
              );
              await formApi?.setFieldValue('code', code);
            },
          },
          { default: () => '生成' },
        ),
      help: "系统按产品类型自动生成九位编号,例如原材料 100100001",
    },
    {
      component: "Input",
@@ -136,6 +118,22 @@
        treeDefaultExpandAll: true,
        treeData: categoryTreeData,
        fieldNames: { label: 'label', value: 'value', children: 'children' },
      },
    },
    {
      component: 'Select',
      fieldName: 'itemType',
      label: '产品类型',
      rules: 'selectRequired',
      componentProps: {
        placeholder: '请选择产品类型',
        options: [
          { label: '原材料(1001)', value: 1 },
          { label: '半成品(1002)', value: 2 },
          { label: '成品(1003)', value: 3 },
          { label: '辅材(1004)', value: 4 },
          { label: '其他(1005)', value: 5 },
        ],
      },
    },
    {
@@ -245,6 +243,56 @@
      },
    },
    {
      component: 'Divider',
      fieldName: 'basicDataDivider',
      label: '基础数据',
      formItemClass: 'col-span-2',
      componentProps: { orientation: 'left', plain: true },
    },
    {
      component: 'Textarea',
      fieldName: 'technicalParameters',
      label: '技术参数',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入产品技术参数',
        rows: 4,
      },
    },
    {
      component: "InputNumber",
      fieldName: "safetyStock",
      label: "库存预警数量",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 2,
        placeholder: "请输入库存预警数量",
      },
    },
    {
      component: "InputNumber",
      fieldName: "minStock",
      label: "最低库存",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 2,
        placeholder: "请输入最低库存",
      },
    },
    {
      component: "InputNumber",
      fieldName: "maxStock",
      label: "最高库存",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 2,
        placeholder: "请输入最高库存",
      },
    },
    {
      component: "InputNumber",
      fieldName: "purchasePrice",
      label: "采购价",
@@ -275,39 +323,6 @@
        min: 0,
        precision: 2,
        placeholder: "请输入成本价",
      },
    },
    {
      component: "InputNumber",
      fieldName: "safetyStock",
      label: "安全库存",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 2,
        placeholder: "请输入安全库存",
      },
    },
    {
      component: "InputNumber",
      fieldName: "minStock",
      label: "最低库存",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 2,
        placeholder: "请输入最低库存",
      },
    },
    {
      component: "InputNumber",
      fieldName: "maxStock",
      label: "最高库存",
      componentProps: {
        class: "!w-full",
        min: 0,
        precision: 2,
        placeholder: "请输入最高库存",
      },
    },
    {
@@ -492,7 +507,7 @@
  return [
    {
      fieldName: "code",
      label: "物料编码",
      label: "产品编号",
      component: "Input",
      componentProps: {
        placeholder: "请输入物料编码",
@@ -527,7 +542,7 @@
    { type: "checkbox", width: 40 },
    {
      field: "code",
      title: "物料编码",
      title: "产品编号",
      minWidth: 120,
    },
    {
src/views/basicData/mdm/modules/form.vue
@@ -1,6 +1,4 @@
<script lang="ts" setup>
import type { MdmItemApi } from '#/api/mdm/item';
import { ref } from 'vue';
import { useVbenModal } from "#/packages/effects/common-ui/src";
@@ -8,7 +6,15 @@
import { message } from "ant-design-vue";
import { useVbenForm } from '#/adapter/form';
import { createItem, getItem, updateItem } from '#/api/mdm/item';
import {
  createItem,
  createTechnicalFile,
  getItem,
  getTechnicalFileList,
  updateItem,
} from '#/api/mdm/item';
import type { MdmItemApi } from '#/api/mdm/item';
import { FileUpload } from '#/components/upload';
import { $t } from '#/locales';
import { useFormSchema, clearUnitListCache, buildCategoryTree } from '../data';
@@ -18,6 +24,8 @@
const emit = defineEmits(['success']);
const formData = ref<MdmItemApi.Item>();
const technicalFiles = ref<MdmItemApi.TechnicalFile[]>([]);
const technicalUploadValue = ref<number[]>([]);
const [Form, formApi] = useVbenForm({
  commonConfig: {
@@ -52,6 +60,8 @@
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      technicalFiles.value = [];
      technicalUploadValue.value = [];
      return;
    }
    const data = modalApi.getData<MdmItemApi.Item>();
@@ -70,20 +80,68 @@
        // åŒæ­¥çŠ¶æ€æ˜ å°„ï¼šè¯¦æƒ…æŽ¥å£è¿”å›ž syncedMes,表单字段为 syncMes
        formData.value.syncMes = formData.value.syncedMes ?? true;
        await formApi.setValues(formData.value);
        technicalFiles.value = await getTechnicalFileList(data.id);
      } finally {
        modalApi.unlock();
      }
    } else {
      // æ–°å¢žæ¨¡å¼
      formData.value = undefined;
      technicalFiles.value = [];
      technicalUploadValue.value = [];
      await formApi.resetForm();
    }
  },
});
async function handleTechnicalFileChange(value: unknown) {
  if (!formData.value?.id || !Array.isArray(value)) {
    return;
  }
  const blobId = value.find((item): item is number => typeof item === 'number');
  if (!blobId) {
    return;
  }
  try {
    await createTechnicalFile(formData.value.id, blobId);
    technicalFiles.value = await getTechnicalFileList(formData.value.id);
    message.success('技术文件已保存,历史版本已保留');
  } finally {
    technicalUploadValue.value = [];
  }
}
</script>
<template>
  <Modal class="w-[1000px]" :title="formData?.id ? $t('ui.actionTitle.edit', ['物料']) : $t('ui.actionTitle.create', ['物料'])">
    <Form class="mx-4" />
    <div v-if="formData?.id" class="mx-4 mb-4 rounded border p-4">
      <div class="mb-3 text-base font-medium">技术文件历史</div>
      <FileUpload
        v-model:value="technicalUploadValue"
        value-key="id"
        :max-number="1"
        :multiple="false"
        :accept="['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.dwg', '.png', '.jpg', '.jpeg']"
        help-text="上传新文件会生成新的版本,历史文件仍可查看"
        @change="handleTechnicalFileChange"
      />
      <a-table
        class="mt-3"
        size="small"
        :data-source="technicalFiles"
        :pagination="false"
        row-key="id"
      >
        <a-table-column key="versionNo" title="版本" data-index="versionNo" />
        <a-table-column key="fileName" title="文件名" data-index="fileName" />
        <a-table-column key="createTime" title="上传时间" data-index="createTime" />
        <a-table-column key="action" title="操作">
          <template #default="{ record }">
            <a :href="record.downloadURL || record.previewURL" target="_blank">查看文件</a>
          </template>
        </a-table-column>
      </a-table>
    </div>
  </Modal>
</template>
src/views/hrm/regularize/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,207 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmRegularizeApi } from '#/api/hrm/regularize';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { getEmployeePage } from '#/api/hrm/employee';
/** è¡¨å•类型 */
export type FormType = 'create' | 'detail' | 'update';
/**
 * ç”¨å·¥æ€§è´¨é€‰é¡¹
 *
 * ä¸ŽåŽç«¯ HrmEmploymentTypeEnum ä¿æŒä¸€è‡´ï¼Œå­—典表中暂未维护该字典。
 */
export const EMPLOYMENT_TYPE_OPTIONS = [
  { label: '正式', value: 1 },
  { label: '劳务', value: 2 },
  { label: '外包', value: 3 },
  { label: '实习', value: 4 },
  { label: '退休返聘', value: 5 },
  { label: '其他', value: 6 },
];
/** ç”³è¯·äººä¸‹æ‹‰é€‰é¡¹ï¼ˆå‘˜å·¥åˆ†é¡µæŽ¥å£é€‰é¡¹è½¬æ¢åŽä¿ç•™çš„业务字段) */
export interface ApplicantOption {
  deptId?: number;
  postId?: number;
  employmentType?: number;
}
/** æ–°å¢ž/修改转正申请的表单 */
export function useFormSchema(
  formType: FormType,
  onApplicantChange?: (option: ApplicantOption) => void,
): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'deptId',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'postId',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'userId',
      label: '申请人',
      component: 'ApiSelect',
      componentProps: {
        // é€‰æ‹©å‘˜å·¥åŽï¼Œvalue å–用户的 userId(后端按 userId å…³è”员工档案)
        api: getEmployeePage,
        params: { pageNo: 1, pageSize: 1000 },
        resultField: 'list',
        labelField: 'name',
        valueField: 'userId',
        // æœªå…³è”系统账号的员工没有 userId,后端无法据此回写员工档案,直接排除
        afterFetch: async (res: { list?: Array<{ userId?: number }> }) => ({
          list: (res?.list ?? []).filter((item) => item.userId != null),
        }),
        placeholder: '请选择申请人(不选默认为本人)',
        showSearch: true,
        optionFilterProp: 'label',
        // ä»…新增时可选择;修改/查看时锁定,避免转正对象被改错
        disabled: formType !== 'create',
        onChange: (_value: unknown, option: unknown) => {
          onApplicantChange?.(option as ApplicantOption);
        },
      },
      help:
        formType === 'create'
          ? '不选择时默认申请人为当前登录用户;人事可代选试用期员工'
          : undefined,
    },
    {
      fieldName: 'employmentType',
      label: '用工性质',
      component: 'Select',
      componentProps: {
        placeholder: '请选择用工性质',
        options: EMPLOYMENT_TYPE_OPTIONS,
      },
    },
    {
      fieldName: 'expectedDate',
      label: '拟转正日期',
      component: 'DatePicker',
      componentProps: {
        placeholder: '请选择拟转正日期',
        valueFormat: 'YYYY-MM-DD',
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'reason',
      label: '转正原因',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入转正原因',
        rows: 3,
        maxlength: 500,
        showCount: true,
      },
      rules: 'required',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      formItemClass: 'col-span-2',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
    },
  ];
}
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'no',
      label: '申请单号',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入申请单号',
      },
    },
    {
      fieldName: 'employmentType',
      label: '用工性质',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: EMPLOYMENT_TYPE_OPTIONS,
        placeholder: '请选择用工性质',
      },
    },
    {
      fieldName: 'status',
      label: '审批状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: getDictOptions(DICT_TYPE.HRM_AUDIT_STATUS, 'number'),
        placeholder: '请选择审批状态',
      },
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmRegularizeApi.RegularizeApplication>['columns'] {
  return [
    { field: 'no', title: '申请单号', width: 150 },
    { field: 'userName', title: '申请人', minWidth: 100 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { field: 'postName', title: '岗位', minWidth: 120 },
    { field: 'employmentTypeName', title: '用工性质', width: 100 },
    { field: 'expectedDate', title: '拟转正日期', width: 120 },
    {
      field: 'status',
      title: '审批状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_AUDIT_STATUS },
      },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 260,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
    },
  ];
}
src/views/hrm/regularize/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,233 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmRegularizeApi } from '#/api/hrm/regularize';
import { ref } from 'vue';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { useUserStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteRegularize,
  getRegularizeApproverIds,
  getRegularizePage,
  submitRegularize,
} from '#/api/hrm/regularize';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import AuditModal from './modules/audit-modal.vue';
import Form from './modules/form.vue';
const userStore = useUserStore();
const currentUserId = userStore.userInfo?.id; // å½“前登录用户 ID,用于审批人权限判断
/** å®¡æ‰¹é…ç½®ä¸­è¯¥ä¸šåŠ¡çš„å®¡æ‰¹äººç¼–å·é›†åˆï¼Œå†³å®šã€Œå®¡æ ¸ / å®¡æ ¸ä¸é€šè¿‡ã€æŒ‰é’®æ˜¯å¦å±•示 */
const approverIds = ref<number[]>([]);
/** æ‹‰å–审批人集合;失败时保持为空(按钮不展示),不阻断列表加载 */
async function loadApproverIds() {
  try {
    approverIds.value = (await getRegularizeApproverIds()) ?? [];
  } catch {
    approverIds.value = [];
  }
}
/** å½“前登录用户是否为该业务的审批人,且申请处于审批中(10) */
function canAudit(row: HrmRegularizeApi.RegularizeApplication) {
  return (
    row.status === 10 &&
    !!currentUserId &&
    approverIds.value.includes(currentUserId)
  );
}
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [AuditModalComp, auditModalApi] = useVbenModal({
  connectedComponent: AuditModal,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ä¸Žå®¡æ‰¹äººé›†åˆ */
function handleRefresh() {
  gridApi.query();
  loadApproverIds();
}
/** åˆ›å»ºè½¬æ­£ç”³è¯· */
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
/** æŸ¥çœ‹è½¬æ­£ç”³è¯· */
function handleDetail(row: HrmRegularizeApi.RegularizeApplication) {
  formModalApi.setData({ id: row.id, formType: 'detail' }).open();
}
/** ç¼–辑转正申请 */
function handleEdit(row: HrmRegularizeApi.RegularizeApplication) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
/** åˆ é™¤è½¬æ­£ç”³è¯· */
async function handleDelete(row: HrmRegularizeApi.RegularizeApplication) {
  const hideLoading = message.loading({
    content: $t('ui.actionMessage.deleting', ['转正申请']),
    duration: 0,
  });
  try {
    await deleteRegularize(row.id!);
    message.success($t('ui.actionMessage.deleteSuccess', ['转正申请']));
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æäº¤å®¡æ‰¹ - å®¡æ‰¹äººç”±ã€Œç³»ç»Ÿç®¡ç† - å®¡æ‰¹é…ç½®ã€ç»Ÿä¸€æŒ‡å®šï¼Œæ­¤å¤„无需选择 */
async function handleSubmit(row: HrmRegularizeApi.RegularizeApplication) {
  const hideLoading = message.loading({
    content: '正在提交审批...',
    duration: 0,
  });
  try {
    await submitRegularize(row.id!);
    message.success('提交审批成功');
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** å®¡æ ¸ - é€šè¿‡ / ä¸é€šè¿‡ */
function handleAudit(
  row: HrmRegularizeApi.RegularizeApplication,
  pass: boolean,
) {
  auditModalApi.setData({ pass, row }).open();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
  },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getRegularizePage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    toolbarConfig: {
      refresh: true,
      search: true,
    },
  } as VxeTableGridOptions<HrmRegularizeApi.RegularizeApplication>,
});
// è¿›å…¥é¡µé¢å³æ‹‰å–审批人集合,用于控制审核按钮显隐
loadApproverIds();
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <AuditModalComp @success="handleRefresh" />
    <Grid table-title="转正申请列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['转正申请']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:regularize:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '查看',
              type: 'link',
              onClick: handleDetail.bind(null, row),
            },
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:regularize:update'],
              ifShow: row.status === 0,
              onClick: handleEdit.bind(null, row),
            },
            {
              label: '提交审批',
              type: 'link',
              auth: ['hrm:regularize:update'],
              // å®¡æ‰¹äººç”±å®¡æ‰¹é…ç½®ç»Ÿä¸€æŒ‡å®šï¼Œæäº¤å‰ä»…二次确认,不再弹选流程框
              popConfirm: {
                title: '确认提交审批?提交后由审批配置中指定的审批人处理。',
                confirm: handleSubmit.bind(null, row),
              },
              // è‰ç¨¿(0) ä¸Ž å®¡æ ¸ä¸é€šè¿‡(30) å‡å¯æäº¤ï¼ŒåŽè€…需修改后重新提交
              ifShow: row.status === 0 || row.status === 30,
            },
            {
              label: '审核',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['hrm:regularize:audit'],
              // ä»…「审批中 + å½“前登录用户在审批配置的审批人名单内」才显示
              ifShow: canAudit(row),
              onClick: handleAudit.bind(null, row, true),
            },
            {
              label: '审核不通过',
              type: 'link',
              danger: true,
              auth: ['hrm:regularize:audit'],
              // åŒä¸Šï¼Œä¸Žã€Œå®¡æ ¸ã€æˆå¯¹å‡ºçް
              ifShow: canAudit(row),
              onClick: handleAudit.bind(null, row, false),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:regularize:delete'],
              // åŽç«¯ä»…允许删除草稿
              ifShow: row.status === 0,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', ['转正申请']),
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/regularize/modules/audit-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '#/adapter/form';
import type { HrmRegularizeApi } from '#/api/hrm/regularize';
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 { auditRegularize } from '#/api/hrm/regularize';
const emit = defineEmits(['success']);
const pass = ref(true); // true=审核通过 / false=审核不通过
const rowData = ref<HrmRegularizeApi.RegularizeApplication>();
const getTitle = computed(() => (pass.value ? '审核通过' : '审核不通过'));
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 90,
  },
  wrapperClass: 'grid-cols-1',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
});
/** å®¡æ ¸è¡¨å•字段:不通过时必须填写原因 */
function useAuditSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'reviewRemark',
      label: '审核意见',
      component: 'Textarea',
      formItemClass: 'col-span-1',
      componentProps: {
        rows: 3,
        placeholder: pass.value
          ? '请输入审核意见(可选)'
          : '请输入不通过原因(必填)',
      },
      rules: pass.value ? '' : 'required',
    },
  ];
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const values = await formApi.getValues();
    try {
      await auditRegularize(
        rowData.value!.id!,
        pass.value,
        values.reviewRemark,
      );
      await modalApi.close();
      emit('success');
      message.success(pass.value ? '审核通过' : '已退回,可修改后重新提交');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      rowData.value = undefined;
      return;
    }
    const data = modalApi.getData<{
      pass: boolean;
      row: HrmRegularizeApi.RegularizeApplication;
    }>();
    rowData.value = data.row;
    pass.value = data.pass;
    formApi.setState({ schema: useAuditSchema() });
    formApi.resetForm();
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-2/5">
    <div v-if="rowData" class="mx-4 mb-4 text-sm text-gray-600">
      <div>申请人:{{ rowData.userName }}</div>
      <div>所属部门:{{ rowData.deptName }}</div>
      <div>岗位:{{ rowData.postName }}</div>
      <div>拟转正日期:{{ rowData.expectedDate }}</div>
      <div>转正原因:{{ rowData.reason }}</div>
    </div>
    <Form class="mx-4" />
  </Modal>
</template>
src/views/hrm/regularize/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,121 @@
<script lang="ts" setup>
import type { ApplicantOption, FormType } from '../data';
import type { HrmRegularizeApi } from '#/api/hrm/regularize';
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 {
  createRegularize,
  getRegularize,
  updateRegularize,
} from '#/api/hrm/regularize';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const isDetail = computed(() => formType.value === 'detail');
const getTitle = computed(() => {
  if (formType.value === 'detail') {
    return '查看转正申请';
  }
  return formType.value === 'update' ? '修改转正申请' : '新增转正申请';
});
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 100,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: [],
  showDefaultActions: false,
});
/** é€‰æ‹©ç”³è¯·äººåŽï¼Œè‡ªåŠ¨å¸¦å…¥å…¶éƒ¨é—¨ã€å²—ä½ä¸Žå½“å‰ç”¨å·¥æ€§è´¨ */
async function handleApplicantChange(option?: ApplicantOption) {
  if (!option) {
    return;
  }
  const patch: Record<string, number> = {};
  if (option.deptId !== undefined && option.deptId !== null) {
    patch.deptId = option.deptId;
  }
  if (option.postId !== undefined && option.postId !== null) {
    patch.postId = option.postId;
  }
  if (
    option.employmentType !== undefined &&
    option.employmentType !== null
  ) {
    patch.employmentType = option.employmentType;
  }
  await formApi.setValues(patch);
}
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (isDetail.value) {
      await modalApi.close();
      return;
    }
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = (await formApi.getValues()) as HrmRegularizeApi.RegularizeApplication;
    try {
      if (data.id) {
        await updateRegularize(data);
      } else {
        await createRegularize(data);
      }
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    formApi.setState({
      schema: useFormSchema(data.formType, handleApplicantChange),
    });
    formApi.setDisabled(data.formType === 'detail');
    modalApi.setState({ showConfirmButton: data.formType !== 'detail' });
    if (!data?.id) {
      return;
    }
    modalApi.lock();
    try {
      const detail = await getRegularize(data.id);
      await formApi.setValues(detail);
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[600px]">
    <Form class="mx-4" />
  </Modal>
</template>
vite.config.ts
@@ -82,7 +82,7 @@
        proxy: {
          "/admin-api": {
            changeOrigin: true,
            target: "http://localhost:48080",
            target: "http://127.0.0.1:48080",
            ws: true,
          },
        },