办学质量监测教学评价系统
Flex
9 小时以前 48954e86178c5c3d95f64b59d9a88f22a51ff1ec
修改校验文本错误以及功能补充
已修改10个文件
已添加17个文件
3218 ■■■■■ 文件已修改
ruoyi-ui/apps/web-antd/src/adapter/component/index.ts 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/components/upload/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/components/upload/src/new-file-upload.vue 247 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/locales/langs/en-US/pages.json 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/locales/langs/zh-CN/pages.json 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/Issued/data.tsx 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/Issued/dept-tree.vue 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/Issued/index.vue 300 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/Issued/info.tsx 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/Issued/user-drawer.vue 213 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/Issued/user-import-modal.vue 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/Issued/user-info-modal.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/Issued/user-reset-pwd-modal.vue 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/myWork/data.tsx 262 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/myWork/dept-tree.vue 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/myWork/index.vue 303 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/myWork/info.tsx 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/myWork/user-assign-work.vue 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/myWork/user-drawer.vue 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/myWork/user-import-modal.vue 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/myWork/user-info-modal.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/myWork/user-report.vue 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/views/work/myWork/user-reset-pwd-modal.vue 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/packages/@core/base/shared/src/constants/dict-enum.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/packages/locales/src/langs/en-US/common.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/packages/locales/src/langs/zh-CN/common.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/apps/web-antd/src/adapter/component/index.ts
@@ -39,7 +39,7 @@
} from 'ant-design-vue';
import { Tinymce as RichTextarea } from '#/components/tinymce';
import { FileUpload, ImageUpload } from '#/components/upload';
import { FileUpload, ImageUpload, NewFileUpload } from '#/components/upload';
const withDefaultPlaceholder = <T extends Component>(
  component: T,
@@ -92,6 +92,7 @@
  | 'DefaultButton'
  | 'Divider'
  | 'FileUpload'
  | 'NewFileUpload'
  | 'IconPicker'
  | 'ImageUpload'
  | 'Input'
@@ -193,6 +194,7 @@
    Upload,
    ImageUpload,
    FileUpload,
    NewFileUpload,
    RichTextarea,
  };
ruoyi-ui/apps/web-antd/src/components/upload/index.ts
@@ -1,2 +1,3 @@
export { default as FileUpload } from './src/file-upload.vue';
export { default as NewFileUpload } from './src/new-file-upload.vue';
export { default as ImageUpload } from './src/image-upload.vue';
ruoyi-ui/apps/web-antd/src/components/upload/src/new-file-upload.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,247 @@
<!-- è¿™æ˜¯ä¸€ä¸ªç”¨æ¥è‡ªå®šä¹‰åŒ–的文件上传组件 -->
<script lang="ts" setup>
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import type { AxiosResponse } from '@vben/request';
import type { AxiosProgressEvent } from '#/api';
import { ref, toRefs, watch } from 'vue';
import { $t } from '@vben/locales';
import { UploadOutlined } from '@ant-design/icons-vue';
import { message, Upload } from 'ant-design-vue';
import { isArray, isFunction, isObject, isString } from 'lodash-es';
import { uploadApi } from '#/api';
import { checkFileType } from './helper';
import { UploadResultStatus } from './typing';
import { useUploadType } from './use-upload';
defineOptions({ name: 'FileUpload', inheritAttrs: false });
const props = withDefaults(
  defineProps<{
    /**
     * å»ºè®®ä½¿ç”¨æ‹“展名(不带.)
     * æˆ–者文件头 image/png等(测试判断不准确)  ä¸æ”¯æŒimage/*类似的写法
     * éœ€è‡ªè¡Œæ”¹é€  ./helper/checkFileType方法
     */
    accept?: string[];
    api?: (
      file: Blob | File,
      onUploadProgress?: AxiosProgressEvent,
    ) => Promise<AxiosResponse<any>>;
    disabled?: boolean;
    helpText?: string;
    // æœ€å¤§æ•°é‡çš„æ–‡ä»¶ï¼ŒInfinity不限制
    maxNumber?: number;
    // æ–‡ä»¶æœ€å¤§å¤šå°‘MB
    maxSize?: number;
    // æ˜¯å¦æ”¯æŒå¤šé€‰
    multiple?: boolean;
    // support xxx.xxx.xx
    // è¿”回的字段 é»˜è®¤url
    resultField?: 'fileName' | 'ossId' | 'url' | string;
    /**
     * æ˜¯å¦æ˜¾ç¤ºä¸‹é¢çš„æè¿°
     */
    showDescription?: boolean;
    value?: string[];
  }>(),
  {
    value: () => [],
    disabled: false,
    helpText: '',
    maxSize: 2,
    maxNumber: 1,
    accept: () => [],
    multiple: false,
    api: uploadApi,
    resultField: '',
    showDescription: true,
  },
);
const emit = defineEmits(['change', 'update:value', 'delete', 'startUpload', 'EndUpload']);
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
const isInnerOperate = ref<boolean>(false);
const { getStringAccept } = useUploadType({
  acceptRef: accept,
  helpTextRef: helpText,
  maxNumberRef: maxNumber,
  maxSizeRef: maxSize,
});
const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true);
const isActMsg = ref<boolean>(true);
const isFirstRender = ref<boolean>(true);
watch(
  () => props.value,
  (v) => {
    if (isInnerOperate.value) {
      isInnerOperate.value = false;
      return;
    }
    let value: string[] = [];
    if (v) {
      if (isArray(v)) {
        value = v;
      } else {
        value.push(v);
      }
      fileList.value = value.map((item, i) => {
        if (item && isString(item)) {
          return {
            uid: `${-i}`,
            name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
            status: 'done',
            url: item,
          };
        } else if (item && isObject(item)) {
          return item;
        }
        return null;
      }) as UploadProps['fileList'];
    }
    if (!isFirstRender.value) {
      emit('change', value);
      isFirstRender.value = false;
    }
  },
  {
    immediate: true,
    deep: true,
  },
);
const handleRemove = async (file: UploadFile) => {
  if (fileList.value) {
    const index = fileList.value.findIndex((item) => item.uid === file.uid);
    index !== -1 && fileList.value.splice(index, 1);
    const value = getValue();
    isInnerOperate.value = true;
    emit('update:value', value);
    emit('change', value);
  }
  emit('delete', file);
};
const beforeUpload = async (file: File) => {
  const { maxSize, accept } = props;
  const isAct = await checkFileType(file, accept);
  if (!isAct) {
    message.error($t('component.upload.acceptUpload', [accept]));
    isActMsg.value = false;
    // é˜²æ­¢å¼¹å‡ºå¤šä¸ªé”™è¯¯æç¤º
    setTimeout(() => (isActMsg.value = true), 1000);
  }
  const isLt = file.size / 1024 / 1024 > maxSize;
  if (isLt) {
    message.error($t('component.upload.maxSizeMultiple', [maxSize]));
    isLtMsg.value = false;
    // é˜²æ­¢å¼¹å‡ºå¤šä¸ªé”™è¯¯æç¤º
    setTimeout(() => (isLtMsg.value = true), 1000);
  }
  return (isAct && !isLt) || Upload.LIST_IGNORE;
};
async function customRequest(info: UploadRequestOption<any>) {
  const { api } = props;
  if (!api || !isFunction(api)) {
    console.warn('upload api must exist and be a function');
    return;
  }
  emit( 'startUpload' )
  try {
    // è¿›åº¦æ¡äº‹ä»¶
    const progressEvent: AxiosProgressEvent = (e) => {
      const percent = Math.trunc((e.loaded / e.total!) * 100);
      info.onProgress!({ percent });
    };
    const res = await api?.(info.file as File, progressEvent);
    /**
     * ç”±getValue处理 ä¼ å¯¹è±¡è¿‡åŽ»
     * ç›´æŽ¥ä¼ string(id)会被转为Number
     * å†…部的逻辑由requestClient.upload处理 è¿™é‡Œä¸ç”¨åˆ¤æ–­ä¸šåŠ¡çŠ¶æ€ç  ä¸ç¬¦åˆä¼šè‡ªåЍreject
     */
    info.onSuccess!(res);
    message.success($t('component.upload.uploadSuccess'));
    // èŽ·å–
    const value = getValue();
    isInnerOperate.value = true;
    emit('update:value', value);
    emit('change', value);
  } catch (error: any) {
    console.error(error);
    info.onError!(error);
  } finally {
    emit( 'EndUpload' )
  }
}
function getValue() {
  const list = (fileList.value || [])
    .filter((item) => item?.status === UploadResultStatus.DONE)
    .map((item: any) => {
      if (item?.response && props?.resultField) {
        return item?.response?.[props.resultField];
      }
      // é€‚用于已经有图片 å›žæ˜¾çš„æƒ…况 ä¼šé»˜è®¤åœ¨init处理为{url: 'xx'}
      if (item?.url) {
        return item.url;
      }
      // æ³¨æ„è¿™é‡Œå–çš„key为 url
      return item?.response?.url;
    });
  return list;
}
</script>
<template>
  <div>
    <Upload
      v-bind="$attrs"
      v-model:file-list="fileList"
      :accept="getStringAccept"
      :before-upload="beforeUpload"
      :custom-request="customRequest"
      :disabled="disabled"
      :max-count="maxNumber"
      :multiple="multiple"
      list-type="text"
      :progress="{ showInfo: true }"
      @remove="handleRemove"
    >
      <div v-if="fileList && fileList.length < maxNumber">
        <a-button>
          <UploadOutlined />
          {{ $t('component.upload.upload') }}
        </a-button>
      </div>
      <div v-if="showDescription" class="mt-2 flex flex-wrap items-center">
        è¯·ä¸Šä¼ ä¸è¶…过
        <div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
        çš„
        <div class="text-primary mx-1 font-bold">{{ accept.join('/') }}</div>
        æ ¼å¼æ–‡ä»¶
      </div>
    </Upload>
  </div>
</template>
<style>
.ant-upload-select-picture-card i {
  font-size: 32px;
  color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {
  margin-top: 8px;
  color: #666;
}
</style>
ruoyi-ui/apps/web-antd/src/locales/langs/en-US/pages.json
@@ -1,7 +1,11 @@
{
  "common": {
    "add": "Add",
    "WorkIssued": "WorkIssued",
    "edit": "Edit",
    "look": "Look",
    "report":"report",
    "AssigningWork":"Assigning Work",
    "delete": "Delete",
    "more": "More",
    "search": "Search",
ruoyi-ui/apps/web-antd/src/locales/langs/zh-CN/pages.json
@@ -1,7 +1,11 @@
{
  "common": {
    "add": "新增",
    "WorkIssued": "工作下发",
    "edit": "编辑",
    "look": "查看",
    "report":"汇报",
    "AssigningWork":"工作下发",
    "delete": "删除",
    "more": "更多",
    "search": "搜索",
ruoyi-ui/apps/web-antd/src/views/work/Issued/data.tsx
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,233 @@
import type { UploadFile, UploadProps } from 'ant-design-vue';
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { DictEnum } from '@vben/constants';
import { getPopupContainer } from '@vben/utils';
import { useAppConfig } from '@vben/hooks';
import { useAccessStore } from '@vben/stores';
import { ref } from 'vue';
import { getDictOptions } from '#/utils/dict';
// import { OptionsItem } from ""
export const querySchema: FormSchemaGetter = () => [
  {
    component: 'Input',
    fieldName: 'workName',
    label: '工作名称',
  },
  {
    component: 'Input',
    fieldName: 'id',
    label: '编号',
  },
  {
    component: 'Input',
    fieldName: 'responsibleDepartment',
    label: '负责部门',
  },
  {
    component: 'Select',
    componentProps: {
      getPopupContainer,
      options: getDictOptions(DictEnum.TASK_STATUS),
    },
    fieldName: 'taskStatus',
    label: '任务状态',
  },
  {
    component: 'Select',
    componentProps: {
      getPopupContainer,
      options: getDictOptions(DictEnum.ASSIGNMENT_STATUS),
    },
    fieldName: 'assignmentStatus',
    label: '分配状态',
  },
  {
    component: 'Input',
    fieldName: 'Annual',
    label: '年度',
  },
];
export const columns: VxeGridProps['columns'] = [
  { type: 'checkbox', width: 60 },
  {
    field: 'workName',
    title: '名称',
  },
  {
    field: 'id',
    title: '编号',
  },
  {
    field: 'responsibleDepartment',
    title: '负责部门',
  },
  {
    field: 'Head',
    title: '负责人',
  },
  {
    field: 'workClass',
    title: '类别',
  },
  {
    field: 'assessmentTime',
    title: '考核时间',
  },
  {
    field: 'taskStatus',
    title: '任务状态',
  },
  {
    field: 'assignmentStatus',
    title: '分配状态',
  },
  {
    field: 'Annual',
    title: '年度',
  },
  {
    field: 'workProgress',
    title: '工作进度',
  },
  {
    field: 'action',
    fixed: 'right',
    slots: { default: 'action' },
    title: '操作',
    width: 180,
  },
];
// è¡¨å•
export const drawerSchema: FormSchemaGetter = () => [
  {
    component: 'Input',
    fieldName: 'workName',
    label: '工作名称',
    rules: 'required'
  },
  {
    component: 'Input',
    fieldName: 'workClass',
    label: '工作类别',
    rules: 'required',
  },
  {
    component: 'Textarea',
    fieldName: 'workContent',
    label: '工作内容'
  },
  {
    component: 'Input',
    fieldName: 'projectBudget',
    label: '项目预算%',
    defaultValue: undefined
  },
  {
    component: 'Input',
    fieldName: 'amountProject',
    defaultValue: undefined,
    label: '项目金额'
  },
  {
    component: 'Select',
    componentProps: {
      getPopupContainer,
      options: []
    },
    fieldName: 'responsibleDepartment',
    label: '负责部门',
    rules: 'required',
  },
  {
    component: 'Select',
    componentProps: {
      getPopupContainer,
      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE)
    },
    fieldName: 'Head',
    label: '负责人',
  },
  {
    component: 'DatePicker',
    componentProps: {
      format: 'YYYY',
      showTime: true,
      valueFormat: 'YYYY',
      picker: "year",
      getPopupContainer,
    },
    fieldName: 'Annual',
    label: '年度',
    rules: 'required',
  },
  {
    component: 'DatePicker',
    componentProps: {
      format: 'YYYY-MM-DD',
      showTime: true,
      valueFormat: 'YYYY-MM-DD',
      getPopupContainer,
    },
    fieldName: 'assessmentTime',
    label: '考核时间',
    rules: 'required',
  },
  {
    component: 'Select',
    fieldName: 'assessmentIndicators',
    formItemClass: 'items-baseline',
    label: '考核指标',
  },
  {
    fieldName: 'File',
    component: 'NewFileUpload',
    componentProps: {
      action: uploadUrl,
      headers: headers,
      change: handleChange,
      maxSize: 20,
      onDelete:(file: UploadFile)=>{
        console.log("存在文件被删除",file)
      },
      onStartUpload:()=>{
        //开始上传
        UploadFileState.value = true
      },
      onEndUpload:()=>{
        //上传结束
        UploadFileState.value = false
      }
    },
    formItemClass: 'items-baseline',
    label: '文件上传',
  }
];
/*
  å½“前被上传文件的文件状态
  true â€”— æ­£åœ¨ä¸Šä¼ 
  false â€”— ä¸Šä¼ ç»“束
*/
export const UploadFileState = ref(false)
const { apiURL, clientId } = useAppConfig(
  import.meta.env,
  import.meta.env.PROD,
);
const uploadUrl = `${apiURL}/knowledge/attach/upload`;
const accessStore = useAccessStore();
const headers = {
  Authorization: `Bearer ${accessStore.accessToken}`,
  clientId,
};
// æ–‡ä»¶é€‰å–
function handleChange(res:any) {
  console.log("选取了文件")
  console.log(res)
}
ruoyi-ui/apps/web-antd/src/views/work/Issued/dept-tree.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,128 @@
<script setup lang="ts">
import type { PropType } from 'vue';
import type { DeptTree } from '#/api/system/user/model';
import { onMounted, ref } from 'vue';
import { SyncOutlined } from '@ant-design/icons-vue';
import { Empty, InputSearch, Skeleton, Tree } from 'ant-design-vue';
import { getDeptTree } from '#/api/system/user';
defineOptions({ inheritAttrs: false });
withDefaults(defineProps<{ showSearch?: boolean }>(), { showSearch: true });
const emit = defineEmits<{
  /**
   * ç‚¹å‡»åˆ·æ–°æŒ‰é’®çš„事件
   */
  reload: [];
  /**
   * ç‚¹å‡»èŠ‚ç‚¹çš„äº‹ä»¶
   */
  select: [];
}>();
const selectDeptId = defineModel('selectDeptId', {
  required: true,
  type: Array as PropType<string[]>,
});
const searchValue = defineModel('searchValue', {
  type: String,
  default: '',
});
/** éƒ¨é—¨æ•°æ®æº */
type DeptTreeArray = DeptTree[];
const deptTreeArray = ref<DeptTreeArray>([]);
/** éª¨æž¶å±åŠ è½½ */
const showTreeSkeleton = ref<boolean>(true);
async function loadTree() {
  showTreeSkeleton.value = true;
  searchValue.value = '';
  selectDeptId.value = [];
  const ret = await getDeptTree();
  console.log( "ret", ret )
  deptTreeArray.value = ret;
  showTreeSkeleton.value = false;
}
async function handleReload() {
  await loadTree();
  emit('reload');
}
onMounted(loadTree);
</script>
<template>
  <div :class="$attrs.class">
    <Skeleton
      :loading="showTreeSkeleton"
      :paragraph="{ rows: 8 }"
      active
      class="p-[8px]"
    >
      <div
        class="bg-background flex h-full flex-col overflow-y-auto rounded-lg"
      >
        <!-- å›ºå®šåœ¨é¡¶éƒ¨ å¿…须加上bg-background背景色 å¦åˆ™ä¼šäº§ç”Ÿ'穿透'效果 -->
        <div
          v-if="showSearch"
          class="bg-background z-100 sticky left-0 top-0 p-[8px]"
        >
          <InputSearch
            v-model:value="searchValue"
            :placeholder="$t('pages.common.search')"
            size="small"
          >
            <template #enterButton>
              <a-button @click="handleReload">
                <SyncOutlined class="text-primary" />
              </a-button>
            </template>
          </InputSearch>
        </div>
        <div class="h-full overflow-x-hidden px-[8px]">
          <Tree
            v-bind="$attrs"
            v-if="deptTreeArray.length > 0"
            v-model:selected-keys="selectDeptId"
            :class="$attrs.class"
            :field-names="{ title: 'label', key: 'id' }"
            :show-line="{ showLeafIcon: false }"
            :tree-data="deptTreeArray"
            :virtual="false"
            default-expand-all
            @select="$emit('select')"
          >
            <template #title="{ label }">
              <span v-if="label.indexOf(searchValue) > -1">
                {{ label.substring(0, label.indexOf(searchValue)) }}
                <span style="color: #f50">{{ searchValue }}</span>
                {{
                  label.substring(
                    label.indexOf(searchValue) + searchValue.length,
                  )
                }}
              </span>
              <span v-else>{{ label }}</span>
            </template>
          </Tree>
          <!-- ä»…本人数据权限 å¯ä»¥è€ƒè™‘直接不显示 -->
          <div v-else class="mt-5">
            <Empty
              :image="Empty.PRESENTED_IMAGE_SIMPLE"
              description="无部门数据"
            />
          </div>
        </div>
      </div>
    </Skeleton>
  </div>
</template>
ruoyi-ui/apps/web-antd/src/views/work/Issued/index.vue
@@ -1,13 +1,295 @@
<template>
  <div>工作下发</div>
</template>
<!-- å·¥ä½œåˆ—表 -->
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
<script>
export default {
  name: "index"
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { User } from '#/api/system/user/model';
import { ref } from 'vue';
import { useAccess } from '@vben/access';
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { preferences } from '@vben/preferences';
import { getVxePopupContainer } from '@vben/utils';
import {
  Avatar,
  Dropdown,
  Menu,
  MenuItem,
  Modal,
  Popconfirm,
  Space,
} from 'ant-design-vue';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
  userExport,
  userList,
  userRemove,
  userStatusChange,
} from '#/api/system/user';
import { TableSwitch } from '#/components/table';
import { commonDownloadExcel } from '#/utils/file/download';
import { columns, querySchema } from './data';
import userDrawer from './user-drawer.vue';
import userImportModal from './user-import-modal.vue';
import userInfoModal from './user-info-modal.vue';
import userResetPwdModal from './user-reset-pwd-modal.vue';
/**
 * å¯¼å…¥
 */
const [UserImpotModal, userImportModalApi] = useVbenModal({
  connectedComponent: userImportModal,
});
function handleImport() {
  userImportModalApi.open();
}
// å·¦è¾¹éƒ¨é—¨ç”¨
const selectDeptId = ref<string[]>([]);
const formOptions: VbenFormProps = {
  schema: querySchema(),
  commonConfig: {
    labelWidth: 80,
    componentProps: {
      allowClear: true,
    },
  },
  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
  handleReset: async () => {
    selectDeptId.value = [];
    const { formApi, reload } = tableApi;
    await formApi.resetForm();
    const formValues = formApi.form.values;
    formApi.setLatestSubmissionValues(formValues);
    await reload(formValues);
  },
  // æ—¥æœŸé€‰æ‹©æ ¼å¼åŒ–
  fieldMappingTime: [
    [
      'createTime',
      ['params[beginTime]', 'params[endTime]'],
      ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
    ],
  ],
};
const gridOptions: VxeGridProps = {
  checkboxConfig: {
    // é«˜äº®
    highlight: true,
    // ç¿»é¡µæ—¶ä¿ç•™é€‰ä¸­çŠ¶æ€
    reserve: true,
    // ç‚¹å‡»è¡Œé€‰ä¸­
    trigger: 'default',
    checkMethod: ({ row }) => row?.userId !== 1,
  },
  columns,
  height: 'auto',
  keepSource: true,
  pagerConfig: {},
  proxyConfig: {
    ajax: {
      // èŽ·å–é¡µé¢æ•°æ®ï¼Œæœç´¢ï¼Œé‡ç½®ä¹Ÿæ˜¯
      /*
        @params page:页码参数
        @params formValues:表单参数
      */
      query: async ({ page }, formValues = {}) => {
        console.log("获取页面数据,")
        const res = {
          rows:[ {
            id:"0",  //工作id,编号
            workName:"工作名称1", //工作名称
            workClass:"工作类别", //工作类别
            workContent:"工作内容", //工作内容
            projectBudget:"项目预算", //项目预算
            amountProject:"项目金额", //项目金额
            responsibleDepartment:"负责部门", //负责部门
            Head:"负责人", //负责人
            Annual:"年度", //年度
            assessmentTime:"考核时间",//考核时间
            assessmentIndicators:"考核指标", //考核指标
            File:"文件地址", //文件上传
            taskStatus:"任务状态", //任务状态
            assignmentStatus:"分配状态", //分配状态
            workProgress:"工作进度" //工作进度
          } ],
          total:1
        }
        console.log( "res", res)
        return res
      },
    },
  },
  rowConfig: {
    keyField: 'userId',
    height: 48,
  },
  id: 'system-user-index',
};
const [BasicTable, tableApi] = useVbenVxeGrid({
  formOptions,
  gridOptions,
});
const [UserDrawer, userDrawerApi] = useVbenDrawer({
  connectedComponent: userDrawer,
});
// å·¥ä½œä¸‹å‘
function handleAdd() {
  userDrawerApi.setData({});
  userDrawerApi.open();
}
// ç¼–辑
function handleEdit(row: any) {
  userDrawerApi.setData({ id: row.id });
  userDrawerApi.open();
}
// æŸ¥çœ‹
function handleLook(row: any) {
  userDrawerApi.setData({ id: row.id, look: true });
  userDrawerApi.open();
}
// åˆ é™¤
async function handleDelete(row: User) {
  await userRemove([row.userId]);
  await tableApi.query();
}
function handleMultiDelete() {
  const rows = tableApi.grid.getCheckboxRecords();
  const ids = rows.map((row: User) => row.userId);
  Modal.confirm({
    title: '提示',
    okType: 'danger',
    content: `确认删除选中的${ids.length}条记录吗?`,
    onOk: async () => {
      await userRemove(ids);
      await tableApi.query();
    },
  });
}
function handleDownloadExcel() {
  commonDownloadExcel(userExport, '用户管理', tableApi.formApi.form.values, {
    fieldMappingTime: formOptions.fieldMappingTime,
  });
}
const [UserInfoModal, userInfoModalApi] = useVbenModal({
  connectedComponent: userInfoModal,
});
function handleUserInfo(row: User) {
  userInfoModalApi.setData({ userId: row.userId });
  userInfoModalApi.open();
}
const [UserResetPwdModal, userResetPwdModalApi] = useVbenModal({
  connectedComponent: userResetPwdModal,
});
function handleResetPwd(record: User) {
  userResetPwdModalApi.setData({ record });
  userResetPwdModalApi.open();
}
const { hasAccessByCodes } = useAccess();
</script>
<style scoped>
</style>
<template>
  <Page :auto-content-height="true">
    <div class="flex h-full gap-[8px]">
      <BasicTable class="flex-1 overflow-hidden" table-title="工作列表">
        <template #toolbar-tools>
          <Space>
            <!-- å¯¼å‡º -->
            <!-- <a-button
              @click="handleDownloadExcel"
            >
              {{ $t('pages.common.export') }}
            </a-button> -->
            <!-- å¯¼å…¥ -->
            <!-- <a-button
              v-access:code="['system:user:import']"
              @click="handleImport"
            >
              {{ $t('pages.common.import') }}
            </a-button> -->
            <!-- åˆ é™¤ -->
            <a-button
              :disabled="!vxeCheckboxChecked(tableApi)"
              danger
              type="primary"
              @click="handleMultiDelete"
            >
              {{ $t('pages.common.delete') }}
            </a-button>
            <!-- æ–°å¢ž -->
            <a-button
              type="primary"
              @click="handleAdd"
            >
              {{ $t('pages.common.WorkIssued') }}
            </a-button>
          </Space>
        </template>
        <template #avatar="{ row }">
          <!-- å¯èƒ½è¦åˆ¤æ–­ç©ºå­—符串情况 æ‰€ä»¥æ²¡æœ‰ä½¿ç”¨?? -->
          <Avatar :src="row.avatar || preferences.app.defaultAvatar" />
        </template>
        <template #status="{ row }">
          <TableSwitch
            v-model="row.status"
            :api="() => userStatusChange(row)"
            :disabled="
              row.userId === 1 || !hasAccessByCodes(['system:user:edit'])
            "
            :reload="() => tableApi.query()"
          />
        </template>
        <template #action="{ row }">
            <Space>
              <!-- æŸ¥çœ‹ -->
              <ghost-button
                @click.stop="handleLook(row)"
              >
                {{ $t('pages.common.look') }}
              </ghost-button>
              <!-- ç¼–辑 -->
              <ghost-button
                @click.stop="handleEdit(row)"
              >
                {{ $t('pages.common.edit') }}
              </ghost-button>
              <!-- åˆ é™¤ -->
              <Popconfirm
                :get-popup-container="getVxePopupContainer"
                placement="left"
                title="确认删除?"
                @confirm="handleDelete(row)"
              >
                <ghost-button
                  danger
                  @click.stop=""
                >
                  {{ $t('pages.common.delete') }}
                </ghost-button>
              </Popconfirm>
            </Space>
        </template>
      </BasicTable>
    </div>
    <UserImpotModal @reload="tableApi.query()" />
    <UserDrawer @reload="tableApi.query()" />
    <UserInfoModal />
    <UserResetPwdModal />
  </Page>
</template>
ruoyi-ui/apps/web-antd/src/views/work/Issued/info.tsx
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,129 @@
import type { DescItem } from '#/components/description';
import { DictEnum } from '@vben/constants';
import { Tag } from 'ant-design-vue';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import { renderDict } from '#/utils/render';
dayjs.extend(duration);
dayjs.extend(relativeTime);
function renderTags(list: string[]) {
  return (
    <div class="flex flex-row flex-wrap gap-0.5">
      {list.map((item) => (
        <Tag key={item}>{item}</Tag>
      ))}
    </div>
  );
}
export const descSchema: DescItem[] = [
  {
    field: 'userId',
    label: '用户ID',
  },
  {
    field: 'status',
    label: '用户状态',
    render(value) {
      return renderDict(value, DictEnum.SYS_NORMAL_DISABLE);
    },
  },
  {
    field: 'nickName',
    label: '用户信息',
    render(_, data) {
      const { deptName = '暂无部门信息', nickName, userName } = data;
      // ä¸ºäº†å…¼å®¹æ–°ç‰ˆæœ¬å’Œæ—§ç‰ˆæœ¬
      let currentDept = deptName;
      if (data.dept && data.dept.deptName) {
        currentDept = data.dept.deptName;
      }
      return `${userName} / ${nickName} / ${currentDept}`;
    },
  },
  {
    field: 'phonenumber',
    label: '手机号',
    render(value) {
      return value || '未设置手机号码';
    },
  },
  {
    field: 'email',
    label: '邮箱',
    render(value) {
      return value || '未设置邮箱地址';
    },
  },
  {
    field: 'postNames',
    label: '岗位',
    render(value) {
      if (Array.isArray(value) && value.length === 0) {
        return '暂无信息';
      }
      return renderTags(value);
    },
  },
  {
    field: 'roleNames',
    label: '权限',
    render(value) {
      if (Array.isArray(value) && value.length === 0) {
        return '暂无信息';
      }
      return renderTags(value);
    },
  },
  {
    field: 'createTime',
    label: '创建时间',
  },
  {
    field: 'loginIp',
    label: '上次登录IP',
    render(value) {
      return value || <span class="text-orange-500">从未登录过</span>;
    },
  },
  {
    field: 'loginDate',
    label: '上次登录时间',
    render(value) {
      if (!value) {
        return <span class="text-orange-500">从未登录过</span>;
      }
      // é»˜è®¤en显示
      dayjs.locale('zh-cn');
      // è®¡ç®—相差秒数
      const diffSeconds = dayjs().diff(dayjs(value), 'second');
      /**
       * è½¬ä¸ºæ—¶é—´æ˜¾ç¤º(x月 x天)
       * https://dayjs.fenxianglu.cn/category/duration.html#%E4%BA%BA%E6%80%A7%E5%8C%96
       *
       */
      const diffText = dayjs.duration(diffSeconds, 'seconds').humanize();
      return (
        <div class="flex gap-2">
          {value}
          <Tag bordered={false} color="cyan">
            {diffText}前
          </Tag>
        </div>
      );
    },
  },
  {
    field: 'remark',
    label: '备注',
    render(value) {
      return value || '无';
    },
  },
];
ruoyi-ui/apps/web-antd/src/views/work/Issued/user-drawer.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,213 @@
<script setup lang="ts">
import type { Role } from '#/api/system/user/model';
import { computed, h, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
import { message, Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { findUserInfo, userAdd, userUpdate } from '#/api/system/user';
import { authScopeOptions } from '#/views/system/role/data';
import { drawerSchema, UploadFileState } from './data';
import { deptList } from '#/api/system/dept/index';
const emit = defineEmits<{ reload: [] }>();
const isUpdate = ref(false); //当前是否为编辑
const isLook = ref(false); //当前是否为查看
const title = computed(() => {
  let text = '';
  if (isLook.value) {
    text = $t('pages.common.look');
  } else if (isUpdate.value) {
    text = $t('pages.common.edit');
  } else if (!isUpdate.value) {
    text = $t('pages.common.add');
  }
  return text;
});
const [BasicForm, formApi] = useVbenForm({
  commonConfig: {
    formItemClass: 'col-span-2',
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 80,
  },
  schema: drawerSchema(),
  showDefaultActions: false,
  wrapperClass: 'grid-cols-2',
});
/**
 * ç”Ÿæˆè§’色的自定义label
 * ä¹Ÿå¯ä»¥ç”¨option插槽来做
 * renderComponentContent: () => ({
    option: ({value, label, [disabled, key, title]}) => '',
  }),
 */
function genRoleOptionlabel(role: Role) {
  const found = authScopeOptions.find((item) => item.value === role.dataScope);
  if (!found) {
    return role.roleName;
  }
  return h('div', { class: 'flex items-center gap-[6px]' }, [
    h('span', null, role.roleName),
    h(Tag, { color: found.color }, () => found.label),
  ]);
}
// æ‰“开以及编辑都会调用该事件
const [BasicDrawer, drawerApi] = useVbenDrawer({
  onCancel: handleCancel,
  onConfirm: handleConfirm,
  async onOpenChange(isOpen) {
    if (isOpen) {
      // åˆå§‹åŒ–一下表单选项
      const deptlist = await (
        await deptList()
      ).map((item) => {
        return {
          ...item,
          value: item.deptId,
          label: item.deptName,
        };
      });
      formApi.updateSchema([
        {
          componentProps: { options: deptlist },
          fieldName: 'responsibleDepartment',
        },
      ]);
    }
    if (!isOpen) {
      // éœ€è¦é‡ç½®å²—位选择
      formApi.updateSchema([
        {
          componentProps: { options: [], placeholder: '请先选择部门' },
          fieldName: 'postIds',
        },
      ]);
      return null;
    }
    drawerApi.drawerLoading(true);
    const { id } = drawerApi.getData() as { id?: number | string };
    const { look } = drawerApi.getData() as { look?: boolean };
    isUpdate.value = !!id;
    isLook.value = !!look;
    /** update时 ç¦ç”¨ç”¨æˆ·åä¿®æ”¹ ä¸æ˜¾ç¤ºå¯†ç æ¡† */
    // å¦‚果为查看则所有项都不可编辑
    formApi.updateSchema([
      { componentProps: { disabled: isLook.value }, fieldName: 'workName' },
      { componentProps: { disabled: isLook.value }, fieldName: 'workClass' },
      { componentProps: { disabled: isLook.value }, fieldName: 'workContent' },
      {
        componentProps: { disabled: isLook.value },
        fieldName: 'projectBudget',
      },
      {
        componentProps: { disabled: isLook.value },
        fieldName: 'amountProject',
      },
      {
        componentProps: { disabled: isLook.value },
        fieldName: 'responsibleDepartment',
      },
      { componentProps: { disabled: isLook.value }, fieldName: 'Head' },
      { componentProps: { disabled: isLook.value }, fieldName: 'Annual' },
      {
        componentProps: { disabled: isLook.value },
        fieldName: 'assessmentTime',
      },
      {
        componentProps: { disabled: isLook.value },
        fieldName: 'assessmentIndicators',
      },
      { componentProps: { disabled: isLook.value }, fieldName: 'File' },
    ]);
    drawerApi.setState({
      showConfirmButton: !isLook.value,
    });
    // æ›´æ–° && èµ‹å€¼
    // const { postIds, posts, roleIds, roles, user } = await findUserInfo(id); //调用接口获取详细信息
    const data = {
      id: '0', //工作id,编号
      workName: '工作名称1', //工作名称
      workClass: '工作类别', //工作类别
      workContent: '工作内容', //工作内容
      projectBudget: '项目预算', //项目预算
      amountProject: '项目金额', //项目金额
      responsibleDepartment: '负责部门', //负责部门
      Head: '负责人', //负责人
      Annual: '2025', //年度
      assessmentTime: '2025-06-25', //考核时间
      assessmentIndicators: '考核指标', //考核指标
      File: '文件地址', //文件上传
      taskStatus: '任务状态', //任务状态
      assignmentStatus: '分配状态', //分配状态
      workProgress: '工作进度', //工作进度
    };
    if (data && id) {
      await Promise.all([
        // æ·»åŠ åŸºç¡€ä¿¡æ¯
        formApi.setValues(data),
      ]);
    }
    drawerApi.drawerLoading(false);
    console.log(isLook.value);
  },
});
// è¡¨å•提交,编辑与新增都会调用这个方法体
async function handleConfirm() {
  if( UploadFileState.value ){
      message.warn("当前有文件正在上传,请耐心等待")
      return
  }
  try {
    drawerApi.drawerLoading(true);
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    const data = cloneDeep(await formApi.getValues());  //表单内的数据
    console.log(data)
    // await (isUpdate.value ? userUpdate(data) : userAdd(data));
    emit('reload');
    await handleCancel();
  } catch (error) {
    console.error(error);
  } finally {
    drawerApi.drawerLoading(false);
  }
}
async function handleCancel() {
  drawerApi.close();
  await formApi.resetForm();
}
function IsShowConfirmButton(params: type) {
  if (isLook.value) {
    return false;
  }
  return true;
}
// åœ¨è¿™åŠ è½½äº›æ•°æ®
</script>
<template>
  <BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]">
    <BasicForm />
  </BasicDrawer>
</template>
ruoyi-ui/apps/web-antd/src/views/work/Issued/user-import-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,108 @@
<script setup lang="ts">
import type { UploadFile } from 'ant-design-vue/es/upload/interface';
import { h, ref, unref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ExcelIcon, InBoxIcon } from '@vben/icons';
import { Modal, Switch, Upload } from 'ant-design-vue';
import { downloadImportTemplate, userImportData } from '#/api/system/user';
import { commonDownloadExcel } from '#/utils/file/download';
const emit = defineEmits<{ reload: [] }>();
const UploadDragger = Upload.Dragger;
const [BasicModal, modalApi] = useVbenModal({
  onCancel: handleCancel,
  onConfirm: handleSubmit,
});
const fileList = ref<UploadFile[]>([]);
const checked = ref(false);
async function handleSubmit() {
  try {
    modalApi.modalLoading(true);
    if (fileList.value.length !== 1) {
      handleCancel();
      return;
    }
    const data = {
      file: fileList.value[0]!.originFileObj as Blob,
      updateSupport: unref(checked),
    };
    const { code, msg } = await userImportData(data);
    let modal = Modal.success;
    if (code === 200) {
      emit('reload');
    } else {
      modal = Modal.error;
    }
    handleCancel();
    modal({
      content: h('div', {
        class: 'max-h-[260px] overflow-y-auto',
        innerHTML: msg, // åŽå°å·²ç»å¤„理xss问题
      }),
      title: '提示',
    });
  } catch (error) {
    console.warn(error);
    modalApi.close();
  } finally {
    modalApi.modalLoading(false);
  }
}
function handleCancel() {
  modalApi.close();
  fileList.value = [];
  checked.value = false;
}
</script>
<template>
  <BasicModal
    :close-on-click-modal="false"
    :fullscreen-button="false"
    title="用户导入"
  >
    <!-- z-index不设置会遮挡模板下载loading -->
    <!-- æ‰‹åŠ¨å¤„ç† è€Œä¸æ˜¯æ”¾å…¥æ–‡ä»¶å°±ä¸Šä¼  -->
    <UploadDragger
      v-model:file-list="fileList"
      :before-upload="() => false"
      :max-count="1"
      :show-upload-list="true"
      accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
    >
      <p class="ant-upload-drag-icon flex items-center justify-center">
        <InBoxIcon class="text-primary size-[48px]" />
      </p>
      <p class="ant-upload-text">点击或者拖拽到此处上传文件</p>
    </UploadDragger>
    <div class="mt-2 flex flex-col gap-2">
      <div class="flex items-center gap-2">
        <span>允许导入xlsx, xls文件</span>
        <a-button
          type="link"
          @click="commonDownloadExcel(downloadImportTemplate, '用户导入模板')"
        >
          <div class="flex items-center gap-[4px]">
            <ExcelIcon />
            <span>下载模板</span>
          </div>
        </a-button>
      </div>
      <div class="flex items-center gap-2">
        <span :class="{ 'text-red-500': checked }">
          æ˜¯å¦æ›´æ–°/覆盖已存在的用户数据
        </span>
        <Switch v-model:checked="checked" />
      </div>
    </div>
  </BasicModal>
</template>
ruoyi-ui/apps/web-antd/src/views/work/Issued/user-info-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
<script setup lang="ts">
import type { User } from '#/api/system/user/model';
import { useVbenModal } from '@vben/common-ui';
import { findUserInfo } from '#/api/system/user';
import { Description, useDescription } from '#/components/description';
import { descSchema } from './info';
const [BasicModal, modalApi] = useVbenModal({
  onOpenChange: handleOpenChange,
});
const [registerDescription, { setDescProps }] = useDescription({
  column: 1,
  labelStyle: {
    minWidth: '150px',
    width: '150px',
  },
  schema: descSchema,
});
async function handleOpenChange(open: boolean) {
  if (!open) {
    return null;
  }
  modalApi.modalLoading(true);
  const { userId } = modalApi.getData() as { userId: number | string };
  const response = await findUserInfo(userId);
  // å¤–部的roleIds postIds才是真正对应的  æ–°å¢žæ—¶ä¸ºç©º
  // posts有为Null的情况 éœ€è¦ç»™é»˜è®¤å€¼
  const { postIds = [], posts = [], roleIds = [], roles = [], user } = response;
  const postNames = posts
    .filter((item) => postIds.includes(item.postId))
    .map((item) => item.postName);
  const roleNames = roles
    .filter((item) => roleIds.includes(item.roleId))
    .map((item) => item.roleName);
  interface UserWithNames extends User {
    postNames: string[];
    roleNames: string[];
  }
  (user as UserWithNames).postNames = postNames;
  (user as UserWithNames).roleNames = roleNames;
  // èµ‹å€¼
  setDescProps({ data: user });
  modalApi.modalLoading(false);
}
</script>
<template>
  <BasicModal :footer="false" :fullscreen-button="false" title="用户信息">
    <Description @register="registerDescription" />
  </BasicModal>
</template>
ruoyi-ui/apps/web-antd/src/views/work/Issued/user-reset-pwd-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
<script setup lang="ts">
import type { ResetPwdParam, User } from '#/api/system/user/model';
import { useVbenModal, z } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import { userResetPassword } from '#/api/system/user';
import { Description, useDescription } from '#/components/description';
const emit = defineEmits<{ reload: [] }>();
const [BasicModal, modalApi] = useVbenModal({
  onCancel: handleCancel,
  onConfirm: handleSubmit,
  onOpenChange: handleOpenChange,
});
const [registerDescription, { setDescProps }] = useDescription({
  column: 1,
  schema: [
    {
      field: 'userId',
      label: '用户ID',
    },
    {
      field: 'userName',
      label: '用户名',
    },
    {
      field: 'nickName',
      label: '昵称',
    },
  ],
});
const [BasicForm, formApi] = useVbenForm({
  schema: [
    {
      component: 'Input',
      dependencies: {
        show: () => false,
        triggerFields: [''],
      },
      fieldName: 'userId',
      label: '用户ID',
      rules: 'required',
    },
    {
      component: 'InputPassword',
      componentProps: {
        placeholder: '请输入新的密码, å¯†ç é•¿åº¦ä¸º5 - 20',
      },
      fieldName: 'password',
      label: '新的密码',
      rules: z
        .string()
        .min(5, { message: '密码长度为5 - 20' })
        .max(20, { message: '密码长度为5 - 20' }),
    },
  ],
  showDefaultActions: false,
  commonConfig: {
    labelWidth: 80,
  },
});
async function handleOpenChange(open: boolean) {
  if (!open) {
    return null;
  }
  const { record } = modalApi.getData() as { record: User };
  setDescProps({ data: record }, true);
  await formApi.setValues({ userId: record.userId });
}
async function handleSubmit() {
  try {
    modalApi.modalLoading(true);
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    const data = await formApi.getValues();
    await userResetPassword(data as ResetPwdParam);
    emit('reload');
    handleCancel();
  } catch (error) {
    console.error(error);
  } finally {
    modalApi.modalLoading(false);
  }
}
async function handleCancel() {
  modalApi.close();
  await formApi.resetForm();
}
</script>
<template>
  <BasicModal
    :close-on-click-modal="false"
    :fullscreen-button="false"
    title="重置密码"
  >
    <div class="flex flex-col gap-[12px]">
      <Description @register="registerDescription" />
      <BasicForm />
    </div>
  </BasicModal>
</template>
ruoyi-ui/apps/web-antd/src/views/work/myWork/data.tsx
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,262 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { DictEnum } from '@vben/constants';
import { getPopupContainer } from '@vben/utils';
import { useAppConfig } from '@vben/hooks';
import { useAccessStore } from '@vben/stores';
import type { UploadFile } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { getDictOptions } from '#/utils/dict';
import { Item } from 'ant-design-vue/es/menu';
// import { OptionsItem } from ""
export const querySchema: FormSchemaGetter = () => [
  {
    component: 'Input',
    fieldName: 'workName',
    label: '工作名称',
  },
  {
    component: 'Select',
    componentProps: {
      getPopupContainer,
      options: getDictOptions(DictEnum.TASK_STATUS),
    },
    fieldName: 'workClass',
    label: '工作类别',
  },
  {
    component: 'Select',
    componentProps: {
      getPopupContainer,
      options: getDictOptions(DictEnum.TASK_STATUS),
    },
    fieldName: 'taskStatus',
    label: '任务状态',
  },
  {
    component: 'Input',
    fieldName: 'Annual',
    label: '年度',
  },
];
export const columns: VxeGridProps['columns'] = [
  { type: 'checkbox', width: 60 },
  {
    field: 'workName',
    title: '名称',
  },
  {
    field: 'id',
    title: '编号',
  },
  {
    field: 'workClass',
    title: '类别',
  },
  {
    field: 'assessmentTime',
    title: '考核时间',
  },
  {
    field: 'taskStatus',
    title: '任务状态',
  },
  {
    field: 'Annual',
    title: '年度',
  },
  {
    field: 'workProgress',
    title: '工作进度',
  },
  {
    field: 'action',
    fixed: 'right',
    slots: { default: 'action' },
    title: '操作',
    width: 180,
  },
];
// å·¥ä½œè¯¦æƒ…
export const drawerSchema: FormSchemaGetter = () => [
  {
    component: 'Input',
    fieldName: 'workName',
    label: '工作名称',
    rules: 'required'
  },
  {
    component: 'Input',
    fieldName: 'workClass',
    label: '工作类别',
    rules: 'required',
  },
  {
    component: 'Input',
    fieldName: 'workContent',
    label: '工作内容'
  },
  {
    component: 'Input',
    fieldName: 'projectBudget',
    label: '项目预算%',
    defaultValue: undefined
  },
  {
    component: 'Input',
    fieldName: 'amountProject',
    defaultValue: undefined,
    label: '项目金额'
  },
  {
    component: 'Select',
    componentProps: {
      getPopupContainer,
      options: []
    },
    fieldName: 'responsibleDepartment',
    label: '负责部门',
    rules: 'required',
  },
  {
    component: 'Select',
    componentProps: {
      getPopupContainer,
      options: getDictOptions(DictEnum.SYS_NORMAL_DISABLE)
    },
    fieldName: 'Head',
    label: '负责人',
  },
  {
    component: 'DatePicker',
    componentProps: {
      format: 'YYYY',
      showTime: true,
      valueFormat: 'YYYY',
      picker: "year",
      getPopupContainer,
    },
    fieldName: 'Annual',
    label: '年度',
    rules: 'required',
  },
  {
    component: 'DatePicker',
    componentProps: {
      format: 'YYYY-MM-DD',
      showTime: true,
      valueFormat: 'YYYY-MM-DD',
      getPopupContainer,
    },
    fieldName: 'assessmentTime',
    label: '考核时间',
    rules: 'required',
  },
  {
    component: 'Select',
    fieldName: 'assessmentIndicators',
    formItemClass: 'items-baseline',
    label: '考核指标',
  },
  {
    fieldName: 'File',
    component: 'NewFileUpload',
    componentProps: {
      action: uploadUrl,
      headers: headers,
      maxSize: 20,
      onDelete:(file: UploadFile)=>{
        console.log("存在文件被删除",file)
      }
    },
    formItemClass: 'items-baseline',
    label: '文件上传',
  }
];
// å·¥ä½œä¸‹å‘
export const AssigningWorkSchema: FormSchemaGetter = () => [
  {
    component: 'Select',
    fieldName: 'assessmentIndicators',
    formItemClass: 'items-baseline',
    label: '负责人',
  },
]
// å·¥ä½œæ±‡æŠ¥
export const ReportScheme: FormSchemaGetter = () => [
  {
    component: 'Input',
    fieldName: 'workName',
    label: '工作名称',
    rules: 'required'
  },
  {
    component: 'Input',
    fieldName: 'workClass',
    label: '工作类别',
    rules: 'required',
  },
  {
    component: 'Textarea',
    fieldName: 'workContent',
    label: '工作内容'
  },
  {
    component: 'DatePicker',
    componentProps: {
      format: 'YYYY-MM-DD',
      showTime: true,
      valueFormat: 'YYYY-MM-DD',
      getPopupContainer,
    },
    fieldName: 'DebriefingTime',
    label: '汇报时间',
    rules: 'required',
  },
  {
    component: 'Input',
    fieldName: 'workProgress',
    label: '完成进度'
  },
  {
    fieldName: 'File',
    component: 'NewFileUpload',
    componentProps: {
      action: uploadUrl,
      headers: headers,
      maxSize: 20,
      onDelete:(file: UploadFile)=>{
        console.log("存在文件被删除",file)
      }
    },
    formItemClass: 'items-baseline',
    label: '文件上传',
  },
  {
    component: 'Input',
    fieldName: 'workProgress',
    label: '资金使用(万元)'
  },
  {
    component: 'Textarea',
    fieldName: 'remark',
    label: '备注'
  },
]
const { apiURL, clientId } = useAppConfig(
  import.meta.env,
  import.meta.env.PROD,
);
const uploadUrl = `${apiURL}/knowledge/attach/upload`;
const accessStore = useAccessStore();
const headers = {
  Authorization: `Bearer ${accessStore.accessToken}`,
  clientId,
};
ruoyi-ui/apps/web-antd/src/views/work/myWork/dept-tree.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,128 @@
<script setup lang="ts">
import type { PropType } from 'vue';
import type { DeptTree } from '#/api/system/user/model';
import { onMounted, ref } from 'vue';
import { SyncOutlined } from '@ant-design/icons-vue';
import { Empty, InputSearch, Skeleton, Tree } from 'ant-design-vue';
import { getDeptTree } from '#/api/system/user';
defineOptions({ inheritAttrs: false });
withDefaults(defineProps<{ showSearch?: boolean }>(), { showSearch: true });
const emit = defineEmits<{
  /**
   * ç‚¹å‡»åˆ·æ–°æŒ‰é’®çš„事件
   */
  reload: [];
  /**
   * ç‚¹å‡»èŠ‚ç‚¹çš„äº‹ä»¶
   */
  select: [];
}>();
const selectDeptId = defineModel('selectDeptId', {
  required: true,
  type: Array as PropType<string[]>,
});
const searchValue = defineModel('searchValue', {
  type: String,
  default: '',
});
/** éƒ¨é—¨æ•°æ®æº */
type DeptTreeArray = DeptTree[];
const deptTreeArray = ref<DeptTreeArray>([]);
/** éª¨æž¶å±åŠ è½½ */
const showTreeSkeleton = ref<boolean>(true);
async function loadTree() {
  showTreeSkeleton.value = true;
  searchValue.value = '';
  selectDeptId.value = [];
  const ret = await getDeptTree();
  console.log( "ret", ret )
  deptTreeArray.value = ret;
  showTreeSkeleton.value = false;
}
async function handleReload() {
  await loadTree();
  emit('reload');
}
onMounted(loadTree);
</script>
<template>
  <div :class="$attrs.class">
    <Skeleton
      :loading="showTreeSkeleton"
      :paragraph="{ rows: 8 }"
      active
      class="p-[8px]"
    >
      <div
        class="bg-background flex h-full flex-col overflow-y-auto rounded-lg"
      >
        <!-- å›ºå®šåœ¨é¡¶éƒ¨ å¿…须加上bg-background背景色 å¦åˆ™ä¼šäº§ç”Ÿ'穿透'效果 -->
        <div
          v-if="showSearch"
          class="bg-background z-100 sticky left-0 top-0 p-[8px]"
        >
          <InputSearch
            v-model:value="searchValue"
            :placeholder="$t('pages.common.search')"
            size="small"
          >
            <template #enterButton>
              <a-button @click="handleReload">
                <SyncOutlined class="text-primary" />
              </a-button>
            </template>
          </InputSearch>
        </div>
        <div class="h-full overflow-x-hidden px-[8px]">
          <Tree
            v-bind="$attrs"
            v-if="deptTreeArray.length > 0"
            v-model:selected-keys="selectDeptId"
            :class="$attrs.class"
            :field-names="{ title: 'label', key: 'id' }"
            :show-line="{ showLeafIcon: false }"
            :tree-data="deptTreeArray"
            :virtual="false"
            default-expand-all
            @select="$emit('select')"
          >
            <template #title="{ label }">
              <span v-if="label.indexOf(searchValue) > -1">
                {{ label.substring(0, label.indexOf(searchValue)) }}
                <span style="color: #f50">{{ searchValue }}</span>
                {{
                  label.substring(
                    label.indexOf(searchValue) + searchValue.length,
                  )
                }}
              </span>
              <span v-else>{{ label }}</span>
            </template>
          </Tree>
          <!-- ä»…本人数据权限 å¯ä»¥è€ƒè™‘直接不显示 -->
          <div v-else class="mt-5">
            <Empty
              :image="Empty.PRESENTED_IMAGE_SIMPLE"
              description="无部门数据"
            />
          </div>
        </div>
      </div>
    </Skeleton>
  </div>
</template>
ruoyi-ui/apps/web-antd/src/views/work/myWork/index.vue
@@ -1,13 +1,298 @@
<template>
  <div>我的工作</div>
</template>
<!-- æˆ‘的工作 -->
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
<script>
export default {
  name: "index"
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { User } from '#/api/system/user/model';
import { ref } from 'vue';
import { useAccess } from '@vben/access';
import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { preferences } from '@vben/preferences';
import { getVxePopupContainer } from '@vben/utils';
import {
  Avatar,
  Dropdown,
  Menu,
  MenuItem,
  Modal,
  Popconfirm,
  Space,
} from 'ant-design-vue';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
  userExport,
  userList,
  userRemove,
  userStatusChange,
} from '#/api/system/user';
import { TableSwitch } from '#/components/table';
import { commonDownloadExcel } from '#/utils/file/download';
import { columns, querySchema } from './data';
import userDrawer from './user-drawer.vue'; //工作详情
import userAssignWork from './user-assign-work.vue'; //工作下发
import userReport from './user-report.vue'; //工作汇报
import userImportModal from './user-import-modal.vue';
import userInfoModal from './user-info-modal.vue';
import userResetPwdModal from './user-reset-pwd-modal.vue';
/**
 * å¯¼å…¥
 */
const [UserImpotModal, userImportModalApi] = useVbenModal({
  connectedComponent: userImportModal,
});
function handleImport() {
  userImportModalApi.open();
}
// å·¦è¾¹éƒ¨é—¨ç”¨
const selectDeptId = ref<string[]>([]);
const formOptions: VbenFormProps = {
  schema: querySchema(),
  commonConfig: {
    labelWidth: 80,
    componentProps: {
      allowClear: true,
    },
  },
  wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
  handleReset: async () => {
    selectDeptId.value = [];
    const { formApi, reload } = tableApi;
    await formApi.resetForm();
    const formValues = formApi.form.values;
    formApi.setLatestSubmissionValues(formValues);
    await reload(formValues);
  },
  // æ—¥æœŸé€‰æ‹©æ ¼å¼åŒ–
  fieldMappingTime: [
    [
      'createTime',
      ['params[beginTime]', 'params[endTime]'],
      ['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
    ],
  ],
};
const gridOptions: VxeGridProps = {
  checkboxConfig: {
    // é«˜äº®
    highlight: true,
    // ç¿»é¡µæ—¶ä¿ç•™é€‰ä¸­çŠ¶æ€
    reserve: true,
    // ç‚¹å‡»è¡Œé€‰ä¸­
    trigger: 'default',
    checkMethod: ({ row }) => row?.userId !== 1,
  },
  columns,
  height: 'auto',
  keepSource: true,
  pagerConfig: {},
  proxyConfig: {
    ajax: {
      // èŽ·å–é¡µé¢æ•°æ®ï¼Œæœç´¢ï¼Œé‡ç½®ä¹Ÿæ˜¯
      /*
        @params page:页码参数
        @params formValues:表单参数
      */
      query: async ({ page }, formValues = {}) => {
        console.log("获取页面数据,")
        const res = {
          rows:[ {
            id:"0",  //工作id,编号
            workName:"工作名称1", //工作名称
            workClass:"工作类别", //工作类别
            workContent:"工作内容", //工作内容
            projectBudget:"项目预算", //项目预算
            amountProject:"项目金额", //项目金额
            responsibleDepartment:"负责部门", //负责部门
            Head:"负责人", //负责人
            Annual:"年度", //年度
            assessmentTime:"考核时间",//考核时间
            assessmentIndicators:"考核指标", //考核指标
            File:"文件地址", //文件上传
            taskStatus:"任务状态", //任务状态
            assignmentStatus:"分配状态", //分配状态
            workProgress:"工作进度" //工作进度
          } ],
          total:1
        }
        console.log( "res", res)
        return res
      },
    },
  },
  rowConfig: {
    keyField: 'userId',
    height: 48,
  },
  id: 'system-user-index',
};
const [BasicTable, tableApi] = useVbenVxeGrid({
  formOptions,
  gridOptions,
});
// æŸ¥çœ‹è¯¦æƒ…
const [UserDrawer, userDrawerApi] = useVbenDrawer({
  connectedComponent: userDrawer,
});
// å·¥ä½œä¸‹å‘
const [userAssignWorkDrawer, userAssignWorkApi] = useVbenDrawer({
  connectedComponent: userAssignWork
})
// å·¥ä½œæ±‡æŠ¥
const [userReportDrawer, userReportApi] = useVbenDrawer({
  connectedComponent: userReport
})
// æ­¤æ–¹æ³•暂未启用
function handleAdd() {
  userDrawerApi.setData({});
  userDrawerApi.open();
}
// æŸ¥çœ‹è¯¦æƒ…
function handleLook(row: any) {
  userDrawerApi.setData({ id: row.id, look: true });
  userDrawerApi.open();
}
// å·¥ä½œä¸‹å‘
function handleAssigningWork(row: any) {
  userAssignWorkApi.setData({id:row.id, look:true})
  userAssignWorkApi.open()
}
// å·¥ä½œæ±‡æŠ¥
function handleReport(row: any) {
  userReportApi.setData({ id: row.id, row: row });
  userReportApi.open();
}
function handleMultiDelete() {
  const rows = tableApi.grid.getCheckboxRecords();
  const ids = rows.map((row: User) => row.userId);
  Modal.confirm({
    title: '提示',
    okType: 'danger',
    content: `确认删除选中的${ids.length}条记录吗?`,
    onOk: async () => {
      await userRemove(ids);
      await tableApi.query();
    },
  });
}
function handleDownloadExcel() {
  commonDownloadExcel(userExport, '用户管理', tableApi.formApi.form.values, {
    fieldMappingTime: formOptions.fieldMappingTime,
  });
}
const [UserInfoModal, userInfoModalApi] = useVbenModal({
  connectedComponent: userInfoModal,
});
function handleUserInfo(row: User) {
  userInfoModalApi.setData({ userId: row.userId });
  userInfoModalApi.open();
}
const [UserResetPwdModal, userResetPwdModalApi] = useVbenModal({
  connectedComponent: userResetPwdModal,
});
function handleResetPwd(record: User) {
  userResetPwdModalApi.setData({ record });
  userResetPwdModalApi.open();
}
const { hasAccessByCodes } = useAccess();
</script>
<style scoped>
</style>
<template>
  <Page :auto-content-height="true">
    <div class="flex h-full gap-[8px]">
      <BasicTable class="flex-1 overflow-hidden" table-title="工作列表">
        <template #toolbar-tools>
          <Space>
            <!-- å¯¼å‡º -->
            <!-- <a-button
              @click="handleDownloadExcel"
            >
              {{ $t('pages.common.export') }}
            </a-button> -->
            <!-- å¯¼å…¥ -->
            <!-- <a-button
              v-access:code="['system:user:import']"
              @click="handleImport"
            >
              {{ $t('pages.common.import') }}
            </a-button> -->
            <!-- åˆ é™¤ -->
            <a-button
              :disabled="!vxeCheckboxChecked(tableApi)"
              danger
              type="primary"
              @click="handleMultiDelete"
            >
              {{ $t('pages.common.delete') }}
            </a-button>
            <!-- æ–°å¢ž -->
            <!-- <a-button
              type="primary"
              @click="handleAdd"
            >
              {{ $t('pages.common.WorkIssued') }}
            </a-button> -->
          </Space>
        </template>
        <template #avatar="{ row }">
          <!-- å¯èƒ½è¦åˆ¤æ–­ç©ºå­—符串情况 æ‰€ä»¥æ²¡æœ‰ä½¿ç”¨?? -->
          <Avatar :src="row.avatar || preferences.app.defaultAvatar" />
        </template>
        <template #status="{ row }">
          <TableSwitch
            v-model="row.status"
            :api="() => userStatusChange(row)"
            :disabled="
              row.userId === 1 || !hasAccessByCodes(['system:user:edit'])
            "
            :reload="() => tableApi.query()"
          />
        </template>
        <template #action="{ row }">
            <Space>
              <!-- æŸ¥çœ‹ -->
              <ghost-button
                @click.stop="handleLook(row)"
              >
                {{ $t('pages.common.look') }}
              </ghost-button>
              <!-- æ±‡æŠ¥ -->
              <ghost-button
                @click.stop="handleReport(row)"
              >
                {{ $t('pages.common.report') }}
              </ghost-button>
              <!-- å·¥ä½œä¸‹å‘ -->
              <ghost-button
                @click.stop="handleAssigningWork(row)"
              >
                {{ $t('pages.common.AssigningWork') }}
              </ghost-button>
            </Space>
        </template>
      </BasicTable>
    </div>
    <UserImpotModal @reload="tableApi.query()" />
    <UserDrawer @reload="tableApi.query()" /> <!-- å·¥ä½œè¯¦æƒ… -->
    <userAssignWorkDrawer @reload="tableApi.query()" /> <!-- å·¥ä½œä¸‹å‘ -->
    <userReportDrawer @reload="tableApi.query()" /> <!-- å·¥ä½œæ±‡æŠ¥ -->
    <UserInfoModal />
    <UserResetPwdModal />
  </Page>
</template>
ruoyi-ui/apps/web-antd/src/views/work/myWork/info.tsx
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,129 @@
import type { DescItem } from '#/components/description';
import { DictEnum } from '@vben/constants';
import { Tag } from 'ant-design-vue';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import { renderDict } from '#/utils/render';
dayjs.extend(duration);
dayjs.extend(relativeTime);
function renderTags(list: string[]) {
  return (
    <div class="flex flex-row flex-wrap gap-0.5">
      {list.map((item) => (
        <Tag key={item}>{item}</Tag>
      ))}
    </div>
  );
}
export const descSchema: DescItem[] = [
  {
    field: 'userId',
    label: '用户ID',
  },
  {
    field: 'status',
    label: '用户状态',
    render(value) {
      return renderDict(value, DictEnum.SYS_NORMAL_DISABLE);
    },
  },
  {
    field: 'nickName',
    label: '用户信息',
    render(_, data) {
      const { deptName = '暂无部门信息', nickName, userName } = data;
      // ä¸ºäº†å…¼å®¹æ–°ç‰ˆæœ¬å’Œæ—§ç‰ˆæœ¬
      let currentDept = deptName;
      if (data.dept && data.dept.deptName) {
        currentDept = data.dept.deptName;
      }
      return `${userName} / ${nickName} / ${currentDept}`;
    },
  },
  {
    field: 'phonenumber',
    label: '手机号',
    render(value) {
      return value || '未设置手机号码';
    },
  },
  {
    field: 'email',
    label: '邮箱',
    render(value) {
      return value || '未设置邮箱地址';
    },
  },
  {
    field: 'postNames',
    label: '岗位',
    render(value) {
      if (Array.isArray(value) && value.length === 0) {
        return '暂无信息';
      }
      return renderTags(value);
    },
  },
  {
    field: 'roleNames',
    label: '权限',
    render(value) {
      if (Array.isArray(value) && value.length === 0) {
        return '暂无信息';
      }
      return renderTags(value);
    },
  },
  {
    field: 'createTime',
    label: '创建时间',
  },
  {
    field: 'loginIp',
    label: '上次登录IP',
    render(value) {
      return value || <span class="text-orange-500">从未登录过</span>;
    },
  },
  {
    field: 'loginDate',
    label: '上次登录时间',
    render(value) {
      if (!value) {
        return <span class="text-orange-500">从未登录过</span>;
      }
      // é»˜è®¤en显示
      dayjs.locale('zh-cn');
      // è®¡ç®—相差秒数
      const diffSeconds = dayjs().diff(dayjs(value), 'second');
      /**
       * è½¬ä¸ºæ—¶é—´æ˜¾ç¤º(x月 x天)
       * https://dayjs.fenxianglu.cn/category/duration.html#%E4%BA%BA%E6%80%A7%E5%8C%96
       *
       */
      const diffText = dayjs.duration(diffSeconds, 'seconds').humanize();
      return (
        <div class="flex gap-2">
          {value}
          <Tag bordered={false} color="cyan">
            {diffText}前
          </Tag>
        </div>
      );
    },
  },
  {
    field: 'remark',
    label: '备注',
    render(value) {
      return value || '无';
    },
  },
];
ruoyi-ui/apps/web-antd/src/views/work/myWork/user-assign-work.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,170 @@
<script setup lang="ts">
import type { Role } from '#/api/system/user/model';
import { computed, h, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
import { Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { findUserInfo, userAdd, userUpdate } from '#/api/system/user';
import { authScopeOptions } from '#/views/system/role/data';
import { drawerSchema, AssigningWorkSchema } from './data';
import { deptList } from '#/api/system/dept/index';
const emit = defineEmits<{ reload: [] }>();
const isUpdate = ref(false); //当前是否为编辑
const isLook = ref(false); //当前是否为查看
const title = computed(() => {
  let text = '';
  text = "工作下发"
  return text;
});
const [BasicForm, formApi] = useVbenForm({
  commonConfig: {
    formItemClass: 'col-span-2',
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 80,
  },
  schema: AssigningWorkSchema(),
  showDefaultActions: false,
  wrapperClass: 'grid-cols-2',
});
/**
 * ç”Ÿæˆè§’色的自定义label
 * ä¹Ÿå¯ä»¥ç”¨option插槽来做
 * renderComponentContent: () => ({
    option: ({value, label, [disabled, key, title]}) => '',
  }),
 */
function genRoleOptionlabel(role: Role) {
  const found = authScopeOptions.find((item) => item.value === role.dataScope);
  if (!found) {
    return role.roleName;
  }
  return h('div', { class: 'flex items-center gap-[6px]' }, [
    h('span', null, role.roleName),
    h(Tag, { color: found.color }, () => found.label),
  ]);
}
// æ‰“开以及编辑都会调用该事件
const [BasicDrawer, drawerApi] = useVbenDrawer({
  onCancel: handleCancel,
  onConfirm: handleConfirm,
  async onOpenChange(isOpen) {
    drawerApi.drawerLoading(true);
    if (isOpen) {
      // åˆå§‹åŒ–一下表单选项
      const deptlist = await (
        await deptList()
      ).map((item) => {
        return {
          ...item,
          value: item.deptId,
          label: item.deptName,
        };
      });
      formApi.updateSchema([
        {
          componentProps: { options: deptlist },
          fieldName: 'responsibleDepartment',
        },
      ]);
    }
    if (!isOpen) {
      // éœ€è¦é‡ç½®å²—位选择
      formApi.updateSchema([
        {
          componentProps: { options: [], placeholder: '请先选择部门' },
          fieldName: 'postIds',
        },
      ]);
      return null;
    }
    const { id } = drawerApi.getData() as { id?: number | string };
    isUpdate.value = !!id;
    // æ›´æ–° && èµ‹å€¼
    // const { postIds, posts, roleIds, roles, user } = await findUserInfo(id); //调用接口获取详细信息
    const data = {
      id: '0', //工作id,编号
      workName: '工作名称1', //工作名称
      workClass: '工作类别', //工作类别
      workContent: '工作内容', //工作内容
      projectBudget: '项目预算', //项目预算
      amountProject: '项目金额', //项目金额
      responsibleDepartment: '负责部门', //负责部门
      Head: '负责人', //负责人
      Annual: '2025', //年度
      assessmentTime: '2025-06-25', //考核时间
      assessmentIndicators: '考核指标', //考核指标
      File: '文件地址', //文件上传
      taskStatus: '任务状态', //任务状态
      assignmentStatus: '分配状态', //分配状态
      workProgress: '工作进度', //工作进度
    };
    console.log(data);
    console.log(id);
    if (data && id) {
      await Promise.all([
        // æ·»åŠ åŸºç¡€ä¿¡æ¯
        formApi.setValues(data),
      ]);
    }
    drawerApi.drawerLoading(false);
    console.log(isLook.value);
  },
});
// è¡¨å•提交,编辑与新增都会调用这个方法体
async function handleConfirm() {
  try {
    drawerApi.drawerLoading(true);
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    const data = cloneDeep(await formApi.getValues());  //表单内的数据
    console.log(data)
    // await (isUpdate.value ? userUpdate(data) : userAdd(data));
    emit('reload');
    await handleCancel();
  } catch (error) {
    console.error(error);
  } finally {
    drawerApi.drawerLoading(false);
  }
}
async function handleCancel() {
  drawerApi.close();
  await formApi.resetForm();
}
function IsShowConfirmButton(params: type) {
  if (isLook.value) {
    return false;
  }
  return true;
}
// åœ¨è¿™åŠ è½½äº›æ•°æ®
</script>
<template>
  <BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]">
    <BasicForm />
  </BasicDrawer>
</template>
ruoyi-ui/apps/web-antd/src/views/work/myWork/user-drawer.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,218 @@
<script setup lang="ts">
import type { Role } from '#/api/system/user/model';
import { computed, h, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
import { Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { findUserInfo, userAdd, userUpdate } from '#/api/system/user';
import { authScopeOptions } from '#/views/system/role/data';
import { drawerSchema } from './data';
import { deptList } from '#/api/system/dept/index';
import { message, Upload } from 'ant-design-vue';
const emit = defineEmits<{ reload: [] }>();
const isUpdate = ref(false); //当前是否为编辑
const isLook = ref(false); //当前是否为查看
const title = computed(() => {
  let text = '';
  if (isLook) {
    text = $t('pages.common.look');
  } else if (isUpdate.value) {
    text = $t('pages.common.edit');
  } else if (!isUpdate.value) {
    text = $t('pages.common.add');
  }
  return text;
});
const [BasicForm, formApi] = useVbenForm({
  commonConfig: {
    formItemClass: 'col-span-2',
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 80,
  },
  schema: drawerSchema(),
  showDefaultActions: false,
  wrapperClass: 'grid-cols-2',
});
/**
 * ç”Ÿæˆè§’色的自定义label
 * ä¹Ÿå¯ä»¥ç”¨option插槽来做
 * renderComponentContent: () => ({
    option: ({value, label, [disabled, key, title]}) => '',
  }),
 */
function genRoleOptionlabel(role: Role) {
  const found = authScopeOptions.find((item) => item.value === role.dataScope);
  if (!found) {
    return role.roleName;
  }
  return h('div', { class: 'flex items-center gap-[6px]' }, [
    h('span', null, role.roleName),
    h(Tag, { color: found.color }, () => found.label),
  ]);
}
// æ‰“开以及编辑都会调用该事件
const [BasicDrawer, drawerApi] = useVbenDrawer({
  onCancel: handleCancel,
  onConfirm: handleConfirm,
  async onOpenChange(isOpen) {
    drawerApi.drawerLoading(true);
    if (isOpen) {
      let deptlist;
      try {
        // åˆå§‹åŒ–一下表单选项
        deptlist = await (
          await deptList()
        ).map((item) => {
          return {
            ...item,
            value: item.deptId,
            label: item.deptName,
          };
        });
      } catch (error) {
        console.log( "Api-deptList", error )
      }
      formApi.updateSchema([
        {
          componentProps: { options: deptlist },
          fieldName: 'responsibleDepartment',
        },
      ]);
    }
    if (!isOpen) {
      // éœ€è¦é‡ç½®å²—位选择
      formApi.updateSchema([
        {
          componentProps: { options: [], placeholder: '请先选择部门' },
          fieldName: 'postIds',
        },
      ]);
      return null;
    }
    const { id } = drawerApi.getData() as { id?: number | string };
    const { look } = drawerApi.getData() as { look?: boolean };
    isUpdate.value = !!id;
    isLook.value = !!look;
    console.log('isLook.value', isLook.value);
    /** update时 ç¦ç”¨ç”¨æˆ·åä¿®æ”¹ ä¸æ˜¾ç¤ºå¯†ç æ¡† */
    // å¦‚果为查看则所有项都不可编辑
    formApi.updateSchema([
      { componentProps: { disabled: isLook.value }, fieldName: 'workName' },
      { componentProps: { disabled: isLook.value }, fieldName: 'workClass' },
      { componentProps: { disabled: isLook.value }, fieldName: 'workContent' },
      {
        componentProps: { disabled: isLook.value },
        fieldName: 'projectBudget',
      },
      {
        componentProps: { disabled: isLook.value },
        fieldName: 'amountProject',
      },
      {
        componentProps: { disabled: isLook.value },
        fieldName: 'responsibleDepartment',
      },
      { componentProps: { disabled: isLook.value }, fieldName: 'Head' },
      { componentProps: { disabled: isLook.value }, fieldName: 'Annual' },
      {
        componentProps: { disabled: isLook.value },
        fieldName: 'assessmentTime',
      },
      {
        componentProps: { disabled: isLook.value },
        fieldName: 'assessmentIndicators',
      },
      { componentProps: { disabled: isLook.value }, fieldName: 'File' },
    ]);
    drawerApi.setState({
      showConfirmButton: !isLook.value,
    });
    // æ›´æ–° && èµ‹å€¼
    // const { postIds, posts, roleIds, roles, user } = await findUserInfo(id); //调用接口获取详细信息
    const data = {
      id: '0', //工作id,编号
      workName: '工作名称1', //工作名称
      workClass: '工作类别', //工作类别
      workContent: '工作内容', //工作内容
      projectBudget: '项目预算', //项目预算
      amountProject: '项目金额', //项目金额
      responsibleDepartment: '负责部门', //负责部门
      Head: '负责人', //负责人
      Annual: '2025', //年度
      assessmentTime: '2025-06-25', //考核时间
      assessmentIndicators: '考核指标', //考核指标
      File: '文件地址', //文件上传
      taskStatus: '任务状态', //任务状态
      assignmentStatus: '分配状态', //分配状态
      workProgress: '工作进度', //工作进度
    };
    console.log(data);
    console.log(id);
    if (data && id) {
      await Promise.all([
        // æ·»åŠ åŸºç¡€ä¿¡æ¯
        formApi.setValues(data),
      ]);
    }
    drawerApi.drawerLoading(false);
    console.log(isLook.value);
  },
});
// è¡¨å•提交,编辑与新增都会调用这个方法体
async function handleConfirm() {
  try {
    drawerApi.drawerLoading(true);
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    const data = cloneDeep(await formApi.getValues()); //表单内的数据
    console.log(data);
    // await (isUpdate.value ? userUpdate(data) : userAdd(data));
    emit('reload');
    await handleCancel();
  } catch (error) {
    console.error(error);
  } finally {
    drawerApi.drawerLoading(false);
  }
}
async function handleCancel() {
  drawerApi.close();
  await formApi.resetForm();
}
function IsShowConfirmButton(params: type) {
  if (isLook.value) {
    return false;
  }
  return true;
}
// åœ¨è¿™åŠ è½½äº›æ•°æ®
</script>
<template>
  <BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]">
    <BasicForm />
  </BasicDrawer>
</template>
ruoyi-ui/apps/web-antd/src/views/work/myWork/user-import-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,108 @@
<script setup lang="ts">
import type { UploadFile } from 'ant-design-vue/es/upload/interface';
import { h, ref, unref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { ExcelIcon, InBoxIcon } from '@vben/icons';
import { Modal, Switch, Upload } from 'ant-design-vue';
import { downloadImportTemplate, userImportData } from '#/api/system/user';
import { commonDownloadExcel } from '#/utils/file/download';
const emit = defineEmits<{ reload: [] }>();
const UploadDragger = Upload.Dragger;
const [BasicModal, modalApi] = useVbenModal({
  onCancel: handleCancel,
  onConfirm: handleSubmit,
});
const fileList = ref<UploadFile[]>([]);
const checked = ref(false);
async function handleSubmit() {
  try {
    modalApi.modalLoading(true);
    if (fileList.value.length !== 1) {
      handleCancel();
      return;
    }
    const data = {
      file: fileList.value[0]!.originFileObj as Blob,
      updateSupport: unref(checked),
    };
    const { code, msg } = await userImportData(data);
    let modal = Modal.success;
    if (code === 200) {
      emit('reload');
    } else {
      modal = Modal.error;
    }
    handleCancel();
    modal({
      content: h('div', {
        class: 'max-h-[260px] overflow-y-auto',
        innerHTML: msg, // åŽå°å·²ç»å¤„理xss问题
      }),
      title: '提示',
    });
  } catch (error) {
    console.warn(error);
    modalApi.close();
  } finally {
    modalApi.modalLoading(false);
  }
}
function handleCancel() {
  modalApi.close();
  fileList.value = [];
  checked.value = false;
}
</script>
<template>
  <BasicModal
    :close-on-click-modal="false"
    :fullscreen-button="false"
    title="用户导入"
  >
    <!-- z-index不设置会遮挡模板下载loading -->
    <!-- æ‰‹åŠ¨å¤„ç† è€Œä¸æ˜¯æ”¾å…¥æ–‡ä»¶å°±ä¸Šä¼  -->
    <UploadDragger
      v-model:file-list="fileList"
      :before-upload="() => false"
      :max-count="1"
      :show-upload-list="true"
      accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
    >
      <p class="ant-upload-drag-icon flex items-center justify-center">
        <InBoxIcon class="text-primary size-[48px]" />
      </p>
      <p class="ant-upload-text">点击或者拖拽到此处上传文件</p>
    </UploadDragger>
    <div class="mt-2 flex flex-col gap-2">
      <div class="flex items-center gap-2">
        <span>允许导入xlsx, xls文件</span>
        <a-button
          type="link"
          @click="commonDownloadExcel(downloadImportTemplate, '用户导入模板')"
        >
          <div class="flex items-center gap-[4px]">
            <ExcelIcon />
            <span>下载模板</span>
          </div>
        </a-button>
      </div>
      <div class="flex items-center gap-2">
        <span :class="{ 'text-red-500': checked }">
          æ˜¯å¦æ›´æ–°/覆盖已存在的用户数据
        </span>
        <Switch v-model:checked="checked" />
      </div>
    </div>
  </BasicModal>
</template>
ruoyi-ui/apps/web-antd/src/views/work/myWork/user-info-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
<script setup lang="ts">
import type { User } from '#/api/system/user/model';
import { useVbenModal } from '@vben/common-ui';
import { findUserInfo } from '#/api/system/user';
import { Description, useDescription } from '#/components/description';
import { descSchema } from './info';
const [BasicModal, modalApi] = useVbenModal({
  onOpenChange: handleOpenChange,
});
const [registerDescription, { setDescProps }] = useDescription({
  column: 1,
  labelStyle: {
    minWidth: '150px',
    width: '150px',
  },
  schema: descSchema,
});
async function handleOpenChange(open: boolean) {
  if (!open) {
    return null;
  }
  modalApi.modalLoading(true);
  const { userId } = modalApi.getData() as { userId: number | string };
  const response = await findUserInfo(userId);
  // å¤–部的roleIds postIds才是真正对应的  æ–°å¢žæ—¶ä¸ºç©º
  // posts有为Null的情况 éœ€è¦ç»™é»˜è®¤å€¼
  const { postIds = [], posts = [], roleIds = [], roles = [], user } = response;
  const postNames = posts
    .filter((item) => postIds.includes(item.postId))
    .map((item) => item.postName);
  const roleNames = roles
    .filter((item) => roleIds.includes(item.roleId))
    .map((item) => item.roleName);
  interface UserWithNames extends User {
    postNames: string[];
    roleNames: string[];
  }
  (user as UserWithNames).postNames = postNames;
  (user as UserWithNames).roleNames = roleNames;
  // èµ‹å€¼
  setDescProps({ data: user });
  modalApi.modalLoading(false);
}
</script>
<template>
  <BasicModal :footer="false" :fullscreen-button="false" title="用户信息">
    <Description @register="registerDescription" />
  </BasicModal>
</template>
ruoyi-ui/apps/web-antd/src/views/work/myWork/user-report.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,176 @@
<script setup lang="ts">
import type { Role } from '#/api/system/user/model';
import { computed, h, ref } from 'vue';
import { useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { cloneDeep } from '@vben/utils';
import { Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { findUserInfo, userAdd, userUpdate } from '#/api/system/user';
import { authScopeOptions } from '#/views/system/role/data';
import { drawerSchema, ReportScheme } from './data';
import { deptList } from '#/api/system/dept/index';
const emit = defineEmits<{ reload: [] }>();
const isUpdate = ref(false); //当前是否为编辑
const isLook = ref(false); //当前是否为查看
const title = computed(() => {
  let text = '';
  text="汇报"
  return text;
});
const [BasicForm, formApi] = useVbenForm({
  commonConfig: {
    formItemClass: 'col-span-2',
    componentProps: {
      class: 'w-full',
    },
    labelWidth: 80,
  },
  schema: ReportScheme(),
  showDefaultActions: false,
  wrapperClass: 'grid-cols-2',
});
/**
 * ç”Ÿæˆè§’色的自定义label
 * ä¹Ÿå¯ä»¥ç”¨option插槽来做
 * renderComponentContent: () => ({
    option: ({value, label, [disabled, key, title]}) => '',
  }),
 */
function genRoleOptionlabel(role: Role) {
  const found = authScopeOptions.find((item) => item.value === role.dataScope);
  if (!found) {
    return role.roleName;
  }
  return h('div', { class: 'flex items-center gap-[6px]' }, [
    h('span', null, role.roleName),
    h(Tag, { color: found.color }, () => found.label),
  ]);
}
// æ‰“开以及编辑都会调用该事件
const [BasicDrawer, drawerApi] = useVbenDrawer({
  onCancel: handleCancel,
  onConfirm: handleConfirm,
  async onOpenChange(isOpen) {
    drawerApi.drawerLoading(true);
    if (isOpen) {
      let deptlist;
      try {
        // åˆå§‹åŒ–一下表单选项
        deptlist = await (
          await deptList()
        ).map((item) => {
          return {
            ...item,
            value: item.deptId,
            label: item.deptName,
          };
        });
      } catch (error) {
        console.error('uReport-Api-deptlist', error);
      }
      formApi.updateSchema([
        {
          componentProps: { options: deptlist },
          fieldName: 'responsibleDepartment',
        },
      ]);
    }
    if (!isOpen) {
      // éœ€è¦é‡ç½®å²—位选择
      formApi.updateSchema([
        {
          componentProps: { options: [], placeholder: '请先选择部门' },
          fieldName: 'postIds',
        },
      ]);
      return null;
    }
    const { id } = drawerApi.getData() as { id?: number | string };
    isUpdate.value = !!id;
    /** update时 ç¦ç”¨ç”¨æˆ·åä¿®æ”¹ ä¸æ˜¾ç¤ºå¯†ç æ¡† */
    // æ›´æ–° && èµ‹å€¼
    // const { postIds, posts, roleIds, roles, user } = await findUserInfo(id); //调用接口获取详细信息
    const data = {
      id: '0', //工作id,编号
      workName: '工作名称1', //工作名称
      workClass: '工作类别', //工作类别
      workContent: '', //工作内容
      projectBudget: '项目预算', //项目预算
      amountProject: '项目金额', //项目金额
      responsibleDepartment: '负责部门', //负责部门
      Head: '负责人', //负责人
      Annual: '2025', //年度
      assessmentTime: '2025-06-25', //考核时间
      assessmentIndicators: '考核指标', //考核指标
      File: '文件地址', //文件上传
      taskStatus: '任务状态', //任务状态
      assignmentStatus: '分配状态', //分配状态
      workProgress: '工作进度', //工作进度
    };
    console.log(data);
    console.log(id);
    if (data && id) {
      await Promise.all([
        // æ·»åŠ åŸºç¡€ä¿¡æ¯
        formApi.setValues(data),
      ]);
    }
    drawerApi.drawerLoading(false);
    console.log(isLook.value);
  },
});
// è¡¨å•提交,编辑与新增都会调用这个方法体
async function handleConfirm() {
  try {
    drawerApi.drawerLoading(true);
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    const data = cloneDeep(await formApi.getValues()); //表单内的数据
    console.log(data);
    // await (isUpdate.value ? userUpdate(data) : userAdd(data));
    emit('reload');
    await handleCancel();
  } catch (error) {
    console.error(error);
  } finally {
    drawerApi.drawerLoading(false);
  }
}
async function handleCancel() {
  drawerApi.close();
  await formApi.resetForm();
}
function IsShowConfirmButton(params: type) {
  if (isLook.value) {
    return false;
  }
  return true;
}
// åœ¨è¿™åŠ è½½äº›æ•°æ®
</script>
<template>
  <BasicDrawer :close-on-click-modal="false" :title="title" class="w-[600px]">
    <BasicForm />
  </BasicDrawer>
</template>
ruoyi-ui/apps/web-antd/src/views/work/myWork/user-reset-pwd-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
<script setup lang="ts">
import type { ResetPwdParam, User } from '#/api/system/user/model';
import { useVbenModal, z } from '@vben/common-ui';
import { useVbenForm } from '#/adapter/form';
import { userResetPassword } from '#/api/system/user';
import { Description, useDescription } from '#/components/description';
const emit = defineEmits<{ reload: [] }>();
const [BasicModal, modalApi] = useVbenModal({
  onCancel: handleCancel,
  onConfirm: handleSubmit,
  onOpenChange: handleOpenChange,
});
const [registerDescription, { setDescProps }] = useDescription({
  column: 1,
  schema: [
    {
      field: 'userId',
      label: '用户ID',
    },
    {
      field: 'userName',
      label: '用户名',
    },
    {
      field: 'nickName',
      label: '昵称',
    },
  ],
});
const [BasicForm, formApi] = useVbenForm({
  schema: [
    {
      component: 'Input',
      dependencies: {
        show: () => false,
        triggerFields: [''],
      },
      fieldName: 'userId',
      label: '用户ID',
      rules: 'required',
    },
    {
      component: 'InputPassword',
      componentProps: {
        placeholder: '请输入新的密码, å¯†ç é•¿åº¦ä¸º5 - 20',
      },
      fieldName: 'password',
      label: '新的密码',
      rules: z
        .string()
        .min(5, { message: '密码长度为5 - 20' })
        .max(20, { message: '密码长度为5 - 20' }),
    },
  ],
  showDefaultActions: false,
  commonConfig: {
    labelWidth: 80,
  },
});
async function handleOpenChange(open: boolean) {
  if (!open) {
    return null;
  }
  const { record } = modalApi.getData() as { record: User };
  setDescProps({ data: record }, true);
  await formApi.setValues({ userId: record.userId });
}
async function handleSubmit() {
  try {
    modalApi.modalLoading(true);
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    const data = await formApi.getValues();
    await userResetPassword(data as ResetPwdParam);
    emit('reload');
    handleCancel();
  } catch (error) {
    console.error(error);
  } finally {
    modalApi.modalLoading(false);
  }
}
async function handleCancel() {
  modalApi.close();
  await formApi.resetForm();
}
</script>
<template>
  <BasicModal
    :close-on-click-modal="false"
    :fullscreen-button="false"
    title="重置密码"
  >
    <div class="flex flex-col gap-[12px]">
      <Description @register="registerDescription" />
      <BasicForm />
    </div>
  </BasicModal>
</template>
ruoyi-ui/packages/@core/base/shared/src/constants/dict-enum.ts
@@ -15,4 +15,7 @@
  WF_BUSINESS_STATUS = 'wf_business_status', // ä¸šåŠ¡çŠ¶æ€
  WF_FORM_TYPE = 'wf_form_type', // è¡¨å•类型
  WF_TASK_STATUS = 'wf_task_status', // ä»»åŠ¡çŠ¶æ€
  TASK_STATUS = 'task_status', // ä»»åŠ¡çŠ¶æ€
  ASSIGNMENT_STATUS = 'assignment_status', // åˆ†é…çŠ¶æ€
  ANNUAL = 'annual' //年度
}
ruoyi-ui/packages/@core/ui-kit/form-ui/src/form-render/form-field.vue
@@ -366,7 +366,7 @@
        </div>
        <Transition name="slide-up">
          <FormMessage class="absolute bottom-1" />
          <FormMessage class="absolute" />
        </Transition>
      </div>
    </FormItem>
ruoyi-ui/packages/locales/src/langs/en-US/common.json
@@ -15,6 +15,7 @@
  "enabled": "Enabled",
  "disabled": "Disabled",
  "edit": "Edit",
  "look": "Look",
  "delete": "Delete",
  "create": "Create",
  "yes": "Yes",
ruoyi-ui/packages/locales/src/langs/zh-CN/common.json
@@ -15,6 +15,7 @@
  "enabled": "已启用",
  "disabled": "已禁用",
  "edit": "修改",
  "look": "查看",
  "delete": "删除",
  "create": "新增",
  "yes": "是",