zhangwei
2025-07-24 4d75f39fca8c6afbe8a120a329af8b2a59a1cd61
‘项目管理页面布局’
3个文件已修改
28个文件已添加
4167 ■■■■■ 已修改文件
src/api/item/index.ts 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/lay-navbar/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/modules/item.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dept/form.vue 403 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dept/index.vue 296 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dept/utils/hook.tsx 311 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dept/utils/rule.ts 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dept/utils/types.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/hooks.ts 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/README.md 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/form.vue 342 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/index.vue 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/utils/enums.ts 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/utils/hook.tsx 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/utils/rule.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/utils/types.ts 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/form.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/index.vue 344 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/utils/hook.tsx 318 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/utils/rule.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/utils/types.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/form/index.vue 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/form/role.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/index.vue 275 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/svg/expand.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/svg/unexpand.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/tree.vue 211 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/utils/hook.tsx 535 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/utils/reset.css 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/utils/rule.ts 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/utils/types.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/item/index.ts
@@ -7,10 +7,7 @@
import { http } from "@/utils/http";
import { baseUrlApi } from "../util";
type Result = {
  success: boolean;
  data: Array<any>;
};
import type { Result } from "../types";
// 获取行政区域列表
export const getRegionList = () => {
@@ -96,53 +93,79 @@
// 首页查询非政府订单处理
export const shouyeOrder = (data?: object) => {
  return http.request("post", baseUrlApi("/api/tenderOrder/shouyeOrder"), {
  return http.request<Result>(
    "post",
    baseUrlApi("/api/tenderOrder/shouyeOrder"),
    {
    data
  });
    }
  );
};
// 招标代理分页查询非政府订单处理
export const zhaobiaoPageOrder = (data?: object) => {
  return http.request("post", baseUrlApi("/api/tenderOrder/page"), { data });
  return http.request<Result>("post", baseUrlApi("/api/tenderOrder/page"), {
    data
  });
};
// 采购代理人增加非政府订单处理
export const caigourenAdd = (data?: object) => {
  return http.request("post", baseUrlApi("/api/tenderOrder/add"), { data });
  return http.request<Result>("post", baseUrlApi("/api/tenderOrder/add"), {
    data
  });
};
// 采购代理人更新非政府订单处理
export const caigourenUpdate = (data?: object) => {
  return http.request("post", baseUrlApi("/api/tenderOrder/update"), { data });
  return http.request<Result>("post", baseUrlApi("/api/tenderOrder/update"), {
    data
  });
};
// 采购代理人更新非政府订单质疑
export const caigourenUpdateZhiyi = (data?: object) => {
  return http.request("post", baseUrlApi("/api/tenderOrder/updateZhiyi"), {
  return http.request<Result>(
    "post",
    baseUrlApi("/api/tenderOrder/updateZhiyi"),
    {
    data
  });
    }
  );
};
// 采购代理人更新非政府订单投诉
export const caigourenUpdateTousu = (data?: object) => {
  return http.request("post", baseUrlApi("/api/tenderOrder/updateTousu"), {
  return http.request<Result>(
    "post",
    baseUrlApi("/api/tenderOrder/updateTousu"),
    {
    data
  });
    }
  );
};
// 采购代理人删除非政府订单处理
export const caigourenDelete = (data?: object) => {
  return http.request("post", baseUrlApi("/api/tenderOrder/delete"), { data });
};
// 采购代理人批量删除非政府订单处理
export const caigourenBatchDelete = (data?: object) => {
  return http.request("post", baseUrlApi("/api/tenderOrder/batchDelete"), {
  return http.request<Result>("post", baseUrlApi("/api/tenderOrder/delete"), {
    data
  });
};
// 采购代理人批量删除非政府订单处理
export const caigourenBatchDelete = (data?: object) => {
  return http.request<Result>(
    "post",
    baseUrlApi("/api/tenderOrder/batchDelete"),
    {
      data
    }
  );
};
// 导出非政府订单处理记录
export const exportFZF = (data?: object) => {
  return http.request("post", baseUrlApi("/api/tenderOrder/export"), { data });
  return http.request<Result>("post", baseUrlApi("/api/tenderOrder/export"), {
    data
  });
};
src/layout/components/lay-navbar/index.vue
@@ -53,7 +53,7 @@
        <span class="el-dropdown-link navbar-bg-hover select-none">
          <!-- <img :src="userAvatar" :style="avatarsStyle" /> -->
          <p class="dark:text-white">
            {{ username || "请完善资料" }}
            你好,{{ username || "请完善资料" }}
            <el-tag effect="plain">{{ userRoles.name }}</el-tag>
          </p>
          <el-icon><CaretBottom /></el-icon>
src/router/modules/item.ts
@@ -1,6 +1,6 @@
export default {
  path: "/item",
  component: () => import("@/views/item/index.vue"),
  component: () => import("@/views/system/dept/index.vue"),
  name: "item",
  // redirect: "/error/403",
  meta: {
src/views/system/dept/form.vue
New file
@@ -0,0 +1,403 @@
<script setup lang="ts">
import { onMounted, ref, reactive } from "vue";
import ReCol from "@/components/ReCol";
import { formRules } from "./utils/rule";
import { FormProps } from "./utils/types";
import { usePublicHooks } from "../hooks";
import { useDept } from "./utils/hook";
import { Operation } from "@element-plus/icons-vue";
import { getCaigoufangshiList } from "@/api/item/index";
const { state } = useDept();
const props = withDefaults(defineProps<FormProps>(), {
  formInline: () => ({
    projectCode: "", // 项目编号(必填)
    projectName: "", // 项目名称(必填)
    hangyepinmu: null, // 行业品目(可选)
    caigoufangshi: null, // 采购方式(可选)
    caigouyusuan: null, // 采购预算(可选)
    dingbiaoguize: null, // 定标规则(可选)
    baomingfei: null, // 报名费(可选)
    toubiaobaozhengjin: null, // 投标保证金(可选)
    lianhetitoubiao: null, // 联合体投标(可选)
    kaibiaofangshi: null, // 开标方式(可选)
    shifoufenbao: "false", // 是否分包(可选)
    shifoutuisongxuanchuan: "true", // 是否推送宣传(可选)
    caigourenmingcheng: null, // 采购人名称(可选)
    xingzhengquyu: null, // 行政区域(可选)
    xingzhengquyuName: null, // 行政区域名称(可选)
    jigoudaima: null, // 机构代码(可选)
    daimaleixing: null, // 代码类型(可选)
    lianxiren: null, // 联系人(可选)
    lianxidianhua: null, // 联系电话(可选)
    tongxindizhi: null, // 通信地址(可选)
    dianziyoujian: null, // 电子邮件(可选)
    xiangmujingbanren: null, // 项目经办人(可选)
    zhiwu: null, // 职务(可选)
    jingbanrendianhua: null, // 经办人电话(可选)
    dailijigoumingcheng: null, // 代理机构名称(可选)
    dailiLianxiren: null, // 代理机构联系人(可选)
    dailiLianxidianhua: null, // 代理机构联系电话(可选)
    dailiDianziyoujian: null, // 代理机构电子邮件(可选)
    dailiTongxindizhi: null, // 代理机构通信地址(可选)
    dailiXiangmujingli: null, // 代理机构项目经理(可选
    dailijingliLianxidianhua: null // 代理机构项目经理联系电话(可选)
  })
});
const ruleFormRef = ref();
const { switchStyle } = usePublicHooks();
const newFormInline = ref(props.formInline);
function getRef() {
  return ruleFormRef.value;
}
defineExpose({ getRef });
onMounted(async () => {});
</script>
<template>
  <el-form
    ref="ruleFormRef"
    :model="newFormInline"
    :rules="formRules"
    label-width="110px"
  >
    <el-row :gutter="30">
      <re-col>
        <p class="flex items-center">
          <el-icon color="#409EFF" class="m-2" size="large">
            <Operation />
          </el-icon>
          <el-text class="mx-1" size="large" type="primary" tag="b">
            项目信息
          </el-text>
        </p>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="项目名称" prop="projectName">
          <el-input
            v-model="newFormInline.projectName"
            clearable
            placeholder="请输入项目名称"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="项目编号" prop="projectCode">
          <el-input
            v-model="newFormInline.projectCode"
            clearable
            placeholder="请输入项目编号"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="行业品目" prop="hangyepinmu">
          <el-select
            v-model="newFormInline.hangyepinmu"
            placeholder="请选择行业品目"
            clearable
            class="w-[100%]!"
          >
            <el-option
              v-for="item in state.hangyepingmuList"
              :key="item.id"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="采购方式">
          <el-select
            v-model="newFormInline.caigoufangshi"
            placeholder="请选择采购方式"
            style="width: 240px"
          >
            <el-option
              v-for="item in state.caigoufangshiList"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="采购预算" prop="caigouyusuan">
          <el-input
            v-model="newFormInline.caigouyusuan"
            clearable
            placeholder="请输入采购预算"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="定制规划" prop="dingbiaoguize">
          <el-checkbox-group
            v-model="newFormInline.dingbiaoguize"
            placeholder="请选择状态"
            clearable
            class="w-[100%]!"
          >
            <el-checkbox label="最低价" :value="1" />
            <el-checkbox label="综合评分" :value="0" />
          </el-checkbox-group>
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="报名费" prop="baomingfei">
          <el-input
            v-model="newFormInline.baomingfei"
            clearable
            placeholder="请输入报名费"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="投标保证金" prop="toubiaobaozhengjin">
          <el-input
            v-model="newFormInline.toubiaobaozhengjin"
            clearable
            placeholder="请输入投标保证金"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="联合体投标" prop="lianhetitoubiao">
          <el-radio-group v-model="newFormInline.lianhetitoubiao">
            <el-radio value="支持">支持</el-radio>
            <el-radio value="不支持">不支持</el-radio>
          </el-radio-group>
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="开标方式" prop="kaibiaofangshi">
          <el-radio-group v-model="newFormInline.kaibiaofangshi">
            <el-radio value="纸质标">纸质标</el-radio>
            <el-radio value="电子标">电子标</el-radio>
          </el-radio-group>
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="是否分包" prop="shifoufenbao">
          <el-radio-group v-model="newFormInline.shifoufenbao">
            <el-radio value="true">是</el-radio>
            <el-radio value="false">否</el-radio>
          </el-radio-group>
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="是否推送宣传" prop="shifoutuisongxuanchuan">
          <el-radio-group v-model="newFormInline.shifoutuisongxuanchuan">
            <el-radio value="true">是</el-radio>
            <el-radio value="false">否</el-radio>
          </el-radio-group>
        </el-form-item>
      </re-col>
      <re-col>
        <p class="flex items-center">
          <el-icon color="#409EFF" class="m-2" size="large">
            <Operation />
          </el-icon>
          <el-text class="mx-1" size="large" type="primary" tag="b">
            采购人信息
          </el-text>
        </p>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="采购人名称" prop="caigourenmingcheng">
          <el-input
            v-model="newFormInline.caigourenmingcheng"
            clearable
            placeholder="请输入采购人名称"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="行政区域" prop="xingzhengquyu">
          <el-cascader
            v-model="newFormInline.xingzhengquyu"
            class="w-full"
            :options="state.regionList"
            :props="{
              value: 'id',
              label: 'name',
              emitPath: false,
              children: 'regions'
            }"
            clearable
            filterable
            placeholder="请选择区域"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="机构代码" prop="jigoudaima">
          <el-input
            v-model="newFormInline.jigoudaima"
            clearable
            placeholder="请输入机构代码"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="代码类型" prop="daimaleixing">
          <el-select
            v-model="newFormInline.daimaleixing"
            placeholder="请选择代码类型"
            style="width: 240px"
          >
            <el-option
              v-for="item in state.daimaleixingList"
              :key="item.value"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="联系人" prop="lianxiren">
          <el-input
            v-model="newFormInline.lianxiren"
            clearable
            placeholder="请输入联系人"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="联系人电话" prop="lianxidianhua">
          <el-input
            v-model="newFormInline.lianxidianhua"
            clearable
            placeholder="请输入联系人电话"
          />
        </el-form-item>
      </re-col>
      <re-col :value="12" :xs="24" :sm="24">
        <el-form-item label="通信地址" prop="tongxindizhi">
          <el-input
            v-model="newFormInline.tongxindizhi"
            clearable
            placeholder="请输入通信地址"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="电子邮箱" prop="dianziyoujian">
          <el-input
            v-model="newFormInline.dianziyoujian"
            clearable
            placeholder="请输入电子邮箱"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="项目经办人" prop="xiangmujingbanren">
          <el-input
            v-model="newFormInline.xiangmujingbanren"
            clearable
            placeholder="请输入项目经办人"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="职务" prop="zhiwu">
          <el-input
            v-model="newFormInline.zhiwu"
            clearable
            placeholder="请输入职务"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="经办人电话" prop="jingbanrendianhua">
          <el-input
            v-model="newFormInline.jingbanrendianhua"
            clearable
            placeholder="请输入经办人电话"
          />
        </el-form-item>
      </re-col>
      <re-col>
        <p class="flex items-center">
          <el-icon color="#409EFF" class="m-2" size="large">
            <Operation />
          </el-icon>
          <el-text class="mx-1" size="large" type="primary" tag="b">
            代理机构信息
          </el-text>
        </p>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="代理机构名称" prop="dailijigoumingcheng">
          <el-input
            v-model="newFormInline.dailijigoumingcheng"
            clearable
            placeholder="请输入代理机构名称"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="联系人人" prop="dailiLianxiren">
          <el-input
            v-model="newFormInline.dailiLianxiren"
            clearable
            placeholder="请输入联系人"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="联系电话" prop="dailiLianxidianhua">
          <el-input
            v-model="newFormInline.dailiLianxidianhua"
            clearable
            placeholder="请输入联系电话"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="电子邮箱" prop="dailiDianziyoujian">
          <el-input
            v-model="newFormInline.dailiDianziyoujian"
            clearable
            placeholder="请输入电子邮箱"
          />
        </el-form-item>
      </re-col>
      <re-col :value="12" :xs="24" :sm="24">
        <el-form-item label="通信地址" prop="dailiTongxindizhi">
          <el-input
            v-model="newFormInline.dailiTongxindizhi"
            clearable
            placeholder="请输入通信地址"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="项目经理" prop="dailiXiangmujingli">
          <el-input
            v-model="newFormInline.dailiXiangmujingli"
            clearable
            placeholder="请输入项目经理"
          />
        </el-form-item>
      </re-col>
      <re-col :value="6" :xs="24" :sm="24">
        <el-form-item label="联系电话" prop="dailijingliLianxidianhua">
          <el-input
            v-model="newFormInline.dailijingliLianxidianhua"
            clearable
            placeholder="请输入联系电话"
          />
        </el-form-item>
      </re-col>
    </el-row>
  </el-form>
</template>
src/views/system/dept/index.vue
New file
@@ -0,0 +1,296 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { useDept } from "./utils/hook";
import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import Delete from "~icons/ep/delete";
import EditPen from "~icons/ep/edit-pen";
import Refresh from "~icons/ep/refresh";
import AddFill from "~icons/ri/add-circle-line";
defineOptions({
  name: "SystemDept"
});
const formRef = ref();
const tableRef = ref();
const {
  form,
  state,
  loading,
  columns,
  dataList,
  onSearch,
  resetForm,
  openDialog,
  handleDelete,
  handleSelectionChange
} = useDept();
function onFullscreen() {
  // 重置表格高度
  tableRef.value.setAdaptive();
}
onMounted(() => {});
</script>
<template>
  <div class="main">
    <!-- class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto" -->
    <el-card shadow="hover" :body-style="{ paddingBottom: '0' }">
      <el-form ref="formRef" :model="form" labelWidth="100">
        <el-row>
          <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
            <el-form-item label="时间:">
              <el-date-picker
                v-model="form.createDateRange"
                type="daterange"
                start-placeholder="开始日期"
                end-placeholder="结束日期"
                value-format="YYYY-MM-DD HH:mm:ss"
                :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
                class="w-[100%]!"
              />
              <!-- start-placeholder="开始日期"
              end-placeholder="结束日期" -->
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="12" :lg="6" :xl="6">
            <el-form-item label="区域:">
              <el-cascader
                v-model="form.xingzhengquyu"
                class="w-full"
                :options="state.regionList"
                :props="{
                  value: 'id',
                  label: 'name',
                  emitPath: false,
                  children: 'regions'
                }"
                clearable
                filterable
                placeholder="请选择区域"
              />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
            <el-form-item label="行业品目:">
              <el-select
                v-model="form.hangyepinmu"
                placeholder="请选择行业品目"
                clearable
                class="w-[100%]!"
              >
                <el-option
                  v-for="item in state.hangyepingmuList"
                  :key="item.id"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
            <el-form-item label="项目进度:">
              <el-select
                v-model="form.orderStatus"
                placeholder="请选择项目进度"
                clearable
                class="w-[100%]!"
              >
                <el-option
                  v-for="item in state.orderStatusList"
                  :key="item.id"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item> </el-col
          ><el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
            <el-form-item label="质疑:">
              <el-checkbox-group
                v-model="form.zhiyi"
                clearable
                class="w-[100%]!"
              >
                <el-checkbox label="有" :value="1" />
                <el-checkbox label="无" :value="0" />
              </el-checkbox-group>
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
            <el-form-item label="投诉:">
              <el-checkbox-group
                v-model="form.tousu"
                clearable
                class="w-[100%]!"
              >
                <el-checkbox label="有" :value="1" />
                <el-checkbox label="无" :value="0" />
              </el-checkbox-group>
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
            <el-form-item label="项目名称:">
              <el-input
                v-model="form.projectName"
                placeholder="请输入项目名称"
                clearable
                class="w-[100%]!"
              />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
            <el-form-item label="代理机构:">
              <el-input
                v-model="form.dailijigoumingcheng"
                placeholder="请输入代理机构"
                clearable
                class="w-[100%]!"
              />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
            <el-form-item label="中标供应商:">
              <el-input
                v-model="form.zhongbiaoName"
                placeholder="请输入中标供应商"
                clearable
                class="w-[100%]!"
              />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
            <el-form-item label="评审专家:">
              <el-input
                v-model="form.zhuanjiaName"
                placeholder="请输入评审专家"
                clearable
                class="w-[100%]!"
              />
            </el-form-item>
          </el-col>
          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4">
            <el-form-item label-width="40">
              <el-button
                type="primary"
                :icon="useRenderIcon('ri/search-line')"
                :loading="loading"
                @click="onSearch"
              >
                搜索
              </el-button>
              <!-- <el-button
                :icon="useRenderIcon(Refresh)"
                @click="resetForm(formRef)"
              >
                重置
              </el-button> -->
              <el-button
                type="primary"
                :icon="useRenderIcon(AddFill)"
                @click="openDialog()"
              >
                新增
              </el-button>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </el-card>
    <PureTableBar
      title=""
      :columns="columns"
      :tableRef="tableRef?.getTableRef()"
      @refresh="onSearch"
      @fullscreen="onFullscreen"
    >
      <!-- <template #buttons>
        <el-button
          type="primary"
          :icon="useRenderIcon(AddFill)"
          @click="openDialog()"
        >
          新增项目
        </el-button>
      </template> -->
      <template v-slot="{ size, dynamicColumns }">
        <pure-table
          ref="tableRef"
          adaptive
          :adaptiveConfig="{ offsetBottom: 45 }"
          align-whole="center"
          row-key="id"
          showOverflowTooltip
          table-layout="auto"
          default-expand-all
          :loading="loading"
          :data="dataList"
          :columns="dynamicColumns"
          :header-cell-style="{
            background: 'var(--el-fill-color-light)',
            color: 'var(--el-text-color-primary)'
          }"
          @selection-change="handleSelectionChange"
        >
          <template #operation="{ row }">
            <el-button
              class="reset-margin"
              link
              type="primary"
              :size="size"
              :icon="useRenderIcon(EditPen)"
              @click="openDialog('修改', row)"
            >
              修改
            </el-button>
            <!-- <el-button
              class="reset-margin"
              link
              type="primary"
              :size="size"
              :icon="useRenderIcon(AddFill)"
              @click="openDialog('新增', { parentId: row.id } as any)"
            >
              新增
            </el-button> -->
            <el-popconfirm
              :title="`是否确认删除部门名称为${row.projectName}的这条数据`"
              @confirm="handleDelete(row)"
            >
              <template #reference>
                <el-button
                  class="reset-margin"
                  link
                  type="primary"
                  :size="size"
                  :icon="useRenderIcon(Delete)"
                >
                  删除
                </el-button>
              </template>
            </el-popconfirm>
          </template>
        </pure-table>
      </template>
    </PureTableBar>
  </div>
</template>
<style lang="scss" scoped>
:deep(.el-table__inner-wrapper::before) {
  height: 0;
}
.main-content {
  margin: 24px 24px 0 !important;
}
.search-form {
  :deep(.el-form-item) {
    margin-bottom: 12px;
  }
}
</style>
src/views/system/dept/utils/hook.tsx
New file
@@ -0,0 +1,311 @@
import dayjs from "dayjs";
import editForm from "../form.vue";
import { handleTree } from "@/utils/tree";
import { message } from "@/utils/message";
import { zhaobiaoPageOrder } from "@/api/item/index";
import {
  getCaigoufangshiList,
  getHangyepingmuList,
  getOrderStatusList,
  getRegionList,
  getDaimaleixingList,
  caigourenAdd,
  caigourenDelete
} from "@/api/item/index";
import { usePublicHooks } from "../../hooks";
import { addDialog } from "@/components/ReDialog";
import { reactive, ref, onMounted, h } from "vue";
import type { FormItemProps } from "../utils/types";
import { cloneDeep, isAllEmpty, deviceDetection } from "@pureadmin/utils";
export function useDept() {
  const form = reactive({
    // 新增日期范围,可为 null,类型为数组
    createDateRange: null,
    // 行政区域,可为 null,类型为字符串
    xingzhengquyu: "",
    // 行业品目,可为 null,类型为字符串
    hangyepinmu: "",
    // 订单状态,可为 null,类型为 32 位整数
    orderStatus: "",
    // 质疑有无,可为 null,类型为布尔值
    zhiyi: null,
    // 投诉有无,可为 null,类型为布尔值
    tousu: null,
    // 项目名称,可为 null,类型为字符串
    projectName: null,
    // 代理机构名称,可为 null,类型为字符串
    dailijigoumingcheng: null,
    // 中标供应商姓名,可为 null,类型为字符串
    zhongbiaoName: null,
    // 专家姓名,可为 null,类型为字符串
    zhuanjiaName: null
  });
  const state = reactive({
    caigoufangshiList: [],
    hangyepingmuList: [],
    orderStatusList: [],
    regionList: [],
    daimaleixingList: []
  });
  //获取采购方式
  const getCaigoufangshiListFun = async () => {
    const res = await getCaigoufangshiList();
    state.caigoufangshiList = res.result;
  };
  //获取行业品目
  const getHangyepingmuListFun = async () => {
    const res = await getHangyepingmuList();
    state.hangyepingmuList = res.result;
  };
  //获取项目进度
  const getOrderStatusListFun = async () => {
    const res = await getOrderStatusList();
    state.orderStatusList = res.result;
  };
  // 获取区域
  const getRegionListFun = async () => {
    const res = await getRegionList();
    state.regionList = res.result;
  };
  // 获取代码类型
  const getDaimaleixingListFun = async () => {
    const res = await getDaimaleixingList();
    state.daimaleixingList = res.result;
  };
  const formRef = ref();
  const dataList = ref([]);
  const loading = ref(true);
  const { tagStyle } = usePublicHooks();
  const getOrderStatus = row => {
    const res = state.orderStatusList.find(item => {
      return row.orderStatus == item.status;
    });
    return res.label;
  };
  const columns: TableColumnList = [
    {
      label: "项目名称",
      prop: "projectName",
      width: 180,
      align: "left"
    },
    {
      label: "代理机构",
      prop: "dailijigoumingcheng",
      minWidth: 70
    },
    {
      label: "项目进度",
      prop: "orderStatus",
      minWidth: 70,
      cellRenderer: ({ row, props }) => getOrderStatus(row)
    },
    {
      label: "报名费",
      prop: "baomingfei",
      minWidth: 70
    },
    {
      label: "投标保证金",
      prop: "toubiaobaozhengjin",
      minWidth: 70
    },
    {
      label: "中标供应商",
      prop: "zhongbiaoName",
      minWidth: 70
    },
    {
      label: "评审专家",
      prop: "zhuanjiaName",
      minWidth: 70
    },
    {
      label: "质疑",
      prop: "zhiyi",
      minWidth: 100,
      cellRenderer: ({ row, props }) => (
        <span>{row.status === 1 ? "有" : "无"}</span>
      )
    },
    {
      label: "投诉",
      prop: "tousu",
      minWidth: 100,
      cellRenderer: ({ row, props }) => (
        // <el-tag size={props.size} style={tagStyle.value(row.status)}>
        <span>{row.status === 1 ? "有" : "无"}</span>
        // </el-tag>
      )
    },
    {
      label: "操作",
      fixed: "right",
      width: 210,
      slot: "operation"
    }
  ];
  function handleSelectionChange(val) {
    console.log("handleSelectionChange", val);
  }
  function resetForm(formEl) {
    if (!formEl) return;
    formEl.resetFields();
    onSearch();
  }
  async function onSearch() {
    loading.value = true;
    const { result } = await zhaobiaoPageOrder(form); // 这里是返回一维数组结构,前端自行处理成树结构,返回格式要求:唯一id加父节点parentId,parentId取父节点id
    const newData = result.items;
    loading.value = false;
    // if (!isAllEmpty(form.name)) {
    //   // 前端搜索部门名称
    //   newData = newData.filter(item => item.name.includes(form.name));
    // }
    // if (!isAllEmpty(form.status)) {
    //   // 前端搜索状态
    //   newData = newData.filter(item => item.status === form.status);
    // }
    dataList.value = handleTree(newData); // 处理成树结构
  }
  function formatHigherDeptOptions(treeList) {
    // 根据返回数据的status字段值判断追加是否禁用disabled字段,返回处理后的树结构,用于上级部门级联选择器的展示(实际开发中也是如此,不可能前端需要的每个字段后端都会返回,这时需要前端自行根据后端返回的某些字段做逻辑处理)
    if (!treeList || !treeList.length) return;
    const newTreeList = [];
    for (let i = 0; i < treeList.length; i++) {
      treeList[i].disabled = treeList[i].status === 0 ? true : false;
      formatHigherDeptOptions(treeList[i].children);
      newTreeList.push(treeList[i]);
    }
    return newTreeList;
  }
  function openDialog(title = "新增", row?: FormItemProps) {
    addDialog({
      title: `${title}项目`,
      props: {
        formInline: {
          higherDeptOptions: formatHigherDeptOptions(cloneDeep(dataList.value)),
          projectCode: row?.projectCode ?? "", // 项目编号(必填)
          projectName: row?.projectName ?? "", // 项目名称(必填)
          hangyepinmu: row?.hangyepinmu ?? null, // 行业品目(可选)
          caigoufangshi: row?.caigoufangshi ?? null, // 采购方式(可选)
          caigouyusuan: row?.caigouyusuan ?? null, // 采购预算(可选)
          dingbiaoguize: row?.dingbiaoguize ?? null, // 定标规则(可选)
          baomingfei: row?.baomingfei ?? null, // 报名费(可选)
          toubiaobaozhengjin: row?.toubiaobaozhengjin ?? null, // 投标保证金(可选)
          lianhetitoubiao: row?.lianhetitoubiao ?? null, // 联合体投标(可选)
          kaibiaofangshi: row?.kaibiaofangshi ?? null, // 开标方式(可选)
          shifoufenbao: row?.shifoufenbao ?? false, // 是否分包(可选)
          shifoutuisongxuanchuan: row?.shifoutuisongxuanchuan ?? true, // 是否推送宣传(可选)
          caigourenmingcheng: row?.caigourenmingcheng ?? null, // 采购人名称(可选)
          xingzhengquyu: row?.xingzhengquyu ?? null, // 行政区域(可选)
          xingzhengquyuName: row?.xingzhengquyuName ?? null, // 行政区域名称(可选)
          jigoudaima: row?.jigoudaima ?? null, // 机构代码(可选)
          daimaleixing: row?.daimaleixing ?? null, // 代码类型(可选)
          lianxiren: row?.lianxiren ?? null, // 联系人(可选)
          lianxidianhua: row?.lianxidianhua ?? null, // 联系电话(可选)
          tongxindizhi: row?.tongxindizhi ?? null, // 通信地址(可选)
          dianziyoujian: row?.dianziyoujian ?? null, // 电子邮件(可选)
          xiangmujingbanren: row?.xiangmujingbanren ?? null, // 项目经办人(可选)
          zhiwu: row?.zhiwu ?? null, // 职务(可选)
          jingbanrendianhua: row?.jingbanrendianhua ?? null, // 经办人电话(可选)
          dailijigoumingcheng: row?.dailijigoumingcheng ?? null, // 代理机构名称(可选)
          dailiLianxiren: row?.dailiLianxiren ?? null, // 代理机构联系人(可选)
          dailiLianxidianhua: row?.dailiLianxidianhua ?? null, // 代理机构联系电话(可选)
          dailiDianziyoujian: row?.dailiDianziyoujian ?? null, // 代理机构电子邮件(可选)
          dailiTongxindizhi: row?.dailiTongxindizhi ?? null, // 代理机构通信地址(可选)
          dailiXiangmujingli: row?.dailiXiangmujingli ?? null, // 代理机构项目经理(可选
          dailijingliLianxidianhua: row?.dailijingliLianxidianhua ?? null // 代理机构项目经理联系电话(可选)
        }
      },
      width: "80%",
      draggable: true,
      fullscreen: deviceDetection(),
      fullscreenIcon: true,
      closeOnClickModal: false,
      contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
      beforeSure: (done, { options }) => {
        const FormRef = formRef.value.getRef();
        const curData = options.props.formInline as FormItemProps;
        async function chores() {
          message(`您${title}了项目名称为${curData.projectName}的这条数据`, {
            type: "success"
          });
          const res = await caigourenAdd(curData);
          if (res.code == "200") {
            done(); // 关闭弹框
            onSearch(); // 刷新表格数据
          } else {
            message(res.message, {
              type: "error"
            });
          }
        }
        FormRef.validate(valid => {
          if (valid) {
            console.log("curData", curData);
            // 表单规则校验通过
            if (title === "新增") {
              // 实际开发先调用新增接口,再进行下面操作
              chores();
            } else {
              // 实际开发先调用修改接口,再进行下面操作
              chores();
            }
          }
        });
      }
    });
  }
  async function handleDelete(row) {
    const res = await caigourenDelete({ id: row.id });
    if (res.code == "200") {
      message(`您删除了项目名称为${row.projectName}的这条数据`, {
        type: "success"
      });
      onSearch();
    } else {
      message(res.message, {
        type: "error"
      });
    }
  }
  onMounted(() => {
    onSearch();
    getCaigoufangshiListFun();
    getHangyepingmuListFun();
    getOrderStatusListFun();
    getRegionListFun();
    getDaimaleixingListFun();
  });
  return {
    form,
    state,
    loading,
    columns,
    dataList,
    /** 搜索 */
    onSearch,
    /** 重置 */
    resetForm,
    /** 新增、修改部门 */
    openDialog,
    /** 删除部门 */
    handleDelete,
    handleSelectionChange
  };
}
src/views/system/dept/utils/rule.ts
New file
@@ -0,0 +1,37 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
import { isPhone, isEmail } from "@pureadmin/utils";
/** 自定义表单规则校验 */
export const formRules = reactive(<FormRules>{
  name: [{ required: true, message: "部门名称为必填项", trigger: "blur" }],
  phone: [
    {
      validator: (rule, value, callback) => {
        if (value === "") {
          callback();
        } else if (!isPhone(value)) {
          callback(new Error("请输入正确的手机号码格式"));
        } else {
          callback();
        }
      },
      trigger: "blur"
      // trigger: "click" // 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
    }
  ],
  email: [
    {
      validator: (rule, value, callback) => {
        if (value === "") {
          callback();
        } else if (!isEmail(value)) {
          callback(new Error("请输入正确的邮箱格式"));
        } else {
          callback();
        }
      },
      trigger: "blur"
    }
  ]
});
src/views/system/dept/utils/types.ts
New file
@@ -0,0 +1,38 @@
interface FormItemProps {
  projectCode: string; // 项目编号(必填)
  projectName: string; // 项目名称(必填)
  hangyepinmu: any | null; // 行业品目(可选)
  caigoufangshi: any | null; // 采购方式(可选)
  caigouyusuan: any | null; // 采购预算(可选)
  dingbiaoguize: any | null; // 定标规则(可选)
  baomingfei: any | null; // 报名费(可选)
  toubiaobaozhengjin: any | null; // 投标保证金(可选)
  lianhetitoubiao: any | null; // 联合体投标(可选)
  kaibiaofangshi: any | null; // 开标方式(可选)
  shifoufenbao: any | null; // 是否分包(可选)
  shifoutuisongxuanchuan: any | null; // 是否推送宣传(可选)
  caigourenmingcheng: string | null; // 采购人名称(可选)
  xingzhengquyu: any | null; // 行政区域(可选)
  xingzhengquyuName: string | null; // 行政区域名称(可选)
  jigoudaima: string | null; // 机构代码(可选)
  daimaleixing: any | null; // 代码类型(可选)
  lianxiren: string | null; // 联系人(可选)
  lianxidianhua: string | null; // 联系电话(可选)
  tongxindizhi: string | null; // 通信地址(可选)
  dianziyoujian: string | null; // 电子邮件(可选)
  xiangmujingbanren: string | null; // 项目经办人(可选)
  zhiwu: string | null; // 职务(可选)
  jingbanrendianhua: string | null; // 经办人电话(可选)
  dailijigoumingcheng: string | null; // 代理机构名称(可选)
  dailiLianxiren: string | null; // 代理机构联系人(可选)
  dailiLianxidianhua: string | null; // 代理机构联系电话(可选)
  dailiDianziyoujian: string | null; // 代理机构电子邮件(可选)
  dailiTongxindizhi: string | null; // 代理机构通信地址(可选)
  dailiXiangmujingli: string | null; // 代理机构项目经理(可选)
  dailijingliLianxidianhua: string | null; // 代理机构项目经理联系电话(可选)
}
interface FormProps {
  formInline: FormItemProps;
}
export type { FormItemProps, FormProps };
src/views/system/hooks.ts
New file
@@ -0,0 +1,39 @@
// 抽离可公用的工具函数等用于系统管理页面逻辑
import { computed } from "vue";
import { useDark } from "@pureadmin/utils";
export function usePublicHooks() {
  const { isDark } = useDark();
  const switchStyle = computed(() => {
    return {
      "--el-switch-on-color": "#6abe39",
      "--el-switch-off-color": "#e84749"
    };
  });
  const tagStyle = computed(() => {
    return (status: number) => {
      return status === 1
        ? {
            "--el-tag-text-color": isDark.value ? "#6abe39" : "#389e0d",
            "--el-tag-bg-color": isDark.value ? "#172412" : "#f6ffed",
            "--el-tag-border-color": isDark.value ? "#274a17" : "#b7eb8f"
          }
        : {
            "--el-tag-text-color": isDark.value ? "#e84749" : "#cf1322",
            "--el-tag-bg-color": isDark.value ? "#2b1316" : "#fff1f0",
            "--el-tag-border-color": isDark.value ? "#58191c" : "#ffa39e"
          };
    };
  });
  return {
    /** 当前网页是否为`dark`模式 */
    isDark,
    /** 表现更鲜明的`el-switch`组件  */
    switchStyle,
    /** 表现更鲜明的`el-tag`组件  */
    tagStyle
  };
}
src/views/system/menu/README.md
New file
@@ -0,0 +1,26 @@
## 字段含义
| 字段              | 说明                                                         |
| :---------------- | :----------------------------------------------------------- |
| `menuType`        | 菜单类型(`0`代表菜单、`1`代表`iframe`、`2`代表外链、`3`代表按钮) |
| `parentId`        |                                                              |
| `title`           | 菜单名称(兼容国际化、非国际化,如果用国际化的写法就必须在根目录的`locales`文件夹下对应添加) |
| `name`            | 路由名称(必须唯一并且和当前路由`component`字段对应的页面里用`defineOptions`包起来的`name`保持一致) |
| `path`            | 路由路径                                                     |
| `component`       | 组件路径(传`component`组件路径,那么`path`可以随便写,如果不传,`component`组件路径会跟`path`保持一致) |
| `rank`            | 菜单排序(平台规定只有`home`路由的`rank`才能为`0`,所以后端在返回`rank`的时候需要从非`0`开始 [点击查看更多](https://pure-admin.cn/pages/routerMenu/#%E8%8F%9C%E5%8D%95%E6%8E%92%E5%BA%8F-rank)) |
| `redirect`        | 路由重定向                                                   |
| `icon`            | 菜单图标                                                     |
| `extraIcon`       | 右侧图标                                                     |
| `enterTransition` | 进场动画(页面加载动画)                                     |
| `leaveTransition` | 离场动画(页面加载动画)                                     |
| `activePath`      | 菜单激活(将某个菜单激活,主要用于通过`query`或`params`传参的路由,当它们通过配置`showLink: false`后不在菜单中显示,就不会有任何菜单高亮,而通过设置`activePath`指定激活菜单即可获得高亮,`activePath`为指定激活菜单的`path`) |
| `auths`           | 权限标识(按钮级别权限设置)                                 |
| `frameSrc`        | 链接地址(需要内嵌的`iframe`链接地址)                       |
| `frameLoading`    | 加载动画(内嵌的`iframe`页面是否开启首次加载动画)           |
| `keepAlive`       | 缓存页面(是否缓存该路由页面,开启后会保存该页面的整体状态,刷新后会清空状态) |
| `hiddenTag`       | 标签页(当前菜单名称或自定义信息禁止添加到标签页)           |
| `fixedTag`        | 固定标签页(当前菜单名称是否固定显示在标签页且不可关闭)           |
| `showLink`        | 菜单(是否显示该菜单)                                       |
| `showParent`      | 父级菜单(是否显示父级菜单 [点击查看更多](https://pure-admin.cn/pages/routerMenu/#%E7%AC%AC%E4%B8%80%E7%A7%8D-%E8%AF%A5%E6%A8%A1%E5%BC%8F%E9%92%88%E5%AF%B9%E7%88%B6%E7%BA%A7%E8%8F%9C%E5%8D%95%E4%B8%8B%E5%8F%AA%E6%9C%89%E4%B8%80%E4%B8%AA%E5%AD%90%E8%8F%9C%E5%8D%95%E7%9A%84%E6%83%85%E5%86%B5-%E5%9C%A8%E5%AD%90%E8%8F%9C%E5%8D%95%E7%9A%84-meta-%E5%B1%9E%E6%80%A7%E4%B8%AD%E5%8A%A0%E4%B8%8A-showparent-true-%E5%8D%B3%E5%8F%AF)) |
src/views/system/menu/form.vue
New file
@@ -0,0 +1,342 @@
<script setup lang="ts">
import { ref } from "vue";
import ReCol from "@/components/ReCol";
import { formRules } from "./utils/rule";
import { FormProps } from "./utils/types";
import { transformI18n } from "@/plugins/i18n";
import { IconSelect } from "@/components/ReIcon";
import Segmented from "@/components/ReSegmented";
import ReAnimateSelector from "@/components/ReAnimateSelector";
import {
  menuTypeOptions,
  showLinkOptions,
  fixedTagOptions,
  keepAliveOptions,
  hiddenTagOptions,
  showParentOptions,
  frameLoadingOptions
} from "./utils/enums";
const props = withDefaults(defineProps<FormProps>(), {
  formInline: () => ({
    menuType: 0,
    higherMenuOptions: [],
    parentId: 0,
    title: "",
    name: "",
    path: "",
    component: "",
    rank: 99,
    redirect: "",
    icon: "",
    extraIcon: "",
    enterTransition: "",
    leaveTransition: "",
    activePath: "",
    auths: "",
    frameSrc: "",
    frameLoading: true,
    keepAlive: false,
    hiddenTag: false,
    fixedTag: false,
    showLink: true,
    showParent: false
  })
});
const ruleFormRef = ref();
const newFormInline = ref(props.formInline);
function getRef() {
  return ruleFormRef.value;
}
defineExpose({ getRef });
</script>
<template>
  <el-form
    ref="ruleFormRef"
    :model="newFormInline"
    :rules="formRules"
    label-width="82px"
  >
    <el-row :gutter="30">
      <re-col>
        <el-form-item label="菜单类型">
          <Segmented
            v-model="newFormInline.menuType"
            :options="menuTypeOptions"
          />
        </el-form-item>
      </re-col>
      <re-col>
        <el-form-item label="上级菜单">
          <el-cascader
            v-model="newFormInline.parentId"
            class="w-full"
            :options="newFormInline.higherMenuOptions"
            :props="{
              value: 'id',
              label: 'title',
              emitPath: false,
              checkStrictly: true
            }"
            clearable
            filterable
            placeholder="请选择上级菜单"
          >
            <template #default="{ node, data }">
              <span>{{ transformI18n(data.title) }}</span>
              <span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
            </template>
          </el-cascader>
        </el-form-item>
      </re-col>
      <re-col :value="12" :xs="24" :sm="24">
        <el-form-item label="菜单名称" prop="title">
          <el-input
            v-model="newFormInline.title"
            clearable
            placeholder="请输入菜单名称"
          />
        </el-form-item>
      </re-col>
      <re-col v-if="newFormInline.menuType !== 3" :value="12" :xs="24" :sm="24">
        <el-form-item label="路由名称" prop="name">
          <el-input
            v-model="newFormInline.name"
            clearable
            placeholder="请输入路由名称"
          />
        </el-form-item>
      </re-col>
      <re-col v-if="newFormInline.menuType !== 3" :value="12" :xs="24" :sm="24">
        <el-form-item label="路由路径" prop="path">
          <el-input
            v-model="newFormInline.path"
            clearable
            placeholder="请输入路由路径"
          />
        </el-form-item>
      </re-col>
      <re-col
        v-show="newFormInline.menuType === 0"
        :value="12"
        :xs="24"
        :sm="24"
      >
        <el-form-item label="组件路径">
          <el-input
            v-model="newFormInline.component"
            clearable
            placeholder="请输入组件路径"
          />
        </el-form-item>
      </re-col>
      <re-col :value="12" :xs="24" :sm="24">
        <el-form-item label="菜单排序">
          <el-input-number
            v-model="newFormInline.rank"
            class="w-full!"
            :min="1"
            :max="9999"
            controls-position="right"
          />
        </el-form-item>
      </re-col>
      <re-col
        v-show="newFormInline.menuType === 0"
        :value="12"
        :xs="24"
        :sm="24"
      >
        <el-form-item label="路由重定向">
          <el-input
            v-model="newFormInline.redirect"
            clearable
            placeholder="请输入默认跳转地址"
          />
        </el-form-item>
      </re-col>
      <re-col
        v-show="newFormInline.menuType !== 3"
        :value="12"
        :xs="24"
        :sm="24"
      >
        <el-form-item label="菜单图标">
          <IconSelect v-model="newFormInline.icon" class="w-full" />
        </el-form-item>
      </re-col>
      <re-col
        v-show="newFormInline.menuType !== 3"
        :value="12"
        :xs="24"
        :sm="24"
      >
        <el-form-item label="右侧图标">
          <el-input
            v-model="newFormInline.extraIcon"
            clearable
            placeholder="菜单名称右侧的额外图标"
          />
        </el-form-item>
      </re-col>
      <re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
        <el-form-item label="进场动画">
          <ReAnimateSelector
            v-model="newFormInline.enterTransition"
            placeholder="请选择页面进场加载动画"
          />
        </el-form-item>
      </re-col>
      <re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
        <el-form-item label="离场动画">
          <ReAnimateSelector
            v-model="newFormInline.leaveTransition"
            placeholder="请选择页面离场加载动画"
          />
        </el-form-item>
      </re-col>
      <re-col
        v-show="newFormInline.menuType === 0"
        :value="12"
        :xs="24"
        :sm="24"
      >
        <el-form-item label="菜单激活">
          <el-input
            v-model="newFormInline.activePath"
            clearable
            placeholder="请输入需要激活的菜单"
          />
        </el-form-item>
      </re-col>
      <re-col v-if="newFormInline.menuType === 3" :value="12" :xs="24" :sm="24">
        <!-- 按钮级别权限设置 -->
        <el-form-item label="权限标识" prop="auths">
          <el-input
            v-model="newFormInline.auths"
            clearable
            placeholder="请输入权限标识"
          />
        </el-form-item>
      </re-col>
      <re-col
        v-show="newFormInline.menuType === 1"
        :value="12"
        :xs="24"
        :sm="24"
      >
        <!-- iframe -->
        <el-form-item label="链接地址">
          <el-input
            v-model="newFormInline.frameSrc"
            clearable
            placeholder="请输入 iframe 链接地址"
          />
        </el-form-item>
      </re-col>
      <re-col v-if="newFormInline.menuType === 1" :value="12" :xs="24" :sm="24">
        <el-form-item label="加载动画">
          <Segmented
            :modelValue="newFormInline.frameLoading ? 0 : 1"
            :options="frameLoadingOptions"
            @change="
              ({ option: { value } }) => {
                newFormInline.frameLoading = value;
              }
            "
          />
        </el-form-item>
      </re-col>
      <re-col
        v-show="newFormInline.menuType !== 3"
        :value="12"
        :xs="24"
        :sm="24"
      >
        <el-form-item label="菜单">
          <Segmented
            :modelValue="newFormInline.showLink ? 0 : 1"
            :options="showLinkOptions"
            @change="
              ({ option: { value } }) => {
                newFormInline.showLink = value;
              }
            "
          />
        </el-form-item>
      </re-col>
      <re-col
        v-show="newFormInline.menuType !== 3"
        :value="12"
        :xs="24"
        :sm="24"
      >
        <el-form-item label="父级菜单">
          <Segmented
            :modelValue="newFormInline.showParent ? 0 : 1"
            :options="showParentOptions"
            @change="
              ({ option: { value } }) => {
                newFormInline.showParent = value;
              }
            "
          />
        </el-form-item>
      </re-col>
      <re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
        <el-form-item label="缓存页面">
          <Segmented
            :modelValue="newFormInline.keepAlive ? 0 : 1"
            :options="keepAliveOptions"
            @change="
              ({ option: { value } }) => {
                newFormInline.keepAlive = value;
              }
            "
          />
        </el-form-item>
      </re-col>
      <re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
        <el-form-item label="标签页">
          <Segmented
            :modelValue="newFormInline.hiddenTag ? 1 : 0"
            :options="hiddenTagOptions"
            @change="
              ({ option: { value } }) => {
                newFormInline.hiddenTag = value;
              }
            "
          />
        </el-form-item>
      </re-col>
      <re-col v-show="newFormInline.menuType < 2" :value="12" :xs="24" :sm="24">
        <el-form-item label="固定标签页">
          <Segmented
            :modelValue="newFormInline.fixedTag ? 0 : 1"
            :options="fixedTagOptions"
            @change="
              ({ option: { value } }) => {
                newFormInline.fixedTag = value;
              }
            "
          />
        </el-form-item>
      </re-col>
    </el-row>
  </el-form>
</template>
src/views/system/menu/index.vue
New file
@@ -0,0 +1,163 @@
<script setup lang="ts">
import { ref } from "vue";
import { useMenu } from "./utils/hook";
import { transformI18n } from "@/plugins/i18n";
import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import Delete from "~icons/ep/delete";
import EditPen from "~icons/ep/edit-pen";
import Refresh from "~icons/ep/refresh";
import AddFill from "~icons/ri/add-circle-line";
defineOptions({
  name: "SystemMenu"
});
const formRef = ref();
const tableRef = ref();
const {
  form,
  loading,
  columns,
  dataList,
  onSearch,
  resetForm,
  openDialog,
  handleDelete,
  handleSelectionChange
} = useMenu();
function onFullscreen() {
  // 重置表格高度
  tableRef.value.setAdaptive();
}
</script>
<template>
  <div class="main">
    <el-form
      ref="formRef"
      :inline="true"
      :model="form"
      class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
    >
      <el-form-item label="菜单名称:" prop="title">
        <el-input
          v-model="form.title"
          placeholder="请输入菜单名称"
          clearable
          class="w-[180px]!"
        />
      </el-form-item>
      <el-form-item>
        <el-button
          type="primary"
          :icon="useRenderIcon('ri/search-line')"
          :loading="loading"
          @click="onSearch"
        >
          搜索
        </el-button>
        <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
          重置
        </el-button>
      </el-form-item>
    </el-form>
    <PureTableBar
      title="菜单管理(仅演示,操作后不生效)"
      :columns="columns"
      :isExpandAll="false"
      :tableRef="tableRef?.getTableRef()"
      @refresh="onSearch"
      @fullscreen="onFullscreen"
    >
      <template #buttons>
        <el-button
          type="primary"
          :icon="useRenderIcon(AddFill)"
          @click="openDialog()"
        >
          新增菜单
        </el-button>
      </template>
      <template v-slot="{ size, dynamicColumns }">
        <pure-table
          ref="tableRef"
          adaptive
          :adaptiveConfig="{ offsetBottom: 45 }"
          align-whole="center"
          row-key="id"
          showOverflowTooltip
          table-layout="auto"
          :loading="loading"
          :size="size"
          :data="dataList"
          :columns="dynamicColumns"
          :header-cell-style="{
            background: 'var(--el-fill-color-light)',
            color: 'var(--el-text-color-primary)'
          }"
          @selection-change="handleSelectionChange"
        >
          <template #operation="{ row }">
            <el-button
              class="reset-margin"
              link
              type="primary"
              :size="size"
              :icon="useRenderIcon(EditPen)"
              @click="openDialog('修改', row)"
            >
              修改
            </el-button>
            <el-button
              v-show="row.menuType !== 3"
              class="reset-margin"
              link
              type="primary"
              :size="size"
              :icon="useRenderIcon(AddFill)"
              @click="openDialog('新增', { parentId: row.id } as any)"
            >
              新增
            </el-button>
            <el-popconfirm
              :title="`是否确认删除菜单名称为${transformI18n(row.title)}的这条数据${row?.children?.length > 0 ? '。注意下级菜单也会一并删除,请谨慎操作' : ''}`"
              @confirm="handleDelete(row)"
            >
              <template #reference>
                <el-button
                  class="reset-margin"
                  link
                  type="primary"
                  :size="size"
                  :icon="useRenderIcon(Delete)"
                >
                  删除
                </el-button>
              </template>
            </el-popconfirm>
          </template>
        </pure-table>
      </template>
    </PureTableBar>
  </div>
</template>
<style lang="scss" scoped>
:deep(.el-table__inner-wrapper::before) {
  height: 0;
}
.main-content {
  margin: 24px 24px 0 !important;
}
.search-form {
  :deep(.el-form-item) {
    margin-bottom: 12px;
  }
}
</style>
src/views/system/menu/utils/enums.ts
New file
@@ -0,0 +1,108 @@
import type { OptionsType } from "@/components/ReSegmented";
const menuTypeOptions: Array<OptionsType> = [
  {
    label: "菜单",
    value: 0
  },
  {
    label: "iframe",
    value: 1
  },
  {
    label: "外链",
    value: 2
  },
  {
    label: "按钮",
    value: 3
  }
];
const showLinkOptions: Array<OptionsType> = [
  {
    label: "显示",
    tip: "会在菜单中显示",
    value: true
  },
  {
    label: "隐藏",
    tip: "不会在菜单中显示",
    value: false
  }
];
const fixedTagOptions: Array<OptionsType> = [
  {
    label: "固定",
    tip: "当前菜单名称固定显示在标签页且不可关闭",
    value: true
  },
  {
    label: "不固定",
    tip: "当前菜单名称不固定显示在标签页且可关闭",
    value: false
  }
];
const keepAliveOptions: Array<OptionsType> = [
  {
    label: "缓存",
    tip: "会保存该页面的整体状态,刷新后会清空状态",
    value: true
  },
  {
    label: "不缓存",
    tip: "不会保存该页面的整体状态",
    value: false
  }
];
const hiddenTagOptions: Array<OptionsType> = [
  {
    label: "允许",
    tip: "当前菜单名称或自定义信息允许添加到标签页",
    value: false
  },
  {
    label: "禁止",
    tip: "当前菜单名称或自定义信息禁止添加到标签页",
    value: true
  }
];
const showParentOptions: Array<OptionsType> = [
  {
    label: "显示",
    tip: "会显示父级菜单",
    value: true
  },
  {
    label: "隐藏",
    tip: "不会显示父级菜单",
    value: false
  }
];
const frameLoadingOptions: Array<OptionsType> = [
  {
    label: "开启",
    tip: "有首次加载动画",
    value: true
  },
  {
    label: "关闭",
    tip: "无首次加载动画",
    value: false
  }
];
export {
  menuTypeOptions,
  showLinkOptions,
  fixedTagOptions,
  keepAliveOptions,
  hiddenTagOptions,
  showParentOptions,
  frameLoadingOptions
};
src/views/system/menu/utils/hook.tsx
New file
@@ -0,0 +1,225 @@
import editForm from "../form.vue";
import { handleTree } from "@/utils/tree";
import { message } from "@/utils/message";
import { getMenuList } from "@/api/system";
import { transformI18n } from "@/plugins/i18n";
import { addDialog } from "@/components/ReDialog";
import { reactive, ref, onMounted, h } from "vue";
import type { FormItemProps } from "../utils/types";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { cloneDeep, isAllEmpty, deviceDetection } from "@pureadmin/utils";
export function useMenu() {
  const form = reactive({
    title: ""
  });
  const formRef = ref();
  const dataList = ref([]);
  const loading = ref(true);
  const getMenuType = (type, text = false) => {
    switch (type) {
      case 0:
        return text ? "菜单" : "primary";
      case 1:
        return text ? "iframe" : "warning";
      case 2:
        return text ? "外链" : "danger";
      case 3:
        return text ? "按钮" : "info";
    }
  };
  const columns: TableColumnList = [
    {
      label: "菜单名称",
      prop: "title",
      align: "left",
      cellRenderer: ({ row }) => (
        <>
          <span class="inline-block mr-1">
            {h(useRenderIcon(row.icon), {
              style: { paddingTop: "1px" }
            })}
          </span>
          <span>{transformI18n(row.title)}</span>
        </>
      )
    },
    {
      label: "菜单类型",
      prop: "menuType",
      width: 100,
      cellRenderer: ({ row, props }) => (
        <el-tag
          size={props.size}
          type={getMenuType(row.menuType)}
          effect="plain"
        >
          {getMenuType(row.menuType, true)}
        </el-tag>
      )
    },
    {
      label: "路由路径",
      prop: "path"
    },
    {
      label: "组件路径",
      prop: "component",
      formatter: ({ path, component }) =>
        isAllEmpty(component) ? path : component
    },
    {
      label: "权限标识",
      prop: "auths"
    },
    {
      label: "排序",
      prop: "rank",
      width: 100
    },
    {
      label: "隐藏",
      prop: "showLink",
      formatter: ({ showLink }) => (showLink ? "否" : "是"),
      width: 100
    },
    {
      label: "操作",
      fixed: "right",
      width: 210,
      slot: "operation"
    }
  ];
  function handleSelectionChange(val) {
    console.log("handleSelectionChange", val);
  }
  function resetForm(formEl) {
    if (!formEl) return;
    formEl.resetFields();
    onSearch();
  }
  async function onSearch() {
    loading.value = true;
    const { data } = await getMenuList(); // 这里是返回一维数组结构,前端自行处理成树结构,返回格式要求:唯一id加父节点parentId,parentId取父节点id
    let newData = data;
    if (!isAllEmpty(form.title)) {
      // 前端搜索菜单名称
      newData = newData.filter(item =>
        transformI18n(item.title).includes(form.title)
      );
    }
    dataList.value = handleTree(newData); // 处理成树结构
    setTimeout(() => {
      loading.value = false;
    }, 500);
  }
  function formatHigherMenuOptions(treeList) {
    if (!treeList || !treeList.length) return;
    const newTreeList = [];
    for (let i = 0; i < treeList.length; i++) {
      treeList[i].title = transformI18n(treeList[i].title);
      formatHigherMenuOptions(treeList[i].children);
      newTreeList.push(treeList[i]);
    }
    return newTreeList;
  }
  function openDialog(title = "新增", row?: FormItemProps) {
    addDialog({
      title: `${title}菜单`,
      props: {
        formInline: {
          menuType: row?.menuType ?? 0,
          higherMenuOptions: formatHigherMenuOptions(cloneDeep(dataList.value)),
          parentId: row?.parentId ?? 0,
          title: row?.title ?? "",
          name: row?.name ?? "",
          path: row?.path ?? "",
          component: row?.component ?? "",
          rank: row?.rank ?? 99,
          redirect: row?.redirect ?? "",
          icon: row?.icon ?? "",
          extraIcon: row?.extraIcon ?? "",
          enterTransition: row?.enterTransition ?? "",
          leaveTransition: row?.leaveTransition ?? "",
          activePath: row?.activePath ?? "",
          auths: row?.auths ?? "",
          frameSrc: row?.frameSrc ?? "",
          frameLoading: row?.frameLoading ?? true,
          keepAlive: row?.keepAlive ?? false,
          hiddenTag: row?.hiddenTag ?? false,
          fixedTag: row?.fixedTag ?? false,
          showLink: row?.showLink ?? true,
          showParent: row?.showParent ?? false
        }
      },
      width: "45%",
      draggable: true,
      fullscreen: deviceDetection(),
      fullscreenIcon: true,
      closeOnClickModal: false,
      contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
      beforeSure: (done, { options }) => {
        const FormRef = formRef.value.getRef();
        const curData = options.props.formInline as FormItemProps;
        function chores() {
          message(
            `您${title}了菜单名称为${transformI18n(curData.title)}的这条数据`,
            {
              type: "success"
            }
          );
          done(); // 关闭弹框
          onSearch(); // 刷新表格数据
        }
        FormRef.validate(valid => {
          if (valid) {
            console.log("curData", curData);
            // 表单规则校验通过
            if (title === "新增") {
              // 实际开发先调用新增接口,再进行下面操作
              chores();
            } else {
              // 实际开发先调用修改接口,再进行下面操作
              chores();
            }
          }
        });
      }
    });
  }
  function handleDelete(row) {
    message(`您删除了菜单名称为${transformI18n(row.title)}的这条数据`, {
      type: "success"
    });
    onSearch();
  }
  onMounted(() => {
    onSearch();
  });
  return {
    form,
    loading,
    columns,
    dataList,
    /** 搜索 */
    onSearch,
    /** 重置 */
    resetForm,
    /** 新增、修改菜单 */
    openDialog,
    /** 删除菜单 */
    handleDelete,
    handleSelectionChange
  };
}
src/views/system/menu/utils/rule.ts
New file
@@ -0,0 +1,10 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
/** 自定义表单规则校验 */
export const formRules = reactive(<FormRules>{
  title: [{ required: true, message: "菜单名称为必填项", trigger: "blur" }],
  name: [{ required: true, message: "路由名称为必填项", trigger: "blur" }],
  path: [{ required: true, message: "路由路径为必填项", trigger: "blur" }],
  auths: [{ required: true, message: "权限标识为必填项", trigger: "blur" }]
});
src/views/system/menu/utils/types.ts
New file
@@ -0,0 +1,30 @@
interface FormItemProps {
  /** 菜单类型(0代表菜单、1代表iframe、2代表外链、3代表按钮)*/
  menuType: number;
  higherMenuOptions: Record<string, unknown>[];
  parentId: number;
  title: string;
  name: string;
  path: string;
  component: string;
  rank: number;
  redirect: string;
  icon: string;
  extraIcon: string;
  enterTransition: string;
  leaveTransition: string;
  activePath: string;
  auths: string;
  frameSrc: string;
  frameLoading: boolean;
  keepAlive: boolean;
  hiddenTag: boolean;
  fixedTag: boolean;
  showLink: boolean;
  showParent: boolean;
}
interface FormProps {
  formInline: FormItemProps;
}
export type { FormItemProps, FormProps };
src/views/system/role/form.vue
New file
@@ -0,0 +1,55 @@
<script setup lang="ts">
import { ref } from "vue";
import { formRules } from "./utils/rule";
import { FormProps } from "./utils/types";
const props = withDefaults(defineProps<FormProps>(), {
  formInline: () => ({
    name: "",
    code: "",
    remark: ""
  })
});
const ruleFormRef = ref();
const newFormInline = ref(props.formInline);
function getRef() {
  return ruleFormRef.value;
}
defineExpose({ getRef });
</script>
<template>
  <el-form
    ref="ruleFormRef"
    :model="newFormInline"
    :rules="formRules"
    label-width="82px"
  >
    <el-form-item label="角色名称" prop="name">
      <el-input
        v-model="newFormInline.name"
        clearable
        placeholder="请输入角色名称"
      />
    </el-form-item>
    <el-form-item label="角色标识" prop="code">
      <el-input
        v-model="newFormInline.code"
        clearable
        placeholder="请输入角色标识"
      />
    </el-form-item>
    <el-form-item label="备注">
      <el-input
        v-model="newFormInline.remark"
        placeholder="请输入备注信息"
        type="textarea"
      />
    </el-form-item>
  </el-form>
</template>
src/views/system/role/index.vue
New file
@@ -0,0 +1,344 @@
<script setup lang="ts">
import { useRole } from "./utils/hook";
import { ref, computed, nextTick, onMounted } from "vue";
import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import {
  delay,
  subBefore,
  deviceDetection,
  useResizeObserver
} from "@pureadmin/utils";
// import Database from "~icons/ri/database-2-line";
// import More from "~icons/ep/more-filled";
import Delete from "~icons/ep/delete";
import EditPen from "~icons/ep/edit-pen";
import Refresh from "~icons/ep/refresh";
import Menu from "~icons/ep/menu";
import AddFill from "~icons/ri/add-circle-line";
import Close from "~icons/ep/close";
import Check from "~icons/ep/check";
defineOptions({
  name: "SystemRole"
});
const iconClass = computed(() => {
  return [
    "w-[22px]",
    "h-[22px]",
    "flex",
    "justify-center",
    "items-center",
    "outline-hidden",
    "rounded-[4px]",
    "cursor-pointer",
    "transition-colors",
    "hover:bg-[#0000000f]",
    "dark:hover:bg-[#ffffff1f]",
    "dark:hover:text-[#ffffffd9]"
  ];
});
const treeRef = ref();
const formRef = ref();
const tableRef = ref();
const contentRef = ref();
const treeHeight = ref();
const {
  form,
  isShow,
  curRow,
  loading,
  columns,
  rowStyle,
  dataList,
  treeData,
  treeProps,
  isLinkage,
  pagination,
  isExpandAll,
  isSelectAll,
  treeSearchValue,
  // buttonClass,
  onSearch,
  resetForm,
  openDialog,
  handleMenu,
  handleSave,
  handleDelete,
  filterMethod,
  transformI18n,
  onQueryChanged,
  // handleDatabase,
  handleSizeChange,
  handleCurrentChange,
  handleSelectionChange
} = useRole(treeRef);
onMounted(() => {
  useResizeObserver(contentRef, async () => {
    await nextTick();
    delay(60).then(() => {
      treeHeight.value = parseFloat(
        subBefore(tableRef.value.getTableDoms().tableWrapper.style.height, "px")
      );
    });
  });
});
</script>
<template>
  <div class="main">
    <el-form
      ref="formRef"
      :inline="true"
      :model="form"
      class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
    >
      <el-form-item label="角色名称:" prop="name">
        <el-input
          v-model="form.name"
          placeholder="请输入角色名称"
          clearable
          class="w-[180px]!"
        />
      </el-form-item>
      <el-form-item label="角色标识:" prop="code">
        <el-input
          v-model="form.code"
          placeholder="请输入角色标识"
          clearable
          class="w-[180px]!"
        />
      </el-form-item>
      <el-form-item label="状态:" prop="status">
        <el-select
          v-model="form.status"
          placeholder="请选择状态"
          clearable
          class="w-[180px]!"
        >
          <el-option label="已启用" value="1" />
          <el-option label="已停用" value="0" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button
          type="primary"
          :icon="useRenderIcon('ri/search-line')"
          :loading="loading"
          @click="onSearch"
        >
          搜索
        </el-button>
        <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
          重置
        </el-button>
      </el-form-item>
    </el-form>
    <div
      ref="contentRef"
      :class="['flex', deviceDetection() ? 'flex-wrap' : '']"
    >
      <PureTableBar
        :class="[isShow && !deviceDetection() ? 'w-[60vw]!' : 'w-full']"
        style="transition: width 220ms cubic-bezier(0.4, 0, 0.2, 1)"
        title="角色管理(仅演示,操作后不生效)"
        :columns="columns"
        @refresh="onSearch"
      >
        <template #buttons>
          <el-button
            type="primary"
            :icon="useRenderIcon(AddFill)"
            @click="openDialog()"
          >
            新增角色
          </el-button>
        </template>
        <template v-slot="{ size, dynamicColumns }">
          <pure-table
            ref="tableRef"
            align-whole="center"
            showOverflowTooltip
            table-layout="auto"
            :loading="loading"
            :size="size"
            adaptive
            :row-style="rowStyle"
            :adaptiveConfig="{ offsetBottom: 108 }"
            :data="dataList"
            :columns="dynamicColumns"
            :pagination="{ ...pagination, size }"
            :header-cell-style="{
              background: 'var(--el-fill-color-light)',
              color: 'var(--el-text-color-primary)'
            }"
            @selection-change="handleSelectionChange"
            @page-size-change="handleSizeChange"
            @page-current-change="handleCurrentChange"
          >
            <template #operation="{ row }">
              <el-button
                class="reset-margin"
                link
                type="primary"
                :size="size"
                :icon="useRenderIcon(EditPen)"
                @click="openDialog('修改', row)"
              >
                修改
              </el-button>
              <el-popconfirm
                :title="`是否确认删除角色名称为${row.name}的这条数据`"
                @confirm="handleDelete(row)"
              >
                <template #reference>
                  <el-button
                    class="reset-margin"
                    link
                    type="primary"
                    :size="size"
                    :icon="useRenderIcon(Delete)"
                  >
                    删除
                  </el-button>
                </template>
              </el-popconfirm>
              <el-button
                class="reset-margin"
                link
                type="primary"
                :size="size"
                :icon="useRenderIcon(Menu)"
                @click="handleMenu(row)"
              >
                权限
              </el-button>
              <!-- <el-dropdown>
              <el-button
                class="ml-3 mt-[2px]"
                link
                type="primary"
                :size="size"
                :icon="useRenderIcon(More)"
              />
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item>
                    <el-button
                      :class="buttonClass"
                      link
                      type="primary"
                      :size="size"
                      :icon="useRenderIcon(Menu)"
                      @click="handleMenu"
                    >
                      菜单权限
                    </el-button>
                  </el-dropdown-item>
                  <el-dropdown-item>
                    <el-button
                      :class="buttonClass"
                      link
                      type="primary"
                      :size="size"
                      :icon="useRenderIcon(Database)"
                      @click="handleDatabase"
                    >
                      数据权限
                    </el-button>
                  </el-dropdown-item>
                </el-dropdown-menu>
              </template>
            </el-dropdown> -->
            </template>
          </pure-table>
        </template>
      </PureTableBar>
      <div
        v-if="isShow"
        class="min-w-[calc(100vw-60vw-268px)]! w-full mt-2 px-2 pb-2 bg-bg_color ml-2 overflow-auto"
      >
        <div class="flex justify-between w-full px-3 pt-5 pb-4">
          <div class="flex">
            <span :class="iconClass">
              <IconifyIconOffline
                v-tippy="{
                  content: '关闭'
                }"
                class="dark:text-white"
                width="18px"
                height="18px"
                :icon="Close"
                @click="handleMenu"
              />
            </span>
            <span :class="[iconClass, 'ml-2']">
              <IconifyIconOffline
                v-tippy="{
                  content: '保存菜单权限'
                }"
                class="dark:text-white"
                width="18px"
                height="18px"
                :icon="Check"
                @click="handleSave"
              />
            </span>
          </div>
          <p class="font-bold truncate">
            菜单权限
            {{ `${curRow?.name ? `(${curRow.name})` : ""}` }}
          </p>
        </div>
        <el-input
          v-model="treeSearchValue"
          placeholder="请输入菜单进行搜索"
          class="mb-1"
          clearable
          @input="onQueryChanged"
        />
        <div class="flex flex-wrap">
          <el-checkbox v-model="isExpandAll" label="展开/折叠" />
          <el-checkbox v-model="isSelectAll" label="全选/全不选" />
          <el-checkbox v-model="isLinkage" label="父子联动" />
        </div>
        <el-tree-v2
          ref="treeRef"
          show-checkbox
          :data="treeData"
          :props="treeProps"
          :height="treeHeight"
          :check-strictly="!isLinkage"
          :filter-method="filterMethod"
        >
          <template #default="{ node }">
            <span>{{ transformI18n(node.label) }}</span>
          </template>
        </el-tree-v2>
      </div>
    </div>
  </div>
</template>
<style lang="scss" scoped>
:deep(.el-dropdown-menu__item i) {
  margin: 0;
}
.main-content {
  margin: 24px 24px 0 !important;
}
.search-form {
  :deep(.el-form-item) {
    margin-bottom: 12px;
  }
}
</style>
src/views/system/role/utils/hook.tsx
New file
@@ -0,0 +1,318 @@
import dayjs from "dayjs";
import editForm from "../form.vue";
import { handleTree } from "@/utils/tree";
import { message } from "@/utils/message";
import { ElMessageBox } from "element-plus";
import { usePublicHooks } from "../../hooks";
import { transformI18n } from "@/plugins/i18n";
import { addDialog } from "@/components/ReDialog";
import type { FormItemProps } from "../utils/types";
import type { PaginationProps } from "@pureadmin/table";
import { getKeyList, deviceDetection } from "@pureadmin/utils";
import { getRoleList, getRoleMenu, getRoleMenuIds } from "@/api/system";
import { type Ref, reactive, ref, onMounted, h, toRaw, watch } from "vue";
export function useRole(treeRef: Ref) {
  const form = reactive({
    name: "",
    code: "",
    status: ""
  });
  const curRow = ref();
  const formRef = ref();
  const dataList = ref([]);
  const treeIds = ref([]);
  const treeData = ref([]);
  const isShow = ref(false);
  const loading = ref(true);
  const isLinkage = ref(false);
  const treeSearchValue = ref();
  const switchLoadMap = ref({});
  const isExpandAll = ref(false);
  const isSelectAll = ref(false);
  const { switchStyle } = usePublicHooks();
  const treeProps = {
    value: "id",
    label: "title",
    children: "children"
  };
  const pagination = reactive<PaginationProps>({
    total: 0,
    pageSize: 10,
    currentPage: 1,
    background: true
  });
  const columns: TableColumnList = [
    {
      label: "角色编号",
      prop: "id"
    },
    {
      label: "角色名称",
      prop: "name"
    },
    {
      label: "角色标识",
      prop: "code"
    },
    {
      label: "状态",
      cellRenderer: scope => (
        <el-switch
          size={scope.props.size === "small" ? "small" : "default"}
          loading={switchLoadMap.value[scope.index]?.loading}
          v-model={scope.row.status}
          active-value={1}
          inactive-value={0}
          active-text="已启用"
          inactive-text="已停用"
          inline-prompt
          style={switchStyle.value}
          onChange={() => onChange(scope as any)}
        />
      ),
      minWidth: 90
    },
    {
      label: "备注",
      prop: "remark",
      minWidth: 160
    },
    {
      label: "创建时间",
      prop: "createTime",
      minWidth: 160,
      formatter: ({ createTime }) =>
        dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
    },
    {
      label: "操作",
      fixed: "right",
      width: 210,
      slot: "operation"
    }
  ];
  // const buttonClass = computed(() => {
  //   return [
  //     "h-[20px]!",
  //     "reset-margin",
  //     "text-gray-500!",
  //     "dark:text-white!",
  //     "dark:hover:text-primary!"
  //   ];
  // });
  function onChange({ row, index }) {
    ElMessageBox.confirm(
      `确认要<strong>${
        row.status === 0 ? "停用" : "启用"
      }</strong><strong style='color:var(--el-color-primary)'>${
        row.name
      }</strong>吗?`,
      "系统提示",
      {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
        dangerouslyUseHTMLString: true,
        draggable: true
      }
    )
      .then(() => {
        switchLoadMap.value[index] = Object.assign(
          {},
          switchLoadMap.value[index],
          {
            loading: true
          }
        );
        setTimeout(() => {
          switchLoadMap.value[index] = Object.assign(
            {},
            switchLoadMap.value[index],
            {
              loading: false
            }
          );
          message(`已${row.status === 0 ? "停用" : "启用"}${row.name}`, {
            type: "success"
          });
        }, 300);
      })
      .catch(() => {
        row.status === 0 ? (row.status = 1) : (row.status = 0);
      });
  }
  function handleDelete(row) {
    message(`您删除了角色名称为${row.name}的这条数据`, { type: "success" });
    onSearch();
  }
  function handleSizeChange(val: number) {
    console.log(`${val} items per page`);
  }
  function handleCurrentChange(val: number) {
    console.log(`current page: ${val}`);
  }
  function handleSelectionChange(val) {
    console.log("handleSelectionChange", val);
  }
  async function onSearch() {
    loading.value = true;
    const { data } = await getRoleList(toRaw(form));
    dataList.value = data.list;
    pagination.total = data.total;
    pagination.pageSize = data.pageSize;
    pagination.currentPage = data.currentPage;
    setTimeout(() => {
      loading.value = false;
    }, 500);
  }
  const resetForm = formEl => {
    if (!formEl) return;
    formEl.resetFields();
    onSearch();
  };
  function openDialog(title = "新增", row?: FormItemProps) {
    addDialog({
      title: `${title}角色`,
      props: {
        formInline: {
          name: row?.name ?? "",
          code: row?.code ?? "",
          remark: row?.remark ?? ""
        }
      },
      width: "40%",
      draggable: true,
      fullscreen: deviceDetection(),
      fullscreenIcon: true,
      closeOnClickModal: false,
      contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
      beforeSure: (done, { options }) => {
        const FormRef = formRef.value.getRef();
        const curData = options.props.formInline as FormItemProps;
        function chores() {
          message(`您${title}了角色名称为${curData.name}的这条数据`, {
            type: "success"
          });
          done(); // 关闭弹框
          onSearch(); // 刷新表格数据
        }
        FormRef.validate(valid => {
          if (valid) {
            console.log("curData", curData);
            // 表单规则校验通过
            if (title === "新增") {
              // 实际开发先调用新增接口,再进行下面操作
              chores();
            } else {
              // 实际开发先调用修改接口,再进行下面操作
              chores();
            }
          }
        });
      }
    });
  }
  /** 菜单权限 */
  async function handleMenu(row?: any) {
    const { id } = row;
    if (id) {
      curRow.value = row;
      isShow.value = true;
      const { data } = await getRoleMenuIds({ id });
      treeRef.value.setCheckedKeys(data);
    } else {
      curRow.value = null;
      isShow.value = false;
    }
  }
  /** 高亮当前权限选中行 */
  function rowStyle({ row: { id } }) {
    return {
      cursor: "pointer",
      background: id === curRow.value?.id ? "var(--el-fill-color-light)" : ""
    };
  }
  /** 菜单权限-保存 */
  function handleSave() {
    const { id, name } = curRow.value;
    // 根据用户 id 调用实际项目中菜单权限修改接口
    console.log(id, treeRef.value.getCheckedKeys());
    message(`角色名称为${name}的菜单权限修改成功`, {
      type: "success"
    });
  }
  /** 数据权限 可自行开发 */
  // function handleDatabase() {}
  const onQueryChanged = (query: string) => {
    treeRef.value!.filter(query);
  };
  const filterMethod = (query: string, node) => {
    return transformI18n(node.title)!.includes(query);
  };
  onMounted(async () => {
    onSearch();
    const { data } = await getRoleMenu();
    treeIds.value = getKeyList(data, "id");
    treeData.value = handleTree(data);
  });
  watch(isExpandAll, val => {
    val
      ? treeRef.value.setExpandedKeys(treeIds.value)
      : treeRef.value.setExpandedKeys([]);
  });
  watch(isSelectAll, val => {
    val
      ? treeRef.value.setCheckedKeys(treeIds.value)
      : treeRef.value.setCheckedKeys([]);
  });
  return {
    form,
    isShow,
    curRow,
    loading,
    columns,
    rowStyle,
    dataList,
    treeData,
    treeProps,
    isLinkage,
    pagination,
    isExpandAll,
    isSelectAll,
    treeSearchValue,
    // buttonClass,
    onSearch,
    resetForm,
    openDialog,
    handleMenu,
    handleSave,
    handleDelete,
    filterMethod,
    transformI18n,
    onQueryChanged,
    // handleDatabase,
    handleSizeChange,
    handleCurrentChange,
    handleSelectionChange
  };
}
src/views/system/role/utils/rule.ts
New file
@@ -0,0 +1,8 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
/** 自定义表单规则校验 */
export const formRules = reactive(<FormRules>{
  name: [{ required: true, message: "角色名称为必填项", trigger: "blur" }],
  code: [{ required: true, message: "角色标识为必填项", trigger: "blur" }]
});
src/views/system/role/utils/types.ts
New file
@@ -0,0 +1,15 @@
// 虽然字段很少 但是抽离出来 后续有扩展字段需求就很方便了
interface FormItemProps {
  /** 角色名称 */
  name: string;
  /** 角色编号 */
  code: string;
  /** 备注 */
  remark: string;
}
interface FormProps {
  formInline: FormItemProps;
}
export type { FormItemProps, FormProps };
src/views/system/user/form/index.vue
New file
@@ -0,0 +1,176 @@
<script setup lang="ts">
import { ref } from "vue";
import ReCol from "@/components/ReCol";
import { formRules } from "../utils/rule";
import { FormProps } from "../utils/types";
import { usePublicHooks } from "../../hooks";
const props = withDefaults(defineProps<FormProps>(), {
  formInline: () => ({
    title: "新增",
    higherDeptOptions: [],
    parentId: 0,
    nickname: "",
    username: "",
    password: "",
    phone: "",
    email: "",
    sex: "",
    status: 1,
    remark: ""
  })
});
const sexOptions = [
  {
    value: 0,
    label: "男"
  },
  {
    value: 1,
    label: "女"
  }
];
const ruleFormRef = ref();
const { switchStyle } = usePublicHooks();
const newFormInline = ref(props.formInline);
function getRef() {
  return ruleFormRef.value;
}
defineExpose({ getRef });
</script>
<template>
  <el-form
    ref="ruleFormRef"
    :model="newFormInline"
    :rules="formRules"
    label-width="82px"
  >
    <el-row :gutter="30">
      <re-col :value="12" :xs="24" :sm="24">
        <el-form-item label="用户昵称" prop="nickname">
          <el-input
            v-model="newFormInline.nickname"
            clearable
            placeholder="请输入用户昵称"
          />
        </el-form-item>
      </re-col>
      <re-col :value="12" :xs="24" :sm="24">
        <el-form-item label="用户名称" prop="username">
          <el-input
            v-model="newFormInline.username"
            clearable
            placeholder="请输入用户名称"
          />
        </el-form-item>
      </re-col>
      <re-col
        v-if="newFormInline.title === '新增'"
        :value="12"
        :xs="24"
        :sm="24"
      >
        <el-form-item label="用户密码" prop="password">
          <el-input
            v-model="newFormInline.password"
            clearable
            placeholder="请输入用户密码"
          />
        </el-form-item>
      </re-col>
      <re-col :value="12" :xs="24" :sm="24">
        <el-form-item label="手机号" prop="phone">
          <el-input
            v-model="newFormInline.phone"
            clearable
            placeholder="请输入手机号"
          />
        </el-form-item>
      </re-col>
      <re-col :value="12" :xs="24" :sm="24">
        <el-form-item label="邮箱" prop="email">
          <el-input
            v-model="newFormInline.email"
            clearable
            placeholder="请输入邮箱"
          />
        </el-form-item>
      </re-col>
      <re-col :value="12" :xs="24" :sm="24">
        <el-form-item label="用户性别">
          <el-select
            v-model="newFormInline.sex"
            placeholder="请选择用户性别"
            class="w-full"
            clearable
          >
            <el-option
              v-for="(item, index) in sexOptions"
              :key="index"
              :label="item.label"
              :value="item.value"
            />
          </el-select>
        </el-form-item>
      </re-col>
      <re-col :value="12" :xs="24" :sm="24">
        <el-form-item label="归属部门">
          <el-cascader
            v-model="newFormInline.parentId"
            class="w-full"
            :options="newFormInline.higherDeptOptions"
            :props="{
              value: 'id',
              label: 'name',
              emitPath: false,
              checkStrictly: true
            }"
            clearable
            filterable
            placeholder="请选择归属部门"
          >
            <template #default="{ node, data }">
              <span>{{ data.name }}</span>
              <span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
            </template>
          </el-cascader>
        </el-form-item>
      </re-col>
      <re-col
        v-if="newFormInline.title === '新增'"
        :value="12"
        :xs="24"
        :sm="24"
      >
        <el-form-item label="用户状态">
          <el-switch
            v-model="newFormInline.status"
            inline-prompt
            :active-value="1"
            :inactive-value="0"
            active-text="启用"
            inactive-text="停用"
            :style="switchStyle"
          />
        </el-form-item>
      </re-col>
      <re-col>
        <el-form-item label="备注">
          <el-input
            v-model="newFormInline.remark"
            placeholder="请输入备注信息"
            type="textarea"
          />
        </el-form-item>
      </re-col>
    </el-row>
  </el-form>
</template>
src/views/system/user/form/role.vue
New file
@@ -0,0 +1,53 @@
<script setup lang="ts">
import { ref } from "vue";
import ReCol from "@/components/ReCol";
import { RoleFormProps } from "../utils/types";
const props = withDefaults(defineProps<RoleFormProps>(), {
  formInline: () => ({
    username: "",
    nickname: "",
    roleOptions: [],
    ids: []
  })
});
const newFormInline = ref(props.formInline);
</script>
<template>
  <el-form :model="newFormInline">
    <el-row :gutter="30">
      <!-- <re-col>
        <el-form-item label="用户名称" prop="username">
          <el-input disabled v-model="newFormInline.username" />
        </el-form-item>
      </re-col> -->
      <re-col>
        <el-form-item label="用户昵称" prop="nickname">
          <el-input v-model="newFormInline.nickname" disabled />
        </el-form-item>
      </re-col>
      <re-col>
        <el-form-item label="角色列表" prop="ids">
          <el-select
            v-model="newFormInline.ids"
            placeholder="请选择"
            class="w-full"
            clearable
            multiple
          >
            <el-option
              v-for="(item, index) in newFormInline.roleOptions"
              :key="index"
              :value="item.id"
              :label="item.name"
            >
              {{ item.name }}
            </el-option>
          </el-select>
        </el-form-item>
      </re-col>
    </el-row>
  </el-form>
</template>
src/views/system/user/index.vue
New file
@@ -0,0 +1,275 @@
<script setup lang="ts">
import { ref } from "vue";
import tree from "./tree.vue";
import { useUser } from "./utils/hook";
import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import Upload from "~icons/ri/upload-line";
import Role from "~icons/ri/admin-line";
import Password from "~icons/ri/lock-password-line";
import More from "~icons/ep/more-filled";
import Delete from "~icons/ep/delete";
import EditPen from "~icons/ep/edit-pen";
import Refresh from "~icons/ep/refresh";
import AddFill from "~icons/ri/add-circle-line";
defineOptions({
  name: "SystemUser"
});
const treeRef = ref();
const formRef = ref();
const tableRef = ref();
const {
  form,
  loading,
  columns,
  dataList,
  treeData,
  treeLoading,
  selectedNum,
  pagination,
  buttonClass,
  deviceDetection,
  onSearch,
  resetForm,
  onbatchDel,
  openDialog,
  onTreeSelect,
  handleUpdate,
  handleDelete,
  handleUpload,
  handleReset,
  handleRole,
  handleSizeChange,
  onSelectionCancel,
  handleCurrentChange,
  handleSelectionChange
} = useUser(tableRef, treeRef);
</script>
<template>
  <div :class="['flex', 'justify-between', deviceDetection() && 'flex-wrap']">
    <tree
      ref="treeRef"
      :class="['mr-2', deviceDetection() ? 'w-full' : 'min-w-[200px]']"
      :treeData="treeData"
      :treeLoading="treeLoading"
      @tree-select="onTreeSelect"
    />
    <div
      :class="[deviceDetection() ? ['w-full', 'mt-2'] : 'w-[calc(100%-200px)]']"
    >
      <el-form
        ref="formRef"
        :inline="true"
        :model="form"
        class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
      >
        <el-form-item label="用户名称:" prop="username">
          <el-input
            v-model="form.username"
            placeholder="请输入用户名称"
            clearable
            class="w-[180px]!"
          />
        </el-form-item>
        <el-form-item label="手机号码:" prop="phone">
          <el-input
            v-model="form.phone"
            placeholder="请输入手机号码"
            clearable
            class="w-[180px]!"
          />
        </el-form-item>
        <el-form-item label="状态:" prop="status">
          <el-select
            v-model="form.status"
            placeholder="请选择"
            clearable
            class="w-[180px]!"
          >
            <el-option label="已开启" value="1" />
            <el-option label="已关闭" value="0" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button
            type="primary"
            :icon="useRenderIcon('ri/search-line')"
            :loading="loading"
            @click="onSearch"
          >
            搜索
          </el-button>
          <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
            重置
          </el-button>
        </el-form-item>
      </el-form>
      <PureTableBar
        title="用户管理(仅演示,操作后不生效)"
        :columns="columns"
        @refresh="onSearch"
      >
        <template #buttons>
          <el-button
            type="primary"
            :icon="useRenderIcon(AddFill)"
            @click="openDialog()"
          >
            新增用户
          </el-button>
        </template>
        <template v-slot="{ size, dynamicColumns }">
          <div
            v-if="selectedNum > 0"
            v-motion-fade
            class="bg-[var(--el-fill-color-light)] w-full h-[46px] mb-2 pl-4 flex items-center"
          >
            <div class="flex-auto">
              <span
                style="font-size: var(--el-font-size-base)"
                class="text-[rgba(42,46,54,0.5)] dark:text-[rgba(220,220,242,0.5)]"
              >
                已选 {{ selectedNum }} 项
              </span>
              <el-button type="primary" text @click="onSelectionCancel">
                取消选择
              </el-button>
            </div>
            <el-popconfirm title="是否确认删除?" @confirm="onbatchDel">
              <template #reference>
                <el-button type="danger" text class="mr-1!">
                  批量删除
                </el-button>
              </template>
            </el-popconfirm>
          </div>
          <pure-table
            ref="tableRef"
            row-key="id"
            adaptive
            :adaptiveConfig="{ offsetBottom: 108 }"
            align-whole="center"
            table-layout="auto"
            :loading="loading"
            :size="size"
            :data="dataList"
            :columns="dynamicColumns"
            :pagination="{ ...pagination, size }"
            :header-cell-style="{
              background: 'var(--el-fill-color-light)',
              color: 'var(--el-text-color-primary)'
            }"
            @selection-change="handleSelectionChange"
            @page-size-change="handleSizeChange"
            @page-current-change="handleCurrentChange"
          >
            <template #operation="{ row }">
              <el-button
                class="reset-margin"
                link
                type="primary"
                :size="size"
                :icon="useRenderIcon(EditPen)"
                @click="openDialog('修改', row)"
              >
                修改
              </el-button>
              <el-popconfirm
                :title="`是否确认删除用户编号为${row.id}的这条数据`"
                @confirm="handleDelete(row)"
              >
                <template #reference>
                  <el-button
                    class="reset-margin"
                    link
                    type="primary"
                    :size="size"
                    :icon="useRenderIcon(Delete)"
                  >
                    删除
                  </el-button>
                </template>
              </el-popconfirm>
              <el-dropdown>
                <el-button
                  class="ml-3! mt-[2px]!"
                  link
                  type="primary"
                  :size="size"
                  :icon="useRenderIcon(More)"
                  @click="handleUpdate(row)"
                />
                <template #dropdown>
                  <el-dropdown-menu>
                    <el-dropdown-item>
                      <el-button
                        :class="buttonClass"
                        link
                        type="primary"
                        :size="size"
                        :icon="useRenderIcon(Upload)"
                        @click="handleUpload(row)"
                      >
                        上传头像
                      </el-button>
                    </el-dropdown-item>
                    <el-dropdown-item>
                      <el-button
                        :class="buttonClass"
                        link
                        type="primary"
                        :size="size"
                        :icon="useRenderIcon(Password)"
                        @click="handleReset(row)"
                      >
                        重置密码
                      </el-button>
                    </el-dropdown-item>
                    <el-dropdown-item>
                      <el-button
                        :class="buttonClass"
                        link
                        type="primary"
                        :size="size"
                        :icon="useRenderIcon(Role)"
                        @click="handleRole(row)"
                      >
                        分配角色
                      </el-button>
                    </el-dropdown-item>
                  </el-dropdown-menu>
                </template>
              </el-dropdown>
            </template>
          </pure-table>
        </template>
      </PureTableBar>
    </div>
  </div>
</template>
<style lang="scss" scoped>
:deep(.el-dropdown-menu__item i) {
  margin: 0;
}
:deep(.el-button:focus-visible) {
  outline: none;
}
.main-content {
  margin: 24px 24px 0 !important;
}
.search-form {
  :deep(.el-form-item) {
    margin-bottom: 12px;
  }
}
</style>
src/views/system/user/svg/expand.svg
New file
@@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M22 4V2H2v2h9v14.17l-5.5-5.5-1.42 1.41L12 22l7.92-7.92-1.42-1.41-5.5 5.5V4z"/></svg>
src/views/system/user/svg/unexpand.svg
New file
@@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M4 2H2v20h2v-9h14.17l-5.5 5.5 1.41 1.42L22 12l-7.92-7.92-1.41 1.42 5.5 5.5H4z"/></svg>
src/views/system/user/tree.vue
New file
@@ -0,0 +1,211 @@
<script setup lang="ts">
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, computed, watch, getCurrentInstance } from "vue";
import Dept from "~icons/ri/git-branch-line";
// import Reset from "~icons/ri/restart-line";
import More2Fill from "~icons/ri/more-2-fill?width=18&height=18";
import OfficeBuilding from "~icons/ep/office-building";
import LocationCompany from "~icons/ep/add-location";
import ExpandIcon from "./svg/expand.svg?component";
import UnExpandIcon from "./svg/unexpand.svg?component";
interface Tree {
  id: number;
  name: string;
  highlight?: boolean;
  children?: Tree[];
}
defineProps({
  treeLoading: Boolean,
  treeData: Array
});
const emit = defineEmits(["tree-select"]);
const treeRef = ref();
const isExpand = ref(true);
const searchValue = ref("");
const highlightMap = ref({});
const { proxy } = getCurrentInstance();
const defaultProps = {
  children: "children",
  label: "name"
};
const buttonClass = computed(() => {
  return [
    "h-[20px]!",
    "text-sm!",
    "reset-margin",
    "text-(--el-text-color-regular)!",
    "dark:text-white!",
    "dark:hover:text-primary!"
  ];
});
const filterNode = (value: string, data: Tree) => {
  if (!value) return true;
  return data.name.includes(value);
};
function nodeClick(value) {
  const nodeId = value.$treeNodeId;
  highlightMap.value[nodeId] = highlightMap.value[nodeId]?.highlight
    ? Object.assign({ id: nodeId }, highlightMap.value[nodeId], {
        highlight: false
      })
    : Object.assign({ id: nodeId }, highlightMap.value[nodeId], {
        highlight: true
      });
  Object.values(highlightMap.value).forEach((v: Tree) => {
    if (v.id !== nodeId) {
      v.highlight = false;
    }
  });
  emit(
    "tree-select",
    highlightMap.value[nodeId]?.highlight
      ? Object.assign({ ...value, selected: true })
      : Object.assign({ ...value, selected: false })
  );
}
function toggleRowExpansionAll(status) {
  isExpand.value = status;
  const nodes = (proxy.$refs["treeRef"] as any).store._getAllNodes();
  for (let i = 0; i < nodes.length; i++) {
    nodes[i].expanded = status;
  }
}
/** 重置部门树状态(选中状态、搜索框值、树初始化) */
function onTreeReset() {
  highlightMap.value = {};
  searchValue.value = "";
  toggleRowExpansionAll(true);
}
watch(searchValue, val => {
  treeRef.value!.filter(val);
});
defineExpose({ onTreeReset });
</script>
<template>
  <div
    v-loading="treeLoading"
    class="h-full bg-bg_color overflow-hidden relative"
    :style="{ minHeight: `calc(100vh - 141px)` }"
  >
    <div class="flex items-center h-[34px]">
      <el-input
        v-model="searchValue"
        class="ml-2"
        size="small"
        placeholder="请输入部门名称"
        clearable
      >
        <template #suffix>
          <el-icon class="el-input__icon">
            <IconifyIconOffline
              v-show="searchValue.length === 0"
              icon="ri/search-line"
            />
          </el-icon>
        </template>
      </el-input>
      <el-dropdown :hide-on-click="false">
        <More2Fill class="w-[28px] cursor-pointer outline-hidden" />
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item>
              <el-button
                :class="buttonClass"
                link
                type="primary"
                :icon="useRenderIcon(isExpand ? ExpandIcon : UnExpandIcon)"
                @click="toggleRowExpansionAll(isExpand ? false : true)"
              >
                {{ isExpand ? "折叠全部" : "展开全部" }}
              </el-button>
            </el-dropdown-item>
            <!-- <el-dropdown-item>
              <el-button
                :class="buttonClass"
                link
                type="primary"
                :icon="useRenderIcon(Reset)"
                @click="onTreeReset"
              >
                重置状态
              </el-button>
            </el-dropdown-item> -->
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </div>
    <el-divider />
    <el-scrollbar height="calc(90vh - 88px)">
      <el-tree
        ref="treeRef"
        :data="treeData"
        node-key="id"
        size="small"
        :props="defaultProps"
        default-expand-all
        :expand-on-click-node="false"
        :filter-node-method="filterNode"
        @node-click="nodeClick"
      >
        <template #default="{ node, data }">
          <div
            :class="[
              'rounded-sm',
              'flex',
              'items-center',
              'select-none',
              'hover:text-primary',
              searchValue.trim().length > 0 &&
                node.label.includes(searchValue) &&
                'text-red-500',
              highlightMap[node.id]?.highlight ? 'dark:text-primary' : ''
            ]"
            :style="{
              color: highlightMap[node.id]?.highlight
                ? 'var(--el-color-primary)'
                : '',
              background: highlightMap[node.id]?.highlight
                ? 'var(--el-color-primary-light-7)'
                : 'transparent'
            }"
          >
            <IconifyIconOffline
              :icon="
                data.type === 1
                  ? OfficeBuilding
                  : data.type === 2
                    ? LocationCompany
                    : Dept
              "
            />
            <span class="w-[120px]! truncate!" :title="node.label">
              {{ node.label }}
            </span>
          </div>
        </template>
      </el-tree>
    </el-scrollbar>
  </div>
</template>
<style lang="scss" scoped>
:deep(.el-divider) {
  margin: 0;
}
:deep(.el-tree) {
  --el-tree-node-hover-bg-color: transparent;
}
</style>
src/views/system/user/utils/hook.tsx
New file
@@ -0,0 +1,535 @@
import "./reset.css";
import dayjs from "dayjs";
import roleForm from "../form/role.vue";
import editForm from "../form/index.vue";
import { zxcvbn } from "@zxcvbn-ts/core";
import { handleTree } from "@/utils/tree";
import { message } from "@/utils/message";
import userAvatar from "@/assets/user.jpg";
import { usePublicHooks } from "../../hooks";
import { addDialog } from "@/components/ReDialog";
import type { PaginationProps } from "@pureadmin/table";
import ReCropperPreview from "@/components/ReCropperPreview";
import type { FormItemProps, RoleFormItemProps } from "../utils/types";
import {
  getKeyList,
  isAllEmpty,
  hideTextAtIndex,
  deviceDetection
} from "@pureadmin/utils";
import {
  getRoleIds,
  getDeptList,
  getUserList,
  getAllRoleList
} from "@/api/system";
import {
  ElForm,
  ElInput,
  ElFormItem,
  ElProgress,
  ElMessageBox
} from "element-plus";
import {
  type Ref,
  h,
  ref,
  toRaw,
  watch,
  computed,
  reactive,
  onMounted
} from "vue";
export function useUser(tableRef: Ref, treeRef: Ref) {
  const form = reactive({
    // 左侧部门树的id
    deptId: "",
    username: "",
    phone: "",
    status: ""
  });
  const formRef = ref();
  const ruleFormRef = ref();
  const dataList = ref([]);
  const loading = ref(true);
  // 上传头像信息
  const avatarInfo = ref();
  const switchLoadMap = ref({});
  const { switchStyle } = usePublicHooks();
  const higherDeptOptions = ref();
  const treeData = ref([]);
  const treeLoading = ref(true);
  const selectedNum = ref(0);
  const pagination = reactive<PaginationProps>({
    total: 0,
    pageSize: 10,
    currentPage: 1,
    background: true
  });
  const columns: TableColumnList = [
    {
      label: "勾选列", // 如果需要表格多选,此处label必须设置
      type: "selection",
      fixed: "left",
      reserveSelection: true // 数据刷新后保留选项
    },
    {
      label: "用户编号",
      prop: "id",
      width: 90
    },
    {
      label: "用户头像",
      prop: "avatar",
      cellRenderer: ({ row }) => (
        <el-image
          fit="cover"
          preview-teleported={true}
          src={row.avatar || userAvatar}
          preview-src-list={Array.of(row.avatar || userAvatar)}
          class="w-[24px] h-[24px] rounded-full align-middle"
        />
      ),
      width: 90
    },
    {
      label: "用户名称",
      prop: "username",
      minWidth: 130
    },
    {
      label: "用户昵称",
      prop: "nickname",
      minWidth: 130
    },
    {
      label: "性别",
      prop: "sex",
      minWidth: 90,
      cellRenderer: ({ row, props }) => (
        <el-tag
          size={props.size}
          type={row.sex === 1 ? "danger" : null}
          effect="plain"
        >
          {row.sex === 1 ? "女" : "男"}
        </el-tag>
      )
    },
    {
      label: "部门",
      prop: "dept.name",
      minWidth: 90
    },
    {
      label: "手机号码",
      prop: "phone",
      minWidth: 90,
      formatter: ({ phone }) => hideTextAtIndex(phone, { start: 3, end: 6 })
    },
    {
      label: "状态",
      prop: "status",
      minWidth: 90,
      cellRenderer: scope => (
        <el-switch
          size={scope.props.size === "small" ? "small" : "default"}
          loading={switchLoadMap.value[scope.index]?.loading}
          v-model={scope.row.status}
          active-value={1}
          inactive-value={0}
          active-text="已启用"
          inactive-text="已停用"
          inline-prompt
          style={switchStyle.value}
          onChange={() => onChange(scope as any)}
        />
      )
    },
    {
      label: "创建时间",
      minWidth: 90,
      prop: "createTime",
      formatter: ({ createTime }) =>
        dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
    },
    {
      label: "操作",
      fixed: "right",
      width: 180,
      slot: "operation"
    }
  ];
  const buttonClass = computed(() => {
    return [
      "h-[20px]!",
      "reset-margin",
      "text-gray-500!",
      "dark:text-white!",
      "dark:hover:text-primary!"
    ];
  });
  // 重置的新密码
  const pwdForm = reactive({
    newPwd: ""
  });
  const pwdProgress = [
    { color: "#e74242", text: "非常弱" },
    { color: "#EFBD47", text: "弱" },
    { color: "#ffa500", text: "一般" },
    { color: "#1bbf1b", text: "强" },
    { color: "#008000", text: "非常强" }
  ];
  // 当前密码强度(0-4)
  const curScore = ref();
  const roleOptions = ref([]);
  function onChange({ row, index }) {
    ElMessageBox.confirm(
      `确认要<strong>${
        row.status === 0 ? "停用" : "启用"
      }</strong><strong style='color:var(--el-color-primary)'>${
        row.username
      }</strong>用户吗?`,
      "系统提示",
      {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
        dangerouslyUseHTMLString: true,
        draggable: true
      }
    )
      .then(() => {
        switchLoadMap.value[index] = Object.assign(
          {},
          switchLoadMap.value[index],
          {
            loading: true
          }
        );
        setTimeout(() => {
          switchLoadMap.value[index] = Object.assign(
            {},
            switchLoadMap.value[index],
            {
              loading: false
            }
          );
          message("已成功修改用户状态", {
            type: "success"
          });
        }, 300);
      })
      .catch(() => {
        row.status === 0 ? (row.status = 1) : (row.status = 0);
      });
  }
  function handleUpdate(row) {
    console.log(row);
  }
  function handleDelete(row) {
    message(`您删除了用户编号为${row.id}的这条数据`, { type: "success" });
    onSearch();
  }
  function handleSizeChange(val: number) {
    console.log(`${val} items per page`);
  }
  function handleCurrentChange(val: number) {
    console.log(`current page: ${val}`);
  }
  /** 当CheckBox选择项发生变化时会触发该事件 */
  function handleSelectionChange(val) {
    selectedNum.value = val.length;
    // 重置表格高度
    tableRef.value.setAdaptive();
  }
  /** 取消选择 */
  function onSelectionCancel() {
    selectedNum.value = 0;
    // 用于多选表格,清空用户的选择
    tableRef.value.getTableRef().clearSelection();
  }
  /** 批量删除 */
  function onbatchDel() {
    // 返回当前选中的行
    const curSelected = tableRef.value.getTableRef().getSelectionRows();
    // 接下来根据实际业务,通过选中行的某项数据,比如下面的id,调用接口进行批量删除
    message(`已删除用户编号为 ${getKeyList(curSelected, "id")} 的数据`, {
      type: "success"
    });
    tableRef.value.getTableRef().clearSelection();
    onSearch();
  }
  async function onSearch() {
    loading.value = true;
    const { data } = await getUserList(toRaw(form));
    dataList.value = data.list;
    pagination.total = data.total;
    pagination.pageSize = data.pageSize;
    pagination.currentPage = data.currentPage;
    setTimeout(() => {
      loading.value = false;
    }, 500);
  }
  const resetForm = formEl => {
    if (!formEl) return;
    formEl.resetFields();
    form.deptId = "";
    treeRef.value.onTreeReset();
    onSearch();
  };
  function onTreeSelect({ id, selected }) {
    form.deptId = selected ? id : "";
    onSearch();
  }
  function formatHigherDeptOptions(treeList) {
    // 根据返回数据的status字段值判断追加是否禁用disabled字段,返回处理后的树结构,用于上级部门级联选择器的展示(实际开发中也是如此,不可能前端需要的每个字段后端都会返回,这时需要前端自行根据后端返回的某些字段做逻辑处理)
    if (!treeList || !treeList.length) return;
    const newTreeList = [];
    for (let i = 0; i < treeList.length; i++) {
      treeList[i].disabled = treeList[i].status === 0 ? true : false;
      formatHigherDeptOptions(treeList[i].children);
      newTreeList.push(treeList[i]);
    }
    return newTreeList;
  }
  function openDialog(title = "新增", row?: FormItemProps) {
    addDialog({
      title: `${title}用户`,
      props: {
        formInline: {
          title,
          higherDeptOptions: formatHigherDeptOptions(higherDeptOptions.value),
          parentId: row?.dept.id ?? 0,
          nickname: row?.nickname ?? "",
          username: row?.username ?? "",
          password: row?.password ?? "",
          phone: row?.phone ?? "",
          email: row?.email ?? "",
          sex: row?.sex ?? "",
          status: row?.status ?? 1,
          remark: row?.remark ?? ""
        }
      },
      width: "46%",
      draggable: true,
      fullscreen: deviceDetection(),
      fullscreenIcon: true,
      closeOnClickModal: false,
      contentRenderer: () => h(editForm, { ref: formRef, formInline: null }),
      beforeSure: (done, { options }) => {
        const FormRef = formRef.value.getRef();
        const curData = options.props.formInline as FormItemProps;
        function chores() {
          message(`您${title}了用户名称为${curData.username}的这条数据`, {
            type: "success"
          });
          done(); // 关闭弹框
          onSearch(); // 刷新表格数据
        }
        FormRef.validate(valid => {
          if (valid) {
            console.log("curData", curData);
            // 表单规则校验通过
            if (title === "新增") {
              // 实际开发先调用新增接口,再进行下面操作
              chores();
            } else {
              // 实际开发先调用修改接口,再进行下面操作
              chores();
            }
          }
        });
      }
    });
  }
  const cropRef = ref();
  /** 上传头像 */
  function handleUpload(row) {
    addDialog({
      title: "裁剪、上传头像",
      width: "40%",
      closeOnClickModal: false,
      fullscreen: deviceDetection(),
      contentRenderer: () =>
        h(ReCropperPreview, {
          ref: cropRef,
          imgSrc: row.avatar || userAvatar,
          onCropper: info => (avatarInfo.value = info)
        }),
      beforeSure: done => {
        console.log("裁剪后的图片信息:", avatarInfo.value);
        // 根据实际业务使用avatarInfo.value和row里的某些字段去调用上传头像接口即可
        done(); // 关闭弹框
        onSearch(); // 刷新表格数据
      },
      closeCallBack: () => cropRef.value.hidePopover()
    });
  }
  watch(
    pwdForm,
    ({ newPwd }) =>
      (curScore.value = isAllEmpty(newPwd) ? -1 : zxcvbn(newPwd).score)
  );
  /** 重置密码 */
  function handleReset(row) {
    addDialog({
      title: `重置 ${row.username} 用户的密码`,
      width: "30%",
      draggable: true,
      closeOnClickModal: false,
      fullscreen: deviceDetection(),
      contentRenderer: () => (
        <>
          <ElForm ref={ruleFormRef} model={pwdForm}>
            <ElFormItem
              prop="newPwd"
              rules={[
                {
                  required: true,
                  message: "请输入新密码",
                  trigger: "blur"
                }
              ]}
            >
              <ElInput
                clearable
                show-password
                type="password"
                v-model={pwdForm.newPwd}
                placeholder="请输入新密码"
              />
            </ElFormItem>
          </ElForm>
          <div class="my-4 flex">
            {pwdProgress.map(({ color, text }, idx) => (
              <div
                class="w-[19vw]"
                style={{ marginLeft: idx !== 0 ? "4px" : 0 }}
              >
                <ElProgress
                  striped
                  striped-flow
                  duration={curScore.value === idx ? 6 : 0}
                  percentage={curScore.value >= idx ? 100 : 0}
                  color={color}
                  stroke-width={10}
                  show-text={false}
                />
                <p
                  class="text-center"
                  style={{ color: curScore.value === idx ? color : "" }}
                >
                  {text}
                </p>
              </div>
            ))}
          </div>
        </>
      ),
      closeCallBack: () => (pwdForm.newPwd = ""),
      beforeSure: done => {
        ruleFormRef.value.validate(valid => {
          if (valid) {
            // 表单规则校验通过
            message(`已成功重置 ${row.username} 用户的密码`, {
              type: "success"
            });
            console.log(pwdForm.newPwd);
            // 根据实际业务使用pwdForm.newPwd和row里的某些字段去调用重置用户密码接口即可
            done(); // 关闭弹框
            onSearch(); // 刷新表格数据
          }
        });
      }
    });
  }
  /** 分配角色 */
  async function handleRole(row) {
    // 选中的角色列表
    const ids = (await getRoleIds({ userId: row.id })).data ?? [];
    addDialog({
      title: `分配 ${row.username} 用户的角色`,
      props: {
        formInline: {
          username: row?.username ?? "",
          nickname: row?.nickname ?? "",
          roleOptions: roleOptions.value ?? [],
          ids
        }
      },
      width: "400px",
      draggable: true,
      fullscreen: deviceDetection(),
      fullscreenIcon: true,
      closeOnClickModal: false,
      contentRenderer: () => h(roleForm),
      beforeSure: (done, { options }) => {
        const curData = options.props.formInline as RoleFormItemProps;
        console.log("curIds", curData.ids);
        // 根据实际业务使用curData.ids和row里的某些字段去调用修改角色接口即可
        done(); // 关闭弹框
      }
    });
  }
  onMounted(async () => {
    treeLoading.value = true;
    onSearch();
    // 归属部门
    const { data } = await getDeptList();
    higherDeptOptions.value = handleTree(data);
    treeData.value = handleTree(data);
    treeLoading.value = false;
    // 角色列表
    roleOptions.value = (await getAllRoleList()).data;
  });
  return {
    form,
    loading,
    columns,
    dataList,
    treeData,
    treeLoading,
    selectedNum,
    pagination,
    buttonClass,
    deviceDetection,
    onSearch,
    resetForm,
    onbatchDel,
    openDialog,
    onTreeSelect,
    handleUpdate,
    handleDelete,
    handleUpload,
    handleReset,
    handleRole,
    handleSizeChange,
    onSelectionCancel,
    handleCurrentChange,
    handleSelectionChange
  };
}
src/views/system/user/utils/reset.css
New file
@@ -0,0 +1,5 @@
/** 局部重置 ElProgress 的部分样式 */
.el-progress-bar__outer,
.el-progress-bar__inner {
  border-radius: 0;
}
src/views/system/user/utils/rule.ts
New file
@@ -0,0 +1,39 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
import { isPhone, isEmail } from "@pureadmin/utils";
/** 自定义表单规则校验 */
export const formRules = reactive(<FormRules>{
  nickname: [{ required: true, message: "用户昵称为必填项", trigger: "blur" }],
  username: [{ required: true, message: "用户名称为必填项", trigger: "blur" }],
  password: [{ required: true, message: "用户密码为必填项", trigger: "blur" }],
  phone: [
    {
      validator: (rule, value, callback) => {
        if (value === "") {
          callback();
        } else if (!isPhone(value)) {
          callback(new Error("请输入正确的手机号码格式"));
        } else {
          callback();
        }
      },
      trigger: "blur"
      // trigger: "click" // 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
    }
  ],
  email: [
    {
      validator: (rule, value, callback) => {
        if (value === "") {
          callback();
        } else if (!isEmail(value)) {
          callback(new Error("请输入正确的邮箱格式"));
        } else {
          callback();
        }
      },
      trigger: "blur"
    }
  ]
});
src/views/system/user/utils/types.ts
New file
@@ -0,0 +1,36 @@
interface FormItemProps {
  id?: number;
  /** 用于判断是`新增`还是`修改` */
  title: string;
  higherDeptOptions: Record<string, unknown>[];
  parentId: number;
  nickname: string;
  username: string;
  password: string;
  phone: string | number;
  email: string;
  sex: string | number;
  status: number;
  dept?: {
    id?: number;
    name?: string;
  };
  remark: string;
}
interface FormProps {
  formInline: FormItemProps;
}
interface RoleFormItemProps {
  username: string;
  nickname: string;
  /** 角色列表 */
  roleOptions: any[];
  /** 选中的角色列表 */
  ids: Record<number, unknown>[];
}
interface RoleFormProps {
  formInline: RoleFormItemProps;
}
export type { FormItemProps, FormProps, RoleFormItemProps, RoleFormProps };