weixin_52219567 2 months ago
parent
commit
4fa611d79f
100 changed files with 6819 additions and 3671 deletions
  1. BIN
      src/assets/images/1、密码登录_slices/Frame_27893.png
  2. BIN
      src/assets/images/1、密码登录_slices/Rectangle 1276.png
  3. BIN
      src/assets/images/1、密码登录_slices/Rectangle 1276@2x.png
  4. BIN
      src/assets/images/1、密码登录_slices/Rectangle_296.png
  5. BIN
      src/assets/images/1、密码登录_slices/image 5.png
  6. BIN
      src/assets/images/1、密码登录_slices/image 5@2x.png
  7. BIN
      src/assets/images/1、密码登录_slices/image_5.png
  8. BIN
      src/assets/images/1、密码登录_slices/image_5@2x.png
  9. BIN
      src/assets/images/layout/layout1.png
  10. BIN
      src/assets/images/layout/layout2.png
  11. BIN
      src/assets/images/layout/layout3.png
  12. BIN
      src/assets/images/layout/layout4.png
  13. 0 0
      src/assets/logo.png
  14. 0 90
      src/components/Breadcrumb/index.vue
  15. 0 99
      src/components/DictTag/index.vue
  16. 0 244
      src/components/Editor/index.vue
  17. 25 0
      src/components/EmptyState/index.vue
  18. 0 241
      src/components/FileUpload/index.vue
  19. 0 35
      src/components/Hamburger/index.vue
  20. 169 0
      src/components/HeaderBar/index.vue
  21. 0 104
      src/components/IconSelect/index.vue
  22. 0 7
      src/components/IconSelect/requireIcons.ts
  23. 0 79
      src/components/ImagePreview/index.vue
  24. 0 242
      src/components/ImageUpload/index.vue
  25. 0 39
      src/components/LangSelect/index.vue
  26. 29 0
      src/components/PageTitle/index.vue
  27. 0 78
      src/components/Pagination/index.vue
  28. 0 3
      src/components/ParentView/index.vue
  29. 0 100
      src/components/Process/MessageType.vue
  30. 0 57
      src/components/Process/approvalButton.vue
  31. 0 127
      src/components/Process/approvalRecord.vue
  32. 0 40
      src/components/Process/flowChart.vue
  33. 0 154
      src/components/Process/flowChartImg.vue
  34. 0 211
      src/components/Process/processMeddle.vue
  35. 0 541
      src/components/Process/submitVerify.vue
  36. 138 0
      src/components/ProductCard/index.vue
  37. 64 0
      src/components/ProductItem/index.vue
  38. 0 102
      src/components/RightToolbar/index.vue
  39. 0 250
      src/components/RoleSelect/index.vue
  40. 0 13
      src/components/RuoYiDoc/index.vue
  41. 0 13
      src/components/RuoYiGit/index.vue
  42. 0 9
      src/components/Screenfull/index.vue
  43. 124 0
      src/components/SearchBar/index.vue
  44. 0 41
      src/components/SizeSelect/index.vue
  45. 47 0
      src/components/StatCards/index.vue
  46. 75 0
      src/components/StatusTabs/index.vue
  47. 0 40
      src/components/SvgIcon/index.vue
  48. 57 0
      src/components/TableActions/index.vue
  49. 35 0
      src/components/TablePagination/index.vue
  50. 0 200
      src/components/TopNav/index.vue
  51. 0 311
      src/components/UserSelect/index.vue
  52. 0 26
      src/components/iFrame/index.vue
  53. 24 0
      src/components/index.ts
  54. 41 0
      src/constants/status.ts
  55. 45 4
      src/layout/components/breadcrumb.vue
  56. 15 16
      src/layout/components/foot.vue
  57. 1 1
      src/layout/components/header.vue
  58. 1 0
      src/layout/components/index.ts
  59. 1 1
      src/layout/components/nav.vue
  60. 24 3
      src/layout/components/search.vue
  61. 239 0
      src/layout/components/workbench.vue
  62. 0 135
      src/layout/index copy.vue
  63. 30 6
      src/layout/index.vue
  64. 242 8
      src/router/index.ts
  65. 11 0
      src/utils/status.ts
  66. 72 0
      src/views/analysis/deptPurchase/index.vue
  67. 199 0
      src/views/analysis/orderAnalysis/index.vue
  68. 65 0
      src/views/analysis/orderStatus/index.vue
  69. 122 0
      src/views/analysis/purchaseDetail/index.vue
  70. 78 0
      src/views/analysis/settlementStatus/index.vue
  71. 159 0
      src/views/cost/itemExpense/index.vue
  72. 173 0
      src/views/cost/quotaControl/apply.vue
  73. 145 0
      src/views/cost/quotaControl/index.vue
  74. 95 0
      src/views/enterprise/addressManage/index.vue
  75. 84 0
      src/views/enterprise/agreementSupply/index.vue
  76. 335 0
      src/views/enterprise/companyInfo/edit.vue
  77. 477 0
      src/views/enterprise/companyInfo/index.vue
  78. 99 0
      src/views/enterprise/invoiceManage/index.vue
  79. 89 0
      src/views/enterprise/messageNotice/index.vue
  80. 91 0
      src/views/enterprise/myCollection/index.vue
  81. 66 0
      src/views/enterprise/myFootprint/index.vue
  82. 144 0
      src/views/enterprise/purchaseHabit/index.vue
  83. 149 0
      src/views/enterprise/purchaseHistory/index.vue
  84. 64 0
      src/views/enterprise/purchasePlan/index.vue
  85. 408 0
      src/views/enterprise/securitySetting/changePhone.vue
  86. 145 0
      src/views/enterprise/securitySetting/index.vue
  87. 350 0
      src/views/enterprise/securitySetting/resetPassword.vue
  88. 183 0
      src/views/organization/approvalFlow/create.vue
  89. 93 0
      src/views/organization/approvalFlow/index.vue
  90. 159 0
      src/views/organization/deptManage/index.vue
  91. 190 0
      src/views/organization/groupEnterprise/index.vue
  92. 15 0
      src/views/organization/itemExpense/index.vue
  93. 167 0
      src/views/organization/personalInfo/index.vue
  94. 230 0
      src/views/organization/roleManage/index.vue
  95. 276 0
      src/views/organization/staffManage/index.vue
  96. 112 0
      src/views/reconciliation/billManage/index.vue
  97. 346 0
      src/views/reconciliation/invoiceManage/index.vue
  98. 1 0
      src/views/shop/cart.vue
  99. 1 0
      src/views/shop/create.vue
  100. 0 1
      src/views/shop/info.vue

BIN
src/assets/images/1、密码登录_slices/Frame_27893.png


BIN
src/assets/images/1、密码登录_slices/Rectangle 1276.png


BIN
src/assets/images/1、密码登录_slices/Rectangle 1276@2x.png


BIN
src/assets/images/1、密码登录_slices/Rectangle_296.png


BIN
src/assets/images/1、密码登录_slices/image 5.png


BIN
src/assets/images/1、密码登录_slices/image 5@2x.png


BIN
src/assets/images/1、密码登录_slices/image_5.png


BIN
src/assets/images/1、密码登录_slices/image_5@2x.png


BIN
src/assets/images/layout/layout1.png


BIN
src/assets/images/layout/layout2.png


BIN
src/assets/images/layout/layout3.png


BIN
src/assets/images/layout/layout4.png


+ 0 - 0
src/assets/logo.png


+ 0 - 90
src/components/Breadcrumb/index.vue

@@ -1,90 +0,0 @@
-<template>
-  <el-breadcrumb class="app-breadcrumb" separator="/">
-    <transition-group name="breadcrumb">
-      <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
-        <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta?.title }}</span>
-        <a v-else @click.prevent="handleLink(item)">{{ item.meta?.title }}</a>
-      </el-breadcrumb-item>
-    </transition-group>
-  </el-breadcrumb>
-</template>
-
-<script setup lang="ts">
-import { RouteLocationMatched } from 'vue-router';
-import { usePermissionStore } from '@/store/modules/permission';
-
-const route = useRoute();
-const router = useRouter();
-const permissionStore = usePermissionStore();
-const levelList = ref<RouteLocationMatched[]>([]);
-
-const getBreadcrumb = () => {
-  // only show routes with meta.title
-  let matched = [];
-  const pathNum = findPathNum(route.path);
-  // multi-level menu
-  if (pathNum > 2) {
-    const reg = /\/\w+/gi;
-    const pathList = route.path.match(reg).map((item, index) => {
-      if (index !== 0) item = item.slice(1);
-      return item;
-    });
-    getMatched(pathList, permissionStore.defaultRoutes, matched);
-  } else {
-    matched = route.matched.filter((item) => item.meta && item.meta.title);
-  }
-  // 判断是否为首页
-  if (!isDashboard(matched[0])) {
-    matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched);
-  }
-  levelList.value = matched.filter((item) => item.meta && item.meta.title && item.meta.breadcrumb !== false);
-};
-const findPathNum = (str, char = '/') => {
-  if (typeof str !== 'string' || str.length === 0) return 0;
-  return str.split(char).length - 1;
-};
-const getMatched = (pathList, routeList, matched) => {
-  const data = routeList.find((item) => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0]);
-  if (data) {
-    matched.push(data);
-    if (data.children && pathList.length) {
-      pathList.shift();
-      getMatched(pathList, data.children, matched);
-    }
-  }
-};
-const isDashboard = (route: RouteLocationMatched) => {
-  const name = route && (route.name as string);
-  if (!name) {
-    return false;
-  }
-  return name.trim() === 'Index';
-};
-const handleLink = (item) => {
-  const { redirect, path } = item;
-  redirect ? router.push(redirect) : router.push(path);
-};
-
-watchEffect(() => {
-  // if you go to the redirect page, do not update the breadcrumbs
-  if (route.path.startsWith('/redirect/')) return;
-  getBreadcrumb();
-});
-onMounted(() => {
-  getBreadcrumb();
-});
-</script>
-
-<style lang="scss" scoped>
-.app-breadcrumb.el-breadcrumb {
-  display: inline-block;
-  font-size: 14px;
-  line-height: 50px;
-  margin-left: 8px;
-
-  .no-redirect {
-    color: #97a8be;
-    cursor: text;
-  }
-}
-</style>

+ 0 - 99
src/components/DictTag/index.vue

@@ -1,99 +0,0 @@
-<template>
-  <div>
-    <template v-for="(item, index) in options">
-      <template v-if="isValueMatch(item.value)">
-        <span
-          v-if="(item.elTagType === 'default' || item.elTagType === '') && (item.elTagClass === '' || item.elTagClass == null)"
-          :key="item.value"
-          :index="index"
-          :class="item.elTagClass"
-        >
-          {{ item.label + ' ' }}
-        </span>
-        <el-tag
-          v-else
-          :key="item.value + ''"
-          :disable-transitions="true"
-          :index="index"
-          :type="
-            item.elTagType === 'primary' ||
-            item.elTagType === 'success' ||
-            item.elTagType === 'info' ||
-            item.elTagType === 'warning' ||
-            item.elTagType === 'danger'
-              ? item.elTagType
-              : 'primary'
-          "
-          :class="item.elTagClass"
-        >
-          {{ item.label + ' ' }}
-        </el-tag>
-      </template>
-    </template>
-    <template v-if="unmatch && showValue">
-      {{ unmatchArray }}
-    </template>
-  </div>
-</template>
-
-<script setup lang="ts">
-interface Props {
-  options: Array<DictDataOption>;
-  value: number | string | Array<number | string>;
-  showValue?: boolean;
-  separator?: string;
-}
-const props = withDefaults(defineProps<Props>(), {
-  showValue: true,
-  separator: ','
-});
-
-const values = computed(() => {
-  if (props.value === '' || props.value === null || typeof props.value === 'undefined') return [];
-  if (typeof props.value === 'number' || typeof props.value === 'boolean') return [props.value]
-  return Array.isArray(props.value) ? props.value.map((item) => '' + item) : String(props.value).split(props.separator);
-});
-
-const unmatch = computed(() => {
-  if (props.options?.length == 0 || props.value === '' || props.value === null || typeof props.value === 'undefined') return false;
-  // 传入值为非数组
-  let unmatch = false; // 添加一个标志来判断是否有未匹配项
-  values.value.forEach((item) => {
-    if (!props.options.some((v) => v.value == item)) {
-      unmatch = true; // 如果有未匹配项,将标志设置为true
-    }
-  });
-  return unmatch; // 返回标志的值
-});
-
-const unmatchArray = computed(() => {
-  // 记录未匹配的项
-  const itemUnmatchArray: Array<string | number> = [];
-  if (props.value !== '' && props.value !== null && typeof props.value !== 'undefined') {
-    values.value.forEach((item) => {
-      if (!props.options.some((v) => v.value === item)) {
-        itemUnmatchArray.push(item);
-      }
-    });
-  }
-  // 没有value不显示
-  return handleArray(itemUnmatchArray);
-});
-
-const handleArray = (array: Array<string | number>) => {
-  if (array.length === 0) return '';
-  return array.reduce((pre, cur) => {
-    return pre + ' ' + cur;
-  });
-};
-
-const isValueMatch = (itemValue: any) => {
-  return values.value.some(val => val == itemValue)
-}
-</script>
-
-<style lang="scss" scoped>
-.el-tag + .el-tag {
-  margin-left: 10px;
-}
-</style>

+ 0 - 244
src/components/Editor/index.vue

@@ -1,244 +0,0 @@
-<template>
-  <div>
-    <el-upload
-      v-if="type === 'url'"
-      :action="upload.url"
-      :before-upload="handleBeforeUpload"
-      :on-success="handleUploadSuccess"
-      :on-error="handleUploadError"
-      class="editor-img-uploader"
-      name="file"
-      :show-file-list="false"
-      :headers="upload.headers"
-    >
-      <i ref="uploadRef"></i>
-    </el-upload>
-  </div>
-  <div class="editor">
-    <quill-editor
-      ref="quillEditorRef"
-      v-model:content="content"
-      content-type="html"
-      :options="options"
-      :style="styles"
-      @text-change="(e: any) => $emit('update:modelValue', content)"
-    />
-  </div>
-</template>
-
-<script setup lang="ts">
-import '@vueup/vue-quill/dist/vue-quill.snow.css';
-
-import { QuillEditor, Quill } from '@vueup/vue-quill';
-import { propTypes } from '@/utils/propTypes';
-import { globalHeaders } from '@/utils/request';
-
-defineEmits(['update:modelValue']);
-
-const props = defineProps({
-  /* 编辑器的内容 */
-  modelValue: propTypes.string,
-  /* 高度 */
-  height: propTypes.number.def(400),
-  /* 最小高度 */
-  minHeight: propTypes.number.def(400),
-  /* 只读 */
-  readOnly: propTypes.bool.def(false),
-  /* 上传文件大小限制(MB) */
-  fileSize: propTypes.number.def(5),
-  /* 类型(base64格式、url格式) */
-  type: propTypes.string.def('url')
-});
-
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-
-const upload = reactive<UploadOption>({
-  headers: globalHeaders(),
-  url: import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'
-});
-const quillEditorRef = ref();
-const uploadRef = ref<HTMLDivElement>();
-
-const options = ref<any>({
-  theme: 'snow',
-  bounds: document.body,
-  debug: 'warn',
-  modules: {
-    // 工具栏配置
-    toolbar: {
-      container: [
-        ['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
-        ['blockquote', 'code-block'], // 引用  代码块
-        [{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
-        [{ indent: '-1' }, { indent: '+1' }], // 缩进
-        [{ size: ['small', false, 'large', 'huge'] }], // 字体大小
-        [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
-        [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
-        [{ align: [] }], // 对齐方式
-        ['clean'], // 清除文本格式
-        ['link', 'image', 'video'] // 链接、图片、视频
-      ],
-      handlers: {
-        image: (value: boolean) => {
-          if (value) {
-            // 调用element图片上传
-            uploadRef.value.click();
-          } else {
-            Quill.format('image', true);
-          }
-        }
-      }
-    }
-  },
-  placeholder: '请输入内容',
-  readOnly: props.readOnly
-});
-
-const styles = computed(() => {
-  const style: any = {};
-  if (props.minHeight) {
-    style.minHeight = `${props.minHeight}px`;
-  }
-  if (props.height) {
-    style.height = `${props.height}px`;
-  }
-  return style;
-});
-
-const content = ref('');
-watch(
-  () => props.modelValue,
-  (v: string) => {
-    if (v !== content.value) {
-      content.value = v || '<p></p>';
-    }
-  },
-  { immediate: true }
-);
-
-// 图片上传成功返回图片地址
-const handleUploadSuccess = (res: any) => {
-  // 如果上传成功
-  if (res.code === 200) {
-    // 获取富文本实例
-    const quill = toRaw(quillEditorRef.value).getQuill();
-    // 获取光标位置
-    const length = quill.selection.savedRange.index;
-    // 插入图片,res为服务器返回的图片链接地址
-    quill.insertEmbed(length, 'image', res.data.url);
-    // 调整光标到最后
-    quill.setSelection(length + 1);
-    proxy?.$modal.closeLoading();
-  } else {
-    proxy?.$modal.msgError('图片插入失败');
-    proxy?.$modal.closeLoading();
-  }
-};
-
-// 图片上传前拦截
-const handleBeforeUpload = (file: any) => {
-  const type = ['image/jpeg', 'image/jpg', 'image/png', 'image/svg'];
-  const isJPG = type.includes(file.type);
-  //检验文件格式
-  if (!isJPG) {
-    proxy?.$modal.msgError(`图片格式错误!`);
-    return false;
-  }
-  // 校检文件大小
-  if (props.fileSize) {
-    const isLt = file.size / 1024 / 1024 < props.fileSize;
-    if (!isLt) {
-      proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
-      return false;
-    }
-  }
-  proxy?.$modal.loading('正在上传文件,请稍候...');
-  return true;
-};
-
-// 图片失败拦截
-const handleUploadError = (err: any) => {
-  proxy?.$modal.msgError('上传文件失败');
-};
-</script>
-
-<style>
-.editor-img-uploader {
-  display: none;
-}
-.editor,
-.ql-toolbar {
-  white-space: pre-wrap !important;
-  line-height: normal !important;
-}
-.quill-img {
-  display: none;
-}
-.ql-snow .ql-tooltip[data-mode='link']::before {
-  content: '请输入链接地址:';
-}
-.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
-  border-right: 0;
-  content: '保存';
-  padding-right: 0;
-}
-.ql-snow .ql-tooltip[data-mode='video']::before {
-  content: '请输入视频地址:';
-}
-.ql-snow .ql-picker.ql-size .ql-picker-label::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item::before {
-  content: '14px';
-}
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
-  content: '10px';
-}
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
-  content: '18px';
-}
-.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
-.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
-  content: '32px';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item::before {
-  content: '文本';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
-  content: '标题1';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
-  content: '标题2';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
-  content: '标题3';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
-  content: '标题4';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
-  content: '标题5';
-}
-.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
-.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
-  content: '标题6';
-}
-.ql-snow .ql-picker.ql-font .ql-picker-label::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item::before {
-  content: '标准字体';
-}
-.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
-  content: '衬线字体';
-}
-.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
-.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
-  content: '等宽字体';
-}
-</style>

+ 25 - 0
src/components/EmptyState/index.vue

@@ -0,0 +1,25 @@
+<template>
+  <div class="empty-state">
+    <el-empty :description="description" :image-size="imageSize">
+      <template v-if="$slots.default" #default>
+        <slot></slot>
+      </template>
+    </el-empty>
+  </div>
+</template>
+
+<script setup lang="ts">
+withDefaults(defineProps<{
+  description?: string
+  imageSize?: number
+}>(), {
+  description: '暂无数据',
+  imageSize: 120
+})
+</script>
+
+<style scoped lang="scss">
+.empty-state {
+  padding: 40px 0;
+}
+</style>

+ 0 - 241
src/components/FileUpload/index.vue

@@ -1,241 +0,0 @@
-<template>
-  <div class="upload-file">
-    <el-upload
-      ref="fileUploadRef"
-      multiple
-      :action="uploadFileUrl"
-      :before-upload="handleBeforeUpload"
-      :file-list="fileList"
-      :limit="limit"
-      :accept="fileAccept"
-      :on-error="handleUploadError"
-      :on-exceed="handleExceed"
-      :on-success="handleUploadSuccess"
-      :show-file-list="false"
-      :headers="headers"
-      class="upload-file-uploader"
-      v-if="!disabled"
-    >
-      <!-- 上传按钮 -->
-      <el-button type="primary">选取文件</el-button>
-    </el-upload>
-    <!-- 上传提示 -->
-    <div v-if="showTip && !disabled" class="el-upload__tip">
-      请上传
-      <template v-if="fileSize">
-        大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
-      </template>
-      <template v-if="fileType">
-        格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
-      </template>
-      的文件
-    </div>
-    <!-- 文件列表 -->
-    <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
-      <li v-for="(file, index) in fileList" :key="file.uid" class="el-upload-list__item ele-upload-list__item-content">
-        <el-link :href="`${file.url}`" :underline="false" target="_blank">
-          <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
-        </el-link>
-        <div class="ele-upload-list__item-content-action">
-          <el-button type="danger" v-if="!disabled" link @click="handleDelete(index)">删除</el-button>
-        </div>
-      </li>
-    </transition-group>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { propTypes } from '@/utils/propTypes';
-import { delOss, listByIds } from '@/api/system/oss';
-import { globalHeaders } from '@/utils/request';
-
-const props = defineProps({
-  modelValue: {
-    type: [String, Object, Array],
-    default: () => []
-  },
-  // 数量限制
-  limit: propTypes.number.def(5),
-  // 大小限制(MB)
-  fileSize: propTypes.number.def(5),
-  // 文件类型, 例如['png', 'jpg', 'jpeg']
-  fileType: propTypes.array.def(['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'pdf']),
-  // 是否显示提示
-  isShowTip: propTypes.bool.def(true),
-  // 禁用组件(仅查看文件)
-  disabled: propTypes.bool.def(false)
-});
-
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const emit = defineEmits(['update:modelValue']);
-const number = ref(0);
-const uploadList = ref<any[]>([]);
-
-const baseUrl = import.meta.env.VITE_APP_BASE_API;
-const uploadFileUrl = ref(baseUrl + '/resource/oss/upload'); // 上传文件服务器地址
-const headers = ref(globalHeaders());
-
-const fileList = ref<any[]>([]);
-const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
-
-const fileUploadRef = ref<ElUploadInstance>();
-
-// 监听 fileType 变化,更新 fileAccept
-const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','));
-
-watch(
-  () => props.modelValue,
-  async (val) => {
-    if (val) {
-      let temp = 1;
-      // 首先将值转为数组
-      let list: any[] = [];
-      if (Array.isArray(val)) {
-        list = val;
-      } else {
-        const res = await listByIds(val);
-        list = res.data.map((oss) => {
-          return {
-            name: oss.originalName,
-            url: oss.url,
-            ossId: oss.ossId
-          };
-        });
-      }
-      // 然后将数组转为对象数组
-      fileList.value = list.map((item) => {
-        item = { name: item.name, url: item.url, ossId: item.ossId };
-        item.uid = item.uid || new Date().getTime() + temp++;
-        return item;
-      });
-    } else {
-      fileList.value = [];
-      return [];
-    }
-  },
-  { deep: true, immediate: true }
-);
-
-// 上传前校检格式和大小
-const handleBeforeUpload = (file: any) => {
-  // 校检文件类型
-  if (props.fileType.length) {
-    const fileName = file.name.split('.');
-    const fileExt = fileName[fileName.length - 1];
-    const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
-    if (!isTypeOk) {
-      proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`);
-      return false;
-    }
-  }
-  // 校检文件名是否包含特殊字符
-  if (file.name.includes(',')) {
-    proxy?.$modal.msgError('文件名不正确,不能包含英文逗号!');
-    return false;
-  }
-  // 校检文件大小
-  if (props.fileSize) {
-    const isLt = file.size / 1024 / 1024 < props.fileSize;
-    if (!isLt) {
-      proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
-      return false;
-    }
-  }
-  proxy?.$modal.loading('正在上传文件,请稍候...');
-  number.value++;
-  return true;
-};
-
-// 文件个数超出
-const handleExceed = () => {
-  proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
-};
-
-// 上传失败
-const handleUploadError = () => {
-  proxy?.$modal.msgError('上传文件失败');
-};
-
-// 上传成功回调
-const handleUploadSuccess = (res: any, file: UploadFile) => {
-  if (res.code === 200) {
-    uploadList.value.push({
-      name: res.data.fileName,
-      url: res.data.url,
-      ossId: res.data.ossId
-    });
-    uploadedSuccessfully();
-  } else {
-    number.value--;
-    proxy?.$modal.closeLoading();
-    proxy?.$modal.msgError(res.msg);
-    fileUploadRef.value?.handleRemove(file);
-    uploadedSuccessfully();
-  }
-};
-
-// 删除文件
-const handleDelete = (index: number) => {
-  const ossId = fileList.value[index].ossId;
-  delOss(ossId);
-  fileList.value.splice(index, 1);
-  emit('update:modelValue', listToString(fileList.value));
-};
-
-// 上传结束处理
-const uploadedSuccessfully = () => {
-  if (number.value > 0 && uploadList.value.length === number.value) {
-    fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
-    uploadList.value = [];
-    number.value = 0;
-    emit('update:modelValue', listToString(fileList.value));
-    proxy?.$modal.closeLoading();
-  }
-};
-
-// 获取文件名称
-const getFileName = (name: string) => {
-  // 如果是url那么取最后的名字 如果不是直接返回
-  if (name.lastIndexOf('/') > -1) {
-    return name.slice(name.lastIndexOf('/') + 1);
-  } else {
-    return name;
-  }
-};
-
-// 对象转成指定字符串分隔
-const listToString = (list: any[], separator?: string) => {
-  let strs = '';
-  separator = separator || ',';
-  list.forEach((item) => {
-    if (item.ossId) {
-      strs += item.ossId + separator;
-    }
-  });
-  return strs != '' ? strs.substring(0, strs.length - 1) : '';
-};
-</script>
-
-<style lang="scss" scoped>
-.upload-file-uploader {
-  margin-bottom: 5px;
-}
-
-.upload-file-list .el-upload-list__item {
-  border: 1px solid #e4e7ed;
-  line-height: 2;
-  margin-bottom: 10px;
-  position: relative;
-}
-
-.upload-file-list .ele-upload-list__item-content {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  color: inherit;
-}
-
-.ele-upload-list__item-content-action .el-link {
-  margin-right: 10px;
-}
-</style>

+ 0 - 35
src/components/Hamburger/index.vue

@@ -1,35 +0,0 @@
-<template>
-  <div style="padding: 0 15px" @click="toggleClick">
-    <svg :class="{ 'is-active': isActive }" class="hamburger" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="64" height="64">
-      <path
-        d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
-      />
-    </svg>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { propTypes } from '@/utils/propTypes';
-
-defineProps({
-  isActive: propTypes.bool.def(false)
-});
-
-const emit = defineEmits(['toggleClick']);
-const toggleClick = () => {
-  emit('toggleClick');
-};
-</script>
-
-<style scoped>
-.hamburger {
-  display: inline-block;
-  vertical-align: middle;
-  width: 20px;
-  height: 20px;
-}
-
-.hamburger.is-active {
-  transform: rotate(180deg);
-}
-</style>

+ 169 - 0
src/components/HeaderBar/index.vue

@@ -0,0 +1,169 @@
+<template>
+  <header class="layout-header">
+    <!-- 顶部工具栏 -->
+    <div class="header-top">
+      <div class="header-top-content">
+        <div class="location"><el-icon><Location /></el-icon> 武汉</div>
+        <div class="top-links">
+          <span>您好,请登录</span>
+          <span class="link highlight">免费注册</span>
+          <span class="divider">|</span>
+          <span class="link">工作台</span>
+          <span class="divider">|</span>
+          <span class="link">采购合作 <el-icon><ArrowDown /></el-icon></span>
+          <span class="divider">|</span>
+          <span class="link">客户服务 <el-icon><ArrowDown /></el-icon></span>
+          <span class="divider">|</span>
+          <span class="link">帮助中心</span>
+          <span class="divider">|</span>
+          <span class="link">在线客服</span>
+        </div>
+      </div>
+    </div>
+    <!-- 主导航栏 -->
+    <div class="header-main">
+      <div class="header-main-content">
+        <div class="logo">
+          <img src="@/assets/logo.png" alt="logo" class="logo-img" />
+          <span class="logo-text">优易</span>
+        </div>
+        <div class="search-box">
+          <el-input v-model="searchKeyword" placeholder="搜索商品、品牌、分类..." class="search-input" @keyup.enter="handleSearch">
+            <template #append>
+              <el-button type="danger" class="search-btn" @click="handleSearch"><el-icon><Search /></el-icon></el-button>
+            </template>
+          </el-input>
+          <div class="hot-keywords">
+            <span v-for="(word, index) in hotKeywords" :key="index" class="keyword" @click="handleKeywordClick(word)">{{ word }}</span>
+          </div>
+        </div>
+        <div class="cart-box">
+          <el-button class="cart-btn" @click="handleCartClick"><el-icon><ShoppingCart /></el-icon> 我的采购车</el-button>
+        </div>
+        <div class="qrcode-box">
+          <div class="qrcode-placeholder"></div>
+        </div>
+      </div>
+    </div>
+  </header>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { Location, ArrowDown, Search, ShoppingCart } from '@element-plus/icons-vue'
+
+const searchKeyword = ref('')
+const hotKeywords = ['家纺', '打印机', '打印耗材', '空调', '取暖', '开门红', '劳保福利']
+
+const emit = defineEmits<{
+  search: [keyword: string]
+  cartClick: []
+}>()
+
+const handleSearch = () => {
+  emit('search', searchKeyword.value)
+}
+
+const handleKeywordClick = (word: string) => {
+  searchKeyword.value = word
+  emit('search', word)
+}
+
+const handleCartClick = () => {
+  emit('cartClick')
+}
+</script>
+
+<style scoped lang="scss">
+.layout-header {
+  background: #fff;
+  
+  .header-top {
+    background: #f5f5f5;
+    font-size: 12px;
+    color: #666;
+    
+    .header-top-content {
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 8px 0;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+    
+    .location {
+      display: flex;
+      align-items: center;
+      gap: 4px;
+    }
+    
+    .top-links {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      
+      .link {
+        cursor: pointer;
+        display: flex;
+        align-items: center;
+        gap: 2px;
+        &:hover { color: #e60012; }
+        &.highlight { color: #e60012; }
+      }
+      .divider { color: #ddd; }
+    }
+  }
+  
+  .header-main {
+    border-bottom: 2px solid #e60012;
+    
+    .header-main-content {
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 20px 0;
+      display: flex;
+      align-items: center;
+      gap: 40px;
+    }
+    
+    .logo {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      cursor: pointer;
+      .logo-img { height: 50px; }
+      .logo-text { font-size: 28px; font-weight: bold; color: #e60012; }
+    }
+    
+    .search-box {
+      flex: 1;
+      .search-input {
+        :deep(.el-input__wrapper) { border-radius: 0; }
+        :deep(.el-input-group__append) { padding: 0; background: #e60012; border: none; }
+      }
+      .search-btn { background: #e60012; border: none; color: #fff; height: 40px; width: 60px; border-radius: 0; }
+      .hot-keywords {
+        margin-top: 8px;
+        display: flex;
+        gap: 16px;
+        .keyword { font-size: 12px; color: #666; cursor: pointer; &:hover { color: #e60012; } }
+      }
+    }
+    
+    .cart-box {
+      .cart-btn {
+        border: 1px solid #e60012;
+        color: #e60012;
+        background: #fff;
+        padding: 10px 20px;
+        &:hover { background: #fff5f5; }
+      }
+    }
+    
+    .qrcode-box {
+      .qrcode-placeholder { width: 80px; height: 80px; background: #f5f5f5; border: 1px solid #eee; }
+    }
+  }
+}
+</style>

+ 0 - 104
src/components/IconSelect/index.vue

@@ -1,104 +0,0 @@
-<template>
-  <div class="relative" :style="{ 'width': width }">
-    <el-input v-model="modelValue" readonly placeholder="点击选择图标" @click="visible = !visible">
-      <template #prepend>
-        <svg-icon :icon-class="modelValue" />
-      </template>
-    </el-input>
-
-    <el-popover shadow="none" :visible="visible" placement="bottom-end" trigger="click" :width="450">
-      <template #reference>
-        <div class="cursor-pointer text-[#999] absolute right-[10px] top-0 height-[32px] leading-[32px]" @click="visible = !visible">
-          <i-ep-caret-top v-show="visible"></i-ep-caret-top>
-          <i-ep-caret-bottom v-show="!visible"></i-ep-caret-bottom>
-        </div>
-      </template>
-
-      <el-input v-model="filterValue" class="p-2" placeholder="搜索图标" clearable @input="filterIcons" />
-
-      <el-scrollbar height="w-[200px]">
-        <ul class="icon-list">
-          <el-tooltip v-for="(iconName, index) in iconNames" :key="index" :content="iconName" placement="bottom" effect="light">
-            <li :class="['icon-item', { active: modelValue == iconName }]" @click="selectedIcon(iconName)">
-              <svg-icon color="var(--el-text-color-regular)" :icon-class="iconName" />
-            </li>
-          </el-tooltip>
-        </ul>
-      </el-scrollbar>
-    </el-popover>
-  </div>
-</template>
-
-<script setup lang="ts">
-import icons from '@/components/IconSelect/requireIcons';
-import { propTypes } from '@/utils/propTypes';
-
-const props = defineProps({
-  modelValue: propTypes.string.isRequired,
-  width: propTypes.string.def('400px')
-});
-
-const emit = defineEmits(['update:modelValue']);
-const visible = ref(false);
-const { modelValue, width } = toRefs(props);
-const iconNames = ref<string[]>(icons);
-
-const filterValue = ref('');
-
-/**
- * 筛选图标
- */
-const filterIcons = () => {
-  if (filterValue.value) {
-    iconNames.value = icons.filter((iconName) => iconName.includes(filterValue.value));
-  } else {
-    iconNames.value = icons;
-  }
-};
-/**
- * 选择图标
- * @param iconName 选择的图标名称
- */
-const selectedIcon = (iconName: string) => {
-  emit('update:modelValue', iconName);
-  visible.value = false;
-};
-</script>
-
-<style lang="scss" scoped>
-.el-scrollbar {
-  max-height: calc(50vh - 100px) !important;
-  overflow-y: auto;
-}
-.el-divider--horizontal {
-  margin: 10px auto !important;
-}
-.icon-list {
-  display: flex;
-  flex-wrap: wrap;
-  padding-left: 10px;
-  margin-top: 10px;
-
-  .icon-item {
-    cursor: pointer;
-    width: 10%;
-    margin: 0 10px 10px 0;
-    padding: 5px;
-    display: flex;
-    flex-direction: column;
-    justify-items: center;
-    align-items: center;
-    border: 1px solid #ccc;
-    &:hover {
-      border-color: var(--el-color-primary);
-      color: var(--el-color-primary);
-      transition: all 0.2s;
-      transform: scaleX(1.1);
-    }
-  }
-  .active {
-    border-color: var(--el-color-primary);
-    color: var(--el-color-primary);
-  }
-}
-</style>

+ 0 - 7
src/components/IconSelect/requireIcons.ts

@@ -1,7 +0,0 @@
-const icons: string[] = [];
-const modules = import.meta.glob('./../../assets/icons/svg/*.svg');
-for (const path in modules) {
-  const p = path.split('assets/icons/svg/')[1].split('.svg')[0];
-  icons.push(p);
-}
-export default icons;

+ 0 - 79
src/components/ImagePreview/index.vue

@@ -1,79 +0,0 @@
-<template>
-  <el-image :src="`${realSrc}`" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="realSrcList" preview-teleported>
-    <template #error>
-      <div class="image-slot">
-        <el-icon><picture-filled /></el-icon>
-      </div>
-    </template>
-  </el-image>
-</template>
-
-<script setup lang="ts">
-import { propTypes } from '@/utils/propTypes';
-
-const props = defineProps({
-  src: propTypes.string.def(''),
-  width: {
-    type: [Number, String],
-    default: ''
-  },
-  height: {
-    type: [Number, String],
-    default: ''
-  }
-});
-
-const realSrc = computed(() => {
-  if (!props.src) {
-    return;
-  }
-  const real_src = props.src.split(',')[0];
-  return real_src;
-});
-
-const realSrcList = computed(() => {
-  if (!props.src) {
-    return [];
-  }
-  const real_src_list = props.src.split(',');
-  const srcList: string[] = [];
-  real_src_list.forEach((item: string) => {
-    if (item.trim() === '') {
-      return;
-    }
-    return srcList.push(item);
-  });
-  return srcList;
-});
-
-const realWidth = computed(() => (typeof props.width == 'string' ? props.width : `${props.width}px`));
-
-const realHeight = computed(() => (typeof props.height == 'string' ? props.height : `${props.height}px`));
-</script>
-
-<style lang="scss" scoped>
-.el-image {
-  border-radius: 5px;
-  background-color: #ebeef5;
-  box-shadow: 0 0 5px 1px #ccc;
-
-  :deep(.el-image__inner) {
-    transition: all 0.3s;
-    cursor: pointer;
-
-    &:hover {
-      transform: scale(1.2);
-    }
-  }
-
-  :deep(.image-slot) {
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    width: 100%;
-    height: 100%;
-    color: #909399;
-    font-size: 30px;
-  }
-}
-</style>

+ 0 - 242
src/components/ImageUpload/index.vue

@@ -1,242 +0,0 @@
-<template>
-  <div class="component-upload-image">
-    <el-upload
-      ref="imageUploadRef"
-      multiple
-      :action="uploadImgUrl"
-      list-type="picture-card"
-      :on-success="handleUploadSuccess"
-      :before-upload="handleBeforeUpload"
-      :limit="limit"
-      :accept="fileAccept"
-      :on-error="handleUploadError"
-      :on-exceed="handleExceed"
-      :before-remove="handleDelete"
-      :show-file-list="true"
-      :headers="headers"
-      :file-list="fileList"
-      :on-preview="handlePictureCardPreview"
-      :class="{ hide: fileList.length >= limit }"
-    >
-      <el-icon class="avatar-uploader-icon">
-        <plus />
-      </el-icon>
-    </el-upload>
-    <!-- 上传提示 -->
-    <div v-if="showTip" class="el-upload__tip">
-      请上传
-      <template v-if="fileSize">
-        大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
-      </template>
-      <template v-if="fileType">
-        格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
-      </template>
-      的文件
-    </div>
-
-    <el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
-      <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
-    </el-dialog>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { listByIds, delOss } from '@/api/system/oss';
-import { OssVO } from '@/api/system/oss/types';
-import { propTypes } from '@/utils/propTypes';
-import { globalHeaders } from '@/utils/request';
-import { compressAccurately } from 'image-conversion';
-
-const props = defineProps({
-  modelValue: {
-    type: [String, Object, Array],
-    default: () => []
-  },
-  // 图片数量限制
-  limit: propTypes.number.def(5),
-  // 大小限制(MB)
-  fileSize: propTypes.number.def(5),
-  // 文件类型, 例如['png', 'jpg', 'jpeg']
-  fileType: propTypes.array.def(['png', 'jpg', 'jpeg']),
-  // 是否显示提示
-  isShowTip: {
-    type: Boolean,
-    default: true
-  },
-  // 是否支持压缩,默认否
-  compressSupport: {
-    type: Boolean,
-    default: false
-  },
-  // 压缩目标大小,单位KB。默认300KB以上文件才压缩,并压缩至300KB以内
-  compressTargetSize: propTypes.number.def(300)
-});
-
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const emit = defineEmits(['update:modelValue']);
-const number = ref(0);
-const uploadList = ref<any[]>([]);
-const dialogImageUrl = ref('');
-const dialogVisible = ref(false);
-
-const baseUrl = import.meta.env.VITE_APP_BASE_API;
-const uploadImgUrl = ref(baseUrl + '/resource/oss/upload'); // 上传的图片服务器地址
-const headers = ref(globalHeaders());
-
-const fileList = ref<any[]>([]);
-const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
-
-const imageUploadRef = ref<ElUploadInstance>();
-
-// 监听 fileType 变化,更新 fileAccept
-const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','));
-
-watch(
-  () => props.modelValue,
-  async (val: string) => {
-    if (val) {
-      // 首先将值转为数组
-      let list: OssVO[] = [];
-      if (Array.isArray(val)) {
-        list = val as OssVO[];
-      } else {
-        const res = await listByIds(val);
-        list = res.data;
-      }
-      // 然后将数组转为对象数组
-      fileList.value = list.map((item) => {
-        // 字符串回显处理 如果此处存的是url可直接回显 如果存的是id需要调用接口查出来
-        let itemData;
-        if (typeof item === 'string') {
-          itemData = { name: item, url: item };
-        } else {
-          // 此处name使用ossId 防止删除出现重名
-          itemData = { name: item.ossId, url: item.url, ossId: item.ossId };
-        }
-        return itemData;
-      });
-    } else {
-      fileList.value = [];
-      return [];
-    }
-  },
-  { deep: true, immediate: true }
-);
-
-/** 上传前loading加载 */
-const handleBeforeUpload = (file: any) => {
-  let isImg = false;
-  if (props.fileType.length) {
-    let fileExtension = '';
-    if (file.name.lastIndexOf('.') > -1) {
-      fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1);
-    }
-    isImg = props.fileType.some((type: any) => {
-      if (file.type.indexOf(type) > -1) return true;
-      if (fileExtension && fileExtension.indexOf(type) > -1) return true;
-      return false;
-    });
-  } else {
-    isImg = file.type.indexOf('image') > -1;
-  }
-  if (!isImg) {
-    proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}图片格式文件!`);
-    return false;
-  }
-  if (file.name.includes(',')) {
-    proxy?.$modal.msgError('文件名不正确,不能包含英文逗号!');
-    return false;
-  }
-  if (props.fileSize) {
-    const isLt = file.size / 1024 / 1024 < props.fileSize;
-    if (!isLt) {
-      proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
-      return false;
-    }
-  }
-
-  //压缩图片,开启压缩并且大于指定的压缩大小时才压缩
-  if (props.compressSupport && file.size / 1024 > props.compressTargetSize) {
-    proxy?.$modal.loading('正在上传图片,请稍候...');
-    number.value++;
-    return compressAccurately(file, props.compressTargetSize);
-  } else {
-    proxy?.$modal.loading('正在上传图片,请稍候...');
-    number.value++;
-  }
-};
-
-// 文件个数超出
-const handleExceed = () => {
-  proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
-};
-
-// 上传成功回调
-const handleUploadSuccess = (res: any, file: UploadFile) => {
-  if (res.code === 200) {
-    uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
-    uploadedSuccessfully();
-  } else {
-    number.value--;
-    proxy?.$modal.closeLoading();
-    proxy?.$modal.msgError(res.msg);
-    imageUploadRef.value?.handleRemove(file);
-    uploadedSuccessfully();
-  }
-};
-
-// 删除图片
-const handleDelete = (file: UploadFile): boolean => {
-  const findex = fileList.value.map((f) => f.name).indexOf(file.name);
-  if (findex > -1 && uploadList.value.length === number.value) {
-    const ossId = fileList.value[findex].ossId;
-    delOss(ossId);
-    fileList.value.splice(findex, 1);
-    emit('update:modelValue', listToString(fileList.value));
-    return false;
-  }
-  return true;
-};
-
-// 上传结束处理
-const uploadedSuccessfully = () => {
-  if (number.value > 0 && uploadList.value.length === number.value) {
-    fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
-    uploadList.value = [];
-    number.value = 0;
-    emit('update:modelValue', listToString(fileList.value));
-    proxy?.$modal.closeLoading();
-  }
-};
-
-// 上传失败
-const handleUploadError = () => {
-  proxy?.$modal.msgError('上传图片失败');
-  proxy?.$modal.closeLoading();
-};
-
-// 预览
-const handlePictureCardPreview = (file: any) => {
-  dialogImageUrl.value = file.url;
-  dialogVisible.value = true;
-};
-
-// 对象转成指定字符串分隔
-const listToString = (list: any[], separator?: string) => {
-  let strs = '';
-  separator = separator || ',';
-  for (const i in list) {
-    if (undefined !== list[i].ossId && list[i].url.indexOf('blob:') !== 0) {
-      strs += list[i].ossId + separator;
-    }
-  }
-  return strs != '' ? strs.substring(0, strs.length - 1) : '';
-};
-</script>
-
-<style lang="scss" scoped>
-// .el-upload--picture-card 控制加号部分
-:deep(.hide .el-upload--picture-card) {
-  display: none;
-}
-</style>

+ 0 - 39
src/components/LangSelect/index.vue

@@ -1,39 +0,0 @@
-<template>
-  <el-dropdown trigger="click" @command="handleLanguageChange">
-    <div class="lang-select--style">
-      <svg-icon icon-class="language" />
-    </div>
-    <template #dropdown>
-      <el-dropdown-menu>
-        <el-dropdown-item :disabled="appStore.language === 'zh_CN'" command="zh_CN"> 中文 </el-dropdown-item>
-        <el-dropdown-item :disabled="appStore.language === 'en_US'" command="en_US"> English </el-dropdown-item>
-      </el-dropdown-menu>
-    </template>
-  </el-dropdown>
-</template>
-
-<script setup lang="ts">
-import { useI18n } from 'vue-i18n';
-import { useAppStore } from '@/store/modules/app';
-import SvgIcon from '@/components/SvgIcon/index.vue';
-
-const appStore = useAppStore();
-const { locale } = useI18n();
-
-const message: any = {
-  zh_CN: '切换语言成功!',
-  en_US: 'Switch Language Successful!'
-};
-const handleLanguageChange = (lang: any) => {
-  locale.value = lang;
-  appStore.changeLanguage(lang);
-  ElMessage.success(message[lang] || '切换语言成功!');
-};
-</script>
-
-<style lang="scss" scoped>
-.lang-select--style {
-  font-size: 18px;
-  line-height: 50px;
-}
-</style>

+ 29 - 0
src/components/PageTitle/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="page-title">
+    <i class="title-bar"></i>
+    <span>{{ title }}</span>
+    <slot name="extra"></slot>
+  </div>
+</template>
+
+<script setup lang="ts">
+defineProps<{ title: string }>()
+</script>
+
+<style scoped lang="scss">
+.page-title {
+  font-size: 16px;
+  font-weight: bold;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 20px;
+}
+.title-bar {
+  display: inline-block;
+  width: 3px;
+  height: 16px;
+  background: #e60012;
+  border-radius: 2px;
+}
+</style>

+ 0 - 78
src/components/Pagination/index.vue

@@ -1,78 +0,0 @@
-<template>
-  <div :class="{ hidden: hidden }" class="pagination-container">
-    <el-pagination
-      v-model:current-page="currentPage"
-      v-model:page-size="pageSize"
-      :background="background"
-      :layout="layout"
-      :page-sizes="pageSizes"
-      :pager-count="pagerCount"
-      :total="total"
-      @size-change="handleSizeChange"
-      @current-change="handleCurrentChange"
-    />
-  </div>
-</template>
-
-<script setup name="Pagination" lang="ts">
-import { scrollTo } from '@/utils/scroll-to';
-import { propTypes } from '@/utils/propTypes';
-
-const props = defineProps({
-  total: propTypes.number,
-  page: propTypes.number.def(1),
-  limit: propTypes.number.def(20),
-  pageSizes: { type: Array<number>, default: () => [10, 20, 30, 50] },
-  // 移动端页码按钮的数量端默认值5
-  pagerCount: propTypes.number.def(document.body.clientWidth < 992 ? 5 : 7),
-  layout: propTypes.string.def('total, sizes, prev, pager, next, jumper'),
-  background: propTypes.bool.def(true),
-  autoScroll: propTypes.bool.def(true),
-  hidden: propTypes.bool.def(false),
-  float: propTypes.string.def('right')
-});
-
-const emit = defineEmits(['update:page', 'update:limit', 'pagination']);
-const currentPage = computed({
-  get() {
-    return props.page;
-  },
-  set(val) {
-    emit('update:page', val);
-  }
-});
-const pageSize = computed({
-  get() {
-    return props.limit;
-  },
-  set(val) {
-    emit('update:limit', val);
-  }
-});
-function handleSizeChange(val: number) {
-  if (currentPage.value * val > props.total) {
-    currentPage.value = 1;
-  }
-  emit('pagination', { page: currentPage.value, limit: val });
-  if (props.autoScroll) {
-    scrollTo(0, 800);
-  }
-}
-function handleCurrentChange(val: number) {
-  emit('pagination', { page: val, limit: pageSize.value });
-  if (props.autoScroll) {
-    scrollTo(0, 800);
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.pagination-container {
-  .el-pagination {
-    float: v-bind(float);
-  }
-}
-.pagination-container.hidden {
-  display: none;
-}
-</style>

+ 0 - 3
src/components/ParentView/index.vue

@@ -1,3 +0,0 @@
-<template>
-  <router-view />
-</template>

+ 0 - 100
src/components/Process/MessageType.vue

@@ -1,100 +0,0 @@
-<template>
-  <el-dialog v-model="visible" :title="props.title" width="50%" draggable :before-close="cancel" center :close-on-click-modal="false">
-    <el-form v-loading="loading" ref="ruleFormRef" :model="form" :rules="rules" label-width="120px">
-      <el-form-item label="消息提醒" prop="messageType">
-        <el-checkbox-group v-model="form.messageType">
-          <el-checkbox value="1" name="type" disabled>站内信</el-checkbox>
-          <el-checkbox value="2" name="type">邮件</el-checkbox>
-          <el-checkbox value="3" name="type">短信</el-checkbox>
-        </el-checkbox-group>
-      </el-form-item>
-      <el-form-item label="消息内容" prop="message">
-        <el-input v-model="form.message" type="textarea" resize="none" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <div class="dialog-footer" style="float: right; padding-bottom: 20px">
-        <el-button :disabled="buttonDisabled" type="primary" @click="submit(ruleFormRef)">确认</el-button>
-        <el-button :disabled="buttonDisabled" @click="cancel">取消</el-button>
-      </div>
-    </template>
-  </el-dialog>
-</template>
-<script setup lang="ts">
-import { ref } from 'vue';
-import { ComponentInternalInstance } from 'vue';
-import { ElForm, FormInstance } from 'element-plus';
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const emits = defineEmits(['submitCallback', 'cancelCallback']);
-const props = defineProps({
-  title: {
-    type: String,
-    default: '提示'
-  }
-});
-const ruleFormRef = ref<FormInstance>();
-//遮罩层
-const loading = ref(true);
-const visible = ref(false);
-const buttonDisabled = ref(true);
-const form = ref<Record<string, any>>({
-  message: undefined,
-  messageType: ['1']
-});
-const rules = reactive<Record<string, any>>({
-  messageType: [
-    {
-      required: true,
-      message: '请选择消息提醒',
-      trigger: 'change'
-    }
-  ],
-  message: [
-    {
-      required: true,
-      message: '请输入消息内容',
-      trigger: 'blur'
-    }
-  ]
-});
-//确认
-//打开弹窗
-const open = async () => {
-  reset();
-  visible.value = true;
-  loading.value = false;
-  buttonDisabled.value = false;
-};
-//关闭弹窗
-const close = async () => {
-  reset();
-  visible.value = false;
-};
-const submit = async (formEl: FormInstance | undefined) => {
-  if (!formEl) return;
-  await formEl.validate((valid, fields) => {
-    if (valid) {
-      emits('submitCallback', form.value);
-    }
-  });
-};
-//取消
-const cancel = async () => {
-  visible.value = false;
-  buttonDisabled.value = false;
-  emits('cancelCallback');
-};
-//重置
-const reset = async () => {
-  form.value.taskIdList = [];
-  form.value.message = '';
-  form.value.messageType = ['1'];
-};
-/**
- * 对外暴露子组件方法
- */
-defineExpose({
-  open,
-  close
-});
-</script>

+ 0 - 57
src/components/Process/approvalButton.vue

@@ -1,57 +0,0 @@
-<template>
-  <div style="display: flex; justify-content: space-between">
-    <div>
-      <el-button v-if="submitButtonShow" :loading="props.buttonLoading" type="info" @click="submitForm('draft', mode)">暂存</el-button>
-      <el-button v-if="submitButtonShow" :loading="props.buttonLoading" type="primary" @click="submitForm('submit', mode)">提 交</el-button>
-      <el-button v-if="approvalButtonShow" :loading="props.buttonLoading" type="primary" @click="approvalVerifyOpen">审批</el-button>
-      <el-button v-if="props.id && props.status !== 'draft'" type="primary" @click="handleApprovalRecord">流程进度</el-button>
-      <slot />
-    </div>
-    <div>
-      <el-button style="float: right" @click="goBack()">返回</el-button>
-    </div>
-  </div>
-</template>
-<script setup lang="ts">
-import { propTypes } from '@/utils/propTypes';
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const props = defineProps({
-  status: propTypes.string.def(''),
-  pageType: propTypes.string.def(''),
-  buttonLoading: propTypes.bool.def(false),
-  id: propTypes.string.def('') || propTypes.number.def(),
-  mode: propTypes.bool.def(false)
-});
-const emits = defineEmits(['submitForm', 'approvalVerifyOpen', 'handleApprovalRecord']);
-//暂存,提交
-const submitForm = async (type, mode) => {
-  emits('submitForm', type, mode);
-};
-//审批
-const approvalVerifyOpen = async () => {
-  emits('approvalVerifyOpen');
-};
-//审批记录
-const handleApprovalRecord = () => {
-  emits('handleApprovalRecord');
-};
-
-//校验提交按钮是否显示
-const submitButtonShow = computed(() => {
-  return (
-    props.pageType === 'add' ||
-    (props.pageType === 'update' && props.status && (props.status === 'draft' || props.status === 'cancel' || props.status === 'back'))
-  );
-});
-
-//校验审批按钮是否显示
-const approvalButtonShow = computed(() => {
-  return props.pageType === 'approval' && props.status && props.status === 'waiting';
-});
-
-//返回
-const goBack = () => {
-  proxy.$tab.closePage(proxy.$route);
-  proxy.$router.go(-1);
-};
-</script>

+ 0 - 127
src/components/Process/approvalRecord.vue

@@ -1,127 +0,0 @@
-<template>
-  <div class="container">
-    <el-dialog v-model="visible" draggable title="审批记录" :width="props.width" :height="props.height" :close-on-click-modal="false">
-      <el-tabs v-model="tabActiveName" class="demo-tabs">
-        <el-tab-pane v-loading="loading" label="流程图" name="image" style="height: 68vh">
-          <flowChart :ins-id="insId" v-if="insId" />
-        </el-tab-pane>
-        <el-tab-pane v-loading="loading" label="审批信息" name="info">
-          <div>
-            <el-table :data="historyList" style="width: 100%" border fit>
-              <el-table-column type="index" label="序号" align="center" width="60"></el-table-column>
-              <el-table-column prop="nodeName" label="任务名称" sortable align="center"></el-table-column>
-              <el-table-column prop="approveName" :show-overflow-tooltip="true" label="办理人" sortable align="center">
-                <template #default="scope">
-                  <template v-if="scope.row.approveName">
-                    <el-tag v-for="(item, index) in scope.row.approveName.split(',')" :key="index" type="success">{{ item }}</el-tag>
-                  </template>
-                  <template v-else> <el-tag type="success">无</el-tag></template>
-                </template>
-              </el-table-column>
-              <el-table-column prop="flowStatus" label="状态" width="80" sortable align="center">
-                <template #default="scope">
-                  <dict-tag :options="wf_task_status" :value="scope.row.flowStatus"></dict-tag>
-                </template>
-              </el-table-column>
-              <el-table-column prop="message" label="审批意见" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
-              <el-table-column prop="createTime" label="开始时间" width="160" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
-              <el-table-column prop="updateTime" label="结束时间" width="160" :show-overflow-tooltip="true" sortable align="center"></el-table-column>
-              <el-table-column
-                prop="runDuration"
-                label="运行时长"
-                width="140"
-                :show-overflow-tooltip="true"
-                sortable
-                align="center"
-              ></el-table-column>
-              <el-table-column prop="attachmentList" width="120" label="附件" align="center">
-                <template #default="scope">
-                  <el-popover v-if="scope.row.attachmentList && scope.row.attachmentList.length > 0" placement="right" :width="310" trigger="click">
-                    <template #reference>
-                      <el-button type="primary" style="margin-right: 16px">附件</el-button>
-                    </template>
-                    <el-table border :data="scope.row.attachmentList">
-                      <el-table-column prop="originalName" width="202" :show-overflow-tooltip="true" label="附件名称"></el-table-column>
-                      <el-table-column prop="name" width="80" align="center" :show-overflow-tooltip="true" label="操作">
-                        <template #default="tool">
-                          <el-button type="text" @click="handleDownload(tool.row.ossId)">下载</el-button>
-                        </template>
-                      </el-table-column>
-                    </el-table>
-                  </el-popover>
-                </template>
-              </el-table-column>
-            </el-table>
-          </div>
-        </el-tab-pane>
-      </el-tabs>
-    </el-dialog>
-  </div>
-</template>
-<script setup lang="ts">
-import { flowHisTaskList } from '@/api/workflow/instance';
-import { propTypes } from '@/utils/propTypes';
-import { listByIds } from '@/api/system/oss';
-import FlowChart from '@/components/Process/flowChart.vue';
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { wf_task_status } = toRefs<any>(proxy?.useDict('wf_task_status'));
-const props = defineProps({
-  width: propTypes.string.def('80%'),
-  height: propTypes.string.def('100%')
-});
-const loading = ref(false);
-const visible = ref(false);
-const historyList = ref<Array<any>>([]);
-const tabActiveName = ref('image');
-const insId = ref(null);
-
-//初始化查询审批记录
-const init = async (businessId: string | number) => {
-  visible.value = true;
-  loading.value = true;
-  tabActiveName.value = 'image';
-  historyList.value = [];
-  flowHisTaskList(businessId).then((resp) => {
-    if (resp.data) {
-      historyList.value = resp.data.list;
-      insId.value = resp.data.instanceId;
-      if (historyList.value.length > 0) {
-        historyList.value.forEach((item) => {
-          if (item.ext) {
-            getIds(item.ext).then((res) => {
-              item.attachmentList = res.data;
-            });
-          } else {
-            item.attachmentList = [];
-          }
-        });
-      }
-      loading.value = false;
-    }
-  });
-};
-const getIds = async (ids: string | number) => {
-  const res = await listByIds(ids);
-  return res;
-};
-
-/** 下载按钮操作 */
-const handleDownload = (ossId: string) => {
-  proxy?.$download.oss(ossId);
-};
-
-/**
- * 对外暴露子组件方法
- */
-defineExpose({
-  init
-});
-</script>
-<style lang="scss" scoped>
-.container {
-  :deep(.el-dialog .el-dialog__body) {
-    max-height: calc(100vh - 170px) !important;
-    min-height: calc(100vh - 170px) !important;
-  }
-}
-</style>

+ 0 - 40
src/components/Process/flowChart.vue

@@ -1,40 +0,0 @@
-<template>
-  <div>
-    <div style="height: 68vh" class="iframe-wrapper">
-      <iframe :src="iframeUrl" style="width: 100%; height: 100%" frameborder="0" scrolling="no" class="custom-iframe" />
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { getToken } from '@/utils/auth';
-
-// Props 定义方式变化
-const props = defineProps({
-  insId: {
-    type: [String, Number],
-    default: null
-  }
-});
-
-const iframeUrl = ref('');
-const baseUrl = import.meta.env.VITE_APP_BASE_API;
-
-onMounted(async () => {
-  const url = baseUrl + `/warm-flow-ui/index.html?id=${props.insId}&type=FlowChart&t=${Date.now()}`;
-  iframeUrl.value = url + '&Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID;
-});
-</script>
-<style scoped>
-.iframe-wrapper {
-  border-radius: 12px;
-  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-  overflow: hidden;
-}
-
-.custom-iframe {
-  width: 100%;
-  border: none;
-  background: transparent;
-}
-</style>

+ 0 - 154
src/components/Process/flowChartImg.vue

@@ -1,154 +0,0 @@
-<template>
-  <div
-    ref="imageWrapperRef"
-    class="image-wrapper"
-    @wheel="handleMouseWheel"
-    @mousedown="handleMouseDown"
-    @mousemove="handleMouseMove"
-    @mouseup="handleMouseUp"
-    @mouseleave="handleMouseLeave"
-    @dblclick="resetTransform"
-    :style="transformStyle"
-  >
-    <el-card class="box-card">
-      <el-image :src="props.imgUrl" class="scalable-image" />
-    </el-card>
-  </div>
-</template>
-
-<script setup lang="ts">
-// Props 定义方式变化
-const props = defineProps({
-  imgUrl: {
-    type: String,
-    default: () => ''
-  }
-});
-
-const imageWrapperRef = ref<HTMLElement | null>(null);
-const scale = ref(1); // 初始缩放比例
-const maxScale = 3; // 最大缩放比例
-const minScale = 0.5; // 最小缩放比例
-
-let isDragging = false;
-let startX = 0;
-let startY = 0;
-let currentTranslateX = 0;
-let currentTranslateY = 0;
-
-const handleMouseWheel = (event: WheelEvent) => {
-  event.preventDefault();
-  let newScale = scale.value - event.deltaY / 1000;
-  newScale = Math.max(minScale, Math.min(newScale, maxScale));
-  if (newScale !== scale.value) {
-    scale.value = newScale;
-    resetDragPosition(); // 重置拖拽位置,使图片居中
-  }
-};
-
-const handleMouseDown = (event: MouseEvent) => {
-  if (scale.value > 1) {
-    event.preventDefault(); // 阻止默认行为,防止拖拽
-    isDragging = true;
-    startX = event.clientX;
-    startY = event.clientY;
-  }
-};
-
-const handleMouseMove = (event: MouseEvent) => {
-  if (!isDragging || !imageWrapperRef.value) return;
-
-  const deltaX = event.clientX - startX;
-  const deltaY = event.clientY - startY;
-  startX = event.clientX;
-  startY = event.clientY;
-
-  currentTranslateX += deltaX;
-  currentTranslateY += deltaY;
-
-  // 边界检测,防止图片被拖出容器
-  const bounds = getBounds();
-  if (currentTranslateX > bounds.maxTranslateX) {
-    currentTranslateX = bounds.maxTranslateX;
-  } else if (currentTranslateX < bounds.minTranslateX) {
-    currentTranslateX = bounds.minTranslateX;
-  }
-
-  if (currentTranslateY > bounds.maxTranslateY) {
-    currentTranslateY = bounds.maxTranslateY;
-  } else if (currentTranslateY < bounds.minTranslateY) {
-    currentTranslateY = bounds.minTranslateY;
-  }
-
-  applyTransform();
-};
-
-const handleMouseUp = () => {
-  isDragging = false;
-};
-
-const handleMouseLeave = () => {
-  isDragging = false;
-};
-
-const resetTransform = () => {
-  scale.value = 1;
-  currentTranslateX = 0;
-  currentTranslateY = 0;
-  applyTransform();
-};
-
-const resetDragPosition = () => {
-  currentTranslateX = 0;
-  currentTranslateY = 0;
-  applyTransform();
-};
-
-const applyTransform = () => {
-  if (imageWrapperRef.value) {
-    imageWrapperRef.value.style.transform = `translate(${currentTranslateX}px, ${currentTranslateY}px) scale(${scale.value})`;
-  }
-};
-
-const getBounds = () => {
-  if (!imageWrapperRef.value) return { minTranslateX: 0, maxTranslateX: 0, minTranslateY: 0, maxTranslateY: 0 };
-
-  const imgRect = imageWrapperRef.value.getBoundingClientRect();
-  const containerRect = imageWrapperRef.value.parentElement?.getBoundingClientRect() ?? imgRect;
-
-  const minTranslateX = (containerRect.width - imgRect.width * scale.value) / 2;
-  const maxTranslateX = -(containerRect.width - imgRect.width * scale.value) / 2;
-  const minTranslateY = (containerRect.height - imgRect.height * scale.value) / 2;
-  const maxTranslateY = -(containerRect.height - imgRect.height * scale.value) / 2;
-
-  return { minTranslateX, maxTranslateX, minTranslateY, maxTranslateY };
-};
-
-const transformStyle = computed(() => ({
-  transition: isDragging ? 'none' : 'transform 0.2s ease'
-}));
-</script>
-
-<style scoped>
-.image-wrapper {
-  width: 100%;
-  overflow: hidden;
-  position: relative;
-  margin: 0 auto;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  user-select: none; /* 禁用文本选择 */
-  cursor: grab; /* 设置初始鼠标指针为可拖动 */
-}
-
-.image-wrapper:active {
-  cursor: grabbing; /* 当正在拖动时改变鼠标指针 */
-}
-
-.scalable-image {
-  object-fit: contain;
-  width: 100%;
-  padding: 15px;
-}
-</style>

+ 0 - 211
src/components/Process/processMeddle.vue

@@ -1,211 +0,0 @@
-<template>
-  <el-dialog v-model="visible" draggable title="流程干预" :width="props.width" :height="props.height" :close-on-click-modal="false">
-    <el-descriptions v-loading="loading" class="margin-top" :title="`${task.flowName}(${task.flowCode})`" :column="2" border>
-      <el-descriptions-item label="任务名称">{{ task.nodeName }}</el-descriptions-item>
-      <el-descriptions-item label="节点编码">{{ task.nodeCode }}</el-descriptions-item>
-      <el-descriptions-item label="开始时间">{{ task.createTime }}</el-descriptions-item>
-      <el-descriptions-item label="流程实例ID">{{ task.instanceId }}</el-descriptions-item>
-      <el-descriptions-item label="版本号">{{ task.version }}.0</el-descriptions-item>
-      <el-descriptions-item label="业务ID">{{ task.businessId }}</el-descriptions-item>
-    </el-descriptions>
-    <template #footer>
-      <span class="dialog-footer">
-        <el-button v-if="task.flowStatus === 'waiting'" :disabled="buttonDisabled" type="primary" @click="openTransferTask"> 转办 </el-button>
-        <el-button
-          v-if="task.flowStatus === 'waiting' && Number(task.nodeRatio) > 0"
-          :disabled="buttonDisabled"
-          type="primary"
-          @click="openMultiInstanceUser"
-        >
-          加签
-        </el-button>
-        <el-button
-          v-if="task.flowStatus === 'waiting' && Number(task.nodeRatio) > 0"
-          :disabled="buttonDisabled"
-          type="primary"
-          @click="handleTaskUser"
-        >
-          减签
-        </el-button>
-        <el-button v-if="task.flowStatus === 'waiting'" :disabled="buttonDisabled" type="danger" @click="handleTerminationTask"> 终止 </el-button>
-      </span>
-    </template>
-    <!-- 转办 -->
-    <UserSelect ref="transferTaskRef" :multiple="false" @confirm-call-back="handleTransferTask"></UserSelect>
-    <!-- 加签组件 -->
-    <UserSelect ref="multiInstanceUserRef" :multiple="true" @confirm-call-back="addMultiInstanceUser"></UserSelect>
-    <el-dialog v-model="deleteSignatureVisible" draggable title="减签人员" width="700px" height="400px" append-to-body :close-on-click-modal="false"
-      ><div>
-        <el-table :data="deleteUserList" border>
-          <el-table-column prop="nodeName" label="任务名称" />
-          <el-table-column prop="nickName" label="办理人" />
-          <el-table-column label="操作" align="center" width="160">
-            <template #default="scope">
-              <el-button type="danger" size="small" icon="Delete" @click="deleteMultiInstanceUser(scope.row)">删除</el-button>
-            </template>
-          </el-table-column>
-        </el-table>
-      </div>
-    </el-dialog>
-  </el-dialog>
-</template>
-<script setup lang="ts">
-import { propTypes } from '@/utils/propTypes';
-import { FlowTaskVO, TaskOperationBo } from '@/api/workflow/task/types';
-import UserSelect from '@/components/UserSelect';
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-import { getTask, taskOperation, currentTaskAllUser, terminationTask } from '@/api/workflow/task';
-const props = defineProps({
-  width: propTypes.string.def('50%'),
-  height: propTypes.string.def('100%')
-});
-const emits = defineEmits(['submitCallback']);
-const transferTaskRef = ref<InstanceType<typeof UserSelect>>();
-const multiInstanceUserRef = ref<InstanceType<typeof UserSelect>>();
-//遮罩层
-const loading = ref(true);
-//按钮
-const buttonDisabled = ref(true);
-const visible = ref(false);
-//减签弹窗
-const deleteSignatureVisible = ref(false);
-//可减签的人员
-const deleteUserList = ref<any>([]);
-//任务
-const task = ref<FlowTaskVO>({
-  id: undefined,
-  createTime: undefined,
-  updateTime: undefined,
-  tenantId: undefined,
-  definitionId: undefined,
-  instanceId: undefined,
-  flowName: undefined,
-  businessId: undefined,
-  nodeCode: undefined,
-  nodeName: undefined,
-  flowCode: undefined,
-  flowStatus: undefined,
-  formCustom: undefined,
-  formPath: undefined,
-  nodeType: undefined,
-  nodeRatio: undefined,
-  version: undefined,
-  applyNode: undefined,
-  buttonList: []
-});
-
-const open = (taskId: string) => {
-  visible.value = true;
-  getTask(taskId).then((response) => {
-    loading.value = false;
-    buttonDisabled.value = false;
-    task.value = response.data;
-  });
-};
-
-//打开转办
-const openTransferTask = () => {
-  transferTaskRef.value.open();
-};
-//转办
-const handleTransferTask = async (data) => {
-  if (data && data.length > 0) {
-    const taskOperationBo = reactive<TaskOperationBo>({
-      userId: data[0].userId,
-      taskId: task.value.id,
-      message: ''
-    });
-    await proxy?.$modal.confirm('是否确认提交?');
-    loading.value = true;
-    buttonDisabled.value = true;
-    await taskOperation(taskOperationBo, 'transferTask').finally(() => {
-      loading.value = false;
-      buttonDisabled.value = false;
-    });
-    visible.value = false;
-    emits('submitCallback');
-    proxy?.$modal.msgSuccess('操作成功');
-  } else {
-    proxy?.$modal.msgWarning('请选择用户!');
-  }
-};
-//加签
-const openMultiInstanceUser = async () => {
-  multiInstanceUserRef.value.open();
-};
-//加签
-const addMultiInstanceUser = async (data) => {
-  if (data && data.length > 0) {
-    const taskOperationBo = reactive<TaskOperationBo>({
-      userIds: data.map((e) => e.userId),
-      taskId: task.value.id,
-      message: ''
-    });
-    await proxy?.$modal.confirm('是否确认提交?');
-    loading.value = true;
-    buttonDisabled.value = true;
-    await taskOperation(taskOperationBo, 'addSignature').finally(() => {
-      loading.value = false;
-      buttonDisabled.value = false;
-    });
-    visible.value = false;
-    emits('submitCallback');
-    proxy?.$modal.msgSuccess('操作成功');
-  } else {
-    proxy?.$modal.msgWarning('请选择用户!');
-  }
-};
-//减签
-const deleteMultiInstanceUser = async (row) => {
-  await proxy?.$modal.confirm('是否确认提交?');
-  loading.value = true;
-  buttonDisabled.value = true;
-  const taskOperationBo = reactive<TaskOperationBo>({
-    userIds: [row.userId],
-    taskId: task.value.id,
-    message: ''
-  });
-  await taskOperation(taskOperationBo, 'reductionSignature').finally(() => {
-    loading.value = false;
-    buttonDisabled.value = false;
-  });
-  visible.value = false;
-  emits('submitCallback');
-  proxy?.$modal.msgSuccess('操作成功');
-};
-//获取办理人
-const handleTaskUser = async () => {
-  const data = await currentTaskAllUser(task.value.id);
-  deleteUserList.value = data.data;
-  if (deleteUserList.value && deleteUserList.value.length > 0) {
-    deleteUserList.value.forEach((e) => {
-      e.nodeName = task.value.nodeName;
-    });
-  }
-  deleteSignatureVisible.value = true;
-};
-
-//终止任务
-const handleTerminationTask = async () => {
-  const params = {
-    taskId: task.value.id,
-    comment: ''
-  };
-  await proxy?.$modal.confirm('是否确认终止?');
-  loading.value = true;
-  buttonDisabled.value = true;
-  await terminationTask(params).finally(() => {
-    loading.value = false;
-    buttonDisabled.value = false;
-  });
-  visible.value = false;
-  emits('submitCallback');
-  proxy?.$modal.msgSuccess('操作成功');
-};
-/**
- * 对外暴露子组件方法
- */
-defineExpose({
-  open
-});
-</script>

+ 0 - 541
src/components/Process/submitVerify.vue

@@ -1,541 +0,0 @@
-<template>
-  <el-dialog v-model="dialog.visible" :title="dialog.title" width="50%" draggable :before-close="cancel" center :close-on-click-modal="false">
-    <el-form v-loading="loading" :model="form" label-width="120px">
-      <el-form-item label="消息提醒">
-        <el-checkbox-group v-model="form.messageType">
-          <el-checkbox value="1" name="type" disabled>站内信</el-checkbox>
-          <el-checkbox value="2" name="type">邮件</el-checkbox>
-          <el-checkbox value="3" name="type">短信</el-checkbox>
-        </el-checkbox-group>
-      </el-form-item>
-      <el-form-item label="附件" v-if="buttonObj.file">
-        <fileUpload v-model="form.fileId" :file-type="['png', 'jpg', 'jpeg', 'doc', 'docx', 'xlsx', 'xls', 'ppt', 'txt', 'pdf']" :file-size="20" />
-      </el-form-item>
-      <el-form-item label="抄送" v-if="buttonObj.copy">
-        <el-button type="primary" icon="Plus" circle @click="openUserSelectCopy" />
-        <el-tag v-for="user in selectCopyUserList" :key="user.userId" closable style="margin: 2px" @close="handleCopyCloseTag(user)">
-          {{ user.userName }}
-        </el-tag>
-      </el-form-item>
-      <el-form-item v-if="buttonObj.pop && nestNodeList && nestNodeList.length > 0" label="下一步审批人" prop="assigneeMap">
-        <div v-for="(item, index) in nestNodeList" :key="index" style="margin-bottom: 5px; width: 500px">
-          <span>【{{ item.nodeName }}】:</span>
-          <el-input v-if="false" v-model="form.assigneeMap[item.nodeCode]" />
-          <el-input placeholder="请选择审批人" readonly v-model="nickName[item.nodeCode]">
-            <template v-slot:append>
-              <el-button @click="choosePeople(item)" icon="search">选择</el-button>
-            </template>
-          </el-input>
-        </div>
-      </el-form-item>
-      <el-form-item v-if="task.flowStatus === 'waiting'" label="审批意见">
-        <el-input v-model="form.message" type="textarea" resize="none" />
-      </el-form-item>
-    </el-form>
-    <template #footer>
-      <span class="dialog-footer">
-        <el-button :disabled="buttonDisabled" type="primary" @click="handleCompleteTask"> 提交 </el-button>
-        <el-button v-if="task.flowStatus === 'waiting' && buttonObj.trust" :disabled="buttonDisabled" type="primary" @click="openDelegateTask">
-          委托
-        </el-button>
-        <el-button v-if="task.flowStatus === 'waiting' && buttonObj.transfer" :disabled="buttonDisabled" type="primary" @click="openTransferTask">
-          转办
-        </el-button>
-        <el-button
-          v-if="task.flowStatus === 'waiting' && Number(task.nodeRatio) > 0 && buttonObj.addSign"
-          :disabled="buttonDisabled"
-          type="primary"
-          @click="openMultiInstanceUser"
-        >
-          加签
-        </el-button>
-        <el-button
-          v-if="task.flowStatus === 'waiting' && Number(task.nodeRatio) > 0 && buttonObj.subSign"
-          :disabled="buttonDisabled"
-          type="primary"
-          @click="handleTaskUser"
-        >
-          减签
-        </el-button>
-        <el-button
-          v-if="task.flowStatus === 'waiting' && buttonObj.termination"
-          :disabled="buttonDisabled"
-          type="danger"
-          @click="handleTerminationTask"
-        >
-          终止
-        </el-button>
-        <el-button v-if="task.flowStatus === 'waiting' && buttonObj.back" :disabled="buttonDisabled" type="danger" @click="handleBackProcessOpen">
-          退回
-        </el-button>
-        <el-button :disabled="buttonDisabled" @click="cancel">取消</el-button>
-      </span>
-    </template>
-    <!-- 抄送 -->
-    <UserSelect ref="userSelectCopyRef" :multiple="true" :data="selectCopyUserIds" @confirm-call-back="userSelectCopyCallBack"></UserSelect>
-    <!-- 转办 -->
-    <UserSelect ref="transferTaskRef" :multiple="false" @confirm-call-back="handleTransferTask"></UserSelect>
-    <!-- 委托 -->
-    <UserSelect ref="delegateTaskRef" :multiple="false" @confirm-call-back="handleDelegateTask"></UserSelect>
-    <!-- 加签组件 -->
-    <UserSelect ref="multiInstanceUserRef" :multiple="true" @confirm-call-back="addMultiInstanceUser"></UserSelect>
-    <!-- 弹窗选人 -->
-    <UserSelect ref="porUserRef" :data="form.assigneeMap[nodeCode]" :multiple="true" :userIds="popUserIds" @confirm-call-back="handlePopUser"></UserSelect>
-
-    <!-- 驳回开始 -->
-    <el-dialog v-model="backVisible" draggable title="驳回" width="40%" :close-on-click-modal="false">
-      <el-form v-if="task.flowStatus === 'waiting'" v-loading="backLoading" :model="backForm" label-width="120px">
-        <el-form-item label="驳回节点">
-          <el-select v-model="backForm.nodeCode" clearable placeholder="请选择" style="width: 300px">
-            <el-option v-for="item in taskNodeList" :key="item.nodeCode" :label="item.nodeName" :value="item.nodeCode" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="消息提醒">
-          <el-checkbox-group v-model="backForm.messageType">
-            <el-checkbox label="1" name="type" disabled>站内信</el-checkbox>
-            <el-checkbox label="2" name="type">邮件</el-checkbox>
-            <el-checkbox label="3" name="type">短信</el-checkbox>
-          </el-checkbox-group>
-        </el-form-item>
-        <el-form-item v-if="task.flowStatus === 'waiting'" label="附件">
-          <fileUpload
-            v-model="backForm.fileId"
-            :file-type="['png', 'jpg', 'jpeg', 'doc', 'docx', 'xlsx', 'xls', 'ppt', 'txt', 'pdf']"
-            :file-size="20"
-          />
-        </el-form-item>
-        <el-form-item label="审批意见">
-          <el-input v-model="backForm.message" type="textarea" resize="none" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer" style="float: right; padding-bottom: 20px">
-          <el-button :disabled="backButtonDisabled" type="primary" @click="handleBackProcess">确认</el-button>
-          <el-button :disabled="backButtonDisabled" @click="backVisible = false">取消</el-button>
-        </div>
-      </template>
-    </el-dialog>
-    <!-- 驳回结束 -->
-    <el-dialog v-model="deleteSignatureVisible" draggable title="减签人员" width="700px" height="400px" append-to-body :close-on-click-modal="false">
-      <div>
-        <el-table :data="deleteUserList" border>
-          <el-table-column prop="nodeName" label="任务名称" />
-          <el-table-column prop="nickName" label="办理人" />
-          <el-table-column label="操作" align="center" width="160">
-            <template #default="scope">
-              <el-button type="danger" size="small" icon="Delete" @click="deleteMultiInstanceUser(scope.row)">删除 </el-button>
-            </template>
-          </el-table-column>
-        </el-table>
-      </div>
-    </el-dialog>
-  </el-dialog>
-</template>
-
-<script setup lang="ts">
-import { ref } from 'vue';
-import { ComponentInternalInstance } from 'vue';
-import { ElForm } from 'element-plus';
-import {
-  completeTask,
-  backProcess,
-  getTask,
-  taskOperation,
-  terminationTask,
-  getBackTaskNode,
-  currentTaskAllUser,
-  getNextNodeList
-} from '@/api/workflow/task';
-import UserSelect from '@/components/UserSelect';
-
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-import { FlowCopyVo, FlowTaskVO, TaskOperationBo } from '@/api/workflow/task/types';
-
-const userSelectCopyRef = ref<InstanceType<typeof UserSelect>>();
-const transferTaskRef = ref<InstanceType<typeof UserSelect>>();
-const delegateTaskRef = ref<InstanceType<typeof UserSelect>>();
-const multiInstanceUserRef = ref<InstanceType<typeof UserSelect>>();
-const porUserRef = ref<InstanceType<typeof UserSelect>>();
-
-const props = defineProps({
-  taskVariables: {
-    type: Object as () => Record<string, any>,
-    default: () => {}
-  }
-});
-//遮罩层
-const loading = ref(true);
-//按钮
-const buttonDisabled = ref(true);
-//任务id
-const taskId = ref<string>('');
-//抄送人
-const selectCopyUserList = ref<FlowCopyVo[]>([]);
-//抄送人id
-const selectCopyUserIds = ref<string>(undefined);
-//自定义节点变量
-const varNodeList = ref<Map<string, string>>(undefined);
-//可减签的人员
-const deleteUserList = ref<any>([]);
-//弹窗可选择的人员id
-const popUserIds = ref<any>([]);
-//驳回是否显示
-const backVisible = ref(false);
-const backLoading = ref(true);
-const backButtonDisabled = ref(true);
-// 可驳回得任务节点
-const taskNodeList = ref([]);
-const nickName = ref({});
-//节点编码
-const nodeCode = ref<string>('');
-const buttonObj = ref<any>({
-  pop: false,
-  trust: false,
-  transfer: false,
-  addSign: false,
-  subSign: false,
-  termination: false,
-  back: false
-});
-//下一节点列表
-const nestNodeList = ref([]);
-//任务
-const task = ref<FlowTaskVO>({
-  id: undefined,
-  createTime: undefined,
-  updateTime: undefined,
-  tenantId: undefined,
-  definitionId: undefined,
-  instanceId: undefined,
-  flowName: undefined,
-  businessId: undefined,
-  nodeCode: undefined,
-  nodeName: undefined,
-  flowCode: undefined,
-  flowStatus: undefined,
-  formCustom: undefined,
-  formPath: undefined,
-  nodeType: undefined,
-  nodeRatio: undefined,
-  applyNode: false,
-  buttonList: [],
-  copyList: [],
-  varList: undefined,
-  businessCode: undefined,
-  businessTitle: undefined
-});
-const dialog = reactive<DialogOption>({
-  visible: false,
-  title: '提示'
-});
-//减签弹窗
-const deleteSignatureVisible = ref(false);
-const form = ref<Record<string, any>>({
-  taskId: undefined,
-  message: undefined,
-  assigneeMap: {},
-  variables: {},
-  messageType: ['1'],
-  flowCopyList: []
-});
-const backForm = ref<Record<string, any>>({
-  taskId: undefined,
-  nodeCode: undefined,
-  message: undefined,
-  variables: {},
-  messageType: ['1']
-});
-
-//打开弹窗
-const openDialog = async (id?: string) => {
-  selectCopyUserIds.value = undefined;
-  selectCopyUserList.value = [];
-  form.value.fileId = undefined;
-  taskId.value = id;
-  form.value.message = undefined;
-  dialog.visible = true;
-  loading.value = true;
-  buttonDisabled.value = true;
-  const response = await getTask(taskId.value);
-  task.value = response.data;
-  buttonObj.value = {};
-  task.value.buttonList?.forEach((e) => {
-    buttonObj.value[e.code] = e.show;
-  });
-  selectCopyUserList.value = task.value.copyList;
-  selectCopyUserIds.value = task.value.copyList.map((e) => e.userId).join(',');
-  varNodeList.value = task.value.varList;
-  console.log('varNodeList', varNodeList.value)
-  buttonDisabled.value = false;
-  try {
-    const data = {
-      taskId: taskId.value,
-      variables: props.taskVariables
-    };
-    const nextData = await getNextNodeList(data);
-    nestNodeList.value = nextData.data;
-  } finally {
-    loading.value = false;
-  }
-};
-
-onMounted(() => {});
-const emits = defineEmits(['submitCallback', 'cancelCallback']);
-
-/** 办理流程 */
-const handleCompleteTask = async () => {
-  form.value.taskId = taskId.value;
-  form.value.variables = props.taskVariables;
-  let verify = false;
-  if (buttonObj.value.pop && nestNodeList.value && nestNodeList.value.length > 0) {
-    nestNodeList.value.forEach((e) => {
-      if (
-        Object.keys(form.value.assigneeMap).length === 0 ||
-        form.value.assigneeMap[e.nodeCode] === '' ||
-        form.value.assigneeMap[e.nodeCode] === null ||
-        form.value.assigneeMap[e.nodeCode] === undefined
-      ) {
-        verify = true;
-      }
-    });
-    if (verify) {
-      proxy?.$modal.msgWarning('请选择审批人!');
-      return false;
-    }
-  } else {
-    form.value.assigneeMap = {};
-  }
-  if (selectCopyUserList.value && selectCopyUserList.value.length > 0) {
-    const flowCopyList = [];
-    selectCopyUserList.value.forEach((e) => {
-      const copyUser = {
-        userId: e.userId,
-        userName: e.userName
-      };
-      flowCopyList.push(copyUser);
-    });
-    form.value.flowCopyList = flowCopyList;
-  }
-  await proxy?.$modal.confirm('是否确认提交?');
-  loading.value = true;
-  buttonDisabled.value = true;
-  try {
-    await completeTask(form.value);
-    dialog.visible = false;
-    emits('submitCallback');
-    proxy?.$modal.msgSuccess('操作成功');
-  } finally {
-    loading.value = false;
-    buttonDisabled.value = false;
-  }
-};
-
-/** 驳回弹窗打开 */
-const handleBackProcessOpen = async () => {
-  backForm.value = {};
-  backForm.value.messageType = ['1'];
-  backVisible.value = true;
-  backLoading.value = true;
-  backButtonDisabled.value = true;
-  const data = await getBackTaskNode(task.value.id, task.value.nodeCode);
-  taskNodeList.value = data.data;
-  backLoading.value = false;
-  backButtonDisabled.value = false;
-  backForm.value.nodeCode = taskNodeList.value[0].nodeCode;
-};
-/** 驳回流程 */
-const handleBackProcess = async () => {
-  backForm.value.taskId = taskId.value;
-  await proxy?.$modal.confirm('是否确认驳回到申请人?');
-  loading.value = true;
-  backLoading.value = true;
-  backButtonDisabled.value = true;
-  await backProcess(backForm.value).finally(() => {
-    loading.value = false;
-    buttonDisabled.value = false;
-  });
-  dialog.visible = false;
-  backLoading.value = false;
-  backButtonDisabled.value = false;
-  emits('submitCallback');
-  proxy?.$modal.msgSuccess('操作成功');
-};
-//取消
-const cancel = async () => {
-  dialog.visible = false;
-  buttonDisabled.value = false;
-  nickName.value = {};
-  form.value.assigneeMap = {};
-  emits('cancelCallback');
-};
-//打开抄送人员
-const openUserSelectCopy = () => {
-  userSelectCopyRef.value.open();
-};
-//确认抄送人员
-const userSelectCopyCallBack = (data: FlowCopyVo[]) => {
-  if (data && data.length > 0) {
-    selectCopyUserList.value = data;
-    selectCopyUserIds.value = selectCopyUserList.value.map((item) => item.userId).join(',');
-  }
-};
-//删除抄送人员
-const handleCopyCloseTag = (user: FlowCopyVo) => {
-  const userId = user.userId;
-  // 使用split删除用户
-  const index = selectCopyUserList.value.findIndex((item) => item.userId === userId);
-  selectCopyUserList.value.splice(index, 1);
-  selectCopyUserIds.value = selectCopyUserList.value.map((item) => item.userId).join(',');
-};
-//加签
-const openMultiInstanceUser = async () => {
-  multiInstanceUserRef.value.open();
-};
-//加签
-const addMultiInstanceUser = async (data) => {
-  if (data && data.length > 0) {
-    const taskOperationBo = reactive<TaskOperationBo>({
-      userIds: data.map((e) => e.userId),
-      taskId: taskId.value,
-      message: form.value.message
-    });
-    await proxy?.$modal.confirm('是否确认提交?');
-    loading.value = true;
-    buttonDisabled.value = true;
-    await taskOperation(taskOperationBo, 'addSignature').finally(() => {
-      loading.value = false;
-      buttonDisabled.value = false;
-    });
-    dialog.visible = false;
-    emits('submitCallback');
-    proxy?.$modal.msgSuccess('操作成功');
-  } else {
-    proxy?.$modal.msgWarning('请选择用户!');
-  }
-};
-//减签
-const deleteMultiInstanceUser = async (row) => {
-  await proxy?.$modal.confirm('是否确认提交?');
-  loading.value = true;
-  buttonDisabled.value = true;
-  const taskOperationBo = reactive<TaskOperationBo>({
-    userIds: [row.userId],
-    taskId: taskId.value,
-    message: form.value.message
-  });
-  await taskOperation(taskOperationBo, 'reductionSignature').finally(() => {
-    loading.value = false;
-    buttonDisabled.value = false;
-  });
-  dialog.visible = false;
-  emits('submitCallback');
-  proxy?.$modal.msgSuccess('操作成功');
-};
-//打开转办
-const openTransferTask = () => {
-  transferTaskRef.value.open();
-};
-//转办
-const handleTransferTask = async (data) => {
-  if (data && data.length > 0) {
-    const taskOperationBo = reactive<TaskOperationBo>({
-      userId: data[0].userId,
-      taskId: taskId.value,
-      message: form.value.message
-    });
-    await proxy?.$modal.confirm('是否确认提交?');
-    loading.value = true;
-    buttonDisabled.value = true;
-    await taskOperation(taskOperationBo, 'transferTask').finally(() => {
-      loading.value = false;
-      buttonDisabled.value = false;
-    });
-    dialog.visible = false;
-    emits('submitCallback');
-    proxy?.$modal.msgSuccess('操作成功');
-  } else {
-    proxy?.$modal.msgWarning('请选择用户!');
-  }
-};
-
-//打开委托
-const openDelegateTask = () => {
-  delegateTaskRef.value.open();
-};
-//委托
-const handleDelegateTask = async (data) => {
-  if (data && data.length > 0) {
-    const taskOperationBo = reactive<TaskOperationBo>({
-      userId: data[0].userId,
-      taskId: taskId.value,
-      message: form.value.message
-    });
-    await proxy?.$modal.confirm('是否确认提交?');
-    loading.value = true;
-    buttonDisabled.value = true;
-    await taskOperation(taskOperationBo, 'delegateTask').finally(() => {
-      loading.value = false;
-      buttonDisabled.value = false;
-    });
-    dialog.visible = false;
-    emits('submitCallback');
-    proxy?.$modal.msgSuccess('操作成功');
-  } else {
-    proxy?.$modal.msgWarning('请选择用户!');
-  }
-};
-//终止任务
-const handleTerminationTask = async () => {
-  const params = {
-    taskId: taskId.value,
-    comment: form.value.message
-  };
-  await proxy?.$modal.confirm('是否确认终止?');
-  loading.value = true;
-  buttonDisabled.value = true;
-  await terminationTask(params).finally(() => {
-    loading.value = false;
-    buttonDisabled.value = false;
-  });
-  dialog.visible = false;
-  emits('submitCallback');
-  proxy?.$modal.msgSuccess('操作成功');
-};
-const handleTaskUser = async () => {
-  const data = await currentTaskAllUser(taskId.value);
-  deleteUserList.value = data.data;
-  if (deleteUserList.value && deleteUserList.value.length > 0) {
-    deleteUserList.value.forEach((e) => {
-      e.nodeName = task.value.nodeName;
-    });
-  }
-  deleteSignatureVisible.value = true;
-};
-// 选择人员
-const choosePeople = async (data) => {
-  if (!data.permissionFlag) {
-    proxy?.$modal.msgError('没有可选择的人员,请联系管理员!');
-  }
-  popUserIds.value = data.permissionFlag;
-  nodeCode.value = data.nodeCode;
-  porUserRef.value.open();
-};
-//确认选择
-const handlePopUser = async (userList) => {
-  const userIds = userList.map((item) => {
-    return item.userId;
-  });
-  const nickNames = userList.map((item) => {
-    return item.nickName;
-  });
-  form.value.assigneeMap[nodeCode.value] = userIds.join(',');
-  nickName.value[nodeCode.value] = nickNames.join(',');
-};
-
-/**
- * 对外暴露子组件方法
- */
-defineExpose({
-  openDialog
-});
-</script>

+ 138 - 0
src/components/ProductCard/index.vue

@@ -0,0 +1,138 @@
+<template>
+  <div class="product-card">
+    <div class="card-header" v-if="showCheckbox || showAction">
+      <el-checkbox v-if="showCheckbox" v-model="checked" @change="handleCheck" />
+      <slot name="action">
+        <span v-if="actionText" class="action-btn" @click="$emit('action')">{{ actionText }}</span>
+      </slot>
+    </div>
+    <div class="product-image">
+      <el-image :src="product.image" fit="contain">
+        <template #error>
+          <div class="image-placeholder">
+            <el-icon :size="40" color="#ccc"><Picture /></el-icon>
+          </div>
+        </template>
+      </el-image>
+    </div>
+    <div class="product-info">
+      <div class="product-name">{{ product.name }}</div>
+      <div class="product-price">
+        <span v-if="product.tag" class="price-tag">{{ product.tag }}</span>
+        <span class="current-price">¥{{ product.price }}</span>
+        <span v-if="product.originalPrice" class="original-price">¥{{ product.originalPrice }}</span>
+        <div v-if="showAddCart" class="add-cart" @click="$emit('addCart')">
+          <el-icon><Plus /></el-icon>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+import { Picture, Plus } from '@element-plus/icons-vue'
+
+interface Product {
+  image?: string
+  name: string
+  price: string | number
+  originalPrice?: string | number
+  tag?: string
+}
+
+const props = defineProps<{
+  product: Product
+  modelValue?: boolean
+  showCheckbox?: boolean
+  showAction?: boolean
+  showAddCart?: boolean
+  actionText?: string
+}>()
+
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean]
+  'action': []
+  'addCart': []
+}>()
+
+const checked = ref(props.modelValue || false)
+watch(() => props.modelValue, (val) => { checked.value = val || false })
+
+const handleCheck = (val: boolean | string | number) => {
+  emit('update:modelValue', !!val)
+}
+</script>
+
+<style scoped lang="scss">
+.product-card {
+  background: #fff;
+  border-radius: 8px;
+  overflow: hidden;
+  border: 1px solid #eee;
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 10px 12px;
+    .action-btn {
+      font-size: 12px;
+      color: #999;
+      cursor: pointer;
+      &:hover { color: #e60012; }
+    }
+  }
+
+  .product-image {
+    height: 160px;
+    background: #f9f9f9;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 10px;
+    .el-image { width: 100%; height: 100%; }
+    .image-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f5f5f5; }
+  }
+
+  .product-info {
+    padding: 12px;
+    .product-name {
+      font-size: 13px;
+      color: #333;
+      line-height: 1.4;
+      height: 36px;
+      overflow: hidden;
+      display: -webkit-box;
+      -webkit-line-clamp: 2;
+      -webkit-box-orient: vertical;
+      margin-bottom: 8px;
+    }
+    .product-price {
+      display: flex;
+      align-items: center;
+      gap: 5px;
+      position: relative;
+      .price-tag { font-size: 12px; color: #e60012; }
+      .current-price { font-size: 16px; font-weight: bold; color: #e60012; }
+      .original-price { font-size: 12px; color: #999; text-decoration: line-through; }
+      .add-cart {
+        position: absolute;
+        right: 0;
+        bottom: 0;
+        width: 24px;
+        height: 24px;
+        border-radius: 50%;
+        border: 1px solid #e60012;
+        color: #e60012;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        cursor: pointer;
+        transition: all 0.2s;
+        &:hover { background: #e60012; color: #fff; }
+      }
+    }
+  }
+}
+</style>

+ 64 - 0
src/components/ProductItem/index.vue

@@ -0,0 +1,64 @@
+<template>
+  <div class="product-item">
+    <div class="product-image">
+      <el-image :src="product.image" fit="contain">
+        <template #error>
+          <div class="image-placeholder">
+            <el-icon :size="30" color="#ccc"><Picture /></el-icon>
+          </div>
+        </template>
+      </el-image>
+    </div>
+    <div class="product-detail">
+      <div class="product-name">{{ product.name }}</div>
+      <div class="product-spec" v-if="product.spec1">{{ product.spec1 }} {{ product.spec2 }}</div>
+      <div class="product-price">¥{{ product.price }}</div>
+    </div>
+    <div class="product-quantity">x{{ product.quantity }}</div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Picture } from '@element-plus/icons-vue'
+
+interface Product {
+  image?: string
+  name: string
+  spec1?: string
+  spec2?: string
+  price: string | number
+  quantity: number
+}
+
+defineProps<{ product: Product }>()
+</script>
+
+<style scoped lang="scss">
+.product-item {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  padding: 15px;
+
+  .product-image {
+    width: 80px;
+    height: 80px;
+    background: #f5f5f5;
+    border-radius: 4px;
+    overflow: hidden;
+    flex-shrink: 0;
+    .el-image { width: 100%; height: 100%; }
+    .image-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
+  }
+
+  .product-detail {
+    flex: 1;
+    .product-name { font-size: 14px; color: #333; margin-bottom: 5px; line-height: 1.4; }
+    .product-spec { font-size: 12px; color: #999; margin-bottom: 5px; }
+    .product-price { font-size: 16px; font-weight: bold; color: #e60012; }
+  }
+
+  .product-quantity { font-size: 13px; color: #666; }
+}
+</style>

+ 0 - 102
src/components/RightToolbar/index.vue

@@ -1,102 +0,0 @@
-<template>
-  <div class="top-right-btn" :style="style">
-    <el-row>
-      <el-tooltip v-if="search" class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top">
-        <el-button circle icon="Search" @click="toggleSearch()" />
-      </el-tooltip>
-      <el-tooltip class="item" effect="dark" content="刷新" placement="top">
-        <el-button circle icon="Refresh" @click="refresh()" />
-      </el-tooltip>
-      <el-tooltip v-if="columns" class="item" effect="dark" content="显示/隐藏列" placement="top">
-        <div class="show-btn">
-          <el-popover placement="bottom" trigger="click">
-            <div class="tree-header">显示/隐藏列</div>
-            <el-tree
-              ref="columnRef"
-              :data="columns"
-              show-checkbox
-              node-key="key"
-              :props="{ label: 'label', children: 'children' } as any"
-              @check="columnChange"
-            ></el-tree>
-            <template #reference>
-              <el-button circle icon="Menu" />
-            </template>
-          </el-popover>
-        </div>
-      </el-tooltip>
-    </el-row>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { propTypes } from '@/utils/propTypes';
-
-const props = defineProps({
-  showSearch: propTypes.bool.def(true),
-  columns: propTypes.fieldOption,
-  search: propTypes.bool.def(true),
-  gutter: propTypes.number.def(10)
-});
-
-const columnRef = ref<ElTreeInstance>();
-const emits = defineEmits(['update:showSearch', 'queryTable']);
-
-const style = computed(() => {
-  const ret: any = {};
-  if (props.gutter) {
-    ret.marginRight = `${props.gutter / 2}px`;
-  }
-  return ret;
-});
-
-// 搜索
-function toggleSearch() {
-  emits('update:showSearch', !props.showSearch);
-}
-
-// 刷新
-function refresh() {
-  emits('queryTable');
-}
-
-// 更改数据列的显示和隐藏
-function columnChange(...args: any[]) {
-  props.columns?.forEach((item) => {
-    item.visible = args[1].checkedKeys.includes(item.key);
-  });
-}
-
-// 显隐列初始默认隐藏列
-onMounted(() => {
-  props.columns?.forEach((item) => {
-    if (item.visible) {
-      columnRef.value?.setChecked(item.key, true, false);
-      // value.value.push(item.key);
-    }
-  });
-});
-</script>
-
-<style lang="scss" scoped>
-:deep(.el-transfer__button) {
-  border-radius: 50%;
-  display: block;
-  margin-left: 0px;
-}
-:deep(.el-transfer__button:first-child) {
-  margin-bottom: 10px;
-}
-
-.my-el-transfer {
-  text-align: center;
-}
-.tree-header {
-  width: 100%;
-  line-height: 24px;
-  text-align: center;
-}
-.show-btn {
-  margin-left: 12px;
-}
-</style>

+ 0 - 250
src/components/RoleSelect/index.vue

@@ -1,250 +0,0 @@
-<template>
-  <div>
-    <el-dialog v-model="roleDialog.visible.value" :title="roleDialog.title.value" width="80%" append-to-body>
-      <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
-        <div v-show="showSearch" class="mb-[10px]">
-          <el-card shadow="hover">
-            <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-              <el-form-item label="角色名称" prop="roleName">
-                <el-input v-model="queryParams.roleName" placeholder="请输入角色名称" clearable @keyup.enter="handleQuery" />
-              </el-form-item>
-              <el-form-item label="权限字符" prop="roleKey">
-                <el-input v-model="queryParams.roleKey" placeholder="请输入权限字符" clearable @keyup.enter="handleQuery" />
-              </el-form-item>
-
-              <el-form-item>
-                <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-                <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-              </el-form-item>
-            </el-form>
-          </el-card>
-        </div>
-      </transition>
-
-      <el-card shadow="hover">
-        <template #header>
-          <el-tag v-for="role in selectRoleList" :key="role.roleId" closable style="margin: 2px" @close="handleCloseTag(role)">
-            {{ role.roleName }}
-          </el-tag>
-        </template>
-
-        <vxe-table
-          ref="tableRef"
-          height="400px"
-          border
-          show-overflow
-          :data="roleList"
-          :loading="loading"
-          :row-config="{ keyField: 'roleId' }"
-          :checkbox-config="{ reserve: true, checkRowKeys: defaultSelectRoleIds }"
-          highlight-current-row
-          @checkbox-all="handleCheckboxAll"
-          @checkbox-change="handleCheckboxChange"
-        >
-          <vxe-column type="checkbox" width="50" align="center" />
-          <vxe-column v-if="false" key="roleId" label="角色编号" />
-          <vxe-column field="roleName" title="角色名称" />
-          <vxe-column field="roleKey" title="权限字符" />
-          <vxe-column field="roleSort" title="显示顺序" width="100" />
-          <vxe-column title="状态" align="center" width="100">
-            <template #default="scope">
-              <dict-tag :options="sys_normal_disable" :value="scope.row.status"></dict-tag>
-            </template>
-          </vxe-column>
-          <vxe-column field="createTime" title="创建时间" align="center">
-            <template #default="scope">
-              <span>{{ proxy.parseTime(scope.row.createTime) }}</span>
-            </template>
-          </vxe-column>
-        </vxe-table>
-
-        <pagination
-          v-if="total > 0"
-          v-model:total="total"
-          v-model:page="queryParams.pageNum"
-          v-model:limit="queryParams.pageSize"
-          @pagination="pageList"
-        />
-      </el-card>
-      <template #footer>
-        <el-button @click="close">取消</el-button>
-        <el-button type="primary" @click="confirm">确定</el-button>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { RoleVO, RoleQuery } from '@/api/system/role/types';
-import { VxeTableInstance } from 'vxe-table';
-import useDialog from '@/hooks/useDialog';
-import api from '@/api/system/role';
-interface PropType {
-  modelValue?: RoleVO[] | RoleVO | undefined;
-  multiple?: boolean;
-  data?: string | number | (string | number)[];
-}
-const prop = withDefaults(defineProps<PropType>(), {
-  multiple: true,
-  modelValue: undefined,
-  data: undefined
-});
-const emit = defineEmits(['update:modelValue', 'confirmCallBack']);
-
-const router = useRouter();
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
-
-const roleList = ref<RoleVO[]>();
-const loading = ref(true);
-const showSearch = ref(true);
-const total = ref(0);
-const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
-const selectRoleList = ref<RoleVO[]>([]);
-
-const roleDialog = useDialog({
-  title: '角色选择'
-});
-
-const queryFormRef = ref<ElFormInstance>();
-const tableRef = ref<VxeTableInstance<RoleVO>>();
-
-const queryParams = ref<RoleQuery>({
-  pageNum: 1,
-  pageSize: 10,
-  roleName: '',
-  roleKey: '',
-  status: ''
-});
-
-const defaultSelectRoleIds = computed(() => computedIds(prop.data));
-
-const confirm = () => {
-  emit('update:modelValue', selectRoleList.value);
-  emit('confirmCallBack', selectRoleList.value);
-  roleDialog.closeDialog();
-};
-
-const computedIds = (data) => {
-  if (data instanceof Array) {
-    return [...data];
-  } else if (typeof data === 'string') {
-    return data.split(',');
-  } else if (typeof data === 'number') {
-    return [data];
-  } else {
-    console.warn('<RoleSelect> The data type of data should be array or string or number, but I received other');
-    return [];
-  }
-};
-
-/**
- * 查询角色列表
- */
-const getList = () => {
-  loading.value = true;
-  api.listRole(proxy?.addDateRange(queryParams.value, dateRange.value)).then((res) => {
-    roleList.value = res.rows;
-    total.value = res.total;
-    loading.value = false;
-  });
-};
-const pageList = async () => {
-  await getList();
-  const roles = roleList.value.filter((item) => {
-    return selectRoleList.value.some((role) => role.roleId === item.roleId);
-  });
-  await tableRef.value.setCheckboxRow(roles, true);
-};
-/**
- * 搜索按钮操作
- */
-const handleQuery = () => {
-  queryParams.value.pageNum = 1;
-  getList();
-};
-
-/** 重置 */
-const resetQuery = () => {
-  dateRange.value = ['', ''];
-  queryFormRef.value?.resetFields();
-  handleQuery();
-};
-
-const handleCheckboxChange = (checked) => {
-  if (!prop.multiple && checked.checked) {
-    tableRef.value.setCheckboxRow(selectRoleList.value, false);
-    selectRoleList.value = [];
-  }
-  const row = checked.row;
-  if (checked.checked) {
-    selectRoleList.value.push(row);
-  } else {
-    selectRoleList.value = selectRoleList.value.filter((item) => {
-      return item.roleId !== row.roleId;
-    });
-  }
-};
-const handleCheckboxAll = (checked) => {
-  const rows = roleList.value;
-  if (checked.checked) {
-    rows.forEach((row) => {
-      if (!selectRoleList.value.some((item) => item.roleId === row.roleId)) {
-        selectRoleList.value.push(row);
-      }
-    });
-  } else {
-    selectRoleList.value = selectRoleList.value.filter((item) => {
-      return !rows.some((row) => row.roleId === item.roleId);
-    });
-  }
-};
-
-const handleCloseTag = (user: RoleVO) => {
-  const roleId = user.roleId;
-  // 使用split删除用户
-  const index = selectRoleList.value.findIndex((item) => item.roleId === roleId);
-  const rows = selectRoleList.value[index];
-  tableRef.value?.setCheckboxRow(rows, false);
-  selectRoleList.value.splice(index, 1);
-};
-/**
- * 初始化选中数据
- */
-const initSelectRole = async () => {
-  if (defaultSelectRoleIds.value.length > 0) {
-    const { data } = await api.optionSelect(defaultSelectRoleIds.value);
-    selectRoleList.value = data;
-    const users = roleList.value.filter((item) => {
-      return defaultSelectRoleIds.value.includes(String(item.roleId));
-    });
-    await nextTick(() => {
-      tableRef.value.setCheckboxRow(users, true);
-    });
-  }
-};
-const close = () => {
-  roleDialog.closeDialog();
-};
-watch(
-  () => roleDialog.visible.value,
-  (newValue: boolean) => {
-    if (newValue) {
-      initSelectRole();
-    } else {
-      tableRef.value.clearCheckboxReserve();
-      tableRef.value.clearCheckboxRow();
-      resetQuery();
-      selectRoleList.value = [];
-    }
-  }
-);
-onMounted(() => {
-  getList(); // 初始化列表数据
-});
-
-defineExpose({
-  open: roleDialog.openDialog,
-  close: roleDialog.closeDialog
-});
-</script>

+ 0 - 13
src/components/RuoYiDoc/index.vue

@@ -1,13 +0,0 @@
-<template>
-  <div>
-    <svg-icon icon-class="question" @click="goto" />
-  </div>
-</template>
-
-<script setup lang="ts">
-const url = ref('https://plus-doc.dromara.org/');
-
-function goto() {
-  window.open(url.value);
-}
-</script>

+ 0 - 13
src/components/RuoYiGit/index.vue

@@ -1,13 +0,0 @@
-<template>
-  <div>
-    <svg-icon icon-class="github" @click="goto" />
-  </div>
-</template>
-
-<script setup lang="ts">
-const url = ref('https://gitee.com/dromara/RuoYi-Vue-Plus');
-
-function goto() {
-  window.open(url.value);
-}
-</script>

+ 0 - 9
src/components/Screenfull/index.vue

@@ -1,9 +0,0 @@
-<template>
-  <div>
-    <svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
-  </div>
-</template>
-
-<script setup lang="ts">
-const { isFullscreen, toggle } = useFullscreen();
-</script>

+ 124 - 0
src/components/SearchBar/index.vue

@@ -0,0 +1,124 @@
+<template>
+  <div class="search-bar">
+    <div class="search-row">
+      <el-input v-model="form.keyword" :placeholder="placeholder" clearable style="width: 200px">
+        <template #prefix>
+          <el-icon><Search /></el-icon>
+        </template>
+      </el-input>
+      <el-date-picker
+        v-if="showDateRange"
+        v-model="form.dateRange"
+        type="daterange"
+        start-placeholder="开始日期"
+        end-placeholder="结束日期"
+        value-format="YYYY-MM-DD"
+        style="width: 220px"
+      />
+    </div>
+    <div class="search-row" v-if="filters.length > 0 || $slots.extra">
+      <div class="filter-item" v-for="filter in filters" :key="filter.field">
+        <span class="label">{{ filter.label }}</span>
+        <el-select v-model="form[filter.field]" placeholder="请选择" clearable style="width: 100px">
+          <el-option v-for="opt in filter.options" :key="opt.value" :label="opt.label" :value="opt.value" />
+        </el-select>
+      </div>
+      <slot name="extra"></slot>
+      <div class="btn-group">
+        <slot name="buttons"></slot>
+        <el-button type="danger" @click="handleSearch">搜索</el-button>
+        <el-button @click="handleReset">重置</el-button>
+      </div>
+    </div>
+    <div class="search-row btn-only" v-else>
+      <slot name="buttons"></slot>
+      <el-button type="danger" @click="handleSearch">搜索</el-button>
+      <el-button @click="handleReset">重置</el-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { Search } from '@element-plus/icons-vue'
+
+export interface FilterOption {
+  label: string
+  value: string
+}
+
+export interface FilterConfig {
+  field: string
+  label: string
+  options: FilterOption[]
+}
+
+const props = withDefaults(defineProps<{
+  form: Record<string, any>
+  filters?: FilterConfig[]
+  showDateRange?: boolean
+  placeholder?: string
+}>(), {
+  filters: () => [],
+  showDateRange: true,
+  placeholder: '搜索'
+})
+
+const emit = defineEmits<{
+  search: []
+  reset: []
+}>()
+
+const handleSearch = () => {
+  emit('search')
+}
+
+const handleReset = () => {
+  // 重置表单
+  Object.keys(props.form).forEach(key => {
+    if (key === 'dateRange') {
+      props.form[key] = []
+    } else {
+      props.form[key] = ''
+    }
+  })
+  emit('reset')
+}
+</script>
+
+<style scoped lang="scss">
+.search-bar {
+  margin-bottom: 20px;
+  
+  .search-row {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    margin-bottom: 12px;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+    
+    &.btn-only {
+      margin-top: 0;
+    }
+  }
+  
+  .filter-item {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    
+    .label {
+      color: #666;
+      font-size: 14px;
+    }
+  }
+  
+  .btn-group {
+    margin-left: auto;
+    display: flex;
+    gap: 10px;
+  }
+}
+</style>

+ 0 - 41
src/components/SizeSelect/index.vue

@@ -1,41 +0,0 @@
-<template>
-  <div>
-    <el-dropdown trigger="click" @command="handleSetSize">
-      <div class="size-icon--style">
-        <svg-icon class-name="size-icon" icon-class="size" />
-      </div>
-      <template #dropdown>
-        <el-dropdown-menu>
-          <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
-            {{ item.label }}
-          </el-dropdown-item>
-        </el-dropdown-menu>
-      </template>
-    </el-dropdown>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { useAppStore } from '@/store/modules/app';
-
-const appStore = useAppStore();
-const size = computed(() => appStore.size);
-
-const sizeOptions = ref([
-  { label: '较大', value: 'large' },
-  { label: '默认', value: 'default' },
-  { label: '稍小', value: 'small' }
-]);
-
-const handleSetSize = (size: 'large' | 'default' | 'small') => {
-  appStore.setSize(size);
-};
-</script>
-
-<style lang="scss" scoped>
-.size-icon--style {
-  font-size: 18px;
-  line-height: 50px;
-  padding-right: 7px;
-}
-</style>

+ 47 - 0
src/components/StatCards/index.vue

@@ -0,0 +1,47 @@
+<template>
+  <div class="stat-cards">
+    <div class="stat-card" v-for="item in data" :key="item.label">
+      <div class="stat-value">{{ item.value }}</div>
+      <div class="stat-label">{{ item.label }}</div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+export interface StatItem {
+  value: string | number
+  label: string
+}
+
+defineProps<{
+  data: StatItem[]
+}>()
+</script>
+
+<style scoped lang="scss">
+.stat-cards {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 20px;
+  
+  .stat-card {
+    flex: 1;
+    padding: 20px;
+    background: #fafafa;
+    border-radius: 4px;
+    text-align: center;
+    
+    .stat-value {
+      font-size: 28px;
+      font-weight: bold;
+      color: #333;
+      margin-bottom: 8px;
+    }
+    
+    .stat-label {
+      font-size: 14px;
+      color: #999;
+    }
+  }
+}
+</style>

+ 75 - 0
src/components/StatusTabs/index.vue

@@ -0,0 +1,75 @@
+<template>
+  <div :class="['status-tabs', type]">
+    <div v-for="tab in tabs" :key="tab.key" :class="['tab-item', { active: modelValue === tab.key }]" @click="handleClick(tab.key)">
+      <el-icon v-if="tab.icon && type === 'line'"><component :is="tab.icon" /></el-icon>
+      <span>{{ tab.label }}</span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { Component } from 'vue'
+
+interface TabItem {
+  key: string
+  label: string
+  icon?: Component
+}
+
+defineProps<{
+  tabs: TabItem[]
+  modelValue: string
+  type?: 'line' | 'pill'  // line=下划线样式, pill=胶囊样式
+}>()
+
+const emit = defineEmits<{
+  'update:modelValue': [value: string]
+  'change': [value: string]
+}>()
+
+const handleClick = (key: string) => {
+  emit('update:modelValue', key)
+  emit('change', key)
+}
+</script>
+
+<style scoped lang="scss">
+.status-tabs {
+  display: flex;
+  gap: 10px;
+  margin-bottom: 15px;
+
+  &.line {
+    gap: 30px;
+    border-bottom: 1px solid #eee;
+    .tab-item {
+      display: flex;
+      align-items: center;
+      gap: 5px;
+      padding: 12px 0;
+      cursor: pointer;
+      color: #666;
+      font-size: 14px;
+      border-bottom: 2px solid transparent;
+      margin-bottom: -1px;
+      background: none;
+      &:hover, &.active { color: #333; }
+      &.active { border-bottom-color: #e60012; }
+    }
+  }
+
+  &.pill {
+    .tab-item {
+      padding: 6px 18px;
+      border-radius: 4px;
+      cursor: pointer;
+      font-size: 13px;
+      color: #666;
+      background: #f5f5f5;
+      transition: all 0.2s;
+      &:hover { color: #e60012; }
+      &.active { background: #e60012; color: #fff; }
+    }
+  }
+}
+</style>

+ 0 - 40
src/components/SvgIcon/index.vue

@@ -1,40 +0,0 @@
-<template>
-  <svg :class="svgClass" aria-hidden="true">
-    <use :xlink:href="iconName" :fill="color" />
-  </svg>
-</template>
-
-<script setup lang="ts">
-import { propTypes } from '@/utils/propTypes';
-
-const props = defineProps({
-  iconClass: propTypes.string.isRequired,
-  className: propTypes.string.def(''),
-  color: propTypes.string.def('')
-});
-const iconName = computed(() => `#icon-${props.iconClass}`);
-const svgClass = computed(() => {
-  if (props.className) {
-    return `svg-icon ${props.className}`;
-  }
-  return 'svg-icon';
-});
-</script>
-
-<style lang="scss" scoped>
-.sub-el-icon,
-.nav-icon {
-  display: inline-block;
-  font-size: 15px;
-  margin-right: 12px;
-  position: relative;
-}
-
-.svg-icon {
-  width: 1em;
-  height: 1em;
-  position: relative;
-  fill: currentColor;
-  vertical-align: -2px;
-}
-</style>

+ 57 - 0
src/components/TableActions/index.vue

@@ -0,0 +1,57 @@
+<template>
+  <div class="table-actions">
+    <el-button 
+      v-if="showEdit" 
+      type="primary" 
+      link 
+      size="small" 
+      @click="$emit('edit')"
+    >
+      编辑
+    </el-button>
+    <el-button 
+      v-if="showDelete" 
+      type="danger" 
+      link 
+      size="small" 
+      @click="$emit('delete')"
+    >
+      删除
+    </el-button>
+    <el-button 
+      v-if="showView" 
+      type="primary" 
+      link 
+      size="small" 
+      @click="$emit('view')"
+    >
+      查看
+    </el-button>
+    <slot></slot>
+  </div>
+</template>
+
+<script setup lang="ts">
+withDefaults(defineProps<{
+  showEdit?: boolean
+  showDelete?: boolean
+  showView?: boolean
+}>(), {
+  showEdit: true,
+  showDelete: true,
+  showView: false
+})
+
+defineEmits<{
+  edit: []
+  delete: []
+  view: []
+}>()
+</script>
+
+<style scoped lang="scss">
+.table-actions {
+  display: inline-flex;
+  gap: 4px;
+}
+</style>

+ 35 - 0
src/components/TablePagination/index.vue

@@ -0,0 +1,35 @@
+<template>
+  <div class="pagination-wrap">
+    <el-pagination
+      v-model:current-page="page"
+      v-model:page-size="pageSize"
+      :total="total"
+      :page-sizes="[10, 20, 50, 100]"
+      layout="total, sizes, prev, pager, next, jumper"
+      background
+      @current-change="$emit('change')"
+      @size-change="$emit('change')"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+const page = defineModel<number>('page', { default: 1 })
+const pageSize = defineModel<number>('pageSize', { default: 10 })
+
+defineProps<{
+  total: number
+}>()
+
+defineEmits<{
+  change: []
+}>()
+</script>
+
+<style scoped lang="scss">
+.pagination-wrap {
+  margin-top: 20px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

+ 0 - 200
src/components/TopNav/index.vue

@@ -1,200 +0,0 @@
-<template>
-  <el-menu :default-active="activeMenu" mode="horizontal" :ellipsis="false" @select="handleSelect">
-    <template v-for="(item, index) in topMenus">
-      <el-menu-item v-if="index < visibleNumber" :key="index" :style="{ '--theme': theme }" :index="item.path"
-        ><svg-icon v-if="item.meta && item.meta.icon && item.meta.icon !== '#'" :icon-class="item.meta ? item.meta.icon : ''" />
-        {{ item.meta?.title }}</el-menu-item
-      >
-    </template>
-
-    <!-- 顶部菜单超出数量折叠 -->
-    <el-sub-menu v-if="topMenus.length > visibleNumber" :style="{ '--theme': theme }" index="more">
-      <template #title>更多菜单</template>
-      <template v-for="(item, index) in topMenus">
-        <el-menu-item v-if="index >= visibleNumber" :key="index" :index="item.path"
-          ><svg-icon :icon-class="item.meta ? item.meta.icon : ''" /> {{ item.meta?.title }}</el-menu-item
-        >
-      </template>
-    </el-sub-menu>
-  </el-menu>
-</template>
-
-<script setup lang="ts">
-import { constantRoutes } from '@/router';
-import { isHttp } from '@/utils/validate';
-import { useAppStore } from '@/store/modules/app';
-import { useSettingsStore } from '@/store/modules/settings';
-import { usePermissionStore } from '@/store/modules/permission';
-import { RouteRecordRaw } from 'vue-router';
-
-// 顶部栏初始数
-const visibleNumber = ref<number>(-1);
-// 当前激活菜单的 index
-const currentIndex = ref<string>();
-// 隐藏侧边栏路由
-const hideList = ['/index', '/user/profile'];
-
-const appStore = useAppStore();
-const settingsStore = useSettingsStore();
-const permissionStore = usePermissionStore();
-const route = useRoute();
-const router = useRouter();
-
-// 主题颜色
-const theme = computed(() => settingsStore.theme);
-// 所有的路由信息
-const routers = computed(() => permissionStore.getTopbarRoutes());
-
-// 顶部显示菜单
-const topMenus = computed(() => {
-  const topMenus: RouteRecordRaw[] = [];
-  routers.value.map((menu) => {
-    if (menu.hidden !== true) {
-      // 兼容顶部栏一级菜单内部跳转
-      if (menu.path === '/' && menu.children) {
-        topMenus.push(menu.children ? menu.children[0] : menu);
-      } else {
-        topMenus.push(menu);
-      }
-    }
-  });
-  return topMenus;
-});
-
-// 设置子路由
-const childrenMenus = computed(() => {
-  const childrenMenus: RouteRecordRaw[] = [];
-  routers.value.map((router) => {
-    router.children?.forEach((item) => {
-      if (item.parentPath === undefined) {
-        if (router.path === '/') {
-          item.path = '/' + item.path;
-        } else {
-          if (!isHttp(item.path)) {
-            item.path = router.path + '/' + item.path;
-          }
-        }
-        item.parentPath = router.path;
-      }
-      childrenMenus.push(item);
-    });
-  });
-  return constantRoutes.concat(childrenMenus);
-});
-
-// 默认激活的菜单
-const activeMenu = computed(() => {
-  let path = route.path;
-  if (path === '/index') {
-    path = '/system/user';
-  }
-  let activePath = path;
-  if (path !== undefined && path.lastIndexOf('/') > 0 && hideList.indexOf(path) === -1) {
-    const tmpPath = path.substring(1, path.length);
-    if (!route.meta.link) {
-      activePath = '/' + tmpPath.substring(0, tmpPath.indexOf('/'));
-      appStore.toggleSideBarHide(false);
-    }
-  } else if (!route.children) {
-    activePath = path;
-    appStore.toggleSideBarHide(true);
-  }
-  activeRoutes(activePath);
-  return activePath;
-});
-
-const setVisibleNumber = () => {
-  const width = document.body.getBoundingClientRect().width / 3;
-  visibleNumber.value = parseInt(String(width / 85));
-};
-
-const handleSelect = (key: string) => {
-  currentIndex.value = key;
-  const route = routers.value.find((item) => item.path === key);
-  if (isHttp(key)) {
-    // http(s):// 路径新窗口打开
-    window.open(key, '_blank');
-  } else if (!route || !route.children) {
-    // 没有子路由路径内部打开
-    const routeMenu = childrenMenus.value.find((item) => item.path === key);
-    if (routeMenu && routeMenu.query) {
-      const query = JSON.parse(routeMenu.query);
-      router.push({ path: key, query: query });
-    } else {
-      router.push({ path: key });
-    }
-    appStore.toggleSideBarHide(true);
-  } else {
-    // 显示左侧联动菜单
-    activeRoutes(key);
-    appStore.toggleSideBarHide(false);
-  }
-};
-
-const activeRoutes = (key: string) => {
-  const routes: RouteRecordRaw[] = [];
-  if (childrenMenus.value && childrenMenus.value.length > 0) {
-    childrenMenus.value.map((item) => {
-      if (key == item.parentPath || (key == 'index' && '' == item.path)) {
-        routes.push(item);
-      }
-    });
-  }
-  if (routes.length > 0) {
-    permissionStore.setSidebarRouters(routes);
-  } else {
-    appStore.toggleSideBarHide(true);
-  }
-  return routes;
-};
-
-onMounted(() => {
-  window.addEventListener('resize', setVisibleNumber);
-});
-onBeforeUnmount(() => {
-  window.removeEventListener('resize', setVisibleNumber);
-});
-
-onMounted(() => {
-  setVisibleNumber();
-});
-</script>
-
-<style lang="scss">
-.topmenu-container.el-menu--horizontal > .el-menu-item {
-  float: left;
-  height: 50px !important;
-  line-height: 50px !important;
-  color: #999093 !important;
-  padding: 0 5px !important;
-  margin: 0 10px !important;
-}
-
-.topmenu-container.el-menu--horizontal > .el-menu-item.is-active,
-.el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
-  border-bottom: 2px solid #{'var(--theme)'} !important;
-  color: #303133;
-}
-
-/* sub-menu item */
-.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
-  float: left;
-  height: 50px !important;
-  line-height: 50px !important;
-  color: #999093 !important;
-  padding: 0 5px !important;
-  margin: 0 10px !important;
-}
-
-/* 背景色隐藏 */
-.topmenu-container.el-menu--horizontal > .el-menu-item:not(.is-disabled):focus,
-.topmenu-container.el-menu--horizontal > .el-menu-item:not(.is-disabled):hover,
-.topmenu-container.el-menu--horizontal > .el-submenu .el-submenu__title:hover {
-  background-color: #ffffff !important;
-}
-
-/* 图标右间距 */
-.topmenu-container .svg-icon {
-  margin-right: 4px;
-}
-</style>

+ 0 - 311
src/components/UserSelect/index.vue

@@ -1,311 +0,0 @@
-<template>
-  <div>
-    <el-dialog v-model="userDialog.visible.value" :title="userDialog.title.value" width="80%" append-to-body>
-      <el-row :gutter="20">
-        <!-- 部门树 -->
-        <el-col :lg="4" :xs="24" style="">
-          <el-card shadow="hover">
-            <el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
-            <el-tree
-              ref="deptTreeRef"
-              class="mt-2"
-              node-key="id"
-              :data="deptOptions"
-              :props="{ label: 'label', children: 'children' } as any"
-              :expand-on-click-node="false"
-              :filter-node-method="filterNode"
-              highlight-current
-              default-expand-all
-              @node-click="handleNodeClick"
-            />
-          </el-card>
-        </el-col>
-        <el-col :lg="20" :xs="24">
-          <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
-            <div v-show="showSearch" class="mb-[10px]">
-              <el-card shadow="hover">
-                <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-                  <el-form-item label="用户名称" prop="userName">
-                    <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
-                  </el-form-item>
-                  <el-form-item label="手机号码" prop="phonenumber">
-                    <el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
-                  </el-form-item>
-                  <el-form-item>
-                    <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-                    <el-button icon="Refresh" @click="() => resetQuery()">重置</el-button>
-                  </el-form-item>
-                </el-form>
-              </el-card>
-            </div>
-          </transition>
-
-          <el-card shadow="hover">
-            <template v-if="prop.multiple" #header>
-              <el-tag v-for="user in selectUserList" :key="user.userId" closable style="margin: 2px" @close="handleCloseTag(user)">
-                {{ user.nickName }}
-              </el-tag>
-            </template>
-
-            <vxe-table
-              ref="tableRef"
-              height="400px"
-              border
-              show-overflow
-              :data="userList"
-              :loading="loading"
-              :row-config="{ keyField: 'userId', isHover: true }"
-              :checkbox-config="{ reserve: true, trigger: 'row', highlight: true, showHeader: prop.multiple }"
-              @checkbox-all="handleCheckboxAll"
-              @checkbox-change="handleCheckboxChange"
-            >
-              <vxe-column type="checkbox" width="50" align="center" />
-              <vxe-column key="userId" title="用户编号" align="center" field="userId" />
-              <vxe-column key="userName" title="用户名称" align="center" field="userName" />
-              <vxe-column key="nickName" title="用户昵称" align="center" field="nickName" />
-              <vxe-column key="deptName" title="部门" align="center" field="deptName" />
-              <vxe-column key="phonenumber" title="手机号码" align="center" field="phonenumber" width="120" />
-              <vxe-column key="status" title="状态" align="center">
-                <template #default="scope">
-                  <dict-tag :options="sys_normal_disable" :value="scope.row.status"></dict-tag>
-                </template>
-              </vxe-column>
-
-              <vxe-column title="创建时间" align="center" width="160">
-                <template #default="scope">
-                  <span>{{ scope.row.createTime }}</span>
-                </template>
-              </vxe-column>
-            </vxe-table>
-
-            <pagination
-              v-show="total > 0"
-              v-model:page="queryParams.pageNum"
-              v-model:limit="queryParams.pageSize"
-              :total="total"
-              @pagination="pageList"
-            />
-          </el-card>
-        </el-col>
-      </el-row>
-
-      <template #footer>
-        <el-button @click="close">取消</el-button>
-        <el-button type="primary" @click="confirm">确定</el-button>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup lang="ts">
-import api from '@/api/system/user';
-import { UserQuery, UserVO } from '@/api/system/user/types';
-import { DeptTreeVO, DeptVO } from '@/api/system/dept/types';
-import { VxeTableInstance } from 'vxe-table';
-import useDialog from '@/hooks/useDialog';
-
-interface PropType {
-  modelValue?: UserVO[] | UserVO | undefined;
-  multiple?: boolean;
-  data?: string | number | (string | number)[] | undefined;
-  userIds?: string | number | (string | number)[] | undefined;
-}
-const prop = withDefaults(defineProps<PropType>(), {
-  multiple: true,
-  modelValue: undefined,
-  data: undefined,
-  userIds: undefined
-});
-const emit = defineEmits(['update:modelValue', 'confirmCallBack']);
-
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
-
-const userList = ref<UserVO[]>();
-const loading = ref(true);
-const showSearch = ref(true);
-const total = ref(0);
-const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
-const deptName = ref('');
-const deptOptions = ref<DeptTreeVO[]>([]);
-const selectUserList = ref<UserVO[]>([]);
-
-const deptTreeRef = ref<ElTreeInstance>();
-const queryFormRef = ref<ElFormInstance>();
-const tableRef = ref<VxeTableInstance<UserVO>>();
-
-const userDialog = useDialog({
-  title: '用户选择'
-});
-
-const queryParams = ref<UserQuery>({
-  pageNum: 1,
-  pageSize: 10,
-  userName: '',
-  phonenumber: '',
-  status: '',
-  deptId: '',
-  roleId: '',
-  userIds: ''
-});
-
-const defaultSelectUserIds = computed(() => computedIds(prop.data));
-
-/** 根据名称筛选部门树 */
-watchEffect(
-  () => {
-    deptTreeRef.value?.filter(deptName.value);
-  },
-  {
-    flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
-  }
-);
-
-const confirm = () => {
-  emit('update:modelValue', selectUserList.value);
-  emit('confirmCallBack', selectUserList.value);
-  userDialog.closeDialog();
-};
-
-const computedIds = (data) => {
-  if (data === '' || data === null || data === undefined) {
-    return [];
-  }
-  if (data instanceof Array) {
-    return data.map((item) => String(item));
-  } else if (typeof data === 'string') {
-    return data.split(',');
-  } else if (typeof data === 'number') {
-    return [String(data)];
-  } else {
-    console.warn('<UserSelect> The data type of data should be array or string or number, but I received other');
-    return [];
-  }
-};
-
-/** 通过条件过滤节点  */
-const filterNode = (value: string, data: any) => {
-  if (!value) return true;
-  return data.label.indexOf(value) !== -1;
-};
-
-/** 查询部门下拉树结构 */
-const getTreeSelect = async () => {
-  const res = await api.deptTreeSelect();
-  deptOptions.value = res.data;
-};
-
-/** 查询用户列表 */
-const getList = async () => {
-  loading.value = true;
-  queryParams.value.userIds = prop.userIds;
-  const res = await api.listUser(proxy?.addDateRange(queryParams.value, dateRange.value));
-  loading.value = false;
-  userList.value = res.rows;
-  total.value = res.total;
-};
-
-const pageList = async () => {
-  await getList();
-  const users = userList.value.filter((item) => {
-    return selectUserList.value.some((user) => user.userId === item.userId);
-  });
-  await tableRef.value.setCheckboxRow(users, true);
-};
-
-/** 节点单击事件 */
-const handleNodeClick = (data: DeptVO) => {
-  queryParams.value.deptId = data.id;
-  handleQuery();
-};
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.value.pageNum = 1;
-  getList();
-};
-/** 重置按钮操作 */
-const resetQuery = (refresh = true) => {
-  dateRange.value = ['', ''];
-  queryFormRef.value?.resetFields();
-  queryParams.value.pageNum = 1;
-  queryParams.value.deptId = undefined;
-  deptTreeRef.value?.setCurrentKey(undefined);
-  refresh && handleQuery();
-};
-
-const handleCheckboxChange = (checked) => {
-  if (!prop.multiple && checked.checked) {
-    tableRef.value.setCheckboxRow(selectUserList.value, false);
-    selectUserList.value = [];
-  }
-  const row = checked.row;
-  if (checked.checked) {
-    selectUserList.value.push(row);
-  } else {
-    selectUserList.value = selectUserList.value.filter((item) => {
-      return item.userId !== row.userId;
-    });
-  }
-};
-const handleCheckboxAll = (checked) => {
-  const rows = userList.value;
-  if (checked.checked) {
-    rows.forEach((row) => {
-      if (!selectUserList.value.some((item) => item.userId === row.userId)) {
-        selectUserList.value.push(row);
-      }
-    });
-  } else {
-    selectUserList.value = selectUserList.value.filter((item) => {
-      return !rows.some((row) => row.userId === item.userId);
-    });
-  }
-};
-
-const handleCloseTag = (user: UserVO) => {
-  const userId = user.userId;
-  // 使用split删除用户
-  const index = selectUserList.value.findIndex((item) => item.userId === userId);
-  const rows = selectUserList.value[index];
-  tableRef.value?.setCheckboxRow(rows, false);
-  selectUserList.value.splice(index, 1);
-};
-
-const initSelectUser = async () => {
-  if (defaultSelectUserIds.value.length > 0) {
-    const { data } = await api.optionSelect(defaultSelectUserIds.value);
-    selectUserList.value = data;
-    const users = userList.value.filter((item) => {
-      return defaultSelectUserIds.value.includes(String(item.userId));
-    });
-    await nextTick(() => {
-      tableRef.value.setCheckboxRow(users, true);
-    });
-  }
-};
-const close = () => {
-  userDialog.closeDialog();
-};
-
-watch(
-  () => userDialog.visible.value,
-  async (newValue: boolean) => {
-    if (newValue) {
-      await getTreeSelect(); // 初始化部门数据
-      await getList(); // 初始化列表数据
-      await initSelectUser();
-    } else {
-      tableRef.value.clearCheckboxReserve();
-      tableRef.value.clearCheckboxRow();
-      resetQuery(false);
-      selectUserList.value = [];
-    }
-  }
-);
-
-defineExpose({
-  open: userDialog.openDialog,
-  close: userDialog.closeDialog
-});
-</script>

+ 0 - 26
src/components/iFrame/index.vue

@@ -1,26 +0,0 @@
-<template>
-  <div v-loading="loading" :style="'height:' + height">
-    <iframe :src="url" frameborder="no" style="width: 100%; height: 100%" scrolling="auto" />
-  </div>
-</template>
-
-<script setup lang="ts">
-import { propTypes } from '@/utils/propTypes';
-
-const props = defineProps({
-  src: propTypes.string.isRequired
-});
-
-const height = ref(document.documentElement.clientHeight - 94.5 + 'px;');
-const loading = ref(true);
-const url = computed(() => props.src);
-
-onMounted(() => {
-  setTimeout(() => {
-    loading.value = false;
-  }, 300);
-  window.onresize = function temp() {
-    height.value = document.documentElement.clientHeight - 94.5 + 'px;';
-  };
-});
-</script>

+ 24 - 0
src/components/index.ts

@@ -0,0 +1,24 @@
+// 公共组件统一导出
+import PageTitle from './PageTitle/index.vue'
+import StatusTabs from './StatusTabs/index.vue'
+import ProductItem from './ProductItem/index.vue'
+import ProductCard from './ProductCard/index.vue'
+import SearchBar from './SearchBar/index.vue'
+import StatCards from './StatCards/index.vue'
+import TablePagination from './TablePagination/index.vue'
+import HeaderBar from './HeaderBar/index.vue'
+import EmptyState from './EmptyState/index.vue'
+import TableActions from './TableActions/index.vue'
+
+export {
+  PageTitle,
+  StatusTabs,
+  ProductItem,
+  ProductCard,
+  SearchBar,
+  StatCards,
+  TablePagination,
+  HeaderBar,
+  EmptyState,
+  TableActions
+}

+ 41 - 0
src/constants/status.ts

@@ -0,0 +1,41 @@
+// 对账状态
+export const BILL_STATUS_OPTIONS = [
+  { label: '全部', value: '' },
+  { label: '已对账', value: '1' },
+  { label: '未对账', value: '0' }
+]
+
+// 开票状态
+export const INVOICE_STATUS_OPTIONS = [
+  { label: '全部', value: '' },
+  { label: '已开票', value: '1' },
+  { label: '未开票', value: '0' }
+]
+
+// 支付状态
+export const PAY_STATUS_OPTIONS = [
+  { label: '全部', value: '' },
+  { label: '已支付', value: '1' },
+  { label: '未支付', value: '0' }
+]
+
+// 结算状态
+export const SETTLE_STATUS_OPTIONS = [
+  { label: '全部', value: '' },
+  { label: '已付款', value: '1' },
+  { label: '待开票', value: '0' }
+]
+
+// 订单状态样式映射
+export const ORDER_STATUS_CLASS: Record<string, string> = {
+  '运输中': 'shipping',
+  '待发货': 'pending',
+  '已收货': 'received'
+}
+
+// 开票状态样式映射
+export const INVOICE_STATUS_CLASS: Record<string, string> = {
+  '已付款': 'paid',
+  '已开票': 'invoiced',
+  '待开票': 'pending-invoice'
+}

+ 45 - 4
src/layout/components/breadcrumb.vue

@@ -1,15 +1,39 @@
 <template>
   <!-- 面包屑组件 -->
-  <div class="breadcrumb flex-row-center">
+  <div class="breadcrumb flex-row-center" :style="{ 'background': meta.breadcrumbColor ? meta.breadcrumbColor : '#ffffff' }">
     <div class="breadcrumb-bos flex-row-start">
-      <div>首页</div>
+      <div class="home" @click="goHome">首页</div>
+      <template v-if="meta.navList && meta.navList.length > 0">
+        <div v-for="(item, index) in meta.navList" :key="index" class="nav-list">
+          <el-icon style="margin: 0 4px"><ArrowRight /></el-icon>
+          <div @click="goPath(item)" class="like">{{ item.title || '' }}</div>
+        </div>
+      </template>
       <el-icon style="margin: 0 4px"><ArrowRight /></el-icon>
-      <div>解决方案</div>
+      <div>{{ meta.title || '' }}</div>
     </div>
   </div>
 </template>
 
-<script setup lang="ts"></script>
+<script setup lang="ts">
+const router = useRouter();
+const route = useRoute();
+
+const meta = ref<any>({});
+meta.value = route.meta;
+
+watch(route, () => {
+  meta.value = route.meta;
+});
+
+const goHome = () => {
+  router.push('/');
+};
+
+const goPath = (res: any) => {
+  router.push(res.url);
+};
+</script>
 
 <style lang="scss" scoped>
 .breadcrumb {
@@ -20,6 +44,23 @@
     width: 1200px;
     font-size: 14px;
     color: #101828;
+    .nav-list {
+      height: 44px;
+      display: flex;
+      align-items: center;
+      .like {
+        cursor: pointer;
+        &:hover {
+          color: var(--el-color-primary);
+        }
+      }
+    }
+    .home {
+      cursor: pointer;
+      &:hover {
+        color: var(--el-color-primary);
+      }
+    }
   }
 }
 </style>

+ 15 - 16
src/layout/components/foot.vue

@@ -40,21 +40,15 @@
           </div>
           <div class="logistics">
             <div class="logistics-box flex-row-start">
-              <div class="logistics-icon flex-row-center">
-                <img src="@/assets/images/dark.svg" alt="" />
-              </div>
+              <img src="@/assets/images/layout/layout3.png" alt="" />
               <div>无忧退换货</div>
             </div>
             <div class="logistics-box flex-row-start">
-              <div class="logistics-icon flex-row-center">
-                <img src="@/assets/images/dark.svg" alt="" />
-              </div>
+              <img src="@/assets/images/layout/layout3.png" alt="" />
               <div>满百包邮</div>
             </div>
             <div class="logistics-box flex-row-start">
-              <div class="logistics-icon flex-row-center">
-                <img src="@/assets/images/dark.svg" alt="" />
-              </div>
+              <img src="@/assets/images/layout/layout3.png" alt="" />
               <div>品质保证</div>
             </div>
           </div>
@@ -141,17 +135,22 @@
             font-size: 16px;
             color: #101828;
             margin-right: 70px;
-            .logistics-icon {
+            img {
               width: 33px;
               height: 33px;
-              border-radius: 33px;
-              border: 1px solid #101828;
               margin-right: 11px;
-              img {
-                height: 16px;
-                width: 16px;
-              }
             }
+            // .logistics-icon {
+            //   width: 33px;
+            //   height: 33px;
+            //   border-radius: 33px;
+            //   border: 1px solid #101828;
+            //   margin-right: 11px;
+            //   img {
+            //     height: 16px;
+            //     width: 16px;
+            //   }
+            // }
           }
         }
       }

+ 1 - 1
src/layout/components/header.vue

@@ -3,7 +3,7 @@
   <div class="header flex-row-center">
     <div class="header-bos flex-row-between">
       <div class="positioning flex-row-start">
-        <img src="@/assets/logo/logo.png" alt="" />
+        <img src="@/assets/images/layout/layout1.png" alt="" />
         <div>武汉</div>
       </div>
       <div class="header-box flex-row-start">

+ 1 - 0
src/layout/components/index.ts

@@ -3,3 +3,4 @@ export { default as Search } from './search.vue';
 export { default as Nav } from './nav.vue';
 export { default as Breadcrumb } from './breadcrumb.vue';
 export { default as Foot } from './foot.vue';
+export { default as Workbench } from './workbench.vue';

+ 1 - 1
src/layout/components/nav.vue

@@ -3,7 +3,7 @@
   <div class="nav flex-row-center">
     <div class="nav-bos">
       <div class="nav-all flex-row-center">
-        <img src="@/assets/logo/logo.png" alt="" />
+        <img src="@/assets/images/layout/layout2.png" alt="" />
         <div>全部商品分类</div>
       </div>
       <div v-for="(item, index) in navList" :key="index" class="nav-list">{{ item.title }}</div>

+ 24 - 3
src/layout/components/search.vue

@@ -1,6 +1,6 @@
 <template>
   <!-- 搜索组件 -->
-  <div class="search flex-row-center">
+  <div class="search flex-row-center" :style="{ 'background': meta.workbench ? '#F4F4F4' : '#ffffff' }">
     <div class="search-bos">
       <img class="logo" src="@/assets/images/head.png" alt="" />
       <div class="search-box">
@@ -8,11 +8,13 @@
           <div class="search-input flex-row-center">
             <el-input class="el-input" v-model="input" placeholder="搜索商品、品牌、分类..." />
             <div class="bnt flex-row-center">
-              <el-icon color="#ffffff" size="20"><Search /></el-icon>
+              <el-icon color="#ffffff" size="20">
+                <Search />
+              </el-icon>
             </div>
           </div>
           <div class="cat-bos flex-row-center">
-            <img src="@/assets/images/code.png" alt="" />
+            <img src="@/assets/images/layout/layout4.png" alt="" />
             <span>我的购物车</span>
           </div>
         </div>
@@ -33,6 +35,13 @@
 
 <script setup lang="ts">
 const input = ref('');
+const route = useRoute();
+const meta = ref<any>({});
+meta.value = route.meta;
+
+watch(route, () => {
+  meta.value = route.meta;
+});
 </script>
 
 <style lang="scss" scoped>
@@ -40,18 +49,22 @@ const input = ref('');
   width: 100%;
   background-color: #ffffff;
   padding-bottom: 25px;
+
   .search-bos {
     width: 1200px;
     display: flex;
+
     .logo {
       width: 185px;
       height: 90px;
       margin-top: 14px;
     }
+
     .search-box {
       flex: 1;
       padding-top: 50px;
       padding-left: 36px;
+
       .search-div {
         .search-input {
           width: 656px;
@@ -59,10 +72,12 @@ const input = ref('');
           border-radius: 10px;
           border: 2px solid #fb2c36;
           padding-right: 4px;
+
           .el-input {
             height: 40px;
             width: 100%;
             font-size: 16px;
+
             :deep(.el-input__wrapper) {
               border: none;
               /* 可选:去除聚焦时的高亮 */
@@ -70,6 +85,7 @@ const input = ref('');
               outline: none;
             }
           }
+
           .bnt {
             width: 68px;
             height: 40px;
@@ -79,6 +95,7 @@ const input = ref('');
             cursor: pointer;
           }
         }
+
         .cat-bos {
           width: 143px;
           height: 48px;
@@ -88,6 +105,7 @@ const input = ref('');
           margin-left: 24px;
           font-size: 16px;
           color: #e7000b;
+
           img {
             width: 16px;
             height: 16px;
@@ -96,16 +114,19 @@ const input = ref('');
           }
         }
       }
+
       .search-text {
         font-size: 14px;
         color: #e7000b;
         display: flex;
         margin-top: 6px;
+
         div {
           margin-left: 10px;
         }
       }
     }
+
     .code {
       height: 90px;
       width: 90px;

+ 239 - 0
src/layout/components/workbench.vue

@@ -0,0 +1,239 @@
+<template>
+  <div>
+    <aside class="layout-sidebar">
+      <div class="sidebar-section" v-for="menu in menuList" :key="menu.path">
+        <div class="section-header" @click="toggleMenu(menu.path)">
+          <el-icon><component :is="menu.icon" /></el-icon>
+          <span>{{ menu.title }}</span>
+          <el-icon class="arrow"><ArrowDown /></el-icon>
+        </div>
+        <div class="section-items" v-show="openedMenus.includes(menu.path)">
+          <div
+            v-for="child in menu.children"
+            :key="child.path"
+            :class="['menu-item', { active: activeMenu === child.path }]"
+            @click="handleMenuSelect(child.path)"
+          >
+            {{ child.title }}
+            <el-icon class="item-arrow"><ArrowRight /></el-icon>
+          </div>
+        </div>
+      </div>
+    </aside>
+  </div>
+</template>
+<script setup lang="ts">
+import { ref, computed, watch, onMounted } from 'vue';
+import { useRouter, useRoute } from 'vue-router';
+import { ArrowDown, ArrowRight, RefreshRight, Van, Medal } from '@element-plus/icons-vue';
+
+const router = useRouter();
+const route = useRoute();
+
+const menuList = [
+  {
+    path: '/enterprise',
+    title: '企业账户',
+    icon: 'User',
+    children: [
+      { path: '/enterprise/companyInfo', title: '企业信息' },
+      { path: '/enterprise/messageNotice', title: '消息通知' },
+      { path: '/enterprise/addressManage', title: '地址管理' },
+      { path: '/enterprise/invoiceManage', title: '发票抬头管理' },
+      { path: '/enterprise/purchasePlan', title: '专属采购方案' },
+      { path: '/enterprise/agreementSupply', title: '协议供货' },
+      { path: '/enterprise/myCollection', title: '我的收藏' },
+      { path: '/enterprise/purchaseHistory', title: '历史购买' },
+      { path: '/enterprise/myFootprint', title: '我的足迹' }
+    ]
+  },
+  {
+    path: '/trade',
+    title: '交易管理',
+    icon: 'ShoppingCart',
+    children: [
+      { path: '/trade/orderManage', title: '订单管理' },
+      { path: '/trade/orderAudit', title: '审核订单' },
+      { path: '/trade/afterSale', title: '售后服务' },
+      { path: '/trade/batchOrder', title: '批量下单' },
+      { path: '/trade/orderEvaluation', title: '订单评价' }
+    ]
+  },
+  {
+    path: '/organization',
+    title: '组织管理',
+    icon: 'OfficeBuilding',
+    children: [
+      { path: '/organization/personalInfo', title: '个人信息' },
+      { path: '/organization/deptManage', title: '部门管理' },
+      { path: '/organization/staffManage', title: '人员管理' },
+      { path: '/organization/roleManage', title: '角色管理' },
+      { path: '/organization/approvalFlow', title: '审批流程' },
+      { path: '/organization/groupEnterprise', title: '集团关联企业' }
+    ]
+  },
+  {
+    path: '/cost',
+    title: '成本管理',
+    icon: 'Money',
+    children: [
+      { path: '/cost/itemExpense', title: '分项费用' },
+      { path: '/cost/quotaControl', title: '额度控制' }
+    ]
+  },
+  {
+    path: '/reconciliation',
+    title: '对账管理',
+    icon: 'Document',
+    children: [
+      { path: '/reconciliation/billManage', title: '对账单管理' },
+      { path: '/reconciliation/invoiceManage', title: '开票管理' }
+    ]
+  },
+  {
+    path: '/valueAdded',
+    title: '增值服务',
+    icon: 'Service',
+    children: [
+      { path: '/valueAdded/maintenance', title: '维保服务' },
+      { path: '/valueAdded/complaint', title: '投诉与建议' }
+    ]
+  },
+  {
+    path: '/analysis',
+    title: '采购分析',
+    icon: 'DataAnalysis',
+    children: [
+      { path: '/analysis/orderAnalysis', title: '订单交易分析' },
+      { path: '/analysis/purchaseDetail', title: '商品采购明细' },
+      { path: '/analysis/orderStatus', title: '订单执行状态' },
+      { path: '/analysis/settlementStatus', title: '对账结算状况' },
+      { path: '/analysis/deptPurchase', title: '部门采购金额' }
+    ]
+  }
+];
+
+const openedMenus = ref<string[]>([]);
+const activeMenu = computed(() => route.path);
+
+// 根据当前路由自动展开对应的父级菜单
+const initOpenedMenus = () => {
+  const currentPath = route.path;
+  for (const menu of menuList) {
+    const hasActiveChild = menu.children?.some((child) => currentPath === child.path || currentPath.startsWith(child.path + '/'));
+    if (hasActiveChild) {
+      // 只展开当前路由对应的菜单
+      openedMenus.value = [menu.path];
+      return;
+    }
+  }
+};
+
+// 页面加载和路由变化时自动展开菜单
+onMounted(() => {
+  initOpenedMenus();
+});
+
+watch(
+  () => route.path,
+  () => {
+    initOpenedMenus();
+  }
+);
+
+const toggleMenu = (path: string) => {
+  const index = openedMenus.value.indexOf(path);
+  if (index > -1) {
+    // 如果已展开,则收起
+    openedMenus.value.splice(index, 1);
+  } else {
+    // 手风琴效果:先收起所有,再展开当前
+    openedMenus.value = [path];
+  }
+};
+
+const handleMenuSelect = (path: string) => {
+  router.push(path);
+};
+
+const handleSearch = (keyword: string) => {
+  console.log('搜索:', keyword);
+  // TODO: 跳转到搜索结果页
+};
+
+const handleCartClick = () => {
+  console.log('打开采购车');
+  // TODO: 跳转到采购车页面
+};
+</script>
+
+<style lang="scss" scoped>
+// 左侧菜单 - 商城风格
+.layout-sidebar {
+  width: 200px;
+  flex-shrink: 0;
+  background: #fff;
+  border-radius: 4px;
+  padding: 10px 0;
+  height: fit-content;
+  margin-right: 10px;
+
+  .sidebar-section {
+    .section-header {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      padding: 12px 16px;
+      cursor: pointer;
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+
+      .arrow {
+        margin-left: auto;
+        transition: transform 0.2s;
+      }
+      &:hover {
+        color: #e60012;
+      }
+    }
+
+    .section-items {
+      .menu-item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 10px 16px 10px 40px;
+        font-size: 13px;
+        color: #666;
+        cursor: pointer;
+        transition: all 0.2s;
+
+        .item-arrow {
+          opacity: 0;
+          color: #e60012;
+        }
+
+        &:hover {
+          color: #e60012;
+          background: #fff5f5;
+          .item-arrow {
+            opacity: 1;
+          }
+        }
+
+        &.active {
+          color: #e60012;
+          background: #fff5f5;
+          font-weight: 500;
+          border-left: 3px solid #e60012;
+          padding-left: 37px;
+          .item-arrow {
+            opacity: 1;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 0 - 135
src/layout/index copy.vue

@@ -1,135 +0,0 @@
-<template>
-  <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
-    <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
-    <side-bar v-if="!sidebar.hide" class="sidebar-container" />
-    <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
-      <!-- <el-scrollbar>
-        <div :class="{ 'fixed-header': fixedHeader }">
-          <navbar ref="navbarRef" @setLayout="setLayout" />
-          <tags-view v-if="needTagsView" />
-        </div>
-        <app-main />
-        <settings ref="settingRef" />
-      </el-scrollbar> -->
-      <div :class="{ 'fixed-header': fixedHeader }">
-        <navbar ref="navbarRef" @set-layout="setLayout" />
-        <tags-view v-if="needTagsView" />
-      </div>
-      <app-main />
-      <settings ref="settingRef" />
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import SideBar from './components/Sidebar/index.vue';
-import { AppMain, Navbar, Settings, TagsView } from './components';
-import { useAppStore } from '@/store/modules/app';
-import { useSettingsStore } from '@/store/modules/settings';
-import { initWebSocket } from '@/utils/websocket';
-import { initSSE } from '@/utils/sse';
-
-const settingsStore = useSettingsStore();
-const theme = computed(() => settingsStore.theme);
-const sidebar = computed(() => useAppStore().sidebar);
-const device = computed(() => useAppStore().device);
-const needTagsView = computed(() => settingsStore.tagsView);
-const fixedHeader = computed(() => settingsStore.fixedHeader);
-
-const classObj = computed(() => ({
-  hideSidebar: !sidebar.value.opened,
-  openSidebar: sidebar.value.opened,
-  withoutAnimation: sidebar.value.withoutAnimation,
-  mobile: device.value === 'mobile'
-}));
-
-const { width } = useWindowSize();
-const WIDTH = 992; // refer to Bootstrap's responsive design
-
-watchEffect(() => {
-  if (device.value === 'mobile') {
-    useAppStore().closeSideBar({ withoutAnimation: false });
-  }
-  if (width.value - 1 < WIDTH) {
-    useAppStore().toggleDevice('mobile');
-    useAppStore().closeSideBar({ withoutAnimation: true });
-  } else {
-    useAppStore().toggleDevice('desktop');
-  }
-});
-
-const navbarRef = ref<InstanceType<typeof Navbar>>();
-const settingRef = ref<InstanceType<typeof Settings>>();
-
-onMounted(() => {
-  nextTick(() => {
-    navbarRef.value?.initTenantList();
-  });
-});
-
-onMounted(() => {
-  const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
-  initWebSocket(protocol + window.location.host + import.meta.env.VITE_APP_BASE_API + '/resource/websocket');
-});
-
-onMounted(() => {
-  initSSE(import.meta.env.VITE_APP_BASE_API + '/resource/sse');
-});
-
-const handleClickOutside = () => {
-  useAppStore().closeSideBar({ withoutAnimation: false });
-};
-
-const setLayout = () => {
-  settingRef.value?.openSetting();
-};
-</script>
-
-<style lang="scss" scoped>
-@use '@/assets/styles/mixin.scss';
-@use '@/assets/styles/variables.module.scss' as *;
-
-.app-wrapper {
-  @include mixin.clearfix;
-  position: relative;
-  height: 100%;
-  width: 100%;
-
-  &.mobile.openSidebar {
-    position: fixed;
-    top: 0;
-  }
-}
-
-.drawer-bg {
-  background: #000;
-  opacity: 0.3;
-  width: 100%;
-  top: 0;
-  height: 100%;
-  position: absolute;
-  z-index: 999;
-}
-
-.fixed-header {
-  position: fixed;
-  top: 0;
-  right: 0;
-  z-index: 9;
-  width: calc(100% - #{$base-sidebar-width});
-  transition: width 0.28s;
-  background: $fixed-header-bg;
-}
-
-.hideSidebar .fixed-header {
-  width: calc(100% - 54px);
-}
-
-.sidebarHide .fixed-header {
-  width: 100%;
-}
-
-.mobile .fixed-header {
-  width: 100%;
-}
-</style>

+ 30 - 6
src/layout/index.vue

@@ -1,16 +1,27 @@
 <template>
   <div class="app-wrapper">
-    <Header />
-    <Search />
-    <Nav />
-    <Breadcrumb />
-    <router-view> </router-view>
+    <Header v-if="meta.header != 'hide'" />
+    <Search v-if="meta.header != 'hide'" />
+    <Nav v-if="meta.nav" />
+    <Breadcrumb v-if="meta.breadcrumb" />
+    <div class="pages-bos" :class="meta.workbench ? 'pages-bos1' : 'pages-bos2'">
+      <Workbench v-if="meta.workbench" />
+      <router-view> </router-view>
+    </div>
     <Foot />
   </div>
 </template>
 
 <script setup lang="ts">
-import { Header, Search, Nav, Breadcrumb, Foot } from './components';
+import { Header, Search, Nav, Breadcrumb, Foot, Workbench } from './components';
+
+const route = useRoute();
+const meta = ref<any>({});
+meta.value = route.meta;
+
+watch(route, () => {
+  meta.value = route.meta;
+});
 </script>
 
 <style lang="scss" scoped>
@@ -21,5 +32,18 @@ import { Header, Search, Nav, Breadcrumb, Foot } from './components';
   min-height: 100%;
   width: 100%;
   background: #f4f4f4;
+
+  .pages-bos {
+    display: flex;
+
+    &.pages-bos1 {
+      width: 1200px;
+      margin: 0 auto;
+    }
+
+    &.pages-bos2 {
+      width: 100%;
+    }
+  }
 }
 </style>

+ 242 - 8
src/router/index.ts

@@ -72,49 +72,283 @@ export const constantRoutes: RouteRecordRaw[] = [
         path: 'solve/index',
         component: () => import('@/views/solve/index.vue'),
         name: 'solveIndex',
-        meta: { title: '解决方案' }
+        meta: { title: '解决方案', nav: true, breadcrumb: true }
       },
       {
         path: 'solve/info',
         component: () => import('@/views/solve/info.vue'),
         name: 'solveInfo',
-        meta: { title: '解决方案详情' }
+        meta: { title: '解决方案详情', nav: true, breadcrumb: true, navList: [{ title: '解决方案', url: '/solve/index' }] }
       },
       {
         path: 'solve/real',
         component: () => import('@/views/solve/real.vue'),
         name: 'solveReal',
-        meta: { title: '资讯' }
+        meta: { title: '资讯详情', nav: true, breadcrumb: true, breadcrumbColor: '#F4F4F4' }
       },
       {
         path: 'shop/info',
         component: () => import('@/views/shop/info.vue'),
         name: 'shopInfo',
-        meta: { title: '商品详情' }
+        meta: { title: '商品详情', nav: true, breadcrumb: true, breadcrumbColor: '#F4F4F4' }
       },
       {
         path: 'shop/cart',
         component: () => import('@/views/shop/cart.vue'),
         name: 'shopCart',
-        meta: { title: '我的购物车' }
+        meta: { title: '我的购物车', nav: true }
       },
       {
         path: 'shop/create',
         component: () => import('@/views/shop/create.vue'),
         name: 'shopCreate',
-        meta: { title: '确认订单信息' }
+        meta: { title: '确认订单信息', nav: true }
       },
       {
         path: 'shop/pay',
         component: () => import('@/views/shop/pay.vue'),
         name: 'shopPay',
-        meta: { title: '支付订单' }
+        meta: { title: '支付订单', nav: true }
       },
       {
         path: 'register/enterprise',
         component: () => import('@/views/register/enterprise.vue'),
         name: 'registerEnterprise',
-        meta: { title: '企业注册' }
+        meta: { title: '企业注册', header: 'hide', search: 'hide' }
+      },
+      {
+        path: 'enterprise/companyInfo',
+        name: 'CompanyInfo',
+        component: () => import('@/views/enterprise/companyInfo/index.vue'),
+        meta: { title: '企业信息', workbench: true }
+      },
+      {
+        path: 'enterprise/companyInfo/edit',
+        name: 'CompanyInfoEdit',
+        component: () => import('@/views/enterprise/companyInfo/edit.vue'),
+        meta: { title: '完善企业信息', hidden: true, workbench: true }
+      },
+      {
+        path: 'enterprise/securitySetting',
+        name: 'SecuritySetting',
+        component: () => import('@/views/enterprise/securitySetting/index.vue'),
+        meta: { title: '安全设置', hidden: true, workbench: true }
+      },
+      {
+        path: 'enterprise/securitySetting/resetPassword',
+        name: 'ResetPassword',
+        component: () => import('@/views/enterprise/securitySetting/resetPassword.vue'),
+        meta: { title: '重置密码', hidden: true, workbench: true }
+      },
+      {
+        path: 'enterprise/securitySetting/changePhone',
+        name: 'ChangePhone',
+        component: () => import('@/views/enterprise/securitySetting/changePhone.vue'),
+        meta: { title: '更换手机号码', hidden: true, workbench: true }
+      },
+      {
+        path: 'enterprise/purchaseHabit',
+        name: 'PurchaseHabit',
+        component: () => import('@/views/enterprise/purchaseHabit/index.vue'),
+        meta: { title: '采购习惯', hidden: true, workbench: true }
+      },
+      {
+        path: 'enterprise/messageNotice',
+        name: 'MessageNotice',
+        component: () => import('@/views/enterprise/messageNotice/index.vue'),
+        meta: { title: '消息通知', workbench: true }
+      },
+      {
+        path: 'enterprise/addressManage',
+        name: 'AddressManage',
+        component: () => import('@/views/enterprise/addressManage/index.vue'),
+        meta: { title: '地址管理', workbench: true }
+      },
+      {
+        path: 'enterprise/invoiceManage',
+        name: 'InvoiceManage',
+        component: () => import('@/views/enterprise/invoiceManage/index.vue'),
+        meta: { title: '发票抬头管理', workbench: true }
+      },
+      {
+        path: 'enterprise/purchasePlan',
+        name: 'PurchasePlan',
+        component: () => import('@/views/enterprise/purchasePlan/index.vue'),
+        meta: { title: '专属采购方案', workbench: true }
+      },
+      {
+        path: 'enterprise/agreementSupply',
+        name: 'AgreementSupply',
+        component: () => import('@/views/enterprise/agreementSupply/index.vue'),
+        meta: { title: '协议供货', workbench: true }
+      },
+      {
+        path: 'enterprise/myCollection',
+        name: 'MyCollection',
+        component: () => import('@/views/enterprise/myCollection/index.vue'),
+        meta: { title: '我的收藏', workbench: true }
+      },
+      {
+        path: 'enterprise/purchaseHistory',
+        name: 'PurchaseHistory',
+        component: () => import('@/views/enterprise/purchaseHistory/index.vue'),
+        meta: { title: '历史购买', workbench: true }
+      },
+      {
+        path: 'enterprise/myFootprint',
+        name: 'MyFootprint',
+        component: () => import('@/views/enterprise/myFootprint/index.vue'),
+        meta: { title: '我的足迹', workbench: true }
+      },
+      {
+        path: 'trade/orderManage',
+        name: 'OrderManage',
+        component: () => import('@/views/trade/orderManage/index.vue'),
+        meta: { title: '订单管理', workbench: true }
+      },
+      {
+        path: 'trade/orderManage/detail/:orderNo',
+        name: 'OrderDetail',
+        component: () => import('@/views/trade/orderManage/detail.vue'),
+        meta: { title: '订单详情', hidden: true, workbench: true }
+      },
+      {
+        path: 'trade/orderAudit',
+        name: 'OrderAudit',
+        component: () => import('@/views/trade/orderAudit/index.vue'),
+        meta: { title: '审核订单', workbench: true }
+      },
+      {
+        path: 'trade/afterSale',
+        name: 'AfterSale',
+        component: () => import('@/views/trade/afterSale/index.vue'),
+        meta: { title: '售后服务', workbench: true }
+      },
+      {
+        path: 'trade/batchOrder',
+        name: 'BatchOrder',
+        component: () => import('@/views/trade/batchOrder/index.vue'),
+        meta: { title: '批量下单', workbench: true }
+      },
+      {
+        path: 'trade/orderEvaluation',
+        name: 'OrderEvaluation',
+        component: () => import('@/views/trade/orderEvaluation/index.vue'),
+        meta: { title: '订单评价', workbench: true }
+      },
+      {
+        path: 'organization/personalInfo',
+        name: 'PersonalInfo',
+        component: () => import('@/views/organization/personalInfo/index.vue'),
+        meta: { title: '个人信息', workbench: true }
+      },
+      {
+        path: 'organization/deptManage',
+        name: 'DeptManage',
+        component: () => import('@/views/organization/deptManage/index.vue'),
+        meta: { title: '部门管理', workbench: true }
+      },
+      {
+        path: 'organization/staffManage',
+        name: 'StaffManage',
+        component: () => import('@/views/organization/staffManage/index.vue'),
+        meta: { title: '人员管理', workbench: true }
+      },
+      {
+        path: 'organization/roleManage',
+        name: 'RoleManage',
+        component: () => import('@/views/organization/roleManage/index.vue'),
+        meta: { title: '角色管理', workbench: true }
+      },
+      {
+        path: 'organization/approvalFlow',
+        name: 'ApprovalFlow',
+        component: () => import('@/views/organization/approvalFlow/index.vue'),
+        meta: { title: '审批流程', workbench: true }
+      },
+      {
+        path: 'organization/approvalFlow/create',
+        name: 'ApprovalFlowCreate',
+        component: () => import('@/views/organization/approvalFlow/create.vue'),
+        meta: { title: '新建审批', hidden: true, workbench: true }
+      },
+      {
+        path: 'organization/groupEnterprise',
+        name: 'GroupEnterprise',
+        component: () => import('@/views/organization/groupEnterprise/index.vue'),
+        meta: { title: '集团关联企业', workbench: true }
+      },
+      {
+        path: 'cost/itemExpense',
+        name: 'ItemExpense',
+        component: () => import('@/views/cost/itemExpense/index.vue'),
+        meta: { title: '分项费用', workbench: true }
+      },
+      {
+        path: 'cost/quotaControl',
+        name: 'QuotaControl',
+        component: () => import('@/views/cost/quotaControl/index.vue'),
+        meta: { title: '额度控制', workbench: true }
+      },
+      {
+        path: 'cost/quotaControl/apply',
+        name: 'QuotaApply',
+        component: () => import('@/views/cost/quotaControl/apply.vue'),
+        meta: { title: '额度申请', hidden: true, workbench: true }
+      },
+      {
+        path: 'reconciliation/billManage',
+        name: 'BillManage',
+        component: () => import('@/views/reconciliation/billManage/index.vue'),
+        meta: { title: '对账单管理', workbench: true }
+      },
+      {
+        path: 'reconciliation/invoiceManage',
+        name: 'ReconciliationInvoiceManage',
+        component: () => import('@/views/reconciliation/invoiceManage/index.vue'),
+        meta: { title: '开票管理', workbench: true }
+      },
+      {
+        path: 'valueAdded/maintenance',
+        name: 'Maintenance',
+        component: () => import('@/views/valueAdded/maintenance/index.vue'),
+        meta: { title: '维保服务', workbench: true }
+      },
+      {
+        path: 'valueAdded/complaint',
+        name: 'Complaint',
+        component: () => import('@/views/valueAdded/complaint/index.vue'),
+        meta: { title: '投诉与建议', workbench: true }
+      },
+      {
+        path: 'analysis/orderAnalysis',
+        name: 'OrderAnalysis',
+        component: () => import('@/views/analysis/orderAnalysis/index.vue'),
+        meta: { title: '订单交易分析', workbench: true }
+      },
+      {
+        path: 'analysis/purchaseDetail',
+        name: 'PurchaseDetail',
+        component: () => import('@/views/analysis/purchaseDetail/index.vue'),
+        meta: { title: '商品采购明细', workbench: true }
+      },
+      {
+        path: 'analysis/orderStatus',
+        name: 'OrderStatus',
+        component: () => import('@/views/analysis/orderStatus/index.vue'),
+        meta: { title: '订单执行状态', workbench: true }
+      },
+      {
+        path: 'analysis/settlementStatus',
+        name: 'SettlementStatus',
+        component: () => import('@/views/analysis/settlementStatus/index.vue'),
+        meta: { title: '对账结算状况', workbench: true }
+      },
+      {
+        path: 'analysis/deptPurchase',
+        name: 'DeptPurchase',
+        component: () => import('@/views/analysis/deptPurchase/index.vue'),
+        meta: { title: '部门采购金额', workbench: true }
       }
     ]
   }

+ 11 - 0
src/utils/status.ts

@@ -0,0 +1,11 @@
+import { ORDER_STATUS_CLASS, INVOICE_STATUS_CLASS } from '@/constants/status'
+
+// 获取订单状态样式类名
+export const getOrderStatusClass = (status: string): string => {
+  return ORDER_STATUS_CLASS[status] || ''
+}
+
+// 获取开票状态样式类名
+export const getInvoiceStatusClass = (status: string): string => {
+  return INVOICE_STATUS_CLASS[status] || ''
+}

+ 72 - 0
src/views/analysis/deptPurchase/index.vue

@@ -0,0 +1,72 @@
+<template>
+  <div class="page-container">
+    <PageTitle title="部门采购金额" />
+    
+    <SearchBar :form="searchForm" :filters="filters" @search="handleSearch" @reset="handleReset">
+      <template #buttons>
+        <el-button type="danger">导出</el-button>
+      </template>
+    </SearchBar>
+
+    <el-table :data="tableData" border style="width: 100%">
+      <el-table-column type="index" label="序号" width="60" align="center" />
+      <el-table-column prop="deptName" label="部门名称" width="120" align="center" />
+      <el-table-column prop="orderPerson" label="下单人" width="100" align="center" />
+      <el-table-column prop="orderAmount" label="下单金额" min-width="120" align="center">
+        <template #default="{ row }">¥{{ row.orderAmount.toLocaleString() }}</template>
+      </el-table-column>
+      <el-table-column prop="completedAmount" label="已完成订单金额" min-width="140" align="center">
+        <template #default="{ row }">¥{{ row.completedAmount.toLocaleString() }}</template>
+      </el-table-column>
+      <el-table-column prop="pendingAmount" label="待完成订单金额" min-width="140" align="center">
+        <template #default="{ row }">¥{{ row.pendingAmount.toLocaleString() }}</template>
+      </el-table-column>
+    </el-table>
+
+    <TablePagination v-model:page="pagination.page" v-model:pageSize="pagination.pageSize" :total="pagination.total" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue'
+import { PageTitle, SearchBar, TablePagination } from '@/components'
+
+const searchForm = reactive({
+  keyword: '',
+  dateRange: [],
+  deptId: ''
+})
+
+const filters = [
+  { field: 'deptId', label: '下单部门', options: [
+    { label: '全部', value: '' },
+    { label: '人事部', value: '1' },
+    { label: '行政部', value: '2' },
+    { label: '设计部', value: '3' },
+    { label: '技术部', value: '4' }
+  ]}
+]
+
+const pagination = reactive({ page: 1, pageSize: 10, total: 100 })
+
+const tableData = ref([
+  { deptName: '人事部', orderPerson: '李世海', orderAmount: 2115.97, completedAmount: 2115.97, pendingAmount: 2115.97 },
+  { deptName: '行政部', orderPerson: '李书萍', orderAmount: 4476.12, completedAmount: 4476.12, pendingAmount: 4476.12 },
+  { deptName: '人事部', orderPerson: '邓文锦', orderAmount: 7751.34, completedAmount: 7751.34, pendingAmount: 7751.34 },
+  { deptName: '设计部', orderPerson: '王凡宏', orderAmount: 7936.37, completedAmount: 7936.37, pendingAmount: 7936.37 },
+  { deptName: '技术部', orderPerson: '赵昊艳', orderAmount: 3931.45, completedAmount: 3931.45, pendingAmount: 3931.45 },
+  { deptName: '人事部', orderPerson: '钱君霞', orderAmount: 6132.75, completedAmount: 6132.75, pendingAmount: 6132.75 },
+  { deptName: '行政部', orderPerson: '周静', orderAmount: 3189.56, completedAmount: 3189.56, pendingAmount: 3189.56 },
+  { deptName: '行政部', orderPerson: '吴彦琛', orderAmount: 993.66, completedAmount: 993.66, pendingAmount: 993.66 },
+  { deptName: '设计部', orderPerson: '钱慧倩', orderAmount: 2763.35, completedAmount: 2763.35, pendingAmount: 2763.35 },
+  { deptName: '技术部', orderPerson: '孙银茹', orderAmount: 9286.45, completedAmount: 9286.45, pendingAmount: 9286.45 }
+])
+
+const handleSearch = () => {
+  // 搜索逻辑
+}
+
+const handleReset = () => {
+  // 重置逻辑
+}
+</script>

+ 199 - 0
src/views/analysis/orderAnalysis/index.vue

@@ -0,0 +1,199 @@
+<template>
+  <div class="page-container">
+    <PageTitle title="订单交易分析" />
+    
+    <StatCards :data="statData" />
+
+    <!-- 采购金额趋势 -->
+    <div class="chart-section">
+      <div class="chart-header">
+        <span class="chart-title">采购金额趋势</span>
+        <div class="chart-filter">
+          <span :class="{ active: trendType === 'month' }" @click="trendType = 'month'">筛选</span>
+          <span :class="{ active: trendType === 'month' }">月度</span>
+        </div>
+      </div>
+      <div class="chart-container" ref="trendChartRef"></div>
+    </div>
+
+    <!-- 下方两个图表 -->
+    <div class="chart-row">
+      <div class="chart-section half">
+        <div class="chart-header">
+          <span class="chart-title">采购品类占比</span>
+          <div class="chart-filter">
+            <span :class="{ active: categoryType === 'month' }" @click="categoryType = 'month'">本月</span>
+            <span :class="{ active: categoryType === 'year' }" @click="categoryType = 'year'">本年</span>
+          </div>
+        </div>
+        <div class="chart-content-row">
+          <div class="chart-container pie" ref="pieChartRef"></div>
+          <div class="category-stats">
+            <div class="category-item" v-for="item in categoryData" :key="item.name">
+              <span class="category-name">{{ item.name }}</span>
+              <span class="category-value">{{ item.value }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="chart-section half">
+        <div class="chart-header">
+          <span class="chart-title">采购商品日变化图</span>
+          <div class="chart-filter">
+            <span>筛选</span>
+            <span>月度</span>
+          </div>
+          <div class="chart-legend">
+            <span class="legend-item"><i class="dot yellow"></i>购买金额</span>
+            <span class="legend-item"><i class="dot blue"></i>购买数量</span>
+          </div>
+        </div>
+        <div class="chart-container" ref="dailyChartRef"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import * as echarts from 'echarts'
+import { PageTitle, StatCards } from '@/components'
+
+const trendType = ref('month')
+const categoryType = ref('month')
+
+const trendChartRef = ref<HTMLElement>()
+const pieChartRef = ref<HTMLElement>()
+const dailyChartRef = ref<HTMLElement>()
+
+const statData = [
+  { value: '10,239.00', label: '购买金额(元)' },
+  { value: '3', label: '购买数量(件)' },
+  { value: '45', label: '订单量(单)' },
+  { value: '78', label: '客单价(元/单)' }
+]
+
+const categoryData = [
+  { name: '品类01', value: 600 },
+  { name: '品类02', value: 200 },
+  { name: '品类03', value: 200 }
+]
+
+onMounted(() => {
+  initTrendChart()
+  initPieChart()
+  initDailyChart()
+})
+
+const initTrendChart = () => {
+  if (!trendChartRef.value) return
+  const chart = echarts.init(trendChartRef.value)
+  chart.setOption({
+    grid: { left: 60, right: 20, top: 30, bottom: 30 },
+    xAxis: {
+      type: 'category',
+      data: ['2025/01', '2025/02', '2025/03', '2025/04', '2025/05', '2025/06', '2025/07', '2025/08', '2025/09', '2025/10', '2025/11', '2025/12'],
+      axisLine: { lineStyle: { color: '#eee' } },
+      axisLabel: { color: '#999' }
+    },
+    yAxis: {
+      type: 'value', name: '销量(万元)', nameTextStyle: { color: '#999' },
+      axisLine: { show: false }, splitLine: { lineStyle: { color: '#eee' } }, axisLabel: { color: '#999' }
+    },
+    series: [{ type: 'line', data: [300, 980, 450, 600, 400, 550, 900, 700, 650, 800, 500, 750], lineStyle: { color: '#e60012', width: 2 }, itemStyle: { color: '#e60012' }, symbol: 'circle', symbolSize: 6 }],
+    tooltip: { trigger: 'axis', formatter: (params: any) => `${params[0].name}<br/>¥${params[0].value.toFixed(2)}` }
+  })
+}
+
+const initPieChart = () => {
+  if (!pieChartRef.value) return
+  const chart = echarts.init(pieChartRef.value)
+  chart.setOption({
+    series: [{
+      type: 'pie', radius: ['40%', '70%'], center: ['50%', '50%'],
+      data: [
+        { value: 600, name: '品类01', itemStyle: { color: '#f4c542' } },
+        { value: 200, name: '品类02', itemStyle: { color: '#3498db' } },
+        { value: 200, name: '品类03', itemStyle: { color: '#1abc9c' } }
+      ],
+      label: { formatter: '{b} {d}%', color: '#666' }
+    }],
+    tooltip: { formatter: '{b}: {c} ({d}%)' }
+  })
+}
+
+const initDailyChart = () => {
+  if (!dailyChartRef.value) return
+  const chart = echarts.init(dailyChartRef.value)
+  chart.setOption({
+    grid: { left: 60, right: 20, top: 40, bottom: 30 },
+    xAxis: { type: 'category', data: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'], axisLine: { lineStyle: { color: '#eee' } }, axisLabel: { color: '#999' } },
+    yAxis: { type: 'value', name: '销量(万元)', nameTextStyle: { color: '#999' }, axisLine: { show: false }, splitLine: { lineStyle: { color: '#eee' } }, axisLabel: { color: '#999' } },
+    series: [
+      { name: '购买金额', type: 'line', data: [400, 500, 350, 600, 450, 640, 500, 550, 400, 600, 450, 500], lineStyle: { color: '#f4c542', width: 2 }, itemStyle: { color: '#f4c542' }, symbol: 'circle', symbolSize: 6 },
+      { name: '购买数量', type: 'line', data: [300, 400, 250, 500, 350, 332, 400, 450, 300, 500, 350, 400], lineStyle: { color: '#3498db', width: 2 }, itemStyle: { color: '#3498db' }, symbol: 'circle', symbolSize: 6 }
+    ],
+    tooltip: { trigger: 'axis' }
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.chart-section {
+  background: #fff;
+  border: 1px solid #eee;
+  border-radius: 4px;
+  padding: 16px;
+  margin-bottom: 20px;
+  
+  &.half { flex: 1; margin-bottom: 0; }
+  
+  .chart-header {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    margin-bottom: 16px;
+    
+    .chart-title { font-size: 14px; font-weight: 500; color: #333; }
+    .chart-filter {
+      display: flex;
+      gap: 12px;
+      span { font-size: 12px; color: #999; cursor: pointer; &.active { color: #333; } }
+    }
+    .chart-legend {
+      margin-left: auto;
+      display: flex;
+      gap: 16px;
+      .legend-item {
+        display: flex;
+        align-items: center;
+        gap: 4px;
+        font-size: 12px;
+        color: #666;
+        .dot { width: 8px; height: 8px; border-radius: 50%; &.yellow { background: #f4c542; } &.blue { background: #3498db; } }
+      }
+    }
+  }
+  
+  .chart-container { height: 280px; &.pie { height: 200px; width: 200px; } }
+  
+  .chart-content-row {
+    display: flex;
+    align-items: center;
+    .category-stats {
+      flex: 1;
+      display: flex;
+      gap: 40px;
+      padding-left: 20px;
+      .category-item {
+        text-align: center;
+        .category-name { display: block; font-size: 12px; color: #999; margin-bottom: 8px; }
+        .category-value { font-size: 20px; font-weight: bold; color: #333; }
+      }
+    }
+  }
+}
+
+.chart-row { display: flex; gap: 20px; }
+</style>

+ 65 - 0
src/views/analysis/orderStatus/index.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="page-container">
+    <PageTitle title="订单执行状态" />
+    
+    <SearchBar :form="searchForm" :filters="filters" />
+
+    <el-table :data="tableData" border style="width: 100%">
+      <el-table-column prop="orderDate" label="下单日期" width="110" align="center" />
+      <el-table-column prop="orderNo" label="订单编号" width="120" align="center" />
+      <el-table-column prop="productName" label="产品" min-width="140" show-overflow-tooltip />
+      <el-table-column prop="quantity" label="数量" width="80" align="center" />
+      <el-table-column prop="price" label="单价" width="100" align="center">
+        <template #default="{ row }">¥{{ row.price.toLocaleString() }}</template>
+      </el-table-column>
+      <el-table-column prop="amount" label="金额" width="100" align="center">
+        <template #default="{ row }">¥{{ row.amount.toLocaleString() }}</template>
+      </el-table-column>
+      <el-table-column prop="shippedQty" label="发货数量" width="90" align="center" />
+      <el-table-column prop="unshippedQty" label="未发货数量" width="100" align="center" />
+      <el-table-column prop="status" label="订单状态" width="100" align="center">
+        <template #default="{ row }">
+          <span :class="['status-tag', getOrderStatusClass(row.status)]">{{ row.status }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <TablePagination v-model:page="pagination.page" v-model:pageSize="pagination.pageSize" :total="pagination.total" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue'
+import { PageTitle, SearchBar, TablePagination } from '@/components'
+import { BILL_STATUS_OPTIONS, INVOICE_STATUS_OPTIONS, PAY_STATUS_OPTIONS } from '@/constants/status'
+import { getOrderStatusClass } from '@/utils/status'
+
+const searchForm = reactive({
+  keyword: '',
+  dateRange: [],
+  billStatus: '',
+  invoiceStatus: '',
+  payStatus: ''
+})
+
+const filters = [
+  { field: 'billStatus', label: '对账状态', options: BILL_STATUS_OPTIONS },
+  { field: 'invoiceStatus', label: '开票状态', options: INVOICE_STATUS_OPTIONS },
+  { field: 'payStatus', label: '支付状态', options: PAY_STATUS_OPTIONS }
+]
+
+const pagination = reactive({ page: 1, pageSize: 10, total: 100 })
+
+const tableData = ref([
+  { orderDate: '2025-11-22', orderNo: '2323232323', productName: '清华同方超...', quantity: 2, price: 2115.97, amount: 2115.97, shippedQty: 2, unshippedQty: 2, status: '运输中' },
+  { orderDate: '2025-12-09', orderNo: '2323412123', productName: '清华同方超...', quantity: 1, price: 4476.12, amount: 4476.12, shippedQty: 1, unshippedQty: 1, status: '待发货' },
+  { orderDate: '2025-11-21', orderNo: '4566784543', productName: '越E500台式...', quantity: 5, price: 7751.34, amount: 7751.34, shippedQty: 5, unshippedQty: 5, status: '已收货' },
+  { orderDate: '2025-11-27', orderNo: '2356565654', productName: '清华同机电...', quantity: 2, price: 7936.37, amount: 7936.37, shippedQty: 2, unshippedQty: 2, status: '运输中' },
+  { orderDate: '2025-12-11', orderNo: '2323412123', productName: '清华同方超...', quantity: 1, price: 3931.45, amount: 3931.45, shippedQty: 1, unshippedQty: 1, status: '待发货' },
+  { orderDate: '2025-11-27', orderNo: '2323232323', productName: '清00台式机...', quantity: 6, price: 6132.75, amount: 6132.75, shippedQty: 6, unshippedQty: 6, status: '已收货' },
+  { orderDate: '2025-11-25', orderNo: '2356565654', productName: '机电脑超越...', quantity: 7, price: 3189.56, amount: 3189.56, shippedQty: 7, unshippedQty: 7, status: '已收货' },
+  { orderDate: '2025-11-20', orderNo: '2323232323', productName: '清华同方超...', quantity: 2, price: 993.66, amount: 993.66, shippedQty: 2, unshippedQty: 2, status: '运输中' },
+  { orderDate: '2025-11-17', orderNo: '2323412123', productName: '超越E500台...', quantity: 1, price: 2763.35, amount: 2763.35, shippedQty: 1, unshippedQty: 1, status: '待发货' },
+  { orderDate: '2025-12-05', orderNo: '2323232323', productName: '机电脑超越...', quantity: 5, price: 9286.45, amount: 9286.45, shippedQty: 5, unshippedQty: 5, status: '已收货' }
+])
+</script>

+ 122 - 0
src/views/analysis/purchaseDetail/index.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="page-container">
+    <PageTitle title="商品采购明细" />
+    
+    <StatCards :data="statData" />
+
+    <!-- 采购效率分析 -->
+    <div class="analysis-section">
+      <div class="section-title">采购效率分析</div>
+      <div class="analysis-cards">
+        <div class="analysis-card" v-for="(item, index) in efficiencyData" :key="index">
+          <div class="card-label">{{ item.label }}</div>
+          <div class="card-content">
+            <span class="card-value">{{ item.value }}</span>
+            <div class="mini-chart" :ref="el => setChartRef(el, index)"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 售后服务分析 -->
+    <div class="analysis-section">
+      <div class="section-title">售后服务分析</div>
+      <div class="analysis-cards">
+        <div class="analysis-card" v-for="(item, index) in afterSaleData" :key="index">
+          <div class="card-label">{{ item.label }}</div>
+          <div class="card-content">
+            <span class="card-value">{{ item.value }}</span>
+            <div class="mini-chart" :ref="el => setChartRef(el, index + 3)"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import * as echarts from 'echarts'
+import { PageTitle, StatCards } from '@/components'
+
+const chartRefs = ref<(HTMLElement | null)[]>([])
+
+const setChartRef = (el: any, index: number) => {
+  if (el) chartRefs.value[index] = el as HTMLElement
+}
+
+const statData = [
+  { value: '12', label: '购买数量(件)' },
+  { value: '3', label: '商品数量(件)' },
+  { value: '45', label: '品牌数量(件)' },
+  { value: '78', label: '三类品类数量(件)' }
+]
+
+const efficiencyData = [
+  { label: '订单平均确认时间(小时)', value: '0.50', data: [30, 40, 35, 50, 45, 60, 55, 70, 65, 80], color: '#e60012', type: 'line' },
+  { label: '商品平均出库时间(小时)', value: '7', data: [3, 5, 4, 6, 5, 7, 6, 8, 7, 9, 8, 7], color: '#3498db', type: 'bar' },
+  { label: '商品平均妥投时间(小时)', value: '16.5', data: [40, 50, 45, 60, 55, 70, 65, 80, 75, 90], color: '#f4c542', type: 'line' }
+]
+
+const afterSaleData = [
+  { label: '售后商品数量(件)', value: '23', data: [20, 30, 25, 40, 35, 50, 45, 60, 55, 70], color: '#e60012', type: 'line' },
+  { label: '平均完成时效(天)', value: '7', data: [2, 4, 3, 5, 4, 6, 5, 7, 6, 8, 7, 6], color: '#3498db', type: 'bar' },
+  { label: '售后商品占比', value: '16.5%', data: [10, 15, 12, 18, 14, 20, 16, 22, 18, 25], color: '#f4c542', type: 'line' }
+]
+
+onMounted(() => {
+  const allData = [...efficiencyData, ...afterSaleData]
+  allData.forEach((item, index) => {
+    const el = chartRefs.value[index]
+    if (!el) return
+    const chart = echarts.init(el)
+    if (item.type === 'line') {
+      chart.setOption({
+        grid: { left: 0, right: 0, top: 5, bottom: 5 },
+        xAxis: { type: 'category', show: false, data: item.data.map((_, i) => i) },
+        yAxis: { type: 'value', show: false },
+        series: [{
+          type: 'line', data: item.data, lineStyle: { color: item.color, width: 2 }, itemStyle: { color: item.color }, symbol: 'none', smooth: true,
+          areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: item.color + '40' }, { offset: 1, color: item.color + '10' }]) }
+        }]
+      })
+    } else {
+      chart.setOption({
+        grid: { left: 0, right: 0, top: 5, bottom: 5 },
+        xAxis: { type: 'category', show: false, data: item.data.map((_, i) => i) },
+        yAxis: { type: 'value', show: false },
+        series: [{ type: 'bar', data: item.data, itemStyle: { color: item.color }, barWidth: 4 }]
+      })
+    }
+  })
+})
+</script>
+
+<style scoped lang="scss">
+.analysis-section {
+  margin-bottom: 24px;
+  
+  .section-title { font-size: 14px; font-weight: 500; color: #333; margin-bottom: 16px; }
+  
+  .analysis-cards {
+    display: flex;
+    gap: 20px;
+    
+    .analysis-card {
+      flex: 1;
+      padding: 16px;
+      border: 1px solid #eee;
+      border-radius: 4px;
+      
+      .card-label { font-size: 12px; color: #999; margin-bottom: 12px; }
+      .card-content {
+        display: flex;
+        align-items: flex-end;
+        justify-content: space-between;
+        .card-value { font-size: 32px; font-weight: bold; color: #333; }
+        .mini-chart { width: 120px; height: 50px; }
+      }
+    }
+  }
+}
+</style>

+ 78 - 0
src/views/analysis/settlementStatus/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <div class="page-container">
+    <PageTitle title="对账结算状况" />
+    
+    <SearchBar :form="searchForm" :filters="filters">
+      <template #buttons>
+        <el-button type="danger">导出</el-button>
+      </template>
+    </SearchBar>
+
+    <el-table :data="tableData" border style="width: 100%">
+      <el-table-column prop="orderNo" label="订单编号" min-width="110" align="center" />
+      <el-table-column prop="orderType" label="订单类型" min-width="90" align="center">
+        <template #default="{ row }">{{ row.orderType || '-' }}</template>
+      </el-table-column>
+      <el-table-column prop="amount" label="金额" min-width="100" align="center">
+        <template #default="{ row }">¥{{ row.amount.toLocaleString() }}</template>
+      </el-table-column>
+      <el-table-column prop="orderDate" label="下单日期" min-width="100" align="center" />
+      <el-table-column prop="orderStatus" label="订单状态" min-width="90" align="center">
+        <template #default="{ row }">
+          <span :class="['status-tag', getOrderStatusClass(row.orderStatus)]">{{ row.orderStatus }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="billStatus" label="对账状态" min-width="90" align="center">
+        <template #default="{ row }">{{ row.billStatus || '-' }}</template>
+      </el-table-column>
+      <el-table-column prop="invoiceStatus" label="开票状态" min-width="90" align="center">
+        <template #default="{ row }">
+          <span :class="['status-tag', getInvoiceStatusClass(row.invoiceStatus)]">{{ row.invoiceStatus }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="settleStatus" label="结算状态" min-width="90" align="center">
+        <template #default="{ row }">
+          <span :style="{ color: row.settleStatus === '已结算' ? '#67c23a' : '' }">{{ row.settleStatus }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <TablePagination v-model:page="pagination.page" v-model:pageSize="pagination.pageSize" :total="pagination.total" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue'
+import { PageTitle, SearchBar, TablePagination } from '@/components'
+import { BILL_STATUS_OPTIONS, INVOICE_STATUS_OPTIONS, SETTLE_STATUS_OPTIONS } from '@/constants/status'
+import { getOrderStatusClass, getInvoiceStatusClass } from '@/utils/status'
+
+const searchForm = reactive({
+  keyword: '',
+  dateRange: [],
+  billStatus: '',
+  invoiceStatus: '',
+  settleStatus: ''
+})
+
+const filters = [
+  { field: 'billStatus', label: '对账状态', options: BILL_STATUS_OPTIONS },
+  { field: 'invoiceStatus', label: '开票状态', options: INVOICE_STATUS_OPTIONS },
+  { field: 'settleStatus', label: '结算状态', options: SETTLE_STATUS_OPTIONS }
+]
+
+const pagination = reactive({ page: 1, pageSize: 10, total: 100 })
+
+const tableData = ref([
+  { orderNo: '232323232', orderType: '普通订单', amount: 2115.97, orderDate: '2025-11-22', orderStatus: '运输中', billStatus: '已对账', invoiceStatus: '已付款', settleStatus: '已结算' },
+  { orderNo: '2323412123', orderType: '普通订单', amount: 4476.12, orderDate: '2025-12-09', orderStatus: '待发货', billStatus: '未对账', invoiceStatus: '待开票', settleStatus: '未结算' },
+  { orderNo: '4566784543', orderType: '协议订单', amount: 7751.34, orderDate: '2025-11-21', orderStatus: '已收货', billStatus: '已对账', invoiceStatus: '已开票', settleStatus: '已结算' },
+  { orderNo: '2356565654', orderType: '普通订单', amount: 7936.37, orderDate: '2025-11-27', orderStatus: '运输中', billStatus: '未对账', invoiceStatus: '已付款', settleStatus: '未结算' },
+  { orderNo: '2323412123', orderType: '协议订单', amount: 3931.45, orderDate: '2025-12-11', orderStatus: '待发货', billStatus: '未对账', invoiceStatus: '待开票', settleStatus: '未结算' },
+  { orderNo: '2323232323', orderType: '普通订单', amount: 6132.75, orderDate: '2025-11-27', orderStatus: '已收货', billStatus: '已对账', invoiceStatus: '已开票', settleStatus: '已结算' },
+  { orderNo: '2356565654', orderType: '普通订单', amount: 3189.56, orderDate: '2025-11-25', orderStatus: '已收货', billStatus: '已对账', invoiceStatus: '已开票', settleStatus: '已结算' },
+  { orderNo: '2323232323', orderType: '协议订单', amount: 993.66, orderDate: '2025-11-20', orderStatus: '运输中', billStatus: '未对账', invoiceStatus: '已付款', settleStatus: '未结算' },
+  { orderNo: '2323412123', orderType: '普通订单', amount: 2763.35, orderDate: '2025-11-17', orderStatus: '待发货', billStatus: '未对账', invoiceStatus: '待开票', settleStatus: '未结算' },
+  { orderNo: '2323232323', orderType: '协议订单', amount: 9286.45, orderDate: '2025-12-05', orderStatus: '已收货', billStatus: '已对账', invoiceStatus: '已开票', settleStatus: '已结算' }
+])
+</script>

+ 159 - 0
src/views/cost/itemExpense/index.vue

@@ -0,0 +1,159 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <PageTitle title="分项费用" />
+      <el-button type="danger" @click="handleAdd">+ 新建</el-button>
+    </div>
+
+    <el-table :data="expenseList" border>
+      <el-table-column prop="name" label="分项费用名称" min-width="200" />
+      <el-table-column prop="currentQuota" label="现有额度(年)" min-width="150" align="center" />
+      <el-table-column prop="usedQuota" label="已用额度(年)" min-width="150" align="center" />
+      <el-table-column prop="status" label="状态" min-width="100" align="center">
+        <template #default="{ row }">
+          <span :class="['status-text', row.status === '启用' ? 'active' : '']">{{ row.status }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="100" align="center">
+        <template #default="{ row }">
+          <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
+          <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close>
+      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
+        <el-form-item label="分项费用名称" prop="name">
+          <el-input v-model="formData.name" placeholder="请输入分项费用名称" />
+        </el-form-item>
+        <el-form-item label="现有额度(年)">
+          <el-input-number v-model="formData.currentQuota" :min="0" :precision="2" controls-position="right" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-radio-group v-model="formData.status">
+            <el-radio label="启用">启用</el-radio>
+            <el-radio label="停用">停用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="handleSubmit">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { PageTitle } from '@/components'
+
+const dialogVisible = ref(false)
+const formRef = ref()
+const editingRow = ref<any>(null)
+
+const formData = reactive({
+  name: '',
+  currentQuota: 0,
+  status: '启用'
+})
+
+const formRules = {
+  name: [{ required: true, message: '请输入分项费用名称', trigger: 'blur' }]
+}
+
+const dialogTitle = computed(() => editingRow.value ? '编辑分项费用' : '新建分项费用')
+
+const expenseList = ref([
+  { id: 1, name: '常规费用', currentQuota: '0.00', usedQuota: '0.00', status: '启用' },
+  { id: 2, name: '办公用品', currentQuota: '0.00', usedQuota: '0.00', status: '启用' },
+  { id: 3, name: '设备采购', currentQuota: '0.00', usedQuota: '0.00', status: '启用' },
+  { id: 4, name: '耗材费用', currentQuota: '0.00', usedQuota: '0.00', status: '启用' },
+  { id: 5, name: '维修费用', currentQuota: '0.00', usedQuota: '0.00', status: '启用' }
+])
+
+const handleAdd = () => {
+  editingRow.value = null
+  formData.name = ''
+  formData.currentQuota = 0
+  formData.status = '启用'
+  dialogVisible.value = true
+}
+
+const handleEdit = (row: any) => {
+  editingRow.value = row
+  formData.name = row.name
+  formData.currentQuota = parseFloat(row.currentQuota) || 0
+  formData.status = row.status
+  dialogVisible.value = true
+}
+
+const handleDelete = (row: any) => {
+  ElMessageBox.confirm(`确定要删除"${row.name}"吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    const index = expenseList.value.findIndex(item => item.id === row.id)
+    if (index > -1) {
+      expenseList.value.splice(index, 1)
+    }
+    ElMessage.success('删除成功')
+  })
+}
+
+const handleSubmit = async () => {
+  const valid = await formRef.value?.validate()
+  if (!valid) return
+  if (editingRow.value) {
+    Object.assign(editingRow.value, {
+      name: formData.name,
+      currentQuota: formData.currentQuota.toFixed(2),
+      status: formData.status
+    })
+    ElMessage.success('编辑成功')
+  } else {
+    expenseList.value.push({
+      id: expenseList.value.length + 1,
+      name: formData.name,
+      currentQuota: formData.currentQuota.toFixed(2),
+      usedQuota: '0.00',
+      status: formData.status
+    })
+    ElMessage.success('新建成功')
+  }
+  dialogVisible.value = false
+}
+</script>
+
+<style scoped lang="scss">
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+
+  :deep(.page-title) {
+    margin-bottom: 0;
+  }
+}
+
+:deep(.el-table) {
+  th.el-table__cell {
+    background: #fafafa;
+    font-weight: 500;
+    color: #333;
+  }
+  .el-table__cell {
+    padding: 10px 0;
+  }
+}
+
+.status-text {
+  color: #999;
+  &.active { color: #67c23a; }
+}
+</style>

+ 173 - 0
src/views/cost/quotaControl/apply.vue

@@ -0,0 +1,173 @@
+<template>
+  <div class="quota-apply-container">
+    <div class="page-header">
+      <el-button type="primary" link @click="handleBack"><el-icon><ArrowLeft /></el-icon>返回</el-button>
+      <span class="page-title">额度申请</span>
+    </div>
+
+    <el-form ref="formRef" :model="formData" :rules="rules" label-position="top" class="apply-form">
+      <div class="form-row">
+        <el-form-item label="月度采购金额" prop="monthAmount">
+          <el-input v-model="formData.monthAmount" placeholder="请输入">
+            <template #suffix>元</template>
+          </el-input>
+        </el-form-item>
+        <el-form-item label="年度采购金额" prop="yearAmount">
+          <el-input v-model="formData.yearAmount" placeholder="请输入">
+            <template #suffix>元</template>
+          </el-input>
+        </el-form-item>
+      </div>
+
+      <div class="form-row">
+        <el-form-item label="申请信用金额" prop="creditAmount">
+          <el-input v-model="formData.creditAmount" placeholder="请输入">
+            <template #suffix>元</template>
+          </el-input>
+        </el-form-item>
+        <el-form-item label="账期时间" prop="accountPeriod">
+          <el-select v-model="formData.accountPeriod" placeholder="请选择" style="width: 100%">
+            <el-option label="30天" value="30" />
+            <el-option label="60天" value="60" />
+            <el-option label="90天" value="90" />
+          </el-select>
+        </el-form-item>
+      </div>
+
+      <el-form-item label="主要办公采购类目" prop="category">
+        <div class="category-tags">
+          <div v-for="item in categoryList" :key="item" :class="['tag-item', { active: formData.category === item }]" @click="formData.category = item">
+            {{ item }}
+          </div>
+        </div>
+      </el-form-item>
+
+      <el-form-item label="其他采购类目" prop="otherCategory">
+        <el-input v-model="formData.otherCategory" type="textarea" :rows="2" placeholder="请输入" maxlength="50" show-word-limit />
+      </el-form-item>
+
+      <el-form-item label="上传合作协议" prop="agreement">
+        <el-upload action="#" :auto-upload="false" :limit="1" list-type="picture-card">
+          <div class="upload-content">
+            <el-icon><Plus /></el-icon>
+            <span>上传</span>
+          </div>
+        </el-upload>
+      </el-form-item>
+
+      <div class="form-actions">
+        <el-button type="danger" @click="handleSave">保存</el-button>
+        <el-button @click="handleBack">取消</el-button>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { useRouter } from 'vue-router'
+import { ArrowLeft, Plus } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+const router = useRouter()
+const formRef = ref()
+
+const formData = reactive({
+  monthAmount: '',
+  yearAmount: '',
+  creditAmount: '',
+  accountPeriod: '',
+  category: '办公耗材',
+  otherCategory: ''
+})
+
+const categoryList = ['办公耗材', '办公设备', '数码设备', '办公日用', '办公日用', '办公日用', '办公日用', '办公日用', '办公日用']
+
+const rules = {
+  monthAmount: [{ required: true, message: '请输入月度采购金额', trigger: 'blur' }],
+  creditAmount: [{ required: true, message: '请输入申请信用金额', trigger: 'blur' }]
+}
+
+const handleBack = () => { router.back() }
+
+const handleSave = async () => {
+  const valid = await formRef.value?.validate()
+  if (!valid) return
+  ElMessage.success('申请提交成功')
+  router.back()
+}
+</script>
+
+<style scoped lang="scss">
+.quota-apply-container {
+  padding: 20px;
+  background: #fff;
+  min-height: 100%;
+}
+
+.page-header {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 25px;
+  .page-title { font-size: 16px; font-weight: bold; color: #333; }
+}
+
+.apply-form {
+  max-width: 900px;
+
+  .form-row {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 20px 40px;
+  }
+
+  :deep(.el-form-item__label) {
+    font-size: 14px;
+    color: #333;
+    padding-bottom: 8px;
+  }
+
+  :deep(.el-input__wrapper),
+  :deep(.el-select__wrapper),
+  :deep(.el-textarea__inner) {
+    background: #f5f5f5;
+    box-shadow: none;
+  }
+}
+
+.category-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+
+  .tag-item {
+    padding: 6px 16px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 13px;
+    color: #666;
+    background: #f5f5f5;
+    transition: all 0.2s;
+
+    &:hover { color: #e60012; }
+    &.active { background: #e60012; color: #fff; }
+  }
+}
+
+.upload-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #999;
+  font-size: 12px;
+  .el-icon { font-size: 20px; margin-bottom: 5px; }
+}
+
+.form-actions {
+  margin-top: 30px;
+  display: flex;
+  gap: 15px;
+}
+</style>

+ 145 - 0
src/views/cost/quotaControl/index.vue

@@ -0,0 +1,145 @@
+<template>
+  <div class="quota-control-container">
+    <div class="page-header">
+      <PageTitle title="额度控制" />
+      <el-button type="danger" @click="handleApplyQuota">额度申请</el-button>
+    </div>
+
+    <!-- 额度统计卡片 -->
+    <div class="quota-cards">
+      <div class="quota-card">
+        <div class="card-icon"><el-icon><CreditCard /></el-icon></div>
+        <div class="card-info">
+          <div class="card-label">当前额度 ></div>
+          <div class="card-value">32</div>
+        </div>
+      </div>
+      <div class="quota-card">
+        <div class="card-info">
+          <div class="card-label">剩余可用额度 ></div>
+          <div class="card-value">8,831.00</div>
+        </div>
+      </div>
+      <div class="quota-card">
+        <div class="card-info">
+          <div class="card-label">已使用额度 ></div>
+          <div class="card-value">8,831.00</div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 信用额度申请记录 -->
+    <div class="section-title">信用额度申请记录</div>
+    <el-table :data="recordList" border>
+      <el-table-column prop="applyTime" label="申请时间" min-width="120" align="center" />
+      <el-table-column prop="monthAmount" label="月度采购金额" min-width="120" align="center" />
+      <el-table-column prop="yearAmount" label="预估年度采购金额" min-width="140" align="center" />
+      <el-table-column prop="creditAmount" label="申请信用金额" min-width="120" align="center" />
+      <el-table-column prop="category" label="主要采购类目" min-width="120" align="center" />
+      <el-table-column prop="status" label="申请状态" min-width="100" align="center">
+        <template #default="{ row }">
+          <span :class="['status-text', getStatusClass(row.status)]">{{ row.status }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { CreditCard } from '@element-plus/icons-vue'
+import { PageTitle } from '@/components'
+
+const router = useRouter()
+
+const recordList = ref([
+  { id: 1, applyTime: '2025-11-30', monthAmount: '0.00', yearAmount: '0.00', creditAmount: '0.00', category: '食品饮料', status: '待审批' },
+  { id: 2, applyTime: '2025-11-29', monthAmount: '0.00', yearAmount: '0.00', creditAmount: '0.00', category: '食品饮料', status: '已通过' },
+  { id: 3, applyTime: '2025-11-25', monthAmount: '0.00', yearAmount: '0.00', creditAmount: '0.00', category: '食品饮料', status: '已通过' },
+  { id: 4, applyTime: '2025-12-05', monthAmount: '0.00', yearAmount: '0.00', creditAmount: '0.00', category: '食品饮料', status: '已通过' },
+  { id: 5, applyTime: '2025-12-07', monthAmount: '0.00', yearAmount: '0.00', creditAmount: '0.00', category: '食品饮料', status: '已通过' },
+  { id: 6, applyTime: '2025-12-07', monthAmount: '0.00', yearAmount: '0.00', creditAmount: '0.00', category: '食品饮料', status: '已通过' }
+])
+
+const getStatusClass = (status: string) => {
+  if (status === '已通过') return 'success'
+  if (status === '待审批') return 'warning'
+  return ''
+}
+
+const handleApplyQuota = () => {
+  router.push('/cost/quotaControl/apply')
+}
+</script>
+
+<style scoped lang="scss">
+.quota-control-container {
+  padding: 20px;
+  background: #fff;
+  min-height: 100%;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  :deep(.page-title) { margin-bottom: 0; }
+}
+
+.quota-cards {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 30px;
+
+  .quota-card {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    gap: 15px;
+    padding: 20px;
+    background: #f9f9f9;
+    border-radius: 4px;
+
+    .card-icon {
+      width: 40px;
+      height: 40px;
+      background: #409eff;
+      border-radius: 4px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: #fff;
+      font-size: 20px;
+    }
+
+    .card-info {
+      .card-label { font-size: 13px; color: #999; margin-bottom: 5px; }
+      .card-value { font-size: 24px; font-weight: bold; color: #333; }
+    }
+  }
+}
+
+.section-title {
+  font-size: 14px;
+  color: #333;
+  margin-bottom: 15px;
+}
+
+:deep(.el-table) {
+  th.el-table__cell {
+    background: #fafafa;
+    font-weight: 500;
+    color: #333;
+  }
+  .el-table__cell {
+    padding: 10px 0;
+  }
+}
+
+.status-text {
+  &.success { color: #67c23a; }
+  &.warning { color: #e6a23c; }
+}
+</style>

+ 95 - 0
src/views/enterprise/addressManage/index.vue

@@ -0,0 +1,95 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <PageTitle title="收货地址" />
+      <el-button type="danger" link @click="handleAdd">
+        <el-icon><Plus /></el-icon>新增收货地址
+      </el-button>
+    </div>
+
+    <!-- 地址列表 -->
+    <div class="address-list">
+      <div v-for="(item, index) in addressList" :key="index" class="address-item">
+        <div class="address-info">
+          <div class="address-name">
+            <span>{{ item.name }}</span>
+            <el-tag v-if="item.isDefault" type="danger" size="small">默认地址</el-tag>
+          </div>
+          <div class="address-detail">{{ item.province }} {{ item.city }} {{ item.district }}{{ item.address }}</div>
+          <div class="address-phone">{{ item.phone }}</div>
+        </div>
+        <div class="address-actions">
+          <el-button type="primary" link @click="handleEdit(item)">编辑</el-button>
+          <el-button type="danger" link @click="handleDelete(item)">删除</el-button>
+          <el-button v-if="!item.isDefault" type="danger" link @click="handleSetDefault(item)">设为默认</el-button>
+        </div>
+      </div>
+      <el-empty v-if="addressList.length === 0" description="暂无收货地址" />
+    </div>
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px">
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="收货人" prop="name">
+          <el-input v-model="form.name" placeholder="请输入收货人姓名" />
+        </el-form-item>
+        <el-form-item label="联系电话" prop="phone">
+          <el-input v-model="form.phone" placeholder="请输入联系电话" />
+        </el-form-item>
+        <el-form-item label="所在地区" prop="province">
+          <el-row :gutter="10">
+            <el-col :span="8"><el-select v-model="form.province" placeholder="省份" style="width: 100%"><el-option label="广东省" value="广东省" /><el-option label="北京市" value="北京市" /></el-select></el-col>
+            <el-col :span="8"><el-select v-model="form.city" placeholder="城市" style="width: 100%"><el-option label="广州市" value="广州市" /><el-option label="深圳市" value="深圳市" /></el-select></el-col>
+            <el-col :span="8"><el-select v-model="form.district" placeholder="区县" style="width: 100%"><el-option label="萝岗区" value="萝岗区" /><el-option label="天河区" value="天河区" /></el-select></el-col>
+          </el-row>
+        </el-form-item>
+        <el-form-item label="详细地址" prop="address">
+          <el-input v-model="form.address" type="textarea" :rows="2" placeholder="请输入详细地址" />
+        </el-form-item>
+        <el-form-item label="设为默认"><el-switch v-model="form.isDefault" /></el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="handleSave">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { Plus } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { PageTitle } from '@/components'
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增收货地址')
+const formRef = ref()
+const editingId = ref<number | null>(null)
+
+const form = reactive({ name: '', phone: '', province: '', city: '', district: '', address: '', isDefault: false })
+const rules = {
+  name: [{ required: true, message: '请输入收货人姓名', trigger: 'blur' }],
+  phone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }, { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }],
+  province: [{ required: true, message: '请选择省份', trigger: 'change' }],
+  address: [{ required: true, message: '请输入详细地址', trigger: 'blur' }]
+}
+
+const addressList = ref([
+  { id: 1, name: '中国南方电网有限公司', phone: '18062697722', province: '广东省', city: '广州市', district: '萝岗区', address: '科学城11号', isDefault: true },
+  { id: 2, name: '中国南方电网有限公司', phone: '18062697722', province: '广东省', city: '广州市', district: '萝岗区', address: '科学城11号', isDefault: false },
+  { id: 3, name: '中国南方电网有限公司', phone: '18062697722', province: '广东省', city: '广州市', district: '萝岗区', address: '科学城11号', isDefault: false }
+])
+
+const resetForm = () => { form.name = ''; form.phone = ''; form.province = ''; form.city = ''; form.district = ''; form.address = ''; form.isDefault = false; editingId.value = null }
+const handleAdd = () => { resetForm(); dialogTitle.value = '新增收货地址'; dialogVisible.value = true }
+const handleEdit = (item: any) => { editingId.value = item.id; form.name = item.name; form.phone = item.phone; form.province = item.province; form.city = item.city; form.district = item.district; form.address = item.address; form.isDefault = item.isDefault; dialogTitle.value = '编辑收货地址'; dialogVisible.value = true }
+const handleSave = async () => { const valid = await formRef.value?.validate(); if (!valid) return; ElMessage.success(editingId.value ? '修改成功' : '新增成功'); dialogVisible.value = false }
+const handleDelete = (item: any) => { ElMessageBox.confirm('确定要删除该收货地址吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { const index = addressList.value.findIndex(i => i.id === item.id); if (index > -1) addressList.value.splice(index, 1); ElMessage.success('删除成功') }) }
+const handleSetDefault = (item: any) => { addressList.value.forEach(i => { i.isDefault = i.id === item.id }); ElMessage.success('设置成功') }
+</script>
+
+<style scoped lang="scss">
+.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; :deep(.page-title) { margin-bottom: 0; } }
+.address-list { .address-item { display: flex; justify-content: space-between; align-items: center; padding: 20px; border: 1px solid #eee; border-radius: 4px; margin-bottom: 15px; &:last-child { margin-bottom: 0; } .address-info { .address-name { display: flex; align-items: center; gap: 10px; font-size: 15px; font-weight: 500; color: #333; margin-bottom: 8px; } .address-detail { font-size: 14px; color: #666; margin-bottom: 5px; } .address-phone { font-size: 14px; color: #666; } } .address-actions { display: flex; gap: 10px; } } }
+</style>

+ 84 - 0
src/views/enterprise/agreementSupply/index.vue

@@ -0,0 +1,84 @@
+<template>
+  <div class="page-container">
+    <PageTitle title="协议供货" />
+    <!-- 搜索栏 -->
+    <div class="search-bar">
+      <el-input v-model="queryParams.keyword" placeholder="搜索" style="width: 180px" clearable>
+        <template #prefix><el-icon><Search /></el-icon></template>
+      </el-input>
+      <div class="price-range">
+        <el-input v-model="queryParams.minPrice" placeholder="¥ 最高价" style="width: 100px" />
+        <span class="range-separator">—</span>
+        <el-input v-model="queryParams.maxPrice" placeholder="¥ 最低价" style="width: 100px" />
+      </div>
+      <el-select v-model="queryParams.category" placeholder="商品类别" style="width: 100px" clearable>
+        <el-option label="空调" value="空调" /><el-option label="电脑" value="电脑" /><el-option label="办公设备" value="办公设备" />
+      </el-select>
+    </div>
+    <!-- 排序栏 -->
+    <div class="sort-bar">
+      <el-select v-model="queryParams.sortType" placeholder="默认排序" style="width: 110px">
+        <el-option label="默认排序" value="default" /><el-option label="销量优先" value="sales" /><el-option label="最新上架" value="newest" />
+      </el-select>
+      <el-select v-model="queryParams.priceSort" placeholder="价格排序" style="width: 110px">
+        <el-option label="价格排序" value="" /><el-option label="价格从低到高" value="asc" /><el-option label="价格从高到低" value="desc" />
+      </el-select>
+    </div>
+    <!-- 商品列表 -->
+    <div class="product-grid">
+      <div v-for="(item, index) in productList" :key="index" class="product-card">
+        <div class="product-image">
+          <el-image :src="item.image" fit="contain">
+            <template #error><div class="image-placeholder"><el-icon :size="40" color="#ccc"><Picture /></el-icon></div></template>
+          </el-image>
+        </div>
+        <div class="product-info">
+          <div class="product-name">{{ item.name }}</div>
+          <div class="product-price">
+            <span v-if="item.tag" class="price-tag">{{ item.tag }}</span>
+            <span class="current-price">¥{{ item.price }}</span>
+            <span class="original-price">¥{{ item.originalPrice }}</span>
+            <div class="add-cart" @click="handleAddCart(item)"><el-icon><Plus /></el-icon></div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <el-empty v-if="productList.length === 0" description="暂无协议供货商品" />
+    <TablePagination v-if="productList.length > 0" v-model:page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :total="total" @change="handleQuery" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { Search, Picture, Plus } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { PageTitle, TablePagination } from '@/components'
+
+const queryParams = reactive({ pageNum: 1, pageSize: 15, keyword: '', minPrice: '', maxPrice: '', category: '', sortType: 'default', priceSort: '' })
+const total = ref(100)
+
+const productList = ref([
+  { id: 1, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '协议价', image: '' },
+  { id: 2, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '' },
+  { id: 3, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '' },
+  { id: 4, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '' },
+  { id: 5, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '' },
+  { id: 6, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '' },
+  { id: 7, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '' },
+  { id: 8, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '' },
+  { id: 9, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '' },
+  { id: 10, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '' }
+])
+
+const handleQuery = () => {}
+const handleAddCart = (_item: any) => { ElMessage.success('已加入购物车') }
+</script>
+
+<style scoped lang="scss">
+
+.search-bar { display: flex; align-items: center; gap: 15px; margin-bottom: 15px; .price-range { display: flex; align-items: center; gap: 5px; .range-separator { color: #999; } } }
+.sort-bar { display: flex; gap: 10px; margin-bottom: 20px; }
+.product-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 15px; }
+.product-card { background: #fff; border-radius: 8px; overflow: hidden; transition: all 0.2s; &:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .product-image { height: 160px; background: #f9f9f9; display: flex; align-items: center; justify-content: center; padding: 10px; .el-image { width: 100%; height: 100%; } .image-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #f5f5f5; } } .product-info { padding: 12px; .product-name { font-size: 13px; color: #333; line-height: 1.4; height: 36px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; margin-bottom: 8px; } .product-price { display: flex; align-items: center; gap: 5px; position: relative; .price-tag { font-size: 12px; color: #e60012; } .current-price { font-size: 16px; font-weight: bold; color: #e60012; } .original-price { font-size: 12px; color: #999; text-decoration: line-through; } .add-cart { position: absolute; right: 0; bottom: 0; width: 24px; height: 24px; border-radius: 50%; border: 1px solid #e60012; color: #e60012; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; &:hover { background: #e60012; color: #fff; } } } } }
+
+</style>

+ 335 - 0
src/views/enterprise/companyInfo/edit.vue

@@ -0,0 +1,335 @@
+<template>
+  <div class="company-edit-container">
+    <!-- 顶部返回 -->
+    <div class="page-header">
+      <el-button link @click="handleBack">
+        <el-icon><ArrowLeft /></el-icon>
+        <span>返回</span>
+      </el-button>
+      <span class="page-title">完善企业信息</span>
+    </div>
+
+    <div class="form-content">
+      <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
+        <!-- 工商信息 -->
+        <div class="section">
+          <div class="section-title"><i class="title-bar"></i>工商信息</div>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="企业名称" prop="companyName">
+                <el-input v-model="form.companyName" placeholder="请输入" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="统一社会信用代码" prop="creditCode">
+                <el-input v-model="form.creditCode" placeholder="请输入" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="法人" prop="legalPerson">
+                <el-input v-model="form.legalPerson" placeholder="请输入" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="注册资本" prop="registeredCapital">
+                <el-input v-model="form.registeredCapital" placeholder="请输入" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="24">
+              <el-form-item label="详细地址" prop="detailAddress">
+                <el-input 
+                  v-model="form.detailAddress" 
+                  placeholder="请输入" 
+                  maxlength="50"
+                  show-word-limit
+                />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="24">
+              <el-form-item label="经营范围" prop="businessScope">
+                <el-input 
+                  v-model="form.businessScope" 
+                  type="textarea"
+                  :rows="3"
+                  placeholder="请输入" 
+                  maxlength="50"
+                  show-word-limit
+                />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
+
+        <!-- 办公信息 -->
+        <div class="section">
+          <div class="section-title"><i class="title-bar"></i>办公信息</div>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="主要办公地址">
+                <el-select v-model="form.officeProvince" placeholder="请选择" style="width: 100%">
+                  <el-option label="广东省" value="广东省" />
+                  <el-option label="北京市" value="北京市" />
+                  <el-option label="上海市" value="上海市" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label=" ">
+                <el-input v-model="form.officeAddress" placeholder="请输入" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="所属行业" prop="industry">
+                <el-select v-model="form.industry" placeholder="请选择" style="width: 100%">
+                  <el-option label="电力行业" value="电力行业" />
+                  <el-option label="制造业" value="制造业" />
+                  <el-option label="互联网" value="互联网" />
+                  <el-option label="金融业" value="金融业" />
+                  <el-option label="其他" value="其他" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="企业规模" prop="scale">
+                <el-select v-model="form.scale" placeholder="请选择" style="width: 100%">
+                  <el-option label="50人以下" value="50人以下" />
+                  <el-option label="50-200人" value="50-200人" />
+                  <el-option label="200-500人" value="200-500人" />
+                  <el-option label="500-1000人" value="500-1000人" />
+                  <el-option label="1000-5000人" value="1000-5000人" />
+                  <el-option label="5000人以上" value="5000人以上" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="联系方式" prop="phone">
+                <el-input v-model="form.phone" placeholder="请输入" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="邮箱" prop="email">
+                <el-input v-model="form.email" placeholder="请输入" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="24">
+              <el-form-item label="网址" prop="website">
+                <el-input 
+                  v-model="form.website" 
+                  placeholder="请输入" 
+                  maxlength="50"
+                  show-word-limit
+                />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="12">
+              <el-form-item label="营业执照" prop="businessLicense">
+                <el-upload
+                  class="upload-box"
+                  action="#"
+                  :auto-upload="false"
+                  :show-file-list="false"
+                  :on-change="(file: any) => handleUpload(file, 'businessLicense')"
+                >
+                  <div v-if="form.businessLicense" class="upload-preview">
+                    <el-image :src="form.businessLicense" fit="cover" />
+                  </div>
+                  <div v-else class="upload-placeholder">
+                    <el-icon :size="24"><Plus /></el-icon>
+                    <span>上传</span>
+                  </div>
+                </el-upload>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="法人身份证" prop="idCard">
+                <el-upload
+                  class="upload-box"
+                  action="#"
+                  :auto-upload="false"
+                  :show-file-list="false"
+                  :on-change="(file: any) => handleUpload(file, 'idCard')"
+                >
+                  <div v-if="form.idCard" class="upload-preview">
+                    <el-image :src="form.idCard" fit="cover" />
+                  </div>
+                  <div v-else class="upload-placeholder">
+                    <el-icon :size="24"><Plus /></el-icon>
+                    <span>上传</span>
+                  </div>
+                </el-upload>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
+      </el-form>
+
+      <!-- 底部按钮 -->
+      <div class="form-footer">
+        <el-button type="danger" @click="handleSave">保存</el-button>
+        <el-button @click="handleBack">取消</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { useRouter } from 'vue-router'
+import { ArrowLeft, Plus } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+const router = useRouter()
+const formRef = ref()
+
+const form = reactive({
+  companyName: '',
+  creditCode: '',
+  legalPerson: '',
+  registeredCapital: '',
+  detailAddress: '',
+  businessScope: '',
+  officeProvince: '',
+  officeAddress: '',
+  industry: '',
+  scale: '',
+  phone: '',
+  email: '',
+  website: '',
+  businessLicense: '',
+  idCard: ''
+})
+
+const rules = {
+  companyName: [{ required: true, message: '请输入企业名称', trigger: 'blur' }],
+  creditCode: [{ required: true, message: '请输入统一社会信用代码', trigger: 'blur' }]
+}
+
+const handleBack = () => {
+  router.push('/enterprise/companyInfo')
+}
+
+const handleUpload = (file: any, field: string) => {
+  const reader = new FileReader()
+  reader.onload = (e) => {
+    (form as any)[field] = e.target?.result as string
+  }
+  reader.readAsDataURL(file.raw)
+}
+
+const handleSave = async () => {
+  const valid = await formRef.value?.validate()
+  if (!valid) return
+  
+  // TODO: 调用保存接口
+  ElMessage.success('保存成功')
+  handleBack()
+}
+</script>
+
+<style scoped lang="scss">
+.company-edit-container {
+  background: #f5f5f5;
+  min-height: 100%;
+}
+
+.page-header {
+  background: #fff;
+  padding: 15px 20px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  border-bottom: 1px solid #eee;
+
+  .page-title {
+    font-size: 16px;
+    font-weight: bold;
+    color: #333;
+  }
+}
+
+.form-content {
+  padding: 20px;
+}
+
+.section {
+  background: #fff;
+  border-radius: 8px;
+  padding: 20px;
+  margin-bottom: 15px;
+
+  .section-title {
+    font-size: 16px;
+    font-weight: bold;
+    margin-bottom: 20px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+}
+
+.title-bar {
+  display: inline-block;
+  width: 3px;
+  height: 16px;
+  background: #e60012;
+  border-radius: 2px;
+}
+
+.upload-box {
+  :deep(.el-upload) {
+    width: 100px;
+    height: 100px;
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    cursor: pointer;
+    overflow: hidden;
+
+    &:hover {
+      border-color: #e60012;
+    }
+  }
+
+  .upload-placeholder {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    color: #999;
+    font-size: 12px;
+    gap: 5px;
+  }
+
+  .upload-preview {
+    width: 100%;
+    height: 100%;
+    
+    .el-image {
+      width: 100%;
+      height: 100%;
+    }
+  }
+}
+
+.form-footer {
+  text-align: center;
+  padding: 20px;
+  background: #fff;
+  border-radius: 8px;
+}
+</style>

+ 477 - 0
src/views/enterprise/companyInfo/index.vue

@@ -0,0 +1,477 @@
+<template>
+  <div class="company-info-container">
+    <!-- 顶部企业头部信息 + Tab导航 -->
+    <div class="company-header-card">
+      <div class="company-header">
+        <div class="header-left">
+          <div class="company-logo">
+            <el-icon :size="40" color="#e60012"><OfficeBuilding /></el-icon>
+          </div>
+          <div class="company-basic">
+            <div class="company-name">
+              <span class="name">{{ companyData.companyName }}</span>
+              <el-tag type="danger" size="small">{{ companyData.customerType }}</el-tag>
+            </div>
+            <div class="company-code">企业编码: {{ companyData.companyCode }}</div>
+          </div>
+        </div>
+        <div class="header-right">
+          <div class="amount-item">
+            <div class="amount-value">¥{{ companyData.availableAmount }}</div>
+            <div class="amount-label">可用额度</div>
+          </div>
+          <div class="amount-item">
+            <div class="amount-value highlight">¥{{ companyData.creditAmount }}</div>
+            <div class="amount-label">授信额度</div>
+          </div>
+        </div>
+      </div>
+      <div class="tab-nav">
+        <div v-for="tab in tabs" :key="tab.key" :class="['tab-item', { active: activeTab === tab.key }]"
+          @click="handleTabClick(tab.key)">
+          <el-icon><Grid /></el-icon>
+          <span>{{ tab.label }}</span>
+          <el-icon><ArrowRight /></el-icon>
+        </div>
+      </div>
+    </div>
+
+    <!-- 主内容区域 -->
+    <div class="main-content">
+      <!-- 左侧:企业信息 -->
+      <div class="info-card">
+        <div class="card-header">
+          <span class="title"><i class="title-bar"></i>企业信息</span>
+          <el-button type="primary" link @click="handleUpdateInfo">
+            <el-icon><Edit /></el-icon>更新企业信息
+          </el-button>
+        </div>
+        <div class="info-table">
+          <div class="info-row">
+            <div class="info-label">企业名称</div>
+            <div class="info-value">{{ companyData.companyName }}</div>
+          </div>
+          <div class="info-row">
+            <div class="info-label">统一社会信用代码</div>
+            <div class="info-value">{{ companyData.creditCode }}</div>
+          </div>
+          <div class="info-row">
+            <div class="info-label">所属行业</div>
+            <div class="info-value">{{ companyData.industry || '-' }}</div>
+          </div>
+          <div class="info-row">
+            <div class="info-label">企业规模</div>
+            <div class="info-value">{{ companyData.scale }}</div>
+          </div>
+          <div class="info-row">
+            <div class="info-label">办公电话</div>
+            <div class="info-value">{{ companyData.phone }}</div>
+          </div>
+          <div class="info-row">
+            <div class="info-label">企业邮箱</div>
+            <div class="info-value">{{ companyData.email }}</div>
+          </div>
+          <div class="info-row">
+            <div class="info-label">企业地址</div>
+            <div class="info-value">{{ companyData.address }}</div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 右侧:专属平台采购公告 -->
+      <div class="notice-card">
+        <div class="card-header">
+          <span class="title"><i class="title-bar"></i>专属平台采购公告</span>
+          <el-button type="primary" link>更多</el-button>
+        </div>
+        <div class="notice-list">
+          <div v-for="(item, index) in noticeList" :key="index" class="notice-item">
+            <el-icon color="#e60012"><Bell /></el-icon>
+            <div class="notice-content">
+              <div class="notice-title">{{ item.title }}</div>
+              <div class="notice-desc">{{ item.description }}</div>
+            </div>
+            <div class="notice-date">{{ item.date }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 专属服务人员 -->
+    <div class="service-section">
+      <div class="section-title"><i class="title-bar"></i>专属服务人员</div>
+      <div class="service-list">
+        <div v-for="(person, index) in servicePersons" :key="index" class="service-card">
+          <el-avatar :size="50" :src="person.avatar">
+            <el-icon :size="30"><User /></el-icon>
+          </el-avatar>
+          <div class="person-info">
+            <div class="person-name">
+              {{ person.name }}
+              <el-tag :type="person.type === '专属采购顾问' ? 'danger' : 'primary'" size="small">{{ person.type }}</el-tag>
+            </div>
+            <div class="person-contact">{{ person.department }}  {{ person.phone }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 企业特权 -->
+    <div class="privilege-section">
+      <div class="section-title"><i class="title-bar"></i>企业特权</div>
+      <div class="privilege-grid">
+        <div v-for="(item, index) in privileges" :key="index" class="privilege-card">
+          <el-icon :size="24" color="#e60012"><Ticket /></el-icon>
+          <div class="privilege-name">{{ item.name }}</div>
+          <div class="privilege-desc">{{ item.description }}</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { useRouter } from 'vue-router'
+import { OfficeBuilding, Grid, ArrowRight, Edit, Bell, User, Ticket } from '@element-plus/icons-vue'
+
+const router = useRouter()
+const activeTab = ref('companyInfo')
+
+const tabs = [
+  { key: 'companyInfo', label: '企业信息' },
+  { key: 'purchaseHabit', label: '采购习惯' },
+  { key: 'security', label: '安全设置' },
+  { key: 'invoice', label: '开票信息' },
+  { key: 'creditApply', label: '额度申请' },
+  { key: 'changePerson', label: '更换负责人' }
+]
+
+const companyData = reactive({
+  companyName: '中国南方电网有限责任公司',
+  companyCode: '123456789',
+  customerType: '品牌客户',
+  availableAmount: '0.00',
+  creditAmount: '29000.88',
+  creditCode: '内容内容',
+  industry: '-',
+  scale: '5000人以上',
+  phone: '020-22222222',
+  email: '2222222@163.com',
+  address: '内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容'
+})
+
+const noticeList = ref([
+  { title: '主标题主标题', description: '副标题副标题副标题副标题副标题副标题副标题', date: '08.27' },
+  { title: '主标题主标题主标题主标题', description: '副标题副标题副标题副标题副标题副标题', date: '08.27' },
+  { title: '主标题主标题主标题主标题主标题主标...', description: '副标题副标题副标题', date: '08.27' },
+  { title: '主标题主标题主标题主标题主标题主标题', description: '副标题副标题副标题副标题副标题', date: '08.27' }
+])
+
+const servicePersons = ref([
+  { name: '刘洋', type: '专属采购顾问', department: '品牌中心', phone: '15783782783', avatar: '' },
+  { name: '刘洋', type: '客服人员', department: '027-5233893', phone: '15783782783', avatar: '' }
+])
+
+const privileges = ref([
+  { name: '满免运费', description: '会员购买商品订单总金额达到标准后可免运费。' },
+  { name: '协议供货', description: '会员购买商品订单总金额达到标准后可免运费。' },
+  { name: '账期采购', description: '会员购买商品订单总金额达到标准后可免运费。' },
+  { name: '专属方案', description: '会员购买商品订单总金额达到标准后可免运费。' },
+  { name: '专属采购顾问', description: '会员购买商品订单总金额达到标准后可免运费。' },
+  { name: '维保服务', description: '会员购买商品订单总金额达到标准后可免运费。' },
+  { name: '专属库存', description: '会员购买商品订单总金额达到标准后可免运费。' }
+])
+
+const handleUpdateInfo = () => { router.push('/enterprise/companyInfo/edit') }
+const handleTabClick = (tabKey: string) => {
+  activeTab.value = tabKey
+  if (tabKey === 'security') router.push('/enterprise/securitySetting')
+  else if (tabKey === 'purchaseHabit') router.push('/enterprise/purchaseHabit')
+  else if (tabKey === 'invoice') router.push('/enterprise/invoiceManage')
+  else if (tabKey === 'creditApply') router.push('/cost/quotaControl/apply')
+}
+</script>
+
+<style scoped lang="scss">
+.company-info-container {
+  padding: 0;
+  background: #f5f5f5;
+  min-height: 100%;
+}
+
+.company-header-card {
+  background: #fff;
+  border-radius: 8px;
+  margin-bottom: 16px;
+}
+
+.company-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24px;
+
+  .header-left {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+
+    .company-logo {
+      width: 70px;
+      height: 70px;
+      border-radius: 50%;
+      background: #fff5f5;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-shrink: 0;
+    }
+
+    .company-basic {
+      .company-name {
+        display: flex;
+        align-items: center;
+        gap: 12px;
+        margin-bottom: 8px;
+        .name {
+          font-size: 20px;
+          font-weight: bold;
+          color: #333;
+        }
+      }
+      .company-code {
+        color: #999;
+        font-size: 14px;
+      }
+    }
+  }
+
+  .header-right {
+    display: flex;
+    gap: 60px;
+
+    .amount-item {
+      text-align: center;
+      .amount-value {
+        font-size: 24px;
+        font-weight: bold;
+        color: #333;
+        &.highlight {
+          color: #e60012;
+        }
+      }
+      .amount-label {
+        font-size: 13px;
+        color: #999;
+        margin-top: 6px;
+      }
+    }
+  }
+}
+
+.tab-nav {
+  display: flex;
+  padding: 0 24px 16px;
+  gap: 8px;
+  flex-wrap: wrap;
+
+  .tab-item {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 12px 20px;
+    cursor: pointer;
+    color: #666;
+    font-size: 14px;
+    border-bottom: 2px solid transparent;
+    transition: all 0.2s;
+
+    &:hover, &.active {
+      color: #e60012;
+    }
+    &.active {
+      border-bottom-color: #e60012;
+    }
+  }
+}
+
+.main-content {
+  display: flex;
+  gap: 16px;
+  margin-bottom: 16px;
+}
+
+.info-card {
+  flex: 1;
+  background: #fff;
+  border-radius: 8px;
+  padding: 24px;
+  min-width: 0;
+}
+
+.notice-card {
+  width: 380px;
+  flex-shrink: 0;
+  background: #fff;
+  border-radius: 8px;
+  padding: 24px;
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+
+  .title {
+    font-size: 16px;
+    font-weight: bold;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+}
+
+.title-bar {
+  display: inline-block;
+  width: 3px;
+  height: 16px;
+  background: #e60012;
+  border-radius: 2px;
+}
+
+.info-table {
+  .info-row {
+    display: flex;
+    border-bottom: 1px solid #f0f0f0;
+    padding: 14px 0;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    .info-label {
+      width: 140px;
+      color: #999;
+      flex-shrink: 0;
+      font-size: 14px;
+    }
+    .info-value {
+      flex: 1;
+      color: #333;
+      font-size: 14px;
+      word-break: break-all;
+    }
+  }
+}
+
+.notice-list {
+  .notice-item {
+    display: flex;
+    align-items: flex-start;
+    gap: 12px;
+    padding: 14px 0;
+    border-bottom: 1px solid #f0f0f0;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    .notice-content {
+      flex: 1;
+      min-width: 0;
+      .notice-title {
+        font-size: 14px;
+        color: #333;
+        margin-bottom: 6px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+      .notice-desc {
+        font-size: 12px;
+        color: #999;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+    }
+    .notice-date {
+      color: #999;
+      font-size: 12px;
+      flex-shrink: 0;
+    }
+  }
+}
+
+.service-section, .privilege-section {
+  background: #fff;
+  border-radius: 8px;
+  padding: 24px;
+  margin-bottom: 16px;
+
+  .section-title {
+    font-size: 16px;
+    font-weight: bold;
+    margin-bottom: 20px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+  }
+}
+
+.service-list {
+  display: flex;
+  gap: 24px;
+  flex-wrap: wrap;
+
+  .service-card {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    padding: 20px 24px;
+    background: linear-gradient(135deg, #fff5f5 0%, #fff 100%);
+    border-radius: 8px;
+    min-width: 300px;
+
+    .person-info {
+      .person-name {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+        font-size: 15px;
+        font-weight: 500;
+        margin-bottom: 8px;
+      }
+      .person-contact {
+        font-size: 13px;
+        color: #666;
+      }
+    }
+  }
+}
+
+.privilege-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+  gap: 16px;
+
+  .privilege-card {
+    padding: 20px;
+    background: linear-gradient(135deg, #fff5f5 0%, #fff 100%);
+    border-radius: 8px;
+
+    .privilege-name {
+      font-size: 15px;
+      font-weight: 500;
+      margin: 12px 0 10px;
+    }
+    .privilege-desc {
+      font-size: 13px;
+      color: #999;
+      line-height: 1.6;
+    }
+  }
+}
+</style>

+ 99 - 0
src/views/enterprise/invoiceManage/index.vue

@@ -0,0 +1,99 @@
+<template>
+  <div class="invoice-manage-container">
+    <div class="page-title"><i class="title-bar"></i><span>发票管理</span></div>
+    <!-- 搜索栏 -->
+    <div class="search-bar">
+      <el-input v-model="queryParams.keyword" placeholder="搜索" style="width: 200px" clearable>
+        <template #prefix><el-icon><Search /></el-icon></template>
+      </el-input>
+      <el-select v-model="queryParams.searchType" placeholder="账户名称" style="width: 120px">
+        <el-option label="账户名称" value="accountName" />
+        <el-option label="发票抬头" value="invoiceTitle" />
+        <el-option label="纳税人识别号" value="taxNo" />
+      </el-select>
+      <div class="search-right"><el-button type="danger" @click="handleAdd">新增开票信息</el-button></div>
+    </div>
+    <!-- 表格 -->
+    <el-table :data="tableData" border style="width: 100%" :resizable="false">
+      <el-table-column prop="invoiceTitle" label="发票抬头" width="140" show-overflow-tooltip :resizable="false" />
+      <el-table-column prop="taxNo" label="纳税人识别号" width="130" :resizable="false" />
+      <el-table-column prop="bankName" label="开户银行" width="120" :resizable="false" />
+      <el-table-column prop="bankAccount" label="开户账户" width="140" :resizable="false" />
+      <el-table-column prop="address" label="地址" min-width="140" show-overflow-tooltip :resizable="false" />
+      <el-table-column prop="phone" label="电话" width="120" :resizable="false" />
+      <el-table-column label="操作" width="120" fixed="right" :resizable="false">
+        <template #default="{ row }">
+          <div style="display: flex; gap: 8px; justify-content: center;">
+            <el-button type="primary" link @click="handleEdit(row)">修改</el-button>
+            <el-button type="danger" link @click="handleDelete(row)">删除</el-button>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <div class="pagination-wrap">
+      <span class="total-text">共计 {{ total }} 条</span>
+      <el-pagination v-model:current-page="queryParams.pageNum" v-model:page-size="queryParams.pageSize"
+        :page-sizes="[10, 20, 50]" :total="total" layout="prev, pager, next, sizes, jumper"
+        @size-change="handleQuery" @current-change="handleQuery" />
+    </div>
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px">
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
+        <el-form-item label="发票抬头" prop="invoiceTitle"><el-input v-model="form.invoiceTitle" placeholder="请输入发票抬头" /></el-form-item>
+        <el-form-item label="纳税人识别号" prop="taxNo"><el-input v-model="form.taxNo" placeholder="请输入纳税人识别号" /></el-form-item>
+        <el-form-item label="开户银行" prop="bankName"><el-input v-model="form.bankName" placeholder="请输入开户银行" /></el-form-item>
+        <el-form-item label="开户账户" prop="bankAccount"><el-input v-model="form.bankAccount" placeholder="请输入开户账户" /></el-form-item>
+        <el-form-item label="地址" prop="address"><el-input v-model="form.address" placeholder="请输入地址" /></el-form-item>
+        <el-form-item label="电话" prop="phone"><el-input v-model="form.phone" placeholder="请输入电话" /></el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="handleSave">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增开票信息')
+const formRef = ref()
+const editingId = ref<number | null>(null)
+const total = ref(0)
+
+const queryParams = reactive({ pageNum: 1, pageSize: 10, keyword: '', searchType: 'accountName' })
+const form = reactive({ invoiceTitle: '', taxNo: '', bankName: '', bankAccount: '', address: '', phone: '' })
+const rules = {
+  invoiceTitle: [{ required: true, message: '请输入发票抬头', trigger: 'blur' }],
+  taxNo: [{ required: true, message: '请输入纳税人识别号', trigger: 'blur' }],
+  bankName: [{ required: true, message: '请输入开户银行', trigger: 'blur' }],
+  bankAccount: [{ required: true, message: '请输入开户账户', trigger: 'blur' }]
+}
+
+const tableData = ref([
+  { id: 1, invoiceTitle: '北京吉浪文化传...', taxNo: '23455a2121', bankName: '湖北交通银行', bankAccount: '3273237283723', address: '福建省泉州市金...', phone: '18525220957' },
+  { id: 2, invoiceTitle: '北京志荣煤拓科...', taxNo: '23455a2121', bankName: '中国银行', bankAccount: '3273237283723', address: '陕西省榆林市神...', phone: '13513660470' },
+  { id: 3, invoiceTitle: '高诚美恒公司', taxNo: '23455a2121', bankName: '中国建设银行', bankAccount: '3273237283723', address: '宁夏回族自治区...', phone: '13881559836' }
+])
+
+onMounted(() => { total.value = 500 })
+const handleQuery = () => {}
+const resetForm = () => { form.invoiceTitle = ''; form.taxNo = ''; form.bankName = ''; form.bankAccount = ''; form.address = ''; form.phone = ''; editingId.value = null }
+const handleAdd = () => { resetForm(); dialogTitle.value = '新增开票信息'; dialogVisible.value = true }
+const handleEdit = (row: any) => { editingId.value = row.id; form.invoiceTitle = row.invoiceTitle; form.taxNo = row.taxNo; form.bankName = row.bankName; form.bankAccount = row.bankAccount; form.address = row.address; form.phone = row.phone; dialogTitle.value = '编辑开票信息'; dialogVisible.value = true }
+const handleSave = async () => { const valid = await formRef.value?.validate(); if (!valid) return; ElMessage.success(editingId.value ? '修改成功' : '新增成功'); dialogVisible.value = false }
+const handleDelete = (row: any) => { ElMessageBox.confirm('确定要删除该开票信息吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { const index = tableData.value.findIndex(i => i.id === row.id); if (index > -1) tableData.value.splice(index, 1); ElMessage.success('删除成功') }) }
+</script>
+
+<style scoped lang="scss">
+.invoice-manage-container { padding: 20px; background: #fff; min-height: 100%; }
+.page-title { font-size: 16px; font-weight: bold; display: flex; align-items: center; gap: 8px; margin-bottom: 20px; }
+.title-bar { display: inline-block; width: 3px; height: 16px; background: #e60012; border-radius: 2px; }
+.search-bar { display: flex; align-items: center; gap: 10px; margin-bottom: 20px; .search-right { flex: 1; display: flex; justify-content: flex-end; } }
+.pagination-wrap { display: flex; justify-content: space-between; align-items: center; margin-top: 20px; .total-text { font-size: 14px; color: #666; } }
+</style>

+ 89 - 0
src/views/enterprise/messageNotice/index.vue

@@ -0,0 +1,89 @@
+<template>
+  <div class="page-container">
+    <PageTitle title="消息通知" />
+
+    <!-- Tab切换 -->
+    <div class="tab-nav">
+      <div v-for="tab in tabs" :key="tab.key" :class="['tab-item', { active: activeTab === tab.key }]" @click="activeTab = tab.key">
+        <el-icon><component :is="tab.icon" /></el-icon>
+        <span>{{ tab.label }}</span>
+      </div>
+    </div>
+
+    <!-- 消息列表 -->
+    <div class="message-list">
+      <div v-for="(item, index) in messageList" :key="index" class="message-item">
+        <div class="message-content">
+          <div :class="['message-icon', item.iconType || 'user']">
+            <el-icon v-if="item.iconType === 'package'" :size="20" color="#e60012"><Box /></el-icon>
+            <template v-else-if="item.iconType === 'budget'"><span class="budget-text">¥</span></template>
+            <el-icon v-else :size="20" color="#fff"><User /></el-icon>
+            <span v-if="item.unread" class="unread-dot"></span>
+          </div>
+          <div class="message-info">
+            <div class="message-title">{{ item.title }}</div>
+            <div class="message-desc">{{ item.desc }}</div>
+          </div>
+          <div class="message-right">
+            <div class="message-time">{{ item.time }}</div>
+            <el-button v-if="item.showAction" type="danger" size="small" @click="handleProcess(item)">去处理</el-button>
+          </div>
+        </div>
+      </div>
+      <el-empty v-if="messageList.length === 0" description="暂无消息" />
+    </div>
+
+    <!-- 分页 -->
+    <TablePagination v-if="messageList.length > 0" v-model:page="queryParams.pageNum" v-model:pageSize="queryParams.pageSize" :total="total" @change="handleQuery" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, watch } from 'vue'
+import { Document, Bell, Warning, User, Box } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { PageTitle, TablePagination } from '@/components'
+
+const activeTab = ref('approval')
+const tabs = [
+  { key: 'approval', label: '审批待办', icon: Document },
+  { key: 'arrival', label: '到货提醒', icon: Bell },
+  { key: 'budget', label: '预算预警', icon: Warning }
+]
+
+const queryParams = reactive({ pageNum: 1, pageSize: 10 })
+const total = ref(0)
+const messageList = ref<any[]>([])
+
+const loadData = () => {
+  if (activeTab.value === 'approval') {
+    messageList.value = [
+      { time: '2025/01/10 11:42:32', title: '您有一个审批流程待处理', desc: '审批名称:办公用品采购申请', unread: true, showAction: true, iconType: 'user' },
+      { time: '2025/01/10 11:42:32', title: '您的办公用品审批申请已通过', desc: '审批名称:办公用品采购申请', unread: true, showAction: false, iconType: 'user' },
+      { time: '2025/01/10 11:42:32', title: '您的办公用品审批申请已通过', desc: '审批名称:办公用品采购申请', unread: true, showAction: false, iconType: 'user' }
+    ]
+    total.value = 3
+  } else if (activeTab.value === 'arrival') {
+    messageList.value = [
+      { time: '2025/01/10 11:42:32', title: '包裹到货提醒', desc: '物流状态:已到达代售点', unread: true, showAction: false, iconType: 'package' },
+      { time: '2025/01/10 11:42:32', title: '包裹到货提醒', desc: '物流状态:已到达代售点', unread: false, showAction: false, iconType: 'package' }
+    ]
+    total.value = 2
+  } else {
+    messageList.value = [
+      { time: '2025/01/10 11:42:32', title: '预算预警', desc: '副标题副副标题', unread: true, showAction: false, iconType: 'budget' },
+      { time: '2025/01/10 11:42:32', title: '预算预警', desc: '副标题副副标题', unread: false, showAction: false, iconType: 'budget' }
+    ]
+    total.value = 2
+  }
+}
+
+watch(activeTab, () => { queryParams.pageNum = 1; loadData() }, { immediate: true })
+const handleQuery = () => { loadData() }
+const handleProcess = (_item: any) => { ElMessage.info('跳转到审批处理页面') }
+</script>
+
+<style scoped lang="scss">
+.tab-nav { display: flex; gap: 30px; border-bottom: 1px solid #eee; margin-bottom: 20px; .tab-item { display: flex; align-items: center; gap: 5px; padding: 10px 0; cursor: pointer; color: #666; font-size: 14px; border-bottom: 2px solid transparent; margin-bottom: -1px; &:hover, &.active { color: #333; } &.active { border-bottom-color: #e60012; } } }
+.message-list { .message-item { padding: 15px 0; border-bottom: 1px solid #f5f5f5; &:last-child { border-bottom: none; } .message-content { display: flex; align-items: center; gap: 15px; .message-icon { position: relative; width: 40px; height: 40px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; &.user { background: #e60012; } &.package { background: #fff5f5; border: 1px solid #ffe0e0; } &.budget { background: #e60012; .budget-text { color: #fff; font-size: 16px; font-weight: bold; } } .unread-dot { position: absolute; top: -2px; right: -2px; width: 8px; height: 8px; border-radius: 50%; background: #e60012; border: 2px solid #fff; } } .message-info { flex: 1; .message-title { font-size: 14px; font-weight: 500; color: #333; margin-bottom: 5px; } .message-desc { font-size: 13px; color: #999; } } .message-right { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; .message-time { font-size: 12px; color: #999; } } } } }
+</style>

+ 91 - 0
src/views/enterprise/myCollection/index.vue

@@ -0,0 +1,91 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <PageTitle title="我的收藏" />
+      <el-button type="danger" link @click="handleAddCategory"><el-icon><Plus /></el-icon>添加分类</el-button>
+    </div>
+    <!-- 分类Tab -->
+    <StatusTabs v-model="activeCategory" :tabs="categoryTabs" type="pill" />
+    <!-- 操作栏 -->
+    <div class="action-bar">
+      <el-checkbox v-model="selectAll" @change="handleSelectAll">全选</el-checkbox>
+      <el-button type="danger" link @click="handleBatchAddCart">加入购物车</el-button>
+      <el-button type="danger" link @click="handleBatchCancel">取消收藏</el-button>
+    </div>
+    <!-- 商品列表 -->
+    <div class="product-grid">
+      <ProductCard
+        v-for="(item, index) in productList"
+        :key="index"
+        :product="item"
+        v-model="item.checked"
+        show-checkbox
+        show-action
+        show-add-cart
+        action-text="取消收藏"
+        @action="handleCancelCollection(item)"
+        @add-cart="handleAddCart(item)"
+      />
+    </div>
+    <el-empty v-if="productList.length === 0" description="暂无收藏商品" />
+    <TablePagination v-if="productList.length > 0" v-model:page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :total="total" @change="handleQuery" />
+    <!-- 添加分类弹窗 -->
+    <el-dialog v-model="categoryDialogVisible" title="添加分类" width="400px">
+      <el-form ref="categoryFormRef" :model="categoryForm" :rules="categoryRules">
+        <el-form-item prop="name"><el-input v-model="categoryForm.name" placeholder="请输入分类名称" /></el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="categoryDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="handleSaveCategory">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, watch } from 'vue'
+import { Plus } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox, type CheckboxValueType } from 'element-plus'
+import { PageTitle, StatusTabs, ProductCard, TablePagination } from '@/components'
+
+const activeCategory = ref('all')
+const selectAll = ref(false)
+const categoryDialogVisible = ref(false)
+const categoryFormRef = ref()
+
+const categoryTabs = ref([
+  { key: 'all', label: '全部' }, { key: 'computer', label: '电脑办公(2)' }, { key: 'industrial', label: '工业品(2)' },
+  { key: 'decoration', label: '家装建材(2)' }, { key: 'appliance', label: '家用电器(2)' }, { key: 'digital', label: '数码(2)' }
+])
+const categoryForm = reactive({ name: '' })
+const categoryRules = { name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }] }
+const queryParams = reactive({ pageNum: 1, pageSize: 15 })
+const total = ref(100)
+
+const productList = ref([
+  { id: 1, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '协议价', image: '', checked: false },
+  { id: 2, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '', checked: false },
+  { id: 3, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '', checked: false },
+  { id: 4, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '', checked: false },
+  { id: 5, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '', checked: false },
+  { id: 6, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '', checked: false },
+  { id: 7, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '', checked: false },
+  { id: 8, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '', checked: false }
+])
+
+watch(activeCategory, () => { queryParams.pageNum = 1; handleQuery() })
+const handleQuery = () => {}
+const handleSelectAll = (val: CheckboxValueType) => { productList.value.forEach(item => { item.checked = !!val }) }
+const handleAddCategory = () => { categoryForm.name = ''; categoryDialogVisible.value = true }
+const handleSaveCategory = async () => { const valid = await categoryFormRef.value?.validate(); if (!valid) return; categoryTabs.value.push({ key: categoryForm.name, label: `${categoryForm.name}(0)` }); ElMessage.success('添加成功'); categoryDialogVisible.value = false }
+const handleAddCart = (_item: any) => { ElMessage.success('已加入购物车') }
+const handleBatchAddCart = () => { const selected = productList.value.filter(item => item.checked); if (selected.length === 0) { ElMessage.warning('请先选择商品'); return }; ElMessage.success(`已将${selected.length}件商品加入购物车`) }
+const handleCancelCollection = (item: any) => { ElMessageBox.confirm('确定要取消收藏该商品吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { const index = productList.value.findIndex(i => i.id === item.id); if (index > -1) productList.value.splice(index, 1); ElMessage.success('已取消收藏') }) }
+const handleBatchCancel = () => { const selected = productList.value.filter(item => item.checked); if (selected.length === 0) { ElMessage.warning('请先选择商品'); return }; ElMessageBox.confirm(`确定要取消收藏选中的${selected.length}件商品吗?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { productList.value = productList.value.filter(item => !item.checked); selectAll.value = false; ElMessage.success('已取消收藏') }) }
+</script>
+
+<style scoped lang="scss">
+.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; :deep(.page-title) { margin-bottom: 0; } }
+.action-bar { display: flex; align-items: center; gap: 20px; padding: 10px 0; border-bottom: 1px solid #eee; margin-bottom: 15px; }
+.product-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; }
+</style>

+ 66 - 0
src/views/enterprise/myFootprint/index.vue

@@ -0,0 +1,66 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <PageTitle title="我的足迹" />
+      <el-button type="danger" link @click="handleClearAll"><el-icon><Delete /></el-icon>删除足迹</el-button>
+    </div>
+    <!-- 按日期分组的足迹列表 -->
+    <div class="footprint-list">
+      <div v-for="(group, groupIndex) in footprintGroups" :key="groupIndex" class="footprint-group">
+        <div class="group-header"><span class="group-date">{{ group.date }} {{ group.label }}</span></div>
+        <div class="product-grid">
+          <ProductCard
+            v-for="(item, itemIndex) in group.products"
+            :key="itemIndex"
+            :product="item"
+            v-model="item.checked"
+            show-checkbox
+          />
+        </div>
+      </div>
+    </div>
+    <el-empty v-if="footprintGroups.length === 0" description="暂无浏览足迹" />
+    <TablePagination v-if="footprintGroups.length > 0" v-model:page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :total="total" @change="handleQuery" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { Delete } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { PageTitle, ProductCard, TablePagination } from '@/components'
+
+const queryParams = reactive({ pageNum: 1, pageSize: 20 })
+const total = ref(100)
+
+const footprintGroups = ref([
+  { date: '12月10日', label: '昨天', products: [
+    { id: 1, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '协议价', image: '', checked: true },
+    { id: 2, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '', checked: false },
+    { id: 3, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '', checked: false },
+    { id: 4, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '', checked: false },
+    { id: 5, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '', checked: false }
+  ]},
+  { date: '12月07日', label: '', products: [
+    { id: 6, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '协议价', image: '', checked: true },
+    { id: 7, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '', checked: true },
+    { id: 8, name: '格力KFR-72LW/定频冷暖空调柜机3P', price: '1,299', originalPrice: '1,899', tag: '', image: '', checked: false }
+  ]}
+])
+
+const handleQuery = () => {}
+const handleClearAll = () => {
+  const selectedItems: any[] = []
+  footprintGroups.value.forEach(group => { group.products.forEach(item => { if (item.checked) selectedItems.push(item) }) })
+  if (selectedItems.length === 0) {
+    ElMessageBox.confirm('确定要清空所有浏览足迹吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { footprintGroups.value = []; ElMessage.success('已清空所有足迹') })
+  } else {
+    ElMessageBox.confirm(`确定要删除选中的${selectedItems.length}条足迹吗?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { footprintGroups.value.forEach(group => { group.products = group.products.filter(item => !item.checked) }); footprintGroups.value = footprintGroups.value.filter(group => group.products.length > 0); ElMessage.success('删除成功') })
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; :deep(.page-title) { margin-bottom: 0; } }
+.footprint-list { .footprint-group { margin-bottom: 25px; .group-header { margin-bottom: 15px; .group-date { font-size: 14px; color: #666; &::before { content: '•'; margin-right: 8px; color: #999; } } } .product-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 15px; } } }
+</style>

+ 144 - 0
src/views/enterprise/purchaseHabit/index.vue

@@ -0,0 +1,144 @@
+<template>
+  <div class="purchase-habit-container">
+    <!-- 顶部返回 -->
+    <div class="page-header">
+      <el-button link @click="handleBack">
+        <el-icon><ArrowLeft /></el-icon>
+        <span>返回</span>
+      </el-button>
+      <span class="page-title">企业采购习惯</span>
+    </div>
+
+    <div class="page-content">
+      <el-form ref="formRef" :model="form" label-position="top">
+        <!-- 采购金额 -->
+        <el-row :gutter="40">
+          <el-col :span="12">
+            <el-form-item label="月度采购金额">
+              <el-input v-model="form.monthlyAmount" placeholder="请输入">
+                <template #suffix>万</template>
+              </el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="年度采购金额">
+              <el-input v-model="form.yearlyAmount" placeholder="请输入">
+                <template #suffix>万</template>
+              </el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <!-- 产品选型 -->
+        <el-form-item label="产品选型">
+          <div class="tag-group">
+            <div v-for="item in productTypeOptions" :key="item"
+              :class="['tag-item', { active: form.productTypes.includes(item) }]"
+              @click="toggleTag(form.productTypes, item)">{{ item }}</div>
+          </div>
+        </el-form-item>
+
+        <!-- 日常打印量 -->
+        <el-form-item label="日常打印量">
+          <div class="tag-group">
+            <div v-for="item in printVolumeOptions" :key="item"
+              :class="['tag-item', { active: form.printVolume === item }]"
+              @click="form.printVolume = item">{{ item }}</div>
+          </div>
+        </el-form-item>
+
+        <!-- 购买原装耗材 & 专人进行技术服务 -->
+        <el-row :gutter="40">
+          <el-col :span="12">
+            <el-form-item label="购买原装耗材">
+              <el-radio-group v-model="form.buyOriginal">
+                <el-radio value="是">是</el-radio>
+                <el-radio value="否">否</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="专人进行技术服务">
+              <el-radio-group v-model="form.techService">
+                <el-radio value="是">是</el-radio>
+                <el-radio value="否">否</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <!-- 主要办公采购类目 -->
+        <el-form-item label="主要办公采购类目">
+          <div class="tag-group">
+            <div v-for="item in categoryOptions" :key="item"
+              :class="['tag-item', { active: form.categories.includes(item) }]"
+              @click="toggleTag(form.categories, item)">{{ item }}</div>
+          </div>
+          <el-input v-model="form.otherCategory" placeholder="其他采购类目" maxlength="50" show-word-limit class="other-input" />
+        </el-form-item>
+
+        <!-- 企业福利 -->
+        <el-form-item label="企业福利">
+          <div class="tag-group">
+            <div v-for="item in welfareOptions" :key="item"
+              :class="['tag-item', { active: form.welfares.includes(item) }]"
+              @click="toggleTag(form.welfares, item)">{{ item }}</div>
+          </div>
+          <el-input v-model="form.otherWelfare" placeholder="其他福利" maxlength="50" show-word-limit class="other-input" />
+        </el-form-item>
+
+        <!-- 产品定制需求 -->
+        <el-form-item label="产品定制需求">
+          <div class="tag-group">
+            <div v-for="item in customOptions" :key="item"
+              :class="['tag-item', { active: form.customs.includes(item) }]"
+              @click="toggleTag(form.customs, item)">{{ item }}</div>
+          </div>
+          <el-input v-model="form.otherCustom" placeholder="其他需求" maxlength="50" show-word-limit class="other-input" />
+        </el-form-item>
+      </el-form>
+
+      <!-- 底部按钮 -->
+      <div class="form-footer">
+        <el-button type="danger" @click="handleSave">保存</el-button>
+        <el-button @click="handleBack">取消</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive } from 'vue'
+import { useRouter } from 'vue-router'
+import { ArrowLeft } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+const router = useRouter()
+
+const productTypeOptions = ['经济适用', '性价比高', '大牌商品']
+const printVolumeOptions = ['100-500张', '500-1000张', '1000-3000张', '3000张以上']
+const categoryOptions = ['办公耗材', '办公设备', '数码设备', '办公日用', '办公家具', '清洁用品', '劳保用品', '食品饮料']
+const welfareOptions = ['新年福利', '中秋福利', '端午福利', '生日福利', '员工体检', '团建活动', '节日礼品', '年终奖品']
+const customOptions = ['服装定制', '礼品定制', '包装定制', '印刷定制', 'LOGO定制', '文创定制', '工艺品定制', '其他定制']
+
+const form = reactive({
+  monthlyAmount: '', yearlyAmount: '', productTypes: ['经济适用'] as string[], printVolume: '100-500张',
+  buyOriginal: '是', techService: '是', categories: ['办公耗材'] as string[], otherCategory: '',
+  welfares: ['新年福利'] as string[], otherWelfare: '', customs: ['服装定制'] as string[], otherCustom: ''
+})
+
+const toggleTag = (arr: string[], item: string) => { const index = arr.indexOf(item); if (index > -1) arr.splice(index, 1); else arr.push(item) }
+const handleBack = () => { router.push('/enterprise/companyInfo') }
+const handleSave = () => { ElMessage.success('保存成功'); handleBack() }
+</script>
+
+<style scoped lang="scss">
+.purchase-habit-container { background: #f5f5f5; min-height: 100%; }
+.page-header { background: #fff; padding: 15px 20px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid #eee; .page-title { font-size: 16px; font-weight: bold; color: #333; } }
+.page-content { padding: 20px; background: #fff; margin: 20px; border-radius: 8px; }
+.tag-group { display: flex; flex-wrap: wrap; gap: 10px; .tag-item { padding: 8px 20px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; font-size: 14px; color: #666; transition: all 0.2s; &:hover { border-color: #e60012; color: #e60012; } &.active { border-color: #e60012; color: #e60012; background: #fff5f5; } } }
+.other-input { margin-top: 10px; }
+.form-footer { text-align: center; padding-top: 30px; border-top: 1px solid #eee; margin-top: 20px; }
+:deep(.el-form-item__label) { font-weight: 500; color: #333; }
+:deep(.el-radio) { margin-right: 30px; }
+</style>

+ 149 - 0
src/views/enterprise/purchaseHistory/index.vue

@@ -0,0 +1,149 @@
+<template>
+  <div class="page-container">
+    <PageTitle title="历史购买" />
+    <!-- 搜索栏 -->
+    <div class="search-bar">
+      <el-input v-model="queryParams.keyword" placeholder="搜索" style="width: 180px" clearable>
+        <template #prefix><el-icon><Search /></el-icon></template>
+      </el-input>
+      <div class="price-range">
+        <el-input v-model="queryParams.minPrice" placeholder="¥ 最高价" style="width: 100px" />
+        <span class="range-separator">—</span>
+        <el-input v-model="queryParams.maxPrice" placeholder="¥ 最低价" style="width: 100px" />
+      </div>
+      <el-date-picker v-model="queryParams.dateRange" type="daterange" range-separator="—" start-placeholder="请选择购买时间" end-placeholder="" style="width: 240px" />
+    </div>
+    <!-- 筛选栏 -->
+    <div class="filter-bar">
+      <span class="filter-label">商品类别</span>
+      <el-select v-model="queryParams.category1" placeholder="请选择" style="width: 100px" clearable @change="handleCategory1Change">
+        <el-option label="电脑" value="电脑" />
+        <el-option label="办公设备" value="办公设备" />
+        <el-option label="家用电器" value="家用电器" />
+      </el-select>
+      <el-select v-model="queryParams.category2" placeholder="请选择" style="width: 100px" clearable @change="handleCategory2Change">
+        <el-option v-for="item in category2Options" :key="item" :label="item" :value="item" />
+      </el-select>
+      <el-select v-model="queryParams.category3" placeholder="请选择" style="width: 100px" clearable>
+        <el-option v-for="item in category3Options" :key="item" :label="item" :value="item" />
+      </el-select>
+      <span class="filter-label">商品品牌</span>
+      <el-select v-model="queryParams.brand" placeholder="请选择" style="width: 100px" clearable>
+        <el-option label="清华同方" value="清华同方" />
+        <el-option label="联想" value="联想" />
+        <el-option label="戴尔" value="戴尔" />
+      </el-select>
+    </div>
+    <!-- 排序栏 -->
+    <div class="sort-bar">
+      <el-select v-model="queryParams.sortType" placeholder="默认排序" style="width: 110px"><el-option label="默认排序" value="default" /><el-option label="购买时间" value="time" /></el-select>
+      <el-select v-model="queryParams.priceSort" placeholder="价格排序" style="width: 110px"><el-option label="价格排序" value="" /><el-option label="价格从低到高" value="asc" /><el-option label="价格从高到低" value="desc" /></el-select>
+    </div>
+    <!-- 订单列表 -->
+    <div class="order-list">
+      <div v-for="(order, orderIndex) in orderList" :key="orderIndex" class="order-card">
+        <div class="order-header">
+          <div class="order-info">
+            <span class="order-date">{{ order.date }}</span>
+            <span class="order-no">订单号:{{ order.orderNo }}</span>
+          </div>
+          <el-button type="danger" link @click="handleReorder(order)">一键复购</el-button>
+        </div>
+        <div class="product-list">
+          <div v-for="(item, itemIndex) in order.products" :key="itemIndex" class="product-item">
+            <div class="product-image">
+              <el-image :src="item.image" fit="contain">
+                <template #error><div class="image-placeholder"><el-icon :size="30" color="#ccc"><Picture /></el-icon></div></template>
+              </el-image>
+            </div>
+            <div class="product-info">
+              <div class="product-name">{{ item.name }}</div>
+              <div class="product-spec">{{ item.spec1 }}</div>
+              <div class="product-spec">{{ item.spec2 }}</div>
+            </div>
+            <div class="product-price">
+              <span class="price">¥{{ item.price }}</span>
+              <span class="quantity">x{{ item.quantity }}</span>
+            </div>
+            <div class="product-total" v-if="itemIndex === 0">
+              <span class="total-label">支付款</span>
+              <span class="total-price">¥{{ order.totalAmount }}</span>
+            </div>
+            <div class="product-action"><el-button type="danger" link @click="handleBuyAgain(item)">再次购买</el-button></div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <el-empty v-if="orderList.length === 0" description="暂无购买记录" />
+    <TablePagination v-if="orderList.length > 0" v-model:page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :total="total" @change="handleQuery" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { Search, Picture } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { PageTitle, TablePagination } from '@/components'
+
+const queryParams = reactive({ pageNum: 1, pageSize: 10, keyword: '', minPrice: '', maxPrice: '', dateRange: null, category1: '', category2: '', category3: '', brand: '', sortType: 'default', priceSort: '' })
+const total = ref(100)
+
+// 三级分类数据
+const categoryData: Record<string, Record<string, string[]>> = {
+  '电脑': {
+    '台式机': ['商用台式机', '家用台式机', '一体机'],
+    '笔记本': ['商务本', '游戏本', '轻薄本'],
+    '平板电脑': ['安卓平板', 'iPad', 'Windows平板']
+  },
+  '办公设备': {
+    '打印机': ['激光打印机', '喷墨打印机', '针式打印机'],
+    '复印机': ['黑白复印机', '彩色复印机'],
+    '投影仪': ['商务投影', '家用投影']
+  },
+  '家用电器': {
+    '空调': ['挂机', '柜机', '中央空调'],
+    '冰箱': ['双门冰箱', '多门冰箱', '对开门冰箱'],
+    '洗衣机': ['滚筒洗衣机', '波轮洗衣机']
+  }
+}
+
+const category2Options = computed(() => {
+  if (!queryParams.category1) return []
+  return Object.keys(categoryData[queryParams.category1] || {})
+})
+
+const category3Options = computed(() => {
+  if (!queryParams.category1 || !queryParams.category2) return []
+  return categoryData[queryParams.category1]?.[queryParams.category2] || []
+})
+
+const handleCategory1Change = () => {
+  queryParams.category2 = ''
+  queryParams.category3 = ''
+}
+
+const handleCategory2Change = () => {
+  queryParams.category3 = ''
+}
+
+const orderList = ref([
+  { date: '2025/12/05', orderNo: '489283929283298392', totalAmount: '181', products: [
+    { id: 1, name: '清华同方超越E500台式机电脑(i3-6100/4G/1T/19.5寸)', spec1: '规格02', spec2: '规格01', price: '181', quantity: 1, image: '' },
+    { id: 2, name: '清华同方超越E500台式机电脑(i3-6100/4G/1T/19.5寸)', spec1: '规格02', spec2: '规格01', price: '181', quantity: 1, image: '' }
+  ]},
+  { date: '2025/12/05', orderNo: '489283929283298393', totalAmount: '181', products: [
+    { id: 3, name: '清华同方超越E500台式机电脑(i3-6100/4G/1T/19.5寸)', spec1: '规格02', spec2: '规格01', price: '181', quantity: 1, image: '' }
+  ]}
+])
+
+const handleQuery = () => {}
+const handleReorder = (_order: any) => { ElMessage.success('已将订单商品加入购物车') }
+const handleBuyAgain = (_item: any) => { ElMessage.success('已加入购物车') }
+</script>
+
+<style scoped lang="scss">
+.search-bar { display: flex; align-items: center; gap: 15px; margin-bottom: 15px; .price-range { display: flex; align-items: center; gap: 5px; .range-separator { color: #999; } } }
+.filter-bar { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; .filter-label { font-size: 14px; color: #666; } }
+.sort-bar { display: flex; gap: 10px; margin-bottom: 20px; }
+.order-list { .order-card { border: 1px solid #eee; border-radius: 4px; margin-bottom: 15px; overflow: hidden; .order-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; background: #f9f9f9; border-bottom: 1px solid #eee; .order-info { display: flex; align-items: center; gap: 20px; .order-date { font-size: 14px; color: #333; font-weight: 500; } .order-no { font-size: 13px; color: #666; } } } .product-list { .product-item { display: flex; align-items: center; padding: 15px; border-bottom: 1px solid #f5f5f5; &:last-child { border-bottom: none; } .product-image { width: 80px; height: 80px; background: #f5f5f5; border-radius: 4px; overflow: hidden; flex-shrink: 0; .el-image { width: 100%; height: 100%; } .image-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; } } .product-info { flex: 1; padding: 0 20px; .product-name { font-size: 14px; color: #333; margin-bottom: 8px; line-height: 1.4; } .product-spec { font-size: 12px; color: #999; margin-bottom: 3px; } } .product-price { width: 100px; text-align: center; .price { display: block; font-size: 16px; font-weight: bold; color: #e60012; } .quantity { display: block; font-size: 12px; color: #999; margin-top: 5px; } } .product-total { width: 120px; text-align: center; .total-label { display: block; font-size: 12px; color: #999; } .total-price { display: block; font-size: 16px; font-weight: bold; color: #e60012; margin-top: 5px; } } .product-action { width: 80px; text-align: right; } } } } }
+</style>

+ 64 - 0
src/views/enterprise/purchasePlan/index.vue

@@ -0,0 +1,64 @@
+<template>
+  <div class="page-container">
+    <PageTitle title="专属采购方案" />
+    <!-- Tab切换 -->
+    <StatusTabs v-model="activeTab" :tabs="tabs" type="pill" />
+    <!-- 方案列表 -->
+    <div class="plan-grid">
+      <div v-for="(item, index) in planList" :key="index" class="plan-card">
+        <div class="plan-image">
+          <el-image :src="item.image" fit="cover">
+            <template #error><div class="image-placeholder"><el-icon :size="40"><Picture /></el-icon></div></template>
+          </el-image>
+        </div>
+        <div class="plan-info">
+          <div class="plan-name">{{ item.name }}</div>
+          <div class="plan-desc">{{ item.description }}</div>
+          <div class="plan-link" @click="handleDetail(item)">了解详情 <el-icon><ArrowRight /></el-icon></div>
+        </div>
+      </div>
+    </div>
+    <el-empty v-if="planList.length === 0" description="暂无采购方案" />
+    <!-- 分页 -->
+    <TablePagination v-if="planList.length > 0" v-model:page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :total="total" @change="handleQuery" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, watch } from 'vue'
+import { Picture, ArrowRight } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { PageTitle, StatusTabs, TablePagination } from '@/components'
+
+const activeTab = ref('purchase')
+const tabs = [{ key: 'purchase', label: '采购方案' }, { key: 'exclusive', label: '专属采购方案' }, { key: 'collection', label: '收藏的采购方案' }]
+const queryParams = reactive({ pageNum: 1, pageSize: 10 })
+const total = ref(500)
+const planList = ref<any[]>([])
+
+const loadData = () => {
+  if (activeTab.value === 'purchase') {
+    planList.value = [
+      { id: 1, name: '2025中秋福利 企业团购方案', description: '千款好礼·百大品牌·个性定制', image: '' },
+      { id: 2, name: '高效会议 一屏搞定', description: '视频会议大屏解决方案,送货上门,免费安装', image: '' },
+      { id: 3, name: '高效会议 一屏搞定', description: '视频会议大屏解决方案,送货上门,免费安装', image: '' }
+    ]
+    total.value = 500
+  } else if (activeTab.value === 'exclusive') {
+    planList.value = [{ id: 1, name: '专属定制方案A', description: '根据企业需求定制的专属采购方案', image: '' }]
+    total.value = 1
+  } else {
+    planList.value = [{ id: 1, name: '收藏的方案', description: '您收藏的采购方案', image: '' }]
+    total.value = 1
+  }
+}
+
+watch(activeTab, () => { queryParams.pageNum = 1; loadData() }, { immediate: true })
+const handleQuery = () => { loadData() }
+const handleDetail = (item: any) => { ElMessage.info('查看方案详情:' + item.name) }
+</script>
+
+<style scoped lang="scss">
+.plan-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
+.plan-card { border-radius: 8px; overflow: hidden; background: #fff; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); .plan-image { height: 180px; background: #f5f5f5; .el-image { width: 100%; height: 100%; } .image-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #ffe4c4 0%, #ffd4a3 100%); color: #e60012; } } .plan-info { padding: 15px; .plan-name { font-size: 15px; font-weight: 500; color: #333; margin-bottom: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .plan-desc { font-size: 13px; color: #999; margin-bottom: 10px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .plan-link { display: flex; align-items: center; gap: 5px; font-size: 13px; color: #e60012; cursor: pointer; &:hover { text-decoration: underline; } } } }
+</style>

+ 408 - 0
src/views/enterprise/securitySetting/changePhone.vue

@@ -0,0 +1,408 @@
+<template>
+  <div class="change-phone-container">
+    <!-- 面包屑导航 -->
+    <div class="breadcrumb">
+      <span class="breadcrumb-item" @click="$router.push('/')">首页</span>
+      <span class="separator">&gt;</span>
+      <span class="breadcrumb-item" @click="$router.push('/enterprise/companyInfo')">客户中心</span>
+      <span class="separator">&gt;</span>
+      <span class="breadcrumb-item" @click="$router.push('/enterprise/securitySetting')">安全设置</span>
+      <span class="separator">&gt;</span>
+      <span class="breadcrumb-item active">更换手机号码</span>
+    </div>
+
+    <!-- 主体内容 -->
+    <div class="change-content">
+      <div class="change-title">更换手机号码</div>
+
+      <!-- 步骤条 -->
+      <div class="steps-container">
+        <div class="step-item" :class="{ active: currentStep >= 1, done: currentStep > 1 }">
+          <div class="step-circle">
+            <el-icon v-if="currentStep > 1"><Check /></el-icon>
+            <span v-else>1</span>
+          </div>
+          <div class="step-label">验证身份</div>
+        </div>
+        <div class="step-line" :class="{ active: currentStep > 1 }"></div>
+        <div class="step-item" :class="{ active: currentStep >= 2, done: currentStep > 2 }">
+          <div class="step-circle">
+            <el-icon v-if="currentStep > 2"><Check /></el-icon>
+            <span v-else>2</span>
+          </div>
+          <div class="step-label">修改手机号码</div>
+        </div>
+        <div class="step-line" :class="{ active: currentStep > 2 }"></div>
+        <div class="step-item" :class="{ active: currentStep >= 3 }">
+          <div class="step-circle">
+            <span>3</span>
+          </div>
+          <div class="step-label">完成更换</div>
+        </div>
+      </div>
+
+      <!-- 步骤1:验证身份 -->
+      <div class="step-content" v-if="currentStep === 1">
+        <div class="phone-info">
+          <span>已绑定的手机:</span>
+          <span class="phone-number">180****7722</span>
+        </div>
+        <div class="phone-tip">若该手机号已无法使用请联系客服</div>
+
+        <el-form ref="step1FormRef" :model="step1Form" :rules="step1Rules" class="verify-form">
+          <el-form-item label="验证码:" prop="code">
+            <el-input v-model="step1Form.code" placeholder="短信验证码" class="code-input" />
+            <el-button link type="primary" class="send-code-btn" :disabled="countdown > 0" @click="handleSendCode">
+              {{ countdown > 0 ? `${countdown}s后重发` : '发送验证码' }}
+            </el-button>
+          </el-form-item>
+          <el-form-item class="verify-checkbox">
+            <el-checkbox v-model="step1Form.verified">点击验证</el-checkbox>
+          </el-form-item>
+        </el-form>
+
+        <div class="step-actions">
+          <el-button type="success" class="next-btn" @click="handleNextStep">下一步</el-button>
+        </div>
+
+        <!-- 温馨提醒 -->
+        <div class="tips-box">
+          <div class="tips-title">温馨提醒</div>
+          <ul class="tips-list">
+            <li>为保障您的帐号安全,变更重要信息需要身份验证</li>
+            <li>若有疑问请联系在线客服或拨打400-111-0027(周一至周日 8:00-18:00)</li>
+          </ul>
+        </div>
+      </div>
+
+      <!-- 步骤2:修改手机号码 -->
+      <div class="step-content" v-if="currentStep === 2">
+        <el-form ref="step2FormRef" :model="step2Form" :rules="step2Rules" label-width="120px" class="phone-form">
+          <el-form-item label="新手机号码:" prop="newPhone">
+            <el-input v-model="step2Form.newPhone" placeholder="请输入新手机号码" class="form-input" />
+          </el-form-item>
+          <el-form-item label="验证码:" prop="newCode">
+            <el-input v-model="step2Form.newCode" placeholder="短信验证码" class="code-input" />
+            <el-button link type="primary" class="send-code-btn" :disabled="newCountdown > 0" @click="handleSendNewCode">
+              {{ newCountdown > 0 ? `${newCountdown}s后重发` : '发送验证码' }}
+            </el-button>
+          </el-form-item>
+        </el-form>
+        <div class="step-actions">
+          <el-button @click="currentStep = 1">上一步</el-button>
+          <el-button type="success" class="next-btn" @click="handleSubmit">确认更换</el-button>
+        </div>
+      </div>
+
+      <!-- 步骤3:完成 -->
+      <div class="step-content" v-if="currentStep === 3">
+        <div class="success-content">
+          <el-icon class="success-icon" color="#52c41a" :size="60"><CircleCheckFilled /></el-icon>
+          <div class="success-title">手机号码更换成功!</div>
+          <div class="success-desc">您的安全手机已更换为 {{ step2Form.newPhone }}</div>
+          <el-button type="success" @click="$router.push('/enterprise/securitySetting')">返回安全设置</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { Check, CircleCheckFilled } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+const currentStep = ref(1)
+
+// 步骤1表单
+const step1FormRef = ref()
+const step1Form = reactive({
+  code: '',
+  verified: false
+})
+
+const countdown = ref(0)
+
+const step1Rules = {
+  code: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
+}
+
+// 步骤2表单
+const step2FormRef = ref()
+const step2Form = reactive({
+  newPhone: '',
+  newCode: ''
+})
+
+const newCountdown = ref(0)
+
+const step2Rules = {
+  newPhone: [
+    { required: true, message: '请输入新手机号码', trigger: 'blur' },
+    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
+  ],
+  newCode: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
+}
+
+// 发送验证码(旧手机)
+const handleSendCode = () => {
+  countdown.value = 60
+  const timer = setInterval(() => {
+    countdown.value--
+    if (countdown.value <= 0) {
+      clearInterval(timer)
+    }
+  }, 1000)
+  ElMessage.success('验证码已发送')
+}
+
+// 发送验证码(新手机)
+const handleSendNewCode = () => {
+  if (!step2Form.newPhone) {
+    ElMessage.warning('请先输入新手机号码')
+    return
+  }
+  newCountdown.value = 60
+  const timer = setInterval(() => {
+    newCountdown.value--
+    if (newCountdown.value <= 0) {
+      clearInterval(timer)
+    }
+  }, 1000)
+  ElMessage.success('验证码已发送')
+}
+
+// 下一步
+const handleNextStep = async () => {
+  const valid = await step1FormRef.value?.validate().catch(() => false)
+  if (!valid) return
+  
+  if (!step1Form.verified) {
+    ElMessage.warning('请先点击验证')
+    return
+  }
+  
+  currentStep.value = 2
+}
+
+// 提交
+const handleSubmit = async () => {
+  const valid = await step2FormRef.value?.validate().catch(() => false)
+  if (!valid) return
+  
+  // TODO: 调用更换手机接口
+  ElMessage.success('手机号码更换成功')
+  currentStep.value = 3
+}
+</script>
+
+<style scoped lang="scss">
+.change-phone-container {
+  background: #f5f5f5;
+  min-height: 100%;
+  padding: 0;
+}
+
+// 面包屑
+.breadcrumb {
+  padding: 15px 20px;
+  font-size: 13px;
+  color: #666;
+  background: #fff;
+  border-bottom: 1px solid #eee;
+  
+  .breadcrumb-item {
+    cursor: pointer;
+    &:hover { color: #e60012; }
+    &.active { color: #333; cursor: default; &:hover { color: #333; } }
+  }
+  .separator { margin: 0 8px; color: #999; }
+}
+
+// 主体内容
+.change-content {
+  max-width: 800px;
+  margin: 30px auto;
+  padding: 30px 40px;
+  background: #fff;
+  border-radius: 4px;
+}
+
+.change-title {
+  font-size: 18px;
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 30px;
+}
+
+// 步骤条
+.steps-container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 50px;
+  padding: 0 60px;
+}
+
+.step-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  
+  .step-circle {
+    width: 32px;
+    height: 32px;
+    border-radius: 50%;
+    border: 2px solid #ddd;
+    background: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 14px;
+    color: #999;
+    margin-bottom: 10px;
+  }
+  
+  .step-label {
+    font-size: 14px;
+    color: #999;
+  }
+  
+  &.active {
+    .step-circle {
+      border-color: #52c41a;
+      color: #52c41a;
+    }
+    .step-label { color: #52c41a; }
+  }
+  
+  &.done {
+    .step-circle {
+      border-color: #52c41a;
+      background: #52c41a;
+      color: #fff;
+    }
+    .step-label { color: #52c41a; }
+  }
+}
+
+.step-line {
+  flex: 1;
+  height: 2px;
+  background: #ddd;
+  margin: 0 20px;
+  margin-bottom: 30px;
+  
+  &.active { background: #52c41a; }
+}
+
+// 步骤内容
+.step-content {
+  padding: 0 40px;
+}
+
+.phone-info {
+  text-align: center;
+  font-size: 16px;
+  color: #333;
+  margin-bottom: 10px;
+  
+  .phone-number {
+    font-weight: 500;
+  }
+}
+
+.phone-tip {
+  text-align: center;
+  font-size: 13px;
+  color: #999;
+  margin-bottom: 30px;
+}
+
+// 表单
+.verify-form, .phone-form {
+  max-width: 400px;
+  margin: 0 auto;
+  
+  .code-input {
+    width: 180px;
+  }
+  
+  .form-input {
+    width: 280px;
+  }
+  
+  .send-code-btn {
+    margin-left: 15px;
+    color: #52c41a;
+  }
+  
+  .verify-checkbox {
+    :deep(.el-form-item__content) {
+      justify-content: center;
+    }
+  }
+}
+
+// 操作按钮
+.step-actions {
+  display: flex;
+  justify-content: center;
+  margin-top: 30px;
+  gap: 15px;
+  
+  .next-btn {
+    width: 200px;
+    height: 40px;
+  }
+}
+
+// 温馨提醒
+.tips-box {
+  margin-top: 40px;
+  padding: 20px;
+  background: #fffbe6;
+  border: 1px solid #ffe58f;
+  border-radius: 4px;
+  
+  .tips-title {
+    font-size: 14px;
+    font-weight: 500;
+    color: #d48806;
+    margin-bottom: 10px;
+  }
+  
+  .tips-list {
+    margin: 0;
+    padding-left: 20px;
+    font-size: 13px;
+    color: #666;
+    line-height: 1.8;
+  }
+}
+
+// 成功页面
+.success-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 40px 0;
+  
+  .success-icon {
+    margin-bottom: 20px;
+  }
+  
+  .success-title {
+    font-size: 20px;
+    font-weight: 500;
+    color: #333;
+    margin-bottom: 10px;
+  }
+  
+  .success-desc {
+    font-size: 14px;
+    color: #999;
+    margin-bottom: 30px;
+  }
+}
+</style>

+ 145 - 0
src/views/enterprise/securitySetting/index.vue

@@ -0,0 +1,145 @@
+<template>
+  <div class="security-setting-container">
+    <!-- 顶部返回 -->
+    <div class="page-header">
+      <el-button link @click="handleBack">
+        <el-icon><ArrowLeft /></el-icon>
+        <span>返回</span>
+      </el-button>
+      <span class="page-title">安全设置</span>
+    </div>
+
+    <div class="page-content">
+      <!-- 企业信息头部 -->
+      <div class="company-header">
+        <div class="company-logo">
+          <el-icon :size="30" color="#999"><OfficeBuilding /></el-icon>
+        </div>
+        <div class="company-info">
+          <div class="company-name">{{ companyData.companyName }}</div>
+          <el-tag type="danger" size="small">{{ companyData.role }}</el-tag>
+        </div>
+      </div>
+
+      <!-- 登录密码 -->
+      <div class="setting-card">
+        <div class="setting-icon">
+          <el-icon :size="20" color="#fff"><Grid /></el-icon>
+        </div>
+        <div class="setting-content">
+          <div class="setting-title">登录密码</div>
+          <div class="setting-desc">建议您定期更换密码,设置安全性高的密码可以使帐号更安全</div>
+        </div>
+        <el-button type="danger" @click="handleModifyPassword">修改</el-button>
+      </div>
+
+      <!-- 安全手机 -->
+      <div class="setting-card">
+        <div class="setting-icon">
+          <el-icon :size="20" color="#fff"><Grid /></el-icon>
+        </div>
+        <div class="setting-content">
+          <div class="setting-title">安全手机{{ securityData.phone }}</div>
+          <div class="setting-desc">安全手机可以用于登录帐号,重置密码或其他安全验证</div>
+        </div>
+        <el-button type="danger" @click="handleChangePhone">更换</el-button>
+      </div>
+    </div>
+
+    <!-- 修改密码弹窗 -->
+    <el-dialog v-model="passwordDialogVisible" title="修改登录密码" width="450px">
+      <el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules" label-width="100px">
+        <el-form-item label="原密码" prop="oldPassword">
+          <el-input v-model="passwordForm.oldPassword" type="password" show-password placeholder="请输入原密码" />
+        </el-form-item>
+        <el-form-item label="新密码" prop="newPassword">
+          <el-input v-model="passwordForm.newPassword" type="password" show-password placeholder="请输入新密码" />
+        </el-form-item>
+        <el-form-item label="确认密码" prop="confirmPassword">
+          <el-input v-model="passwordForm.confirmPassword" type="password" show-password placeholder="请再次输入新密码" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="passwordDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="handleSavePassword">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 修改手机弹窗 -->
+    <el-dialog v-model="phoneDialogVisible" title="修改安全手机" width="450px">
+      <el-form ref="phoneFormRef" :model="phoneForm" :rules="phoneRules" label-width="100px">
+        <el-form-item label="新手机号" prop="phone">
+          <el-input v-model="phoneForm.phone" placeholder="请输入新手机号" />
+        </el-form-item>
+        <el-form-item label="验证码" prop="code">
+          <div class="code-input">
+            <el-input v-model="phoneForm.code" placeholder="请输入验证码" />
+            <el-button type="danger" :disabled="countdown > 0" @click="handleSendCode">
+              {{ countdown > 0 ? `${countdown}s后重发` : '获取验证码' }}
+            </el-button>
+          </div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="phoneDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="handleSavePhone">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { useRouter } from 'vue-router'
+import { ArrowLeft, OfficeBuilding, Grid } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+const router = useRouter()
+
+const companyData = reactive({ companyName: '中国南方电网', role: '采购负责人' })
+const securityData = reactive({ phone: '180****7722' })
+
+const passwordDialogVisible = ref(false)
+const passwordFormRef = ref()
+const passwordForm = reactive({ oldPassword: '', newPassword: '', confirmPassword: '' })
+
+const validateConfirmPassword = (_rule: any, value: string, callback: any) => {
+  if (value !== passwordForm.newPassword) {
+    callback(new Error('两次输入的密码不一致'))
+  } else {
+    callback()
+  }
+}
+
+const passwordRules = {
+  oldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
+  newPassword: [{ required: true, message: '请输入新密码', trigger: 'blur' }, { min: 6, max: 20, message: '密码长度为6-20位', trigger: 'blur' }],
+  confirmPassword: [{ required: true, message: '请再次输入新密码', trigger: 'blur' }, { validator: validateConfirmPassword, trigger: 'blur' }]
+}
+
+const phoneDialogVisible = ref(false)
+const phoneFormRef = ref()
+const phoneForm = reactive({ phone: '', code: '' })
+const countdown = ref(0)
+
+const phoneRules = {
+  phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }, { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }],
+  code: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
+}
+
+const handleBack = () => { router.push('/enterprise/companyInfo') }
+const handleModifyPassword = () => { router.push('/enterprise/securitySetting/resetPassword') }
+const handleSavePassword = async () => { const valid = await passwordFormRef.value?.validate(); if (!valid) return; ElMessage.success('密码修改成功'); passwordDialogVisible.value = false }
+const handleChangePhone = () => { router.push('/enterprise/securitySetting/changePhone') }
+const handleSendCode = () => { if (!phoneForm.phone) { ElMessage.warning('请先输入手机号'); return }; countdown.value = 60; const timer = setInterval(() => { countdown.value--; if (countdown.value <= 0) clearInterval(timer) }, 1000); ElMessage.success('验证码已发送') }
+const handleSavePhone = async () => { const valid = await phoneFormRef.value?.validate(); if (!valid) return; ElMessage.success('手机号修改成功'); phoneDialogVisible.value = false }
+</script>
+
+<style scoped lang="scss">
+.security-setting-container { background: #f5f5f5; min-height: 100%; width: 1200px;margin: 0 auto;}
+.page-header { background: #fff; padding: 15px 20px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid #eee; .page-title { font-size: 16px; font-weight: bold; color: #333; } }
+.page-content { padding: 20px; }
+.company-header { display: flex; align-items: center; gap: 15px; padding: 20px; background: #fff; border-radius: 8px; margin-bottom: 15px; .company-logo { width: 50px; height: 50px; border-radius: 8px; background: #f5f5f5; display: flex; align-items: center; justify-content: center; border: 1px solid #eee; } .company-info { .company-name { font-size: 16px; font-weight: bold; color: #333; margin-bottom: 5px; } } }
+.setting-card { display: flex; align-items: center; gap: 15px; padding: 20px; background: #fff; border-radius: 8px; margin-bottom: 15px; .setting-icon { width: 40px; height: 40px; border-radius: 8px; background: #e60012; display: flex; align-items: center; justify-content: center; } .setting-content { flex: 1; .setting-title { font-size: 15px; font-weight: 500; color: #333; margin-bottom: 5px; } .setting-desc { font-size: 13px; color: #999; } } }
+.code-input { display: flex; gap: 10px; .el-input { flex: 1; } }
+</style>

+ 350 - 0
src/views/enterprise/securitySetting/resetPassword.vue

@@ -0,0 +1,350 @@
+<template>
+  <div class="reset-password-container">
+    <!-- 面包屑导航 -->
+    <div class="breadcrumb">
+      <span class="breadcrumb-item" @click="$router.push('/')">首页</span>
+      <span class="separator">&gt;</span>
+      <span class="breadcrumb-item" @click="$router.push('/enterprise/companyInfo')">客户中心</span>
+      <span class="separator">&gt;</span>
+      <span class="breadcrumb-item" @click="$router.push('/enterprise/securitySetting')">安全设置</span>
+      <span class="separator">&gt;</span>
+      <span class="breadcrumb-item active">重置密码</span>
+    </div>
+
+    <!-- 主体内容 -->
+    <div class="reset-content">
+      <div class="reset-title">重置密码</div>
+
+      <!-- 步骤条 -->
+      <div class="steps-container">
+        <div class="step-item" :class="{ active: currentStep >= 1, done: currentStep > 1 }">
+          <div class="step-circle">
+            <el-icon v-if="currentStep > 1"><Check /></el-icon>
+            <span v-else>1</span>
+          </div>
+          <div class="step-label">验证身份</div>
+        </div>
+        <div class="step-line" :class="{ active: currentStep > 1 }"></div>
+        <div class="step-item" :class="{ active: currentStep >= 2, done: currentStep > 2 }">
+          <div class="step-circle">
+            <el-icon v-if="currentStep > 2"><Check /></el-icon>
+            <span v-else>2</span>
+          </div>
+          <div class="step-label">设置新密码</div>
+        </div>
+        <div class="step-line" :class="{ active: currentStep > 2 }"></div>
+        <div class="step-item" :class="{ active: currentStep >= 3 }">
+          <div class="step-circle">
+            <span>3</span>
+          </div>
+          <div class="step-label">完成</div>
+        </div>
+      </div>
+
+      <!-- 步骤1:验证身份 -->
+      <div class="step-content" v-if="currentStep === 1">
+        <el-form ref="step1FormRef" :model="step1Form" :rules="step1Rules" label-width="100px" class="verify-form">
+          <el-form-item label="手机号码:" prop="phone">
+            <el-input v-model="step1Form.phone" disabled class="form-input" />
+            <span class="form-tip">若该手机号已无法使用请联系客服</span>
+          </el-form-item>
+          <el-form-item label="验证码:" prop="code">
+            <el-input v-model="step1Form.code" placeholder="短信验证码" class="form-input code-input" />
+            <el-button link type="primary" class="send-code-btn" :disabled="countdown > 0" @click="handleSendCode">
+              {{ countdown > 0 ? `${countdown}s后重发` : '发送验证码' }}
+            </el-button>
+          </el-form-item>
+          <el-form-item label="" class="verify-checkbox">
+            <el-checkbox v-model="step1Form.verified">点击验证</el-checkbox>
+          </el-form-item>
+        </el-form>
+        <div class="step-actions">
+          <el-button type="danger" class="next-btn" @click="handleNextStep">下一步</el-button>
+        </div>
+      </div>
+
+      <!-- 步骤2:设置新密码 -->
+      <div class="step-content" v-if="currentStep === 2">
+        <el-form ref="step2FormRef" :model="step2Form" :rules="step2Rules" label-width="100px" class="password-form">
+          <el-form-item label="新密码:" prop="newPassword">
+            <el-input v-model="step2Form.newPassword" type="password" show-password placeholder="请输入新密码" class="form-input" />
+          </el-form-item>
+          <el-form-item label="确认密码:" prop="confirmPassword">
+            <el-input v-model="step2Form.confirmPassword" type="password" show-password placeholder="请再次输入新密码" class="form-input" />
+          </el-form-item>
+        </el-form>
+        <div class="step-actions">
+          <el-button @click="currentStep = 1">上一步</el-button>
+          <el-button type="danger" class="next-btn" @click="handleSubmit">确认修改</el-button>
+        </div>
+      </div>
+
+      <!-- 步骤3:完成 -->
+      <div class="step-content" v-if="currentStep === 3">
+        <div class="success-content">
+          <el-icon class="success-icon" color="#52c41a" :size="60"><CircleCheckFilled /></el-icon>
+          <div class="success-title">密码重置成功!</div>
+          <div class="success-desc">您的登录密码已重置成功,请使用新密码登录</div>
+          <el-button type="danger" @click="$router.push('/enterprise/securitySetting')">返回安全设置</el-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { Check, CircleCheckFilled } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+const currentStep = ref(1)
+
+// 步骤1表单
+const step1FormRef = ref()
+const step1Form = reactive({
+  phone: '18062697722',
+  code: '',
+  verified: false
+})
+
+const countdown = ref(0)
+
+const step1Rules = {
+  code: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
+}
+
+// 步骤2表单
+const step2FormRef = ref()
+const step2Form = reactive({
+  newPassword: '',
+  confirmPassword: ''
+})
+
+const validateConfirmPassword = (_rule: any, value: string, callback: any) => {
+  if (value !== step2Form.newPassword) {
+    callback(new Error('两次输入的密码不一致'))
+  } else {
+    callback()
+  }
+}
+
+const step2Rules = {
+  newPassword: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    { min: 6, max: 20, message: '密码长度为6-20位', trigger: 'blur' }
+  ],
+  confirmPassword: [
+    { required: true, message: '请再次输入新密码', trigger: 'blur' },
+    { validator: validateConfirmPassword, trigger: 'blur' }
+  ]
+}
+
+// 发送验证码
+const handleSendCode = () => {
+  countdown.value = 60
+  const timer = setInterval(() => {
+    countdown.value--
+    if (countdown.value <= 0) {
+      clearInterval(timer)
+    }
+  }, 1000)
+  ElMessage.success('验证码已发送')
+}
+
+// 下一步
+const handleNextStep = async () => {
+  const valid = await step1FormRef.value?.validate().catch(() => false)
+  if (!valid) return
+  
+  if (!step1Form.verified) {
+    ElMessage.warning('请先点击验证')
+    return
+  }
+  
+  currentStep.value = 2
+}
+
+// 提交
+const handleSubmit = async () => {
+  const valid = await step2FormRef.value?.validate().catch(() => false)
+  if (!valid) return
+  
+  // TODO: 调用修改密码接口
+  ElMessage.success('密码重置成功')
+  currentStep.value = 3
+}
+</script>
+
+<style scoped lang="scss">
+.reset-password-container {
+  background: #f5f5f5;
+  min-height: 100%;
+  padding: 0;
+}
+
+// 面包屑
+.breadcrumb {
+  padding: 15px 20px;
+  font-size: 13px;
+  color: #666;
+  background: #fff;
+  border-bottom: 1px solid #eee;
+  
+  .breadcrumb-item {
+    cursor: pointer;
+    &:hover { color: #e60012; }
+    &.active { color: #333; cursor: default; &:hover { color: #333; } }
+  }
+  .separator { margin: 0 8px; color: #999; }
+}
+
+// 主体内容
+.reset-content {
+  max-width: 800px;
+  margin: 30px auto;
+  padding: 30px 40px;
+  background: #fff;
+  border-radius: 4px;
+}
+
+.reset-title {
+  font-size: 18px;
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 30px;
+}
+
+// 步骤条
+.steps-container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 50px;
+  padding: 0 60px;
+}
+
+.step-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  
+  .step-circle {
+    width: 32px;
+    height: 32px;
+    border-radius: 50%;
+    border: 2px solid #ddd;
+    background: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 14px;
+    color: #999;
+    margin-bottom: 10px;
+  }
+  
+  .step-label {
+    font-size: 14px;
+    color: #999;
+  }
+  
+  &.active {
+    .step-circle {
+      border-color: #e60012;
+      color: #e60012;
+    }
+    .step-label { color: #e60012; }
+  }
+  
+  &.done {
+    .step-circle {
+      border-color: #52c41a;
+      background: #52c41a;
+      color: #fff;
+    }
+    .step-label { color: #52c41a; }
+  }
+}
+
+.step-line {
+  flex: 1;
+  height: 2px;
+  background: #ddd;
+  margin: 0 20px;
+  margin-bottom: 30px;
+  
+  &.active { background: #52c41a; }
+}
+
+// 表单
+.step-content {
+  padding: 0 60px;
+}
+
+.verify-form, .password-form {
+  max-width: 500px;
+  margin: 0 auto;
+  
+  .form-input {
+    width: 280px;
+  }
+  
+  .code-input {
+    width: 180px;
+  }
+  
+  .form-tip {
+    margin-left: 15px;
+    font-size: 12px;
+    color: #e60012;
+    cursor: pointer;
+  }
+  
+  .send-code-btn {
+    margin-left: 15px;
+    color: #e60012;
+  }
+  
+  .verify-checkbox {
+    :deep(.el-form-item__content) {
+      margin-left: 100px !important;
+    }
+  }
+}
+
+// 操作按钮
+.step-actions {
+  display: flex;
+  justify-content: center;
+  margin-top: 40px;
+  
+  .next-btn {
+    width: 200px;
+    height: 40px;
+  }
+}
+
+// 成功页面
+.success-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 40px 0;
+  
+  .success-icon {
+    margin-bottom: 20px;
+  }
+  
+  .success-title {
+    font-size: 20px;
+    font-weight: 500;
+    color: #333;
+    margin-bottom: 10px;
+  }
+  
+  .success-desc {
+    font-size: 14px;
+    color: #999;
+    margin-bottom: 30px;
+  }
+}
+</style>

+ 183 - 0
src/views/organization/approvalFlow/create.vue

@@ -0,0 +1,183 @@
+<template>
+  <div class="create-flow-container">
+    <div class="page-header">
+      <el-button type="primary" link @click="handleBack"><el-icon><ArrowLeft /></el-icon>返回</el-button>
+      <span class="page-title">新建审批</span>
+    </div>
+
+    <div class="flow-tip">
+      <i class="title-bar"></i>
+      <span>最多可设置5级审批</span>
+    </div>
+
+    <div class="flow-form">
+      <div class="form-item">
+        <label>流程名称</label>
+        <el-input v-model="flowName" placeholder="请输入" style="width: 100%" />
+      </div>
+    </div>
+
+    <!-- 流程设计器 -->
+    <div class="flow-designer">
+      <!-- 流程发起节点 -->
+      <div class="flow-node start-node">
+        <div class="node-header">流程发起</div>
+        <div class="node-content">
+          <span>发起人:所有人</span>
+          <el-icon><ArrowRight /></el-icon>
+        </div>
+      </div>
+
+      <!-- 连接线 + 添加按钮 -->
+      <div class="flow-line">
+        <div class="line"></div>
+        <div class="add-btn" @click="handleAddNode"><el-icon><Plus /></el-icon></div>
+        <div class="line"></div>
+      </div>
+
+      <!-- 审批人节点 -->
+      <div v-for="(node, index) in approvalNodes" :key="index" class="flow-node approval-node">
+        <div class="node-header">
+          <span>审批人</span>
+          <div class="node-actions">
+            <el-icon @click="handleEditNode(index)"><Edit /></el-icon>
+            <el-icon @click="handleDeleteNode(index)"><Delete /></el-icon>
+          </div>
+        </div>
+        <div class="node-content">
+          <span>审批人:{{ node.approvers }}</span>
+          <el-icon><ArrowRight /></el-icon>
+        </div>
+
+        <!-- 连接线 -->
+        <div class="flow-line">
+          <div class="line"></div>
+          <div class="add-btn" @click="handleAddNodeAfter(index)"><el-icon><Plus /></el-icon></div>
+          <div class="line"></div>
+        </div>
+      </div>
+
+      <!-- 审批结束节点 -->
+      <div class="flow-node end-node">
+        <div class="node-btn">审批结束</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { ArrowLeft, ArrowRight, Plus, Edit, Delete } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+const router = useRouter()
+const flowName = ref('')
+
+const approvalNodes = ref([
+  { approvers: '周杰伦,杨幂,党群行政人事部...' }
+])
+
+const handleBack = () => {
+  router.back()
+}
+
+const handleAddNode = () => {
+  if (approvalNodes.value.length >= 5) {
+    ElMessage.warning('最多可设置5级审批')
+    return
+  }
+  approvalNodes.value.unshift({ approvers: '请选择审批人' })
+}
+
+const handleAddNodeAfter = (index: number) => {
+  if (approvalNodes.value.length >= 5) {
+    ElMessage.warning('最多可设置5级审批')
+    return
+  }
+  approvalNodes.value.splice(index + 1, 0, { approvers: '请选择审批人' })
+}
+
+const handleEditNode = (index: number) => {
+  ElMessage.info(`编辑第${index + 1}级审批人`)
+}
+
+const handleDeleteNode = (index: number) => {
+  approvalNodes.value.splice(index, 1)
+}
+</script>
+
+<style scoped lang="scss">
+.create-flow-container {
+  padding: 20px;
+  background: #fff;
+  min-height: 100%;
+}
+
+.page-header {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 20px;
+  .page-title { font-size: 16px; font-weight: bold; color: #333; }
+}
+
+.flow-tip {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 20px;
+  font-size: 14px;
+  color: #333;
+  .title-bar { display: inline-block; width: 3px; height: 14px; background: #e60012; border-radius: 2px; }
+}
+
+.flow-form {
+  margin-bottom: 30px;
+  .form-item {
+    label { display: block; font-size: 14px; color: #333; margin-bottom: 8px; }
+    :deep(.el-input__wrapper) { background: #f5f5f5; box-shadow: none; }
+  }
+}
+
+.flow-designer {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 20px 0;
+}
+
+.flow-node {
+  width: 280px;
+  border-radius: 4px;
+  overflow: hidden;
+
+  &.start-node {
+    .node-header { background: #ff6b6b; color: #fff; padding: 8px 15px; font-size: 13px; }
+    .node-content { background: #fff; border: 1px solid #eee; border-top: none; padding: 12px 15px; display: flex; justify-content: space-between; align-items: center; font-size: 13px; color: #666; cursor: pointer; }
+  }
+
+  &.approval-node {
+    .node-header { background: #409eff; color: #fff; padding: 8px 15px; font-size: 13px; display: flex; justify-content: space-between; align-items: center;
+      .node-actions { display: flex; gap: 10px; .el-icon { cursor: pointer; &:hover { opacity: 0.8; } } }
+    }
+    .node-content { background: #fff; border: 1px solid #eee; border-top: none; padding: 12px 15px; display: flex; justify-content: space-between; align-items: center; font-size: 13px; color: #666; cursor: pointer; }
+  }
+
+  &.end-node {
+    .node-btn { background: #909399; color: #fff; padding: 10px 40px; border-radius: 4px; font-size: 13px; text-align: center; }
+  }
+}
+
+.flow-line {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 10px 0;
+
+  .line { width: 1px; height: 20px; background: #ddd; }
+  .add-btn { width: 20px; height: 20px; border-radius: 50%; background: #409eff; color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 12px; margin: 5px 0;
+    &:hover { background: #66b1ff; }
+  }
+}
+</style>

+ 93 - 0
src/views/organization/approvalFlow/index.vue

@@ -0,0 +1,93 @@
+<template>
+  <div class="approval-flow-container">
+    <div class="page-header">
+      <PageTitle title="审批流程" />
+      <el-button type="danger" @click="handleAddFlow">新增流程</el-button>
+    </div>
+
+    <el-table :data="flowList" border>
+      <el-table-column prop="id" label="序号" width="100" align="center" />
+      <el-table-column prop="name" label="流程名称" min-width="200" />
+      <el-table-column prop="status" label="流程状态" min-width="150">
+        <template #default="{ row }">
+          <span :class="['status-text', row.status === '生效' ? 'active' : '']">{{ row.status }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="150" align="center">
+        <template #default="{ row }">
+          <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
+          <el-button type="danger" link size="small" @click="handleDisable(row)">禁用</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { PageTitle } from '@/components'
+
+const router = useRouter()
+
+const flowList = ref([
+  { id: 1, name: '物资采购审批', status: '生效' },
+  { id: 2, name: '流程名称', status: '生效' },
+  { id: 3, name: '测试流程', status: '生效' }
+])
+
+const handleAddFlow = () => {
+  router.push('/organization/approvalFlow/create')
+}
+
+const handleEdit = (row: any) => {
+  router.push(`/organization/approvalFlow/create?id=${row.id}`)
+}
+
+const handleDisable = (row: any) => {
+  ElMessageBox.confirm(`确定要禁用"${row.name}"吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    row.status = '禁用'
+    ElMessage.success('已禁用')
+  }).catch(() => {})
+}
+</script>
+
+<style scoped lang="scss">
+.approval-flow-container {
+  padding: 20px;
+  background: #fff;
+  min-height: 100%;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+
+  :deep(.page-title) {
+    margin-bottom: 0;
+  }
+}
+
+:deep(.el-table) {
+  th.el-table__cell {
+    background: #fafafa;
+    font-weight: 500;
+    color: #333;
+  }
+  .el-table__cell {
+    padding: 12px 0;
+  }
+}
+
+.status-text {
+  color: #999;
+  &.active { color: #67c23a; }
+}
+</style>

+ 159 - 0
src/views/organization/deptManage/index.vue

@@ -0,0 +1,159 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <PageTitle title="部门管理" />
+      <el-button type="danger" @click="handleAdd(null)">+ 新增部门</el-button>
+    </div>
+
+    <el-table :data="deptList" row-key="id" default-expand-all :tree-props="{ children: 'children' }" :indent="24">
+      <el-table-column prop="name" label="部门" min-width="250" />
+      <el-table-column prop="status" label="状态" width="100" align="center" />
+      <el-table-column prop="currentQuota" label="现有额度(年)" width="130" align="center" />
+      <el-table-column prop="usedQuota" label="已用额度(年)" width="130" align="center" />
+      <el-table-column prop="remainQuota" label="剩余额度(年)" width="130" align="center" />
+      <el-table-column label="操作" width="180" align="center">
+        <template #default="{ row }">
+          <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
+          <el-button type="danger" link size="small" @click="handleAdd(row)">新增</el-button>
+          <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 新增/编辑部门弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close>
+      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
+        <el-form-item label="上级部门">
+          <el-input :value="formData.parentName || '无(顶级部门)'" disabled />
+        </el-form-item>
+        <el-form-item label="部门名称" prop="name">
+          <el-input v-model="formData.name" placeholder="请输入部门名称" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="formData.status">
+            <el-radio label="启用">启用</el-radio>
+            <el-radio label="停用">停用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="年度额度">
+          <el-input-number v-model="formData.currentQuota" :min="0" :precision="2" controls-position="right" style="width: 200px" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="handleSubmit">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { PageTitle } from '@/components'
+
+const dialogVisible = ref(false)
+const formRef = ref()
+const editingRow = ref<any>(null)
+const parentRow = ref<any>(null)
+
+const formData = reactive({
+  name: '',
+  status: '启用',
+  currentQuota: 0,
+  parentName: ''
+})
+
+const formRules = {
+  name: [{ required: true, message: '请输入部门名称', trigger: 'blur' }]
+}
+
+const dialogTitle = computed(() => editingRow.value ? '编辑部门' : '新增部门')
+
+const deptList = ref([
+  {
+    id: 1, name: '技术部', status: '启用', currentQuota: '0.00', usedQuota: '0.00', remainQuota: '0.00',
+    children: [
+      { id: 11, name: '前端组', status: '启用', currentQuota: '0.00', usedQuota: '0.00', remainQuota: '0.00' },
+      { id: 12, name: '后端组', status: '启用', currentQuota: '0.00', usedQuota: '0.00', remainQuota: '0.00' }
+    ]
+  },
+  { id: 2, name: '财务部', status: '启用', currentQuota: '0.00', usedQuota: '0.00', remainQuota: '0.00' },
+  { id: 3, name: '设计部', status: '启用', currentQuota: '0.00', usedQuota: '0.00', remainQuota: '0.00' },
+  { id: 4, name: '财务部', status: '启用', currentQuota: '0.00', usedQuota: '0.00', remainQuota: '0.00' },
+  {
+    id: 5, name: '人事部...', status: '启用', currentQuota: '0.00', usedQuota: '0.00', remainQuota: '0.00',
+    children: [
+      { id: 51, name: '人事部01', status: '启用', currentQuota: '0.00', usedQuota: '0.00', remainQuota: '0.00' },
+      { id: 52, name: '人事部02', status: '启用', currentQuota: '0.00', usedQuota: '0.00', remainQuota: '0.00' }
+    ]
+  }
+])
+
+const handleAdd = (parent: any) => {
+  editingRow.value = null
+  parentRow.value = parent
+  formData.name = ''
+  formData.status = '启用'
+  formData.currentQuota = 0
+  formData.parentName = parent ? parent.name : ''
+  dialogVisible.value = true
+}
+
+const handleEdit = (row: any) => {
+  editingRow.value = row
+  parentRow.value = null
+  formData.name = row.name
+  formData.status = row.status
+  formData.currentQuota = parseFloat(row.currentQuota) || 0
+  formData.parentName = ''
+  dialogVisible.value = true
+}
+
+const handleDelete = (row: any) => {
+  ElMessageBox.confirm(`确定要删除部门"${row.name}"吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    ElMessage.success('删除成功')
+  })
+}
+
+const handleSubmit = async () => {
+  const valid = await formRef.value?.validate()
+  if (!valid) return
+  if (editingRow.value) {
+    ElMessage.success('编辑成功')
+  } else {
+    ElMessage.success('新增成功')
+  }
+  dialogVisible.value = false
+}
+</script>
+
+<style scoped lang="scss">
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  
+  :deep(.page-title) {
+    margin-bottom: 0;
+  }
+}
+
+:deep(.el-table) {
+  th.el-table__cell {
+    background: #fafafa;
+    font-weight: 500;
+    color: #333;
+  }
+  
+  .el-table__expand-icon {
+    color: #666;
+    font-size: 12px;
+  }
+}
+</style>

+ 190 - 0
src/views/organization/groupEnterprise/index.vue

@@ -0,0 +1,190 @@
+<template>
+  <div class="page-container">
+    <div class="page-header">
+      <PageTitle title="集团关联企业管理" />
+      <el-button type="danger" @click="handleAdd">+ 新建</el-button>
+    </div>
+
+    <el-table :data="enterpriseList" border>
+      <el-table-column label="序号" width="50" align="center">
+        <template #default="{ $index }">{{ $index + 1 }}</template>
+      </el-table-column>
+      <el-table-column prop="companyName" label="公司名称" min-width="180" show-overflow-tooltip />
+      <el-table-column prop="contact" label="联系人" width="70" align="center" />
+      <el-table-column prop="phone" label="电话" width="120" align="center" />
+      <el-table-column prop="monthLimit" label="月度采购限额" min-width="95" align="center" />
+      <el-table-column prop="yearLimit" label="年度采购限额" min-width="95" align="center" />
+      <el-table-column prop="creditLimit" label="平台信用额度" min-width="95" align="center" />
+      <el-table-column prop="remainLimit" label="实时剩余额度" min-width="95" align="center" />
+      <el-table-column prop="purchaseStatus" label="采购情况" width="70" align="center">
+        <template #default="{ row }">
+          <span>{{ row.purchaseStatus || '-' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="100" align="center">
+        <template #default="{ row }">
+          <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
+          <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px" destroy-on-close>
+      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
+        <el-form-item label="公司名称" prop="companyName">
+          <el-input v-model="formData.companyName" placeholder="请输入公司名称" />
+        </el-form-item>
+        <el-form-item label="联系人" prop="contact">
+          <el-input v-model="formData.contact" placeholder="请输入联系人" />
+        </el-form-item>
+        <el-form-item label="电话" prop="phone">
+          <el-input v-model="formData.phone" placeholder="请输入电话" />
+        </el-form-item>
+        <el-form-item label="月度采购限额">
+          <el-input-number v-model="formData.monthLimit" :min="0" :precision="2" controls-position="right" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="年度采购限额">
+          <el-input-number v-model="formData.yearLimit" :min="0" :precision="2" controls-position="right" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="平台信用额度">
+          <el-input-number v-model="formData.creditLimit" :min="0" :precision="2" controls-position="right" style="width: 100%" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="handleSubmit">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { PageTitle } from '@/components'
+
+const dialogVisible = ref(false)
+const formRef = ref()
+const editingRow = ref<any>(null)
+
+const formData = reactive({
+  companyName: '',
+  contact: '',
+  phone: '',
+  monthLimit: 0,
+  yearLimit: 0,
+  creditLimit: 0
+})
+
+const formRules = {
+  companyName: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
+  contact: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
+  phone: [{ required: true, message: '请输入电话', trigger: 'blur' }]
+}
+
+const dialogTitle = computed(() => editingRow.value ? '编辑关联企业' : '新建关联企业')
+
+const enterpriseList = ref([
+  { id: 1, companyName: '台州古德贸易有限公司', contact: '李婷', phone: '18062433912', monthLimit: '0.00', yearLimit: '0.00', creditLimit: '0.00', remainLimit: '0.00', purchaseStatus: '' },
+  { id: 2, companyName: '上海飞索半导体有限公司', contact: '孙思达', phone: '13442197054', monthLimit: '0.00', yearLimit: '0.00', creditLimit: '0.00', remainLimit: '0.00', purchaseStatus: '' },
+  { id: 3, companyName: '荆门格林沃德新材料有限公司', contact: '赵奕艳', phone: '15179893205', monthLimit: '0.00', yearLimit: '0.00', creditLimit: '0.00', remainLimit: '0.00', purchaseStatus: '' },
+  { id: 4, companyName: '大兴科里科技有限公司', contact: '王凡玄', phone: '15858669874', monthLimit: '0.00', yearLimit: '0.00', creditLimit: '0.00', remainLimit: '0.00', purchaseStatus: '' },
+  { id: 5, companyName: '滨州宏华国际贸易有限公司', contact: '周小艺', phone: '18402944521', monthLimit: '0.00', yearLimit: '0.00', creditLimit: '0.00', remainLimit: '0.00', purchaseStatus: '' },
+  { id: 6, companyName: '广西民锐建筑工程有限公司', contact: '李书萍', phone: '13905679687', monthLimit: '0.00', yearLimit: '0.00', creditLimit: '0.00', remainLimit: '0.00', purchaseStatus: '' },
+  { id: 7, companyName: '佳木斯晶雨轩商贸有限公司', contact: '吴彦漆', phone: '18928852431', monthLimit: '0.00', yearLimit: '0.00', creditLimit: '0.00', remainLimit: '0.00', purchaseStatus: '' },
+  { id: 8, companyName: '青海诺维信企业管理有限公司', contact: '周海', phone: '15558661691', monthLimit: '0.00', yearLimit: '0.00', creditLimit: '0.00', remainLimit: '0.00', purchaseStatus: '' },
+  { id: 9, companyName: '天津坤州港发物流有限公司', contact: '冯云', phone: '15108535283', monthLimit: '0.00', yearLimit: '0.00', creditLimit: '0.00', remainLimit: '0.00', purchaseStatus: '' },
+  { id: 10, companyName: '乌兰察布清门科技有限公司', contact: '郑盈', phone: '13782318088', monthLimit: '0.00', yearLimit: '0.00', creditLimit: '0.00', remainLimit: '0.00', purchaseStatus: '' }
+])
+
+const handleAdd = () => {
+  editingRow.value = null
+  formData.companyName = ''
+  formData.contact = ''
+  formData.phone = ''
+  formData.monthLimit = 0
+  formData.yearLimit = 0
+  formData.creditLimit = 0
+  dialogVisible.value = true
+}
+
+const handleEdit = (row: any) => {
+  editingRow.value = row
+  formData.companyName = row.companyName
+  formData.contact = row.contact
+  formData.phone = row.phone
+  formData.monthLimit = parseFloat(row.monthLimit) || 0
+  formData.yearLimit = parseFloat(row.yearLimit) || 0
+  formData.creditLimit = parseFloat(row.creditLimit) || 0
+  dialogVisible.value = true
+}
+
+const handleDelete = (row: any) => {
+  ElMessageBox.confirm(`确定要删除"${row.companyName}"吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    const index = enterpriseList.value.findIndex(item => item.id === row.id)
+    if (index > -1) {
+      enterpriseList.value.splice(index, 1)
+    }
+    ElMessage.success('删除成功')
+  })
+}
+
+const handleSubmit = async () => {
+  const valid = await formRef.value?.validate()
+  if (!valid) return
+  if (editingRow.value) {
+    Object.assign(editingRow.value, {
+      companyName: formData.companyName,
+      contact: formData.contact,
+      phone: formData.phone,
+      monthLimit: formData.monthLimit.toFixed(2),
+      yearLimit: formData.yearLimit.toFixed(2),
+      creditLimit: formData.creditLimit.toFixed(2)
+    })
+    ElMessage.success('编辑成功')
+  } else {
+    enterpriseList.value.push({
+      id: enterpriseList.value.length + 1,
+      companyName: formData.companyName,
+      contact: formData.contact,
+      phone: formData.phone,
+      monthLimit: formData.monthLimit.toFixed(2),
+      yearLimit: formData.yearLimit.toFixed(2),
+      creditLimit: formData.creditLimit.toFixed(2),
+      remainLimit: '0.00',
+      purchaseStatus: ''
+    })
+    ElMessage.success('新建成功')
+  }
+  dialogVisible.value = false
+}
+</script>
+
+<style scoped lang="scss">
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+
+  :deep(.page-title) {
+    margin-bottom: 0;
+  }
+}
+
+:deep(.el-table) {
+  th.el-table__cell {
+    background: #fafafa;
+    font-weight: 500;
+    color: #333;
+  }
+  .el-table__cell {
+    padding: 10px 0;
+  }
+}
+</style>

+ 15 - 0
src/views/organization/itemExpense/index.vue

@@ -0,0 +1,15 @@
+<template>
+  <div class="item-expense-container">
+    <PageTitle title="分项费用" />
+    <!-- 等待参考界面 -->
+    <el-empty description="待开发" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { PageTitle } from '@/components'
+</script>
+
+<style scoped lang="scss">
+.item-expense-container { padding: 20px; background: #fff; min-height: 100%; }
+</style>

+ 167 - 0
src/views/organization/personalInfo/index.vue

@@ -0,0 +1,167 @@
+<template>
+  <div class="personal-info-container">
+    <PageTitle title="个人信息" />
+    
+    <el-form ref="formRef" :model="formData" :rules="rules" label-position="top" class="info-form">
+      <div class="form-row">
+        <el-form-item label="用户ID" prop="userId">
+          <el-input v-model="formData.userId" placeholder="请输入" />
+        </el-form-item>
+        <el-form-item label="手机号码" prop="phone">
+          <el-input v-model="formData.phone" placeholder="请输入" />
+          <div class="form-tip">手机号码用于登录账号、重置密码或其他安全认证 <el-button type="danger" link>更换手机号</el-button></div>
+        </el-form-item>
+      </div>
+
+      <div class="form-row">
+        <el-form-item label="真实姓名" prop="realName">
+          <el-input v-model="formData.realName" placeholder="请输入" />
+        </el-form-item>
+        <el-form-item label="办公电话" prop="officePhone">
+          <el-input v-model="formData.officePhone" placeholder="请输入" />
+        </el-form-item>
+      </div>
+
+      <div class="form-row">
+        <el-form-item label="性别" prop="gender">
+          <el-select v-model="formData.gender" placeholder="请选择" style="width: 100%">
+            <el-option label="男" value="male" />
+            <el-option label="女" value="female" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="角色" prop="role">
+          <el-input v-model="formData.role" placeholder="请输入" />
+        </el-form-item>
+      </div>
+
+      <div class="form-row">
+        <el-form-item label="生日" prop="birthday">
+          <el-date-picker v-model="formData.birthday" type="date" placeholder="请选择日期" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="微信号" prop="wechat">
+          <el-input v-model="formData.wechat" placeholder="请输入" />
+        </el-form-item>
+      </div>
+
+      <div class="form-row">
+        <el-form-item label="邮箱" prop="email">
+          <el-input v-model="formData.email" placeholder="请输入" />
+        </el-form-item>
+        <el-form-item label="所属部门" prop="department">
+          <el-select v-model="formData.department" placeholder="请选择" style="width: 100%">
+            <el-option label="技术部" value="tech" />
+            <el-option label="销售部" value="sales" />
+            <el-option label="财务部" value="finance" />
+          </el-select>
+        </el-form-item>
+      </div>
+
+      <div class="form-row single">
+        <el-form-item label="所属部门" prop="subDepartment">
+          <el-select v-model="formData.subDepartment" placeholder="请选择" style="width: 100%">
+            <el-option label="研发组" value="dev" />
+            <el-option label="测试组" value="test" />
+          </el-select>
+        </el-form-item>
+      </div>
+
+      <div class="form-row single">
+        <el-form-item label="详细地址" prop="address">
+          <el-input v-model="formData.address" placeholder="请输入" maxlength="50" show-word-limit />
+        </el-form-item>
+      </div>
+
+      <div class="form-row single">
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入" maxlength="50" show-word-limit />
+        </el-form-item>
+      </div>
+
+      <div class="form-actions">
+        <el-button type="danger" @click="handleSave">保存</el-button>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import { ElMessage } from 'element-plus'
+import { PageTitle } from '@/components'
+
+const formRef = ref()
+
+const formData = reactive({
+  userId: '',
+  phone: '',
+  realName: '',
+  officePhone: '',
+  gender: '',
+  role: '',
+  birthday: '',
+  wechat: '',
+  email: '',
+  department: '',
+  subDepartment: '',
+  address: '',
+  remark: ''
+})
+
+const rules = {
+  realName: [{ required: true, message: '请输入真实姓名', trigger: 'blur' }],
+  phone: [{ required: true, message: '请输入手机号码', trigger: 'blur' }]
+}
+
+const handleSave = async () => {
+  const valid = await formRef.value?.validate()
+  if (!valid) return
+  ElMessage.success('保存成功')
+}
+</script>
+
+<style scoped lang="scss">
+.personal-info-container {
+  padding: 20px;
+  background: #fff;
+  min-height: 100%;
+}
+
+.info-form {
+  max-width: 900px;
+
+  .form-row {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    gap: 20px 40px;
+
+    &.single {
+      grid-template-columns: 1fr;
+    }
+  }
+
+  .form-tip {
+    font-size: 12px;
+    color: #999;
+    margin-top: 5px;
+  }
+
+  :deep(.el-form-item__label) {
+    font-size: 14px;
+    color: #333;
+    padding-bottom: 8px;
+  }
+
+  :deep(.el-input__wrapper),
+  :deep(.el-select__wrapper),
+  :deep(.el-textarea__inner) {
+    background: #f5f5f5;
+    box-shadow: none;
+    border: none;
+  }
+
+  .form-actions {
+    margin-top: 30px;
+    text-align: center;
+  }
+}
+</style>

+ 230 - 0
src/views/organization/roleManage/index.vue

@@ -0,0 +1,230 @@
+<template>
+  <div class="role-manage-container">
+    <PageTitle title="角色管理" />
+
+    <!-- 角色Tab + 新增按钮 -->
+    <div class="role-header">
+      <div class="role-tabs">
+        <div v-for="tab in roleTabs" :key="tab.key" :class="['tab-item', { active: activeRole === tab.key }]" @click="activeRole = tab.key">
+          {{ tab.label }}
+        </div>
+      </div>
+      <el-button type="danger" link @click="handleAddRole"><el-icon><Plus /></el-icon>新增角色</el-button>
+    </div>
+
+    <!-- 人员表格 -->
+    <el-table :data="staffList" border>
+      <el-table-column prop="name" label="姓名" min-width="120" align="center" />
+      <el-table-column prop="phone" label="手机号" min-width="150" align="center" />
+      <el-table-column prop="status" label="状态" min-width="100" align="center">
+        <template #default="{ row }">
+          <span :class="['status-text', row.status === '启用' ? 'active' : '']">{{ row.status }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" min-width="120" align="center">
+        <template #default="{ row }">
+          <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
+          <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 新增/编辑角色弹窗 -->
+    <el-dialog v-model="roleDialogVisible" :title="roleDialogTitle" width="500px" destroy-on-close>
+      <el-form ref="roleFormRef" :model="roleFormData" :rules="roleFormRules" label-width="100px">
+        <el-form-item label="角色名称" prop="name">
+          <el-input v-model="roleFormData.name" placeholder="请输入角色名称" />
+        </el-form-item>
+        <el-form-item label="角色描述">
+          <el-input v-model="roleFormData.description" type="textarea" :rows="3" placeholder="请输入角色描述" />
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-radio-group v-model="roleFormData.status">
+            <el-radio label="启用">启用</el-radio>
+            <el-radio label="停用">停用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="roleDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="handleRoleSubmit">确定</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 编辑人员弹窗 -->
+    <el-dialog v-model="staffDialogVisible" title="编辑人员" width="500px" destroy-on-close>
+      <el-form ref="staffFormRef" :model="staffFormData" :rules="staffFormRules" label-width="100px">
+        <el-form-item label="姓名" prop="name">
+          <el-input v-model="staffFormData.name" placeholder="请输入姓名" />
+        </el-form-item>
+        <el-form-item label="手机号" prop="phone">
+          <el-input v-model="staffFormData.phone" placeholder="请输入手机号" />
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-radio-group v-model="staffFormData.status">
+            <el-radio label="启用">启用</el-radio>
+            <el-radio label="停用">停用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="staffDialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="handleStaffSubmit">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { Plus } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { PageTitle } from '@/components'
+
+const activeRole = ref('role01')
+const roleDialogVisible = ref(false)
+const staffDialogVisible = ref(false)
+const roleFormRef = ref()
+const staffFormRef = ref()
+const editingRole = ref<any>(null)
+
+const roleFormData = reactive({
+  name: '',
+  description: '',
+  status: '启用'
+})
+
+const staffFormData = reactive({
+  name: '',
+  phone: '',
+  status: '启用'
+})
+
+const roleFormRules = {
+  name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }]
+}
+
+const staffFormRules = {
+  name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+  phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
+}
+
+const roleDialogTitle = computed(() => editingRole.value ? '编辑角色' : '新增角色')
+
+const roleTabs = ref([
+  { key: 'role01', label: '角色01' },
+  { key: 'role02', label: '角色02' },
+  { key: 'role03', label: '角色03' },
+  { key: 'role04', label: '角色04' },
+  { key: 'role05', label: '角色05' },
+  { key: 'role06', label: '角色06' }
+])
+
+const staffList = ref([
+  { id: 1, name: '郑婷雅', phone: '137 8651 5262', status: '启用' },
+  { id: 2, name: '冯艺莲', phone: '158 6084 7617', status: '启用' },
+  { id: 3, name: '吴彦琛', phone: '158 0598 5923', status: '启用' },
+  { id: 4, name: '吴彦漆', phone: '137 8231 8088', status: '启用' },
+  { id: 5, name: '周碰', phone: '180 6243 3980', status: '启用' },
+  { id: 6, name: '吴萱萱', phone: '131 1251 9348', status: '启用' },
+  { id: 7, name: '王凡玄', phone: '185 3264 9149', status: '启用' },
+  { id: 8, name: '冯启彬', phone: '184 5857 8572', status: '启用' },
+  { id: 9, name: '郑文锦', phone: '189 2860 9388', status: '启用' },
+  { id: 10, name: '赵吾光', phone: '181 0834 1643', status: '启用' }
+])
+
+const handleAddRole = () => {
+  editingRole.value = null
+  roleFormData.name = ''
+  roleFormData.description = ''
+  roleFormData.status = '启用'
+  roleDialogVisible.value = true
+}
+
+const handleRoleSubmit = async () => {
+  const valid = await roleFormRef.value?.validate()
+  if (!valid) return
+  if (editingRole.value) {
+    ElMessage.success('编辑成功')
+  } else {
+    // 添加新角色到tabs
+    const newKey = 'role' + (roleTabs.value.length + 1).toString().padStart(2, '0')
+    roleTabs.value.push({ key: newKey, label: roleFormData.name })
+    ElMessage.success('新增成功')
+  }
+  roleDialogVisible.value = false
+}
+
+const handleEdit = (row: any) => {
+  staffFormData.name = row.name
+  staffFormData.phone = row.phone
+  staffFormData.status = row.status
+  staffDialogVisible.value = true
+}
+
+const handleStaffSubmit = async () => {
+  const valid = await staffFormRef.value?.validate()
+  if (!valid) return
+  ElMessage.success('编辑成功')
+  staffDialogVisible.value = false
+}
+
+const handleDelete = (row: any) => {
+  ElMessageBox.confirm(`确定要删除"${row.name}"吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    ElMessage.success('删除成功')
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.role-manage-container {
+  padding: 20px;
+  background: #fff;
+  min-height: 100%;
+}
+
+.role-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.role-tabs {
+  display: flex;
+  gap: 10px;
+
+  .tab-item {
+    padding: 6px 16px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 13px;
+    color: #666;
+    background: #f5f5f5;
+    transition: all 0.2s;
+
+    &:hover { color: #e60012; }
+    &.active { background: #e60012; color: #fff; }
+  }
+}
+
+:deep(.el-table) {
+  th.el-table__cell {
+    background: #fafafa;
+    font-weight: 500;
+    color: #333;
+  }
+  .el-table__cell {
+    padding: 10px 0;
+  }
+}
+
+.status-text {
+  color: #999;
+  &.active { color: #67c23a; }
+}
+</style>

+ 276 - 0
src/views/organization/staffManage/index.vue

@@ -0,0 +1,276 @@
+<template>
+  <div class="page-container staff-manage">
+    <PageTitle title="人员管理" />
+    
+    <div class="main-content">
+      <!-- 左侧部门树 -->
+      <div class="dept-tree">
+        <el-tree
+          :data="deptTreeData"
+          :props="{ label: 'name', children: 'children' }"
+          node-key="id"
+          default-expand-all
+          highlight-current
+          @node-click="handleDeptClick"
+        >
+          <template #default="{ node, data }">
+            <span :class="['tree-node', { active: currentDeptId === data.id }]">{{ node.label }}</span>
+          </template>
+        </el-tree>
+      </div>
+
+      <!-- 右侧人员列表 -->
+      <div class="staff-list">
+        <!-- 操作栏 -->
+        <div class="action-bar">
+          <el-button type="danger" @click="handleAdd">+ 新增</el-button>
+          <el-button :disabled="selectedRows.length === 0" @click="handleBatchDelete">批量删除</el-button>
+        </div>
+
+        <!-- 表格 -->
+        <el-table :data="staffList" border @selection-change="handleSelectionChange">
+          <el-table-column type="selection" width="50" align="center" />
+          <el-table-column prop="name" label="姓名" min-width="100" align="center" />
+          <el-table-column prop="phone" label="手机号" min-width="140" align="center" />
+          <el-table-column prop="role" label="角色" min-width="120" align="center" />
+          <el-table-column prop="deptName" label="部门名称" min-width="120" align="center" />
+          <el-table-column prop="status" label="状态" min-width="80" align="center">
+            <template #default="{ row }">
+              <span :class="['status-text', row.status === '启用' ? 'active' : '']">{{ row.status }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="100" align="center">
+            <template #default="{ row }">
+              <el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
+              <el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <!-- 分页 -->
+        <TablePagination v-model:page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :total="total" @change="handleQuery" />
+      </div>
+    </div>
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close>
+      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
+        <el-form-item label="姓名" prop="name">
+          <el-input v-model="formData.name" placeholder="请输入姓名" />
+        </el-form-item>
+        <el-form-item label="手机号" prop="phone">
+          <el-input v-model="formData.phone" placeholder="请输入手机号" />
+        </el-form-item>
+        <el-form-item label="所属部门" prop="deptId">
+          <el-tree-select v-model="formData.deptId" :data="deptTreeData" :props="{ label: 'name', children: 'children' }" node-key="id" placeholder="请选择部门" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="角色" prop="role">
+          <el-select v-model="formData.role" placeholder="请选择角色" style="width: 100%">
+            <el-option label="采购负责人" value="采购负责人" />
+            <el-option label="普通员工" value="普通员工" />
+            <el-option label="部门主管" value="部门主管" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-radio-group v-model="formData.status">
+            <el-radio label="启用">启用</el-radio>
+            <el-radio label="停用">停用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="danger" @click="handleSubmit">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { PageTitle, TablePagination } from '@/components'
+
+const currentDeptId = ref<number | null>(null)
+const dialogVisible = ref(false)
+const formRef = ref()
+const editingRow = ref<any>(null)
+const selectedRows = ref<any[]>([])
+
+const queryParams = reactive({ pageNum: 1, pageSize: 10 })
+const total = ref(50)
+
+const formData = reactive({
+  name: '',
+  phone: '',
+  deptId: null as number | null,
+  role: '',
+  status: '启用'
+})
+
+const formRules = {
+  name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+  phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
+  deptId: [{ required: true, message: '请选择部门', trigger: 'change' }]
+}
+
+const dialogTitle = computed(() => editingRow.value ? '编辑人员' : '新增人员')
+
+// 部门树数据
+const deptTreeData = ref([
+  {
+    id: 1, name: '人事部',
+    children: [
+      { id: 11, name: '人事部01' },
+      { id: 12, name: '人事部02' }
+    ]
+  },
+  {
+    id: 2, name: '技术部',
+    children: [
+      { id: 21, name: '技术部01' },
+      { id: 22, name: '技术部02' }
+    ]
+  },
+  { id: 3, name: '财务部' }
+])
+
+// 人员列表数据
+const staffList = ref([
+  { id: 1, name: '郑婷婷', phone: '137 8651 5262', role: '采购负责人', deptName: '技术部', status: '启用' },
+  { id: 2, name: '冯艺莲', phone: '156 6084 7617', role: '采购负责人', deptName: '技术部', status: '启用' },
+  { id: 3, name: '吴彦琛', phone: '158 0598 5923', role: '采购负责人', deptName: '财务部', status: '启用' },
+  { id: 4, name: '吴彦漆', phone: '137 8231 8088', role: '采购负责人', deptName: '设计部', status: '启用' },
+  { id: 5, name: '周雄', phone: '180 6243 3980', role: '采购负责人', deptName: '财务部', status: '启用' },
+  { id: 6, name: '吴誉誉', phone: '131 1251 9348', role: '采购负责人', deptName: '人事部', status: '启用' },
+  { id: 7, name: '王凡玄', phone: '185 3264 9149', role: '采购负责人', deptName: '财务部', status: '启用' },
+  { id: 8, name: '冯启彬', phone: '184 5857 8572', role: '采购负责人', deptName: '设计部', status: '启用' },
+  { id: 9, name: '郑文锦', phone: '189 2860 9388', role: '采购负责人', deptName: '财务部', status: '启用' },
+  { id: 10, name: '赵吾光', phone: '181 0834 1643', role: '采购负责人', deptName: '人事部', status: '启用' }
+])
+
+const handleDeptClick = (data: any) => {
+  currentDeptId.value = data.id
+  handleQuery()
+}
+
+const handleQuery = () => {
+  // 根据选中的部门筛选人员
+}
+
+const handleSelectionChange = (rows: any[]) => {
+  selectedRows.value = rows
+}
+
+const handleAdd = () => {
+  editingRow.value = null
+  formData.name = ''
+  formData.phone = ''
+  formData.deptId = currentDeptId.value
+  formData.role = ''
+  formData.status = '启用'
+  dialogVisible.value = true
+}
+
+const handleEdit = (row: any) => {
+  editingRow.value = row
+  formData.name = row.name
+  formData.phone = row.phone
+  formData.deptId = null
+  formData.role = row.role
+  formData.status = row.status
+  dialogVisible.value = true
+}
+
+const handleBatchDelete = () => {
+  ElMessageBox.confirm(`确定要删除选中的 ${selectedRows.value.length} 名人员吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    ElMessage.success('删除成功')
+  })
+}
+
+const handleDelete = (row: any) => {
+  ElMessageBox.confirm(`确定要删除"${row.name}"吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    const index = staffList.value.findIndex(item => item.id === row.id)
+    if (index > -1) {
+      staffList.value.splice(index, 1)
+    }
+    ElMessage.success('删除成功')
+  })
+}
+
+const handleSubmit = async () => {
+  const valid = await formRef.value?.validate()
+  if (!valid) return
+  if (editingRow.value) {
+    ElMessage.success('编辑成功')
+  } else {
+    ElMessage.success('新增成功')
+  }
+  dialogVisible.value = false
+}
+</script>
+
+<style scoped lang="scss">
+.staff-manage {
+  .main-content {
+    display: flex;
+    gap: 20px;
+    min-height: 500px;
+  }
+
+  .dept-tree {
+    width: 200px;
+    flex-shrink: 0;
+    border-right: 1px solid #eee;
+    padding-right: 15px;
+
+    .tree-node {
+      font-size: 14px;
+      color: #333;
+      &.active { color: #e60012; }
+    }
+
+    :deep(.el-tree-node__content) {
+      height: 36px;
+    }
+
+    :deep(.el-tree-node.is-current > .el-tree-node__content) {
+      background-color: #fff5f5;
+      .tree-node { color: #e60012; }
+    }
+  }
+
+  .staff-list {
+    flex: 1;
+    min-width: 0;
+
+    .action-bar {
+      margin-bottom: 15px;
+      display: flex;
+      justify-content: flex-end;
+      gap: 10px;
+    }
+
+    .status-text {
+      color: #999;
+      &.active { color: #67c23a; }
+    }
+  }
+}
+
+:deep(.el-table) {
+  th.el-table__cell {
+    background: #fafafa;
+    font-weight: 500;
+    color: #333;
+  }
+}
+</style>

+ 112 - 0
src/views/reconciliation/billManage/index.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="page-container">
+    <PageTitle title="对账单管理" />
+    
+    <SearchBar :form="searchForm" :filters="filters" placeholder="对账编号" />
+
+    <el-table :data="tableData" border style="width: 100%">
+      <el-table-column type="index" label="序号" width="60" align="center" />
+      <el-table-column prop="billNo" label="对账编号" min-width="130" align="center" />
+      <el-table-column prop="billDate" label="对账日期" min-width="110" align="center" />
+      <el-table-column prop="amount" label="对账单金额" min-width="110" align="center">
+        <template #default="{ row }">¥{{ row.amount.toFixed(2) }}</template>
+      </el-table-column>
+      <el-table-column prop="billStatus" label="对账状态" min-width="90" align="center">
+        <template #default="{ row }">{{ row.billStatus === '1' ? '已对账' : '未对账' }}</template>
+      </el-table-column>
+      <el-table-column prop="invoiceStatus" label="开票状态" min-width="90" align="center">
+        <template #default="{ row }">
+          <span :style="{ color: row.invoiceStatus === '1' ? '#e60012' : '' }">
+            {{ row.invoiceStatus === '1' ? '已开票' : '-' }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="payStatus" label="支付状态" min-width="90" align="center">
+        <template #default="{ row }">{{ row.payStatus === '1' ? '已支付' : '-' }}</template>
+      </el-table-column>
+      <el-table-column label="操作" width="100" align="center">
+        <template #default="{ row }">
+          <el-button type="primary" link size="small" @click="handleView(row)">查看</el-button>
+          <el-button type="danger" link size="small" @click="handleConfirm(row)" :disabled="row.billStatus === '1'">确认</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <TablePagination v-model:page="pagination.page" v-model:pageSize="pagination.pageSize" :total="pagination.total" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { PageTitle, SearchBar, TablePagination } from '@/components'
+import { BILL_STATUS_OPTIONS, INVOICE_STATUS_OPTIONS, PAY_STATUS_OPTIONS } from '@/constants/status'
+
+const searchForm = reactive({
+  keyword: '',  // 对账编号
+  dateRange: [],
+  billStatus: '',
+  invoiceStatus: '',
+  payStatus: ''
+})
+
+const filters = [
+  { field: 'billStatus', label: '对账状态', options: BILL_STATUS_OPTIONS },
+  { field: 'invoiceStatus', label: '开票状态', options: INVOICE_STATUS_OPTIONS },
+  { field: 'payStatus', label: '支付状态', options: PAY_STATUS_OPTIONS }
+]
+
+const pagination = reactive({ page: 1, pageSize: 10, total: 100 })
+
+// 原始数据
+const rawData = ref([
+  { id: 1, billNo: '2323232323', billDate: '2025-11-22', amount: 2115.97, billStatus: '1', invoiceStatus: '1', payStatus: '1' },
+  { id: 2, billNo: '2323412123', billDate: '2025-12-09', amount: 4476.12, billStatus: '1', invoiceStatus: '1', payStatus: '1' },
+  { id: 3, billNo: '4566784543', billDate: '2025-11-21', amount: 7751.34, billStatus: '', invoiceStatus: '1', payStatus: '1' },
+  { id: 4, billNo: '2356565654', billDate: '2025-11-27', amount: 7936.37, billStatus: '1', invoiceStatus: '1', payStatus: '1' },
+  { id: 5, billNo: '2323412123', billDate: '2025-12-11', amount: 3931.45, billStatus: '', invoiceStatus: '1', payStatus: '1' },
+  { id: 6, billNo: '2323232323', billDate: '2025-11-27', amount: 6132.75, billStatus: '1', invoiceStatus: '1', payStatus: '1' },
+  { id: 7, billNo: '2356565654', billDate: '2025-11-25', amount: 3189.56, billStatus: '1', invoiceStatus: '1', payStatus: '1' },
+  { id: 8, billNo: '2323232323', billDate: '2025-11-20', amount: 993.66, billStatus: '', invoiceStatus: '1', payStatus: '1' },
+  { id: 9, billNo: '2323412123', billDate: '2025-11-17', amount: 2763.35, billStatus: '1', invoiceStatus: '1', payStatus: '1' },
+  { id: 10, billNo: '2323232323', billDate: '2025-12-05', amount: 9286.45, billStatus: '', invoiceStatus: '1', payStatus: '0' }
+])
+
+// 过滤后的数据
+const tableData = computed(() => {
+  return rawData.value.filter(item => {
+    // 按对账编号过滤
+    if (searchForm.keyword && !item.billNo.includes(searchForm.keyword)) {
+      return false
+    }
+    // 按对账状态过滤
+    if (searchForm.billStatus && item.billStatus !== searchForm.billStatus) {
+      return false
+    }
+    // 按开票状态过滤
+    if (searchForm.invoiceStatus && item.invoiceStatus !== searchForm.invoiceStatus) {
+      return false
+    }
+    // 按支付状态过滤
+    if (searchForm.payStatus && item.payStatus !== searchForm.payStatus) {
+      return false
+    }
+    return true
+  })
+})
+
+const handleView = (row: any) => {
+  ElMessage.info(`查看对账单:${row.billNo}`)
+}
+
+const handleConfirm = (row: any) => {
+  ElMessageBox.confirm(`确定要确认对账单"${row.billNo}"吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    row.billStatus = '1'
+    ElMessage.success('确认成功')
+  })
+}
+</script>

+ 346 - 0
src/views/reconciliation/invoiceManage/index.vue

@@ -0,0 +1,346 @@
+<template>
+  <div class="invoice-manage-container">
+    <PageTitle title="开票管理" />
+    
+    <!-- 搜索栏 -->
+    <div class="search-bar">
+      <el-input v-model="searchForm.keyword" placeholder="搜索" clearable style="width: 180px">
+        <template #prefix>
+          <el-icon><Search /></el-icon>
+        </template>
+      </el-input>
+      <div class="date-filter">
+        <span class="label">订单下单时间</span>
+        <el-date-picker
+          v-model="searchForm.orderDateRange"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          value-format="YYYY-MM-DD"
+          style="width: 240px"
+        />
+      </div>
+      <div class="date-filter">
+        <span class="label">订单完成时间</span>
+        <el-date-picker
+          v-model="searchForm.completeDateRange"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          value-format="YYYY-MM-DD"
+          style="width: 240px"
+        />
+      </div>
+    </div>
+
+    <!-- 操作按钮 -->
+    <div class="action-bar">
+      <el-button>订单导出</el-button>
+      <el-button>Excel批量导入开票</el-button>
+    </div>
+
+    <!-- 未开发票订单列表 -->
+    <div class="order-list-title">未开发票订单列表</div>
+    
+    <div class="order-list">
+      <div v-for="order in orderList" :key="order.id" class="order-card">
+        <div class="order-header">
+          <el-checkbox v-model="order.checked" />
+          <span class="order-time">{{ order.orderTime }}</span>
+          <span class="order-no">订单号:{{ order.orderNo }}</span>
+        </div>
+        <div class="order-content">
+          <el-checkbox v-model="order.checked" class="item-checkbox" />
+          <img :src="order.productImg" class="product-img" />
+          <div class="product-info">
+            <div class="product-name">{{ order.productName }}</div>
+            <div class="product-spec">
+              <span>{{ order.spec1 }}</span>
+              <span>{{ order.spec2 }}</span>
+              <span class="quantity">x{{ order.quantity }}</span>
+            </div>
+            <div class="product-price">¥{{ order.price }}</div>
+          </div>
+          <div class="pay-info">
+            <div class="pay-amount">实付款 <span class="amount">¥{{ order.payAmount }}</span></div>
+            <div class="pay-method">{{ order.payMethod }}</div>
+          </div>
+          <div class="order-status">
+            <span class="status-text">{{ order.status }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 底部操作栏 -->
+    <div class="bottom-bar">
+      <div class="select-all">
+        <el-checkbox v-model="selectAll" @change="handleSelectAll">全选</el-checkbox>
+      </div>
+      <div class="summary">
+        <span>已勾选 <em>{{ selectedCount }}/{{ orderList.length }}</em> 个订单</span>
+        <span class="total">共计 <em>¥{{ totalAmount.toFixed(2) }}</em></span>
+        <el-button type="danger" :disabled="selectedCount === 0">申请开票</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref, computed } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+import { PageTitle } from '@/components'
+import type { CheckboxValueType } from 'element-plus'
+
+// 搜索表单
+const searchForm = reactive({
+  keyword: '',
+  orderDateRange: [],
+  completeDateRange: []
+})
+
+// 全选
+const selectAll = ref(false)
+
+// 模拟订单数据
+const orderList = ref([
+  {
+    id: 1,
+    checked: true,
+    orderTime: '2025/12/05 16:15:06',
+    orderNo: '489283929283298392',
+    productImg: 'https://via.placeholder.com/80',
+    productName: '清华同方超越E500台式机电脑超越E500台式机电脑(i3-6100/4G/1T/19.5寸)',
+    spec1: '规格02',
+    spec2: '规格01',
+    quantity: 1,
+    price: 181,
+    payAmount: 181,
+    payMethod: '微信支付',
+    status: '已完成'
+  },
+  {
+    id: 2,
+    checked: false,
+    orderTime: '2025/12/05 16:15:06',
+    orderNo: '489283929283298392',
+    productImg: 'https://via.placeholder.com/80',
+    productName: '清华同方超越E500台式机电脑超越E500台式机电脑(i3-6100/4G/1T/19.5寸)',
+    spec1: '规格02',
+    spec2: '规格01',
+    quantity: 1,
+    price: 181,
+    payAmount: 181,
+    payMethod: '微信支付',
+    status: '已完成'
+  },
+  {
+    id: 3,
+    checked: false,
+    orderTime: '2025/12/05 16:15:06',
+    orderNo: '489283929283298392',
+    productImg: 'https://via.placeholder.com/80',
+    productName: '清华同方超越E500台式机电脑超越E500台式机电脑(i3-6100/4G/1T/19.5寸)',
+    spec1: '规格02',
+    spec2: '规格01',
+    quantity: 1,
+    price: 181,
+    payAmount: 181,
+    payMethod: '微信支付',
+    status: '已完成'
+  }
+])
+
+// 计算选中数量
+const selectedCount = computed(() => orderList.value.filter(item => item.checked).length)
+
+// 计算总金额
+const totalAmount = computed(() => {
+  return orderList.value.filter(item => item.checked).reduce((sum, item) => sum + item.payAmount, 0)
+})
+
+// 全选处理
+const handleSelectAll = (val: CheckboxValueType) => {
+  orderList.value.forEach(item => {
+    item.checked = !!val
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.invoice-manage-container {
+  padding: 20px;
+  background: #fff;
+  min-height: 100%;
+}
+
+.search-bar {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  margin-bottom: 16px;
+  
+  .date-filter {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    
+    .label {
+      color: #666;
+      font-size: 14px;
+      white-space: nowrap;
+    }
+  }
+}
+
+.action-bar {
+  margin-bottom: 20px;
+}
+
+.order-list-title {
+  font-size: 14px;
+  color: #333;
+  margin-bottom: 16px;
+}
+
+.order-list {
+  .order-card {
+    border: 1px solid #eee;
+    margin-bottom: 16px;
+    
+    .order-header {
+      display: flex;
+      align-items: center;
+      gap: 16px;
+      padding: 12px 16px;
+      background: #fafafa;
+      border-bottom: 1px solid #eee;
+      
+      .order-time {
+        color: #666;
+        font-size: 14px;
+      }
+      
+      .order-no {
+        color: #666;
+        font-size: 14px;
+      }
+    }
+    
+    .order-content {
+      display: flex;
+      align-items: center;
+      padding: 16px;
+      gap: 16px;
+      
+      .item-checkbox {
+        display: none;
+      }
+      
+      .product-img {
+        width: 80px;
+        height: 80px;
+        object-fit: cover;
+        border: 1px solid #eee;
+      }
+      
+      .product-info {
+        flex: 1;
+        
+        .product-name {
+          font-size: 14px;
+          color: #333;
+          margin-bottom: 8px;
+          line-height: 1.4;
+        }
+        
+        .product-spec {
+          font-size: 12px;
+          color: #999;
+          margin-bottom: 8px;
+          display: flex;
+          gap: 12px;
+          
+          .quantity {
+            color: #666;
+          }
+        }
+        
+        .product-price {
+          font-size: 14px;
+          color: #e60012;
+          font-weight: bold;
+        }
+      }
+      
+      .pay-info {
+        width: 120px;
+        text-align: center;
+        
+        .pay-amount {
+          font-size: 14px;
+          color: #333;
+          margin-bottom: 4px;
+          
+          .amount {
+            color: #e60012;
+            font-weight: bold;
+          }
+        }
+        
+        .pay-method {
+          font-size: 12px;
+          color: #999;
+        }
+      }
+      
+      .order-status {
+        width: 80px;
+        text-align: center;
+        
+        .status-text {
+          color: #e60012;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+
+.bottom-bar {
+  margin-top: 20px;
+  padding: 16px;
+  background: #fafafa;
+  border: 1px solid #eee;
+  border-radius: 4px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  
+  .select-all {
+    display: flex;
+    align-items: center;
+  }
+  
+  .summary {
+    display: flex;
+    align-items: center;
+    gap: 20px;
+    
+    span {
+      font-size: 14px;
+      color: #666;
+      
+      em {
+        color: #e60012;
+        font-style: normal;
+      }
+    }
+    
+    .total em {
+      font-size: 16px;
+      font-weight: bold;
+    }
+  }
+}
+</style>

+ 1 - 0
src/views/shop/cart.vue

@@ -145,6 +145,7 @@ const checked1 = ref(false);
   }
   .cart-head {
     margin-bottom: 20px;
+    padding-top: 20px;
     .head1 {
       font-weight: 600;
       font-size: 16px;

+ 1 - 0
src/views/shop/create.vue

@@ -187,6 +187,7 @@ const options = [
     .create-title {
       font-size: 16px;
       color: #101828;
+      padding-top: 16px;
     }
     .create-head {
       width: 1200px;

+ 0 - 1
src/views/shop/info.vue

@@ -175,7 +175,6 @@ const handleChange = (val: any) => {
 
   .shop-head {
     width: 1200px;
-    padding-top: 16px;
 
     .head-left {
       width: 106px;

Some files were not shown because too many files changed in this diff