Prechádzať zdrojové kódy

Merge branch 'dev' of https://git.citupro.com/zhengnaiwen_citu/menduner into dev

zhengnaiwen_citu 3 mesiacov pred
rodič
commit
c84c74a1f0

+ 2 - 0
components.d.ts

@@ -31,6 +31,7 @@ declare module 'vue' {
     CtTextField: typeof import('./src/components/CtVuetify/CtTextField/index.vue')['default']
     DatePicker: typeof import('./src/components/DatePicker/index.vue')['default']
     Echarts: typeof import('./src/components/Echarts/index.vue')['default']
+    ElCascader: typeof import('element-plus/es')['ElCascader']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElTree: typeof import('element-plus/es')['ElTree']
     Empty: typeof import('./src/components/Empty/index.vue')['default']
@@ -42,6 +43,7 @@ declare module 'vue' {
     ImgCropper: typeof import('./src/components/ImgCropper/index.vue')['default']
     Imgs: typeof import('./src/components/Upload/imgs.vue')['default']
     'Index copy': typeof import('./src/components/CtForm/index copy.vue')['default']
+    IndexCopy: typeof import('./src/components/FormUI/datePicker/indexCopy.vue')['default']
     IndustryTypeCard: typeof import('./src/components/industryTypeCard/index.vue')['default']
     Info: typeof import('./src/components/Enterprise/info.vue')['default']
     InitPay: typeof import('./src/components/personalRecharge/initPay.vue')['default']

+ 0 - 0
src/api/mall copy.js → src/api/mallCopy.js


+ 0 - 0
src/components/FormUI/datePicker/index copy.vue → src/components/FormUI/datePicker/indexCopy.vue


+ 2 - 4
src/config/axios/service.js

@@ -23,8 +23,6 @@ const errorData = []
 
 const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
 const { result_code, base_url, request_timeout } = config
-// const entUrlStr = '/recruit' // 包含这个就是企业接口,特殊的在api function中设置tokenIndex(优先)
-// let isEnterprise = config.url.includes(entUrlStr)
 
 // 需要忽略的提示。忽略后,自动 Promise.reject('error')
 const ignoreMsgs = [
@@ -67,7 +65,7 @@ service.interceptors.request.use(
     config.headers['Accept-Language'] = getCurrentLocaleLang() ?? 'zh_CN'
     // 是否需要设置 token
     let isToken = (config.headers || {}).isToken === false
-    // token类型
+    // token类型. api》function中设置tokenIndex(优先)
     const tokenIndex = config.tokenIndex ? config.tokenIndex : getIsEnterprise() ? 1 : 2
     console.log('令牌类型', tokenIndex === 1 ? '企业:' : '个人:', getToken(tokenIndex))
     whiteList.some((v) => {
@@ -208,7 +206,7 @@ service.interceptors.response.use(
     }
     if (code === 401) {
       // 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
-      // token类型
+      // token类型. api》function中设置tokenIndex(优先)
       const tokenIndex = config.tokenIndex ? config.tokenIndex : getIsEnterprise() ? 1 : 2
 
       if (!isRefreshToken) {

+ 4 - 4
src/hooks/web/useIM.js

@@ -6,7 +6,7 @@ import { getConversationSync, getMessageSync, getChatKey, setUnread, deleteConve
 import { Base64 } from 'js-base64'
 
 import { useUserStore } from '@/store/user'
-import { isEnterprise } from '@/utils/auth'
+import { getIsEnterprise } from '@/utils/auth'
 import { useIMStore } from '@/store/im'
 
 
@@ -97,7 +97,7 @@ export function useDataSource () {
     const query = {
       msg_count: 1
     }
-    if (isEnterprise()) {
+    if (getIsEnterprise()) {
       Object.assign(query, { enterpriseId: userStore.entBaseInfo.enterpriseId })
     }
     const resultConversations = []
@@ -130,7 +130,7 @@ export function useDataSource () {
       limit,
       pull_mode,
     }
-    if (isEnterprise()) {
+    if (getIsEnterprise()) {
       Object.assign(query, { enterpriseId: userStore.entBaseInfo.enterpriseId })
     }
     const resp = await getMessageSync(query)
@@ -159,7 +159,7 @@ async function getKey () {
     // userId: userStore.accountInfo.userId
     userId: JSON.parse(localStorage.getItem('accountInfo')).userId
   }
-  if (isEnterprise()) {
+  if (getIsEnterprise()) {
     Object.assign(keyQuery, { enterpriseId: userStore.entBaseInfo.enterpriseId })
     console.log('企业模式', keyQuery)
   }

+ 0 - 15
src/store/loginType.js

@@ -1,15 +0,0 @@
-import { defineStore } from 'pinia'
-import { isEnterprise } from '@/utils/auth'
-
-export const useLoginType = defineStore('changeLoginType', {
-  state: () => ({
-    // loginType: 0
-    loginType: isEnterprise() ? 'enterprise' : 'personal'
-  }),
-  actions: {
-    change(type) {
-      this.loginType = type
-      localStorage.setItem('loginType', type) // 用户登录类型,0:企业, 1:用户
-    }
-  }
-})

+ 8 - 17
src/utils/auth.js

@@ -1,23 +1,14 @@
-import router from '@/router'
+const ENTERPRISE_PATH = '/recruit/enterprise'
 
-export const isEnterprise = () => {
-  const currentRoute = router.currentRoute.value
-  const substr = '/recruit/enterprise'
-
-  // 判断是否是企业路由
-  let bool = currentRoute?.path?.includes(substr)
-  if (currentRoute?.path === '/enterpriseVerification') bool = true
-
-  // 本地环境保存代码热更新会导致路径缺失问题
-  const testUsePath = localStorage.getItem('routerTest')
-  if (currentRoute?.path === '/' && (testUsePath?.includes(substr) || testUsePath?.includes('/enterpriseVerification'))) bool = true
-  // console.log('currentRoute', currentRoute.path)
-  console.log('=========================', bool, currentRoute.path, testUsePath)
-  return bool
+// 是否是企业路由
+export const getIsEnterprise = () => {
+  const PATH_NAME = window.location.pathname
+  if (PATH_NAME === '/enterpriseVerification') {
+    return true
+  }
+  return PATH_NAME.includes(ENTERPRISE_PATH)
 }
 
-export const getIsEnterprise = () => { return isEnterprise() } // 是否是企业路由
-
 // 获取token
 export const getToken = (index = 2) => { // index=1: 使用招聘token; index=2: 使用求职token
   const arr = ['ENT_ACCESS_TOKEN', 'PER_ACCESS_TOKEN']

+ 57 - 1
src/utils/date.js

@@ -112,4 +112,60 @@ export const getInterviewInviteDefaultTime = () => {
     time,
     timeStamp: time.getTime()
   }
-}
+}
+
+// 返回时间差,最小计量单位为一个月。示例用法:
+// const startTime = 1633072800000; // 2021-10-01 00:00:00 UTC(秒级时间戳)
+// const endTime = 1667244000000;  // 2022-11-01 00:00:00 UTC(秒级时间戳)
+export const getTimeDifferenceInChinese = (startTime, endTime) => {
+  // 将时间戳转换为 Date 对象(假设时间戳单位为毫秒)
+  const startDate = startTime ? new Date(startTime) : new Date();
+  const endDate = endTime ? new Date(endTime) : new Date();
+
+  // 计算年份差
+  let yearsDiff = endDate.getFullYear() - startDate.getFullYear();
+  // 计算月份差(考虑年份差后调整)
+  let monthsDiff = endDate.getMonth() - startDate.getMonth();
+  // 如果月份差为负,则从上一年借月
+  if (monthsDiff < 0) {
+    yearsDiff--;
+    monthsDiff += 12;
+  }
+  // 计算日期差(考虑月份差后调整,如果日期差为负,则从上一月借天)
+  let daysDiff = endDate.getDate() - startDate.getDate();
+  if (daysDiff < 0) {
+    monthsDiff--;
+    // 获取 startDate 所在月的最后一天
+    const lastDayOfMonth = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 0).getDate();
+    daysDiff += lastDayOfMonth; // 加上最后一天以补全月份差
+  }
+
+  // 构建结果字符串
+  let result = "";
+  if (yearsDiff > 0) {
+    result += `${yearsDiff}年`;
+  }
+  if (monthsDiff > 0) {
+    if (result) {
+      // 如果已经有年份差异,则直接添加月份数(不带单位),后面正则替换会处理
+      result += monthsDiff;
+    } else {
+      // 如果没有年份差异,则正常添加月份和单位
+      result += `${monthsDiff}个月`;
+      // 特别处理只有1个月的情况
+      if (monthsDiff === 1) {
+        result = "不到1个月"; // 直接替换为“不到1个月”,避免后续复杂的正则替换
+      }
+    }
+  } else if (!result && daysDiff >= 0) {
+    // 如果没有年份和月份差异,但天数差异存在(这里其实没处理天数,只是为完整性添加)
+    // 理论上应该处理天数,但题目要求只到月份,所以这里直接返回“不到1个月”
+    result = "不到1个月";
+  }
+ 
+  // 如果之前添加了月份数但没有年份(且不是直接处理的1个月情况),则需要去除末尾多余的数字并添加“个月”
+  if (result && !/\d年$/.test(result) && /\d$/.test(result)) {
+    result += "个月";
+  }
+  return result
+}

+ 81 - 3
src/views/recruit/enterprise/search/retrieval/index.vue

@@ -64,11 +64,41 @@
           <div class="d-flex">
             <div class="mr-10">{{ item.subTitle }}</div>
             <div class="mr-10">首次工作时间: {{ timesTampChange(item.firstWorkTime, 'Y-M-D') }}</div>
-            <div>所在城市: {{ item.areaName }}</div>
+            <!-- <div class="mr-10">所在城市: {{ item.areaName }}</div> -->
+            <div v-if="item.interestedList?.length" class="mr-10 d-flex flex-wrap">
+              <div>求职意向:</div>
+              <div
+                v-for="(interested, index) in item.interestedList"
+                :key="interested.id"
+              >
+                <span :class="{'mx-3': index}">{{ (index ? '|' : '') }}</span>
+                <span>{{ positionData.find(e => e.id === interested.positionId)?.nameCn ?? '暂无' }}</span>
+              </div>
+            </div>
           </div>
         </template>
         
-        <div style="padding-left: 84px;" class="pb-5 d-flex align-center">
+        
+        <div class="px-5 pb-8">
+          <div class="experience" v-if="item.workList.length">
+            <div class="second-title">工作经验</div>
+            <v-timeline density="compact" align="start" side="end" truncate-line="both">
+              <v-timeline-item v-for="(j, i) in item.workList" :key="i" dot-color="primary" size="small">
+                <div v-if="j.show" class="timeline-item mt-1">
+                  <div>
+                    <span>{{ j.startTimeStr }}</span>
+                    <span v-if="j.endTimeStr"> - {{ j.endTimeStr }}</span>
+                    <span v-if="j.year"> ({{ j.year }})</span>
+                  </div>
+                  <div class="timeline-item-name ellipsis">{{ j.enterpriseName }}</div>
+                  <div class="timeline-item-name ellipsis">{{ j.positionName }}</div>
+                </div>
+              </v-timeline-item>
+            </v-timeline>
+          </div>
+        </div>
+
+        <!-- <div style="padding-left: 84px;" class="pb-5 d-flex align-center">
           <div class="d-flex align-center mr-10">
             <div class="mr-3">最新职位:</div>
             <div>
@@ -78,6 +108,7 @@
               >{{ item.workList[0]?.positionName ?? '暂无' }}</v-chip>
             </div>
           </div>
+          
           <div class="d-flex align-center">
             <div class="mr-3">求职意向:</div>
             <v-chip
@@ -87,7 +118,7 @@
               color="primary"
             >{{ positionData.find(e => e.id === interested.positionId)?.nameCn ?? '暂无' }}</v-chip>
           </div>
-        </div>
+        </div> -->
         <!-- <v-divider></v-divider> -->
         <!-- <v-card-text class="d-flex mb-10"> -->
           <!-- <div style="width: 50%;" class="d-flex align-center">
@@ -200,6 +231,7 @@ import { saveInterviewInvite } from '@/api/recruit/enterprise/interview'
 import { useRouter } from 'vue-router'; const router = useRouter()
 import InvitePage from '@/views/recruit/enterprise/interviewManagement/components/invite'
 import { getDict } from '@/hooks/web/useDictionaries'
+import { getTimeDifferenceInChinese } from '@/utils/date'
 
 const textItem = ref({
   type: 'text',
@@ -274,6 +306,15 @@ const getData = async () => {
       e.regName = e.reg?.str ?? ''
       e.areaName = e.area?.str ?? '暂无'
       e.subTitle = [e.jobStatusName, e.expName ? e.expName + '工作经验' : null, e.eduName, e.maritalStatusName].filter(k => k).join(' | ')
+      if (e.workList?.length) {
+        e.workList.forEach(exp => {
+          exp.startTimeStr = exp.startTime ? timesTampChange(exp.startTime, 'Y-M') : '未填写工作时间'
+          exp.endTimeStr = exp.startTime ? exp.endTime ? timesTampChange(exp.endTime, 'Y-M') : '至今' : ''
+          exp.year = exp.endTimeStr ? getTimeDifferenceInChinese(exp.startTime, exp.endTime) : ''
+          // 未填写工作经验内容,不展示
+          exp.show = Boolean(exp.year) || Boolean(exp.enterpriseName) || Boolean(exp.positionName)
+        })
+      }
       return e
     })
     console.log(items.value)
@@ -416,4 +457,41 @@ const badgeIcon = computed(() => (item) => {
 .clear:hover {
   color: var(--v-primary-base);
 }
+
+.experience {
+  width: 54%;
+  height: 100%;
+}
+
+.second-title {
+  color: var(--color-666);
+  font-size: 15px;
+}
+
+.timeline-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  color: var(--color-666);
+  font-size: 13px;
+  .timeline-item-name {
+    width: 26%;
+  }
+}
+:deep(.v-timeline-item__body) {
+  width: 100%;
+}
+:deep(.v-timeline--vertical.v-timeline) {
+  row-gap: 0;
+}
+:deep(.v-timeline-divider__dot--size-small) {
+  width: 10px !important;
+  height: 10px !important;
+  margin-top: 10px !important;
+}
+:deep(.v-timeline-divider__inner-dot) {
+  width: 10px !important;
+  height: 10px !important;
+}
 </style>

+ 5 - 17
src/views/recruit/enterprise/systemManagement/groupAccount/index.vue

@@ -46,14 +46,6 @@
         >
           <template #name="{ item }">
             <div class="d-flex align-center">
-              <!-- <v-badge
-                v-if="(item?.sex === '1' || item?.sex === '2') && showBadge"
-                bordered
-                offset-y="6"
-                :color="badgeColor(item)"
-                :icon="badgeIcon(item)">
-                <v-avatar size="40" :image="getUserAvatar(item.avatar, item.sex)"></v-avatar>
-              </v-badge> -->
               <v-avatar size="40" :image="getUserAvatar(item.avatar, item.sex)"></v-avatar>
               <span class="ml-3">{{ item?.name }}</span>
             </div>
@@ -62,7 +54,7 @@
             <v-btn v-if="item.userType === '0'" color="primary" variant="text" @click="handleEdit(item)">编辑</v-btn>
             <v-btn v-if="item.status === '1' && item.userType !== '1'" color="primary" variant="text" @click="handleAction('', 0, item)">{{ $t('enterprise.userManagement.enable') }}</v-btn>
             <v-btn v-if="item.status === '0' && item.userType !== '1'" color="primary" variant="text" @click="handleAction('', 1, item)">{{ $t('enterprise.userManagement.disable') }}</v-btn>
-            <v-btn v-if="item.status === '0' && item.userType !== '1'" color="primary" variant="text" @click="handleRole(item)">分配角色</v-btn>
+            <v-btn v-if="item.status === '0' && item.userType !== '1' && item.enterpriseId.toString() === enterpriseInfo?.enterpriseId.toString()" color="primary" variant="text" @click="handleRole(item)">分配角色</v-btn>
           </template>
         </CtTable>
       </v-col>
@@ -125,7 +117,6 @@ import {
 } from '@/api/recruit/enterprise/system/role'
 
 const { t } = useI18n()
-const showBadge = ref(false) // 性别设置甲方要求已去掉
 const total = ref(0)
 const loading = ref(false)
 const query = ref({
@@ -134,6 +125,10 @@ const query = ref({
   enterpriseId: '',
   name: null
 })
+
+// 企业基本信息
+const enterpriseInfo = ref(localStorage.getItem('entBaseInfo') ? JSON.parse(localStorage.getItem('entBaseInfo')) : {})
+
 const tableData = ref([])
 const treeData = ref([])
 const headers = [
@@ -153,13 +148,6 @@ const textItem = ref({
   clearable: true,
   label: '请输入用户名称搜索'
 })
-// const badgeColor = computed(() => (item) => {
-//   return (item && item.sex) ? (item.sex === '1' ? '#1867c0' : 'error') : 'error'
-// })
-
-// const badgeIcon = computed(() => (item) => {
-//   return (item && item.sex) ? (item.sex === '1' ? 'mdi-gender-male' : 'mdi-gender-female') : 'mdi-gender-female'
-// })
 
 
 // 角色分配

+ 0 - 0
src/views/recruit/enterprise/talentPool/index copy.vue → src/views/recruit/enterprise/talentPool/indexCopy.vue


+ 28 - 64
src/views/recruit/personal/PersonalCenter/resume/attachment/index.vue

@@ -6,64 +6,25 @@
     </div>
     <p class="font-size-14 color-999">最多只能上传5份附件简历</p>
     <div v-if="attachmentList.length" class="mt-5">
-      <table style="width: 100%;">
-        <tr>
-          <th>名称</th>
-          <th>文件类型</th>
-          <th>上传时间</th>
-          <th>操作</th>
-        </tr>
-        <tr
-          v-for="(k, i) in attachmentList" :key="i"
-          :class="['mx-n2', 'px-2']"
-          @click.self="checkboxClick(i, !k.choose)"
-        >
-          <td>
-            <div class="d-flex flex" style="max-width: 250px;">
-              <v-checkbox-btn
-                v-if="props.analysis"
-                v-model="k.choose"
-                :label="k.title"
-                color="primary"
-                class="pe-2"
-                style="color: #333;"
-                @update:modelValue="bool => checkboxClick(i, bool)"
-              ></v-checkbox-btn>
-              <!-- <span @click="checkboxClick(i, !k.choose)">{{ k.title }}</span> -->
-              <span v-else>{{ k.title }}</span>
-            </div>
-          </td>
-          <td style="text-align: center;">.{{ k.fileType }}</td>
-          <td style="text-align: center;">{{ timesTampChange(k.createTime, 'Y-M-D h:m') }}</td>
-          <td style="text-align: center; width: 268px;">
-            <div>
-              <v-btn variant="text" color="primary" prepend-icon="mdi-eye-outline" @click="previewFile(k.url)">预览</v-btn>
-              <v-btn variant="text" color="primary" prepend-icon="mdi-arrow-down-bold-outline" @click="handleDownload(k)">下载</v-btn>
-              <v-btn variant="text" color="primary" prepend-icon="mdi-trash-can-outline" @click="handleDelete(k)">{{ $t('common.delete') }}</v-btn>
-            </div>
-          </td>
-        </tr>
-      </table>
-
-      <!-- <div
-        :class="['position-item', 'mx-n2', 'px-2']" 
-        v-for="(k, i) in attachmentList" 
-        :key="i" 
-        @mouseenter="k.active = true" 
-        @mouseleave="k.active = false"
-        @click.self="checkboxClick(i, !k.choose)"
+      <CtTable
+        class="mt-3"
+        :items="attachmentList"
+        :headers="headers"
+        :loading="false"
+        :elevation="0"
+        :isTools="false"
+        :showPage="false"
+        :showSelect="props.analysis"
+        selectStrategy="single"
+        itemKey="id"
+        @selected="handleSelected"
       >
-        <div class="d-flex">
-          <v-checkbox-btn v-if="props.analysis" v-model="k.choose" color="primary" class="pe-2" @update:modelValue="bool => checkboxClick(i, bool)"></v-checkbox-btn>
-          <span @click="checkboxClick(i, !k.choose)">{{ k.title }}</span>
-        </div>
-        <div class="float-right" v-if="k.active">
-          <v-btn variant="text" color="primary" prepend-icon="mdi-eye-outline" @click="previewFile(k.url)">预览</v-btn>
-          <v-btn variant="text" color="primary" prepend-icon="mdi-arrow-down-bold-outline" @click="handleDownload(k)">下载</v-btn>
-          <v-btn variant="text" color="primary" prepend-icon="mdi-trash-can-outline" @click="handleDelete(k)">{{ $t('common.delete') }}</v-btn>
-        </div>
-      </div> -->
-
+        <template #actions="{ item }">
+          <v-btn color="primary" @click.stop="previewFile(item0.url)" variant="text">预览</v-btn>
+          <v-btn color="error" @click.stop="handleDownload(item)" variant="text">下载</v-btn>
+          <v-btn v-if="!item.defaultStatus" color="success" @click.stop="handleDelete(item)" variant="text">{{ $t('common.delete') }}</v-btn>
+        </template>
+      </CtTable>
       <div v-if="props.analysis" class="d-flex  flex-column align-center mt-15">
         <v-btn class="buttons" color="primary" @click="handleAnalysis">开始解析</v-btn>
         <v-btn class="mt-2" variant="text" color="primary" to="/recruit/personal/personalCenter/resume/online">返回在线简历</v-btn>
@@ -111,6 +72,13 @@ const props = defineProps({
 
 const { t } = useI18n()
 
+const headers = [
+  { title: '名称', key: 'title', sortable: false, width: '20' },
+  { title: '文件类型', key: 'fileType', sortable: false },
+  { title: '上传时间', key: 'createTime', sortable: false, value: item => timesTampChange(item.createTime, 'Y-M-D h:m') },
+  { title: '操作', key: 'actions', sortable: false }
+]
+
 const CtFormRef = ref()
 const formItems = ref({
   options: [
@@ -205,14 +173,10 @@ const handleDownload = (k) => {
 }
 
 const fileUrl = ref('')
-const checkboxClick = (i, bool) => {
-  if (!props.analysis) return
-  let item = null
-  attachmentList.value.forEach((e, index) => {
-    e.choose = i === index ? bool : false
-    if (e.choose) item = e
-  })
+const handleSelected = (e) => {
+  const item = e?.length ? attachmentList.value.find(k => k.id === e[0]) : null
   if (item?.url) fileUrl.value = encodeURIComponent(item.url)
+  else fileUrl.value = ''
 }
 
 const handleAnalysis = () => {