Explorar o código

UI美化完成;按钮权限重做

Huanyi hai 3 semanas
pai
achega
d1e16ee1c9
Modificáronse 37 ficheiros con 2979 adicións e 2174 borrados
  1. 1 0
      src/api/system/menu/types.ts
  2. 3 0
      src/api/system/user/types.ts
  3. 230 0
      src/components/PermiSelect/TreeNode.vue
  4. 277 0
      src/components/PermiSelect/index.vue
  5. 10 0
      src/components/PermiSelect/types.ts
  6. 3 3
      src/layout/components/Navbar.vue
  7. 4 0
      src/types/components.d.ts
  8. 1 1
      src/views/archieves/customer/index.vue
  9. 1 1
      src/views/archieves/pet/index.vue
  10. 7 7
      src/views/fulfiller/anamaly/index.vue
  11. 3 3
      src/views/fulfiller/audit/index.vue
  12. 11 10
      src/views/fulfiller/level/index.vue
  13. 10 17
      src/views/fulfiller/pool/index.vue
  14. 3 3
      src/views/fulfiller/tag/index.vue
  15. 3 3
      src/views/login.vue
  16. 190 98
      src/views/monitor/logininfor/index.vue
  17. 214 120
      src/views/monitor/operlog/index.vue
  18. 2 2
      src/views/order/dispatch/components/OrderListPanel.vue
  19. 2 3
      src/views/order/dispatch/components/RiderListPanel.vue
  20. 95 23
      src/views/order/dispatch/index.vue
  21. 7 8
      src/views/order/orderList/index.vue
  22. 4 3
      src/views/order/purchase/index.vue
  23. 3 3
      src/views/register.vue
  24. 165 132
      src/views/service/list/index.vue
  25. 97 48
      src/views/system/areaStation/index.vue
  26. 6 6
      src/views/system/dict/index.vue
  27. 247 326
      src/views/system/menu/index.vue
  28. 239 294
      src/views/system/oss/config.vue
  29. 3 3
      src/views/system/role/authUser.vue
  30. 21 6
      src/views/system/role/index.vue
  31. 316 350
      src/views/system/store/index.vue
  32. 181 215
      src/views/system/tenant/index.vue
  33. 159 138
      src/views/system/tenantCategories/index.vue
  34. 7 7
      src/views/system/tenantPackage/index.vue
  35. 78 7
      src/views/system/user/index.vue
  36. 165 79
      src/views/systemConfig/index.vue
  37. 211 255
      src/views/systemConfig/sms/index.vue

+ 1 - 0
src/api/system/menu/types.ts

@@ -8,6 +8,7 @@ export interface MenuTreeOption {
   label: string;
   parentId: string | number;
   weight: number;
+  visible?: string;
   children?: MenuTreeOption[];
 }
 

+ 3 - 0
src/api/system/user/types.ts

@@ -49,6 +49,7 @@ export interface UserVO extends BaseEntity {
   postIds: any;
   roleId: any;
   admin: boolean;
+  areaStations?: (string | number)[];
 }
 
 /**
@@ -68,6 +69,7 @@ export interface UserForm {
   remark?: string;
   postIds: string[];
   roleIds: string[];
+  areaStations?: (string | number)[];
 }
 
 export interface UserInfoVO {
@@ -76,6 +78,7 @@ export interface UserInfoVO {
   roleIds: string[];
   posts: PostVO[];
   postIds: string[];
+  areaStations?: (string | number)[];
   roleGroup: string;
   postGroup: string;
 }

+ 230 - 0
src/components/PermiSelect/TreeNode.vue

@@ -0,0 +1,230 @@
+<template>
+  <div class="tree-node">
+    <div class="node-content" :class="{ disabled: isDisabled, 'is-expanded': isExpanded }" @click="handleExpand">
+      <!-- 展开/收起图标 -->
+      <span v-if="hasChildren" class="expand-icon" :class="{ 'is-expanded': isExpanded }">
+        <el-icon><ArrowRight /></el-icon>
+      </span>
+      <span v-else class="expand-placeholder"></span>
+
+      <!-- 自定义复选框 -->
+      <label class="checkbox-container" @click.stop>
+        <input 
+          type="checkbox" 
+          :checked="isChecked" 
+          :indeterminate="isIndeterminate" 
+          :disabled="isDisabled" 
+          @change="handleCheck" 
+        />
+        <span class="checkmark"></span>
+      </label>
+
+      <!-- 节点文案 -->
+      <span class="node-label">
+        <slot :node="node" :data="node">
+          <span>{{ node.name }}</span>
+        </slot>
+      </span>
+    </div>
+
+    <!-- 子节点区域 - 使用动画过场 -->
+    <el-collapse-transition>
+      <div v-if="isExpanded && hasChildren" class="children">
+        <tree-node
+          v-for="child in node.children"
+          :key="child.id"
+          :node="child"
+          :selected-keys="selectedKeys"
+          :indeterminate-keys="indeterminateKeys"
+          :disabled-keys="disabledKeys"
+          :expanded-keys="expandedKeys"
+          @check="handleChildCheck"
+          @expand="handleChildExpand"
+        >
+          <template #default="{ node: childNode, data }">
+            <slot :node="childNode" :data="data"></slot>
+          </template>
+        </tree-node>
+      </div>
+    </el-collapse-transition>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue';
+import type { TreeData } from './types.ts';
+import { ArrowRight } from '@element-plus/icons-vue';
+
+interface Props {
+  node: TreeData;
+  selectedKeys: (string | number)[];
+  indeterminateKeys: Set<string | number>;
+  disabledKeys: Set<string | number>;
+  expandedKeys: Set<string | number>;
+}
+
+interface Emits {
+  (e: 'check', node: TreeData, checked: boolean): void;
+  (e: 'expand', nodeId: string | number): void;
+}
+
+const props = defineProps<Props>();
+const emit = defineEmits<Emits>();
+
+// 检查节点是否被选中
+const isChecked = computed(() => {
+  return props.selectedKeys.includes(props.node.id);
+});
+
+// 检查节点是否为半选状态
+const isIndeterminate = computed(() => {
+  return props.indeterminateKeys.has(props.node.id);
+});
+
+// 检查节点是否被禁用
+const isDisabled = computed(() => {
+  return props.disabledKeys.has(props.node.id);
+});
+
+// 检查是否有子节点
+const hasChildren = computed(() => {
+  return props.node.children && props.node.children.length > 0;
+});
+
+// 检查是否展开
+const isExpanded = computed(() => {
+  return props.expandedKeys.has(props.node.id);
+});
+
+// 处理选中状态变化
+const handleCheck = (event: Event) => {
+  const target = event.target as HTMLInputElement;
+  emit('check', props.node, target.checked);
+};
+
+// 处理展开/折叠
+const handleExpand = () => {
+  if (hasChildren.value) {
+    emit('expand', props.node.id);
+  }
+};
+
+// 处理子节点事件转发
+const handleChildCheck = (node: TreeData, checked: boolean) => emit('check', node, checked);
+const handleChildExpand = (nodeId: number) => emit('expand', nodeId);
+</script>
+
+<style scoped lang="scss">
+.tree-node {
+  width: 100%;
+}
+
+.node-content {
+  display: flex;
+  align-items: center;
+  padding: 4px 8px;
+  cursor: pointer;
+  border-radius: 4px;
+  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+  margin-bottom: 1px;
+  
+  &:hover {
+    background-color: #f5f7fa;
+  }
+}
+
+.expand-icon {
+  width: 18px;
+  height: 18px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 4px;
+  color: #909399;
+  transition: transform 0.25s ease;
+  
+  &.is-expanded {
+    transform: rotate(90deg);
+  }
+  
+  .el-icon { font-size: 12px; }
+}
+
+.expand-placeholder {
+  width: 18px;
+  height: 18px;
+  margin-right: 4px;
+}
+
+.checkbox-container {
+  display: flex;
+  align-items: center;
+  margin-right: 8px;
+  position: relative;
+  
+  input {
+    position: absolute;
+    opacity: 0;
+    cursor: pointer;
+    height: 0;
+    width: 0;
+  }
+}
+
+.checkmark {
+  height: 16px;
+  width: 16px;
+  background-color: #fff;
+  border: 1.5px solid #dcdfe6;
+  border-radius: 3px;
+  transition: all 0.2s ease;
+  position: relative;
+  box-sizing: border-box;
+}
+
+.checkbox-container:hover input ~ .checkmark {
+  border-color: #409eff;
+}
+
+.checkbox-container input:checked ~ .checkmark,
+.checkbox-container input:indeterminate ~ .checkmark {
+  background-color: #409eff;
+  border-color: #409eff;
+}
+
+.checkbox-container input:checked ~ .checkmark::after {
+  content: '';
+  position: absolute;
+  left: 5px;
+  top: 1px;
+  width: 3px;
+  height: 7px;
+  border: solid white;
+  border-width: 0 2px 2px 0;
+  transform: rotate(45deg);
+}
+
+.checkbox-container input:indeterminate ~ .checkmark::after {
+  content: '';
+  position: absolute;
+  left: 3px;
+  top: 6px;
+  width: 8px;
+  height: 2px;
+  background-color: white;
+}
+
+.node-label {
+  font-size: 13px;
+  color: #303133;
+  flex: 1;
+  user-select: none;
+  font-weight: 400;
+}
+
+.children {
+  margin-left: 20px;
+  border-left: 1px dashed #ebeef5;
+  padding-left: 4px;
+}
+</style>

+ 277 - 0
src/components/PermiSelect/index.vue

@@ -0,0 +1,277 @@
+<template>
+  <div class="permi-select-container">
+    <div class="tree-header" v-if="$slots.header">
+      <slot name="header"></slot>
+    </div>
+    
+    <div class="tree-body">
+      <tree-node
+        v-for="node in data"
+        :key="node.id"
+        :node="node"
+        :selected-keys="computedSelectedKeys"
+        :indeterminate-keys="indeterminateKeys"
+        :disabled-keys="disabledKeys"
+        :expanded-keys="expandedKeys"
+        @check="handleCheck"
+        @expand="handleExpand"
+      >
+        <!-- 转发插槽 -->
+        <template #default="{ node: childNode, data: childData }">
+          <slot :node="childNode" :data="childData"></slot>
+        </template>
+      </tree-node>
+    </div>
+    
+    <div class="tree-footer" v-if="$slots.footer">
+      <slot name="footer"></slot>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, onMounted } from 'vue';
+import TreeNode from './TreeNode.vue';
+import type { TreeData, CheckedKeys } from './types.ts';
+
+interface Props {
+  data?: TreeData[];
+  selectedKeys?: (string | number)[];
+  disabled?: boolean;
+}
+
+interface Emits {
+  (e: 'update:selectedKeys', keys: (string | number)[]): void;
+  (e: 'check', keys: CheckedKeys): void;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  data: () => [],
+  selectedKeys: () => [],
+  disabled: false
+});
+
+const emit = defineEmits<Emits>();
+
+// 内部状态
+const innerSelectedKeys = ref<(string | number)[]>([]);
+const expandedKeys = ref<Set<string | number>>(new Set());
+const indeterminateKeys = ref<Set<string | number>>(new Set());
+
+/**
+ * 视觉上的级联选中键值(核心显示效果)
+ * 如果父节点被选中,其所有子孙节点在视觉上都显示为选中状态
+ */
+const computedSelectedKeys = computed(() => {
+  const allSelectedKeys = new Set(innerSelectedKeys.value);
+
+  const addDescendants = (nodes: TreeData[], parentSelected: boolean) => {
+    nodes.forEach((node) => {
+      // 如果父节点已被视觉选中,或者当前节点本身被选中的话
+      const isSelected = parentSelected || innerSelectedKeys.value.includes(node.id);
+
+      if (isSelected) {
+        allSelectedKeys.add(node.id);
+      }
+
+      if (node.children && node.children.length > 0) {
+        addDescendants(node.children, isSelected);
+      }
+    });
+  };
+
+  addDescendants(props.data, false);
+  return Array.from(allSelectedKeys);
+});
+
+// 计算禁用的键值
+const disabledKeys = computed(() => {
+  const keys = new Set<number>();
+  if (props.disabled) {
+    const collectKeys = (nodes: TreeData[]) => {
+      nodes.forEach((node) => {
+        keys.add(node.id);
+        if (node.children && node.children.length > 0) {
+          collectKeys(node.children);
+        }
+      });
+    };
+    collectKeys(props.data);
+  }
+  return keys;
+});
+
+/** 更新半选状态 */
+const updateIndeterminateState = () => {
+  const newIndeterminateKeys = new Set<number>();
+
+  const traverse = (nodes: TreeData[]) => {
+    nodes.forEach((node) => {
+      if (node.children && node.children.length > 0) {
+        traverse(node.children);
+
+        // 核心:如果子节点中有被选中的(不管是实际选中还是视觉选中)或者有半选的,则父节点为半选
+        // 注意:由于逻辑是选中父节点就移除子节点,所以这里需要检查视觉选中集合
+        const childVisibleKeys = node.children.map(c => c.id);
+        const hasCheckedChild = childVisibleKeys.some(id => innerSelectedKeys.value.includes(id) || newIndeterminateKeys.has(id));
+        
+        // 特殊逻辑:由于我们选中父节点会移除子节点,
+        // 只有当父节点未被选中,但子节点被选中时,父节点才显示半选
+        if (hasCheckedChild && !innerSelectedKeys.value.includes(node.id)) {
+          newIndeterminateKeys.add(node.id);
+        }
+      }
+    });
+  };
+
+  traverse(props.data);
+  indeterminateKeys.value = newIndeterminateKeys;
+};
+
+/** 获取所有后代节点 ID */
+const getAllDescendantIds = (node: TreeData): (string | number)[] => {
+  const ids: (string | number)[] = [];
+  const traverse = (children?: TreeData[]) => {
+    if (children) {
+      children.forEach((child) => {
+        ids.push(child.id);
+        traverse(child.children);
+      });
+    }
+  };
+  traverse(node.children);
+  return ids;
+};
+
+/** 查找节点路径(用于查找祖先) */
+const findNodePath = (nodes: TreeData[], targetId: string | number): TreeData[] | null => {
+  for (const node of nodes) {
+    if (node.id === targetId) return [node];
+    if (node.children) {
+      const childPath = findNodePath(node.children, targetId);
+      if (childPath) return [node, ...childPath];
+    }
+  }
+  return null;
+};
+
+/** 处理节点选中与取消 */
+const handleCheck = (node: TreeData, checked: boolean) => {
+  if (props.disabled || disabledKeys.value.has(node.id)) return;
+
+  let newSelectedKeys = [...innerSelectedKeys.value];
+  const keyIndex = newSelectedKeys.indexOf(node.id);
+
+  if (checked) {
+    if (keyIndex === -1) {
+      newSelectedKeys.push(node.id);
+    }
+
+    /** 核心逻辑:
+     * 1. 选中当前节点。
+     * 2. 移除所有后代,因为当前节点已经代表了整个分支。
+     * 3. 移除所有祖先,因为祖先如果被选中,当前节点的选中就是冗余的,
+     *    且如果是因为点击了子节点而取消祖先,表示更细粒度的控制。
+     */
+    const descendantIds = getAllDescendantIds(node);
+    newSelectedKeys = newSelectedKeys.filter((id) => !descendantIds.includes(id));
+
+    const path = findNodePath(props.data, node.id);
+    if (path) {
+      const ancestorIds = path.slice(0, -1).map((a) => a.id);
+      newSelectedKeys = newSelectedKeys.filter((id) => !ancestorIds.includes(id));
+    }
+  } else {
+    /** 核心逻辑:取消选中时
+     * 如果当前节点在选中列表中,直接移除即可。
+     */
+    if (keyIndex !== -1) {
+      newSelectedKeys.splice(keyIndex, 1);
+    }
+    
+    /** 进阶:如果点击的是一个视觉选中的节点(其祖先在列表中)
+     * 应该取消那个选中的祖先,并选中除该节点及其路径外的所有同级节点?
+     * 但根据用户提供的 snippet,简单的逻辑是:点击哪个选中哪个。
+     * 视觉选中效果主要用于反馈覆盖范围。
+     */
+  }
+
+  innerSelectedKeys.value = newSelectedKeys;
+  updateIndeterminateState();
+
+  emit('update:selectedKeys', newSelectedKeys);
+  emit('check', {
+    checked: newSelectedKeys,
+    indeterminate: Array.from(indeterminateKeys.value)
+  });
+};
+
+/** 处理展开/折叠 */
+const handleExpand = (nodeId: string | number) => {
+  const newExpandedKeys = new Set(expandedKeys.value);
+  if (newExpandedKeys.has(nodeId)) {
+    newExpandedKeys.delete(nodeId);
+  } else {
+    newExpandedKeys.add(nodeId);
+  }
+  expandedKeys.value = newExpandedKeys;
+};
+
+// 初始化与同步
+const initialize = () => {
+  innerSelectedKeys.value = [...props.selectedKeys];
+  updateIndeterminateState();
+  
+  // 默认展开全部
+  const expandAll = (nodes: TreeData[]) => {
+    nodes.forEach((node) => {
+      expandedKeys.value.add(node.id);
+      if (node.children) expandAll(node.children);
+    });
+  };
+  expandAll(props.data);
+};
+
+onMounted(initialize);
+watch(() => props.selectedKeys, (val) => {
+  innerSelectedKeys.value = [...val];
+  updateIndeterminateState();
+}, { deep: true });
+watch(() => props.data, updateIndeterminateState, { deep: true });
+
+</script>
+
+<style scoped lang="scss">
+.permi-select-container {
+  width: 100%;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  background-color: #fff;
+  overflow: hidden;
+}
+
+.tree-header {
+  padding: 6px 12px;
+  border-bottom: 1px solid #f0f2f5;
+  background-color: #fafbfc;
+}
+
+.tree-body {
+  padding: 4px;
+  max-height: 160px; /* 控制高度并显示滚动条 */
+  overflow-y: auto;
+  
+  /* 自定义滚动条 */
+  &::-webkit-scrollbar { width: 4px; }
+  &::-webkit-scrollbar-thumb {
+    background: #e4e7ed;
+    border-radius: 4px;
+  }
+}
+
+.tree-footer {
+  padding: 6px 12px;
+  border-top: 1px solid #f0f2f5;
+  background-color: #fafbfc;
+}
+</style>

+ 10 - 0
src/components/PermiSelect/types.ts

@@ -0,0 +1,10 @@
+export interface TreeData {
+  id: string | number;
+  name: string;
+  children?: TreeData[];
+}
+
+export interface CheckedKeys {
+  checked: (string | number)[];
+  indeterminate: (string | number)[];
+}

+ 3 - 3
src/layout/components/Navbar.vue

@@ -70,9 +70,9 @@
           </div>
           <template #dropdown>
             <el-dropdown-menu>
-              <router-link v-if="!dynamic" to="/user/profile">
-                <el-dropdown-item>{{ proxy.$t('navbar.personalCenter') }}</el-dropdown-item>
-              </router-link>
+<!--              <router-link v-if="!dynamic" to="/user/profile">-->
+<!--                <el-dropdown-item>{{ proxy.$t('navbar.personalCenter') }}</el-dropdown-item>-->
+<!--              </router-link>-->
 <!--              <el-dropdown-item v-if="settingsStore.showSettings" command="setLayout">-->
 <!--                <span>{{ proxy.$t('navbar.layoutSetting') }}</span>-->
 <!--              </el-dropdown-item>-->

+ 4 - 0
src/types/components.d.ts

@@ -14,6 +14,7 @@ declare module 'vue' {
     CustomerDetailDrawer: typeof import('./../components/CustomerDetailDrawer/index.vue')['default']
     DictTag: typeof import('./../components/DictTag/index.vue')['default']
     Editor: typeof import('./../components/Editor/index.vue')['default']
+    ElAlert: typeof import('element-plus/es')['ElAlert']
     ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElBadge: typeof import('element-plus/es')['ElBadge']
@@ -25,6 +26,7 @@ declare module 'vue' {
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
+    ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
     ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
@@ -88,6 +90,7 @@ declare module 'vue' {
     PageSelect: typeof import('./../components/PageSelect/index.vue')['default']
     Pagination: typeof import('./../components/Pagination/index.vue')['default']
     ParentView: typeof import('./../components/ParentView/index.vue')['default']
+    PermiSelect: typeof import('./../components/PermiSelect/index.vue')['default']
     PetDetailDrawer: typeof import('./../components/PetDetailDrawer/index.vue')['default']
     ProcessMeddle: typeof import('./../components/Process/processMeddle.vue')['default']
     RightToolbar: typeof import('./../components/RightToolbar/index.vue')['default']
@@ -101,6 +104,7 @@ declare module 'vue' {
     SubmitVerify: typeof import('./../components/Process/submitVerify.vue')['default']
     SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default']
     TopNav: typeof import('./../components/TopNav/index.vue')['default']
+    TreeNode: typeof import('./../components/PermiSelect/TreeNode.vue')['default']
     UserSelect: typeof import('./../components/UserSelect/index.vue')['default']
   }
   export interface GlobalDirectives {

+ 1 - 1
src/views/archieves/customer/index.vue

@@ -81,7 +81,7 @@
               </el-button>
               <template #dropdown>
                 <el-dropdown-menu>
-                  <el-dropdown-item command="remark" v-hasPermi="['archieves:customer:edit']">添加备注</el-dropdown-item>
+                  <el-dropdown-item command="remark" v-hasPermi="['archieves:customer:remark']">添加备注</el-dropdown-item>
                   <el-dropdown-item command="delete" style="color: #F56C6C" v-hasPermi="['archieves:customer:remove']">删除用户</el-dropdown-item>
                 </el-dropdown-menu>
               </template>

+ 1 - 1
src/views/archieves/pet/index.vue

@@ -55,7 +55,7 @@
           <template #default="scope">
             <el-button link type="primary" @click="handleDetail(scope.row)" v-hasPermi="['archieves:pet:query']">详情</el-button>
             <el-button link type="primary" @click="handleEdit(scope.row)" v-hasPermi="['archieves:pet:edit']">编辑</el-button>
-            <el-button link type="primary" @click="handleRemark(scope.row)" v-hasPermi="['archieves:pet:edit']">备注</el-button>
+            <el-button link type="primary" @click="handleRemark(scope.row)" v-hasPermi="['archieves:pet:remark']">备注</el-button>
             <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['archieves:pet:remove']">删除</el-button>
           </template>
         </el-table-column>

+ 7 - 7
src/views/fulfiller/anamaly/index.vue

@@ -18,7 +18,7 @@
             </el-select>
             <el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
             <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-            <el-button type="primary" icon="Plus" @click="handleAdd">新增上报</el-button>
+            <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['fulfiller:anamaly:add']">新增上报</el-button>
           </div>
         </div>
       </template>
@@ -73,13 +73,13 @@
         <el-table-column label="操作" width="200" fixed="right" align="center">
           <template #default="scope">
             <template v-if="scope.row.status === 0">
-              <el-button link type="primary" size="small" @click="handleOpenDrawer(scope.row, 'audit')">审核</el-button>
-              <el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
-              <el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
+              <el-button link type="primary" size="small" @click="handleOpenDrawer(scope.row, 'audit')" v-hasPermi="['fulfiller:anamaly:audit']">审核</el-button>
+              <el-button link type="primary" size="small" @click="handleEdit(scope.row)" v-hasPermi="['fulfiller:anamaly:edit']">编辑</el-button>
+              <el-button link type="danger" size="small" @click="handleDelete(scope.row)" v-hasPermi="['fulfiller:anamaly:remove']">删除</el-button>
             </template>
             <template v-else>
-              <el-button link type="primary" size="small" @click="handleOpenDrawer(scope.row, 'view')">详情</el-button>
-              <el-button link type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
+              <el-button link type="primary" size="small" @click="handleOpenDrawer(scope.row, 'view')" v-hasPermi="['fulfiller:anamaly:query']>详情</el-button>
+              <el-button link type="danger" size="small" @click="handleDelete(scope.row)" v-hasPermi="['fulfiller:anamaly:remove']">删除</el-button>
             </template>
           </template>
         </el-table-column>
@@ -150,7 +150,7 @@
           </el-form>
           <div class="drawer-footer-actions">
             <el-button @click="drawerVisible = false">取消</el-button>
-            <el-button type="primary" @click="submitAudit">提交审核</el-button>
+            <el-button type="primary" @click="submitAudit" v-hasPermi="['fulfiller:anamaly:audit']">提交审核</el-button>
           </div>
         </div>
 

+ 3 - 3
src/views/fulfiller/audit/index.vue

@@ -88,7 +88,7 @@
         <el-table-column label="操作" width="150" fixed="right">
           <template #default="scope">
             <div class="op-cell">
-              <el-button v-if="scope.row.status === 0" link type="primary" size="small" @click="handleDetail(scope.row)" v-hasPermi="['fulfiller:audit:approve', 'fulfiller:audit:reject']">审核</el-button>
+              <el-button v-if="scope.row.status === 0" link type="primary" size="small" @click="handleDetail(scope.row)" v-hasPermi="['fulfiller:audit:audit']">审核</el-button>
               <el-button link type="primary" size="small" @click="handleDetail(scope.row)" v-hasPermi="['fulfiller:audit:query']">详情</el-button>
             </div>
           </template>
@@ -199,8 +199,8 @@
       </div>
       <template #footer>
         <span class="dialog-footer" v-if="currentItem?.status === 0">
-          <el-button type="danger" plain @click="rejectDialogVisible = true" v-hasPermi="['fulfiller:audit:reject']">驳回申请</el-button>
-          <el-button type="primary" @click="handlePass" v-hasPermi="['fulfiller:audit:approve']">通过审核</el-button>
+          <el-button type="danger" plain @click="rejectDialogVisible = true" v-hasPermi="['fulfiller:audit:audit']">驳回申请</el-button>
+          <el-button type="primary" @click="handlePass" v-hasPermi="['fulfiller:audit:audit']">通过审核</el-button>
         </span>
         <span class="dialog-footer" v-else>
           <el-button @click="dialogVisible = false">关闭</el-button>

+ 11 - 10
src/views/fulfiller/level/index.vue

@@ -1,12 +1,12 @@
 <template>
   <div class="page-container">
     <el-tabs v-model="activeTab" type="card" class="level-tabs">
-      <el-tab-pane label="等级配置" name="levels">
+      <el-tab-pane label="等级配置" name="levels" v-if="checkPermi(['fulfiller:levelConfig:list'])">
         <el-card shadow="never" class="content-card">
           <template v-slot:header>
             <div class="card-header">
               <span class="title">等级体系列表</span>
-              <el-button type="primary" icon="Plus" @click="handleEditLevel(null)">新增等级</el-button>
+              <el-button type="primary" icon="Plus" @click="handleEditLevel(null)" v-hasPermi="['fulfiller:levelConfig:add']">新增等级</el-button>
             </div>
           </template>
 
@@ -43,20 +43,20 @@
             </el-table-column>
             <el-table-column label="操作" width="180">
               <template #default="{ row }">
-                <el-button link type="primary" @click="handleEditLevel(row)">配置</el-button>
-                <el-button link type="danger" @click="handleDeleteLevel(row)">删除</el-button>
+                <el-button link type="primary" @click="handleEditLevel(row)" v-hasPermi="['fulfiller:levelConfig:config']">配置</el-button>
+                <el-button link type="danger" @click="handleDeleteLevel(row)" v-hasPermi="['fulfiller:levelConfig:remove']">删除</el-button>
               </template>
             </el-table-column>
           </el-table>
         </el-card>
       </el-tab-pane>
 
-      <el-tab-pane label="权益库管理" name="rights">
+      <el-tab-pane label="权益库管理" name="rights" v-if="checkPermi(['fulfiller:levelRights:list'])">
         <el-card shadow="never" class="content-card">
           <template v-slot:header>
             <div class="card-header">
               <span class="title">等级权益库</span>
-              <el-button type="primary" icon="Plus" @click="handleEditRight(null)">新增权益</el-button>
+              <el-button type="primary" icon="Plus" @click="handleEditRight(null)" v-hasPermi="['fulfiller:levelRights:add']">新增权益</el-button>
             </div>
           </template>
           <el-table :data="rightsList" style="width: 100%">
@@ -72,14 +72,14 @@
             </el-table-column>
             <el-table-column prop="status" label="状态" width="100">
               <template #default="{ row }">
-                <el-switch v-model="row.status" size="small" @change="handleRightStatusChange(row)" />
+                <el-switch v-model="row.status" size="small" @change="handleRightStatusChange(row)" v-hasPermi="['fulfiller:levelRights:edit']" />
               </template>
             </el-table-column>
             <el-table-column prop="statement" label="权益说明" show-overflow-tooltip />
             <el-table-column label="操作" width="150">
               <template #default="{ row }">
-                <el-button link type="primary" @click="handleEditRight(row)">编辑</el-button>
-                <el-button link type="danger" @click="handleDeleteRight(row)">删除</el-button>
+                <el-button link type="primary" @click="handleEditRight(row)" v-hasPermi="['fulfiller:levelRights:edit']">编辑</el-button>
+                <el-button link type="danger" @click="handleDeleteRight(row)" v-hasPermi="['fulfiller:levelRights:remove']">删除</el-button>
               </template>
             </el-table-column>
           </el-table>
@@ -202,13 +202,14 @@
 <script setup lang="ts">
 import { ref, reactive, computed, onMounted } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
+import { checkPermi } from '@/utils/permission';
 import { listAllLevelRights, addLevelRights, updateLevelRights, delLevelRights, changeLevelRightsStatus } from '@/api/fulfiller/levelRights';
 import { listAllLevelConfig, addLevelConfig, updateLevelConfig, delLevelConfig } from '@/api/fulfiller/levelConfig';
 import { FlfLevelConfigVO, FlfLevelConfigForm } from '@/api/fulfiller/levelConfig/types';
 import { FlfLevelRightsForm, FlfLevelRightsVO } from '@/api/fulfiller/levelRights/types';
 import ImageUpload from '@/components/ImageUpload/index.vue';
 
-const activeTab = ref('levels');
+const activeTab = ref(checkPermi(['fulfiller:levelConfig:list']) ? 'levels' : 'rights');
 
 // Data: Rights
 // Data: Rights

+ 10 - 17
src/views/fulfiller/pool/index.vue

@@ -8,8 +8,7 @@
             <el-tag type="info" effect="plain" style="margin-left: 10px;">共 {{ total }} 人</el-tag>
           </div>
           <div class="right-panel">
-            <el-button type="primary" icon="Plus" style="margin-right: 15px" @click="handleCreate"
-              v-hasPermi="['fulfiller:pool:add']">新增履约者</el-button>
+            <el-button type="primary" icon="Plus" style="margin-right: 15px" @click="handleCreate" v-hasPermi="['fulfiller:pool:add']">新增履约者</el-button>
             <el-input v-model="searchKey" placeholder="搜索姓名/手机号/身份证" class="search-input" prefix-icon="Search" clearable
               @keyup.enter="handleSearch" @clear="handleSearch" />
             <el-cascader v-model="filterCascaderValue" :options="cityCascaderOptions" :props="{ checkStrictly: true }"
@@ -133,26 +132,20 @@
         <el-table-column label="操作" width="240" fixed="right">
           <template #default="scope">
             <div class="op-cell">
-              <el-button link type="primary" size="small" @click="handleDetail(scope.row)"
-                v-hasPermi="['fulfiller:pool:query']">详情</el-button>
-              <el-button link type="primary" size="small" @click="handleEdit(scope.row)"
-                v-hasPermi="['fulfiller:pool:edit']">编辑</el-button>
-              <el-button link type="warning" size="small" @click="handleReward(scope.row)"
-                v-hasPermi="['fulfiller:pool:edit']">奖惩</el-button>
+              <el-button link type="primary" size="small" @click="handleDetail(scope.row)" v-hasPermi="['fulfiller:pool:query']">详情</el-button>
+              <el-button link type="primary" size="small" @click="handleEdit(scope.row)" v-hasPermi="['fulfiller:pool:edit']">编辑</el-button>
+              <el-button link type="warning" size="small" @click="handleReward(scope.row)" v-hasPermi="['fulfiller:pool:reward']">奖惩</el-button>
               <el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, scope.row)">
                 <el-button link type="primary">更多<el-icon class="el-icon--right"><arrow-down /></el-icon></el-button>
                 <template #dropdown>
                   <el-dropdown-menu>
-                    <el-dropdown-item command="adjustPoints"
-                      v-hasPermi="['fulfiller:pool:edit']">修改积分</el-dropdown-item>
-                    <el-dropdown-item command="adjustBalance"
-                      v-hasPermi="['fulfiller:pool:edit']">余额增减</el-dropdown-item>
+                    <el-dropdown-item command="adjustPoints" v-hasPermi="['fulfiller:pool:editScore']">修改积分</el-dropdown-item>
+                    <el-dropdown-item command="adjustBalance" v-hasPermi="['fulfiller:pool:editBalance']">余额增减</el-dropdown-item>
                     <el-dropdown-item v-if="scope.row.status !== 'disabled'" command="disable" divided
-                      style="color: #f56c6c" v-hasPermi="['fulfiller:pool:edit']">禁用账号</el-dropdown-item>
-                    <el-dropdown-item v-else command="enable" divided style="color: #67c23a"
-                      v-hasPermi="['fulfiller:pool:edit']">启用账号</el-dropdown-item>
-                    <el-dropdown-item command="violation" v-hasPermi="['fulfiller:pool:edit']">违规记录</el-dropdown-item>
-                    <el-dropdown-item command="resetPwd" v-hasPermi="['fulfiller:pool:edit']">重置密码</el-dropdown-item>
+                      style="color: #f56c6c" v-hasPermi="['fulfiller:pool:disable']">禁用账号</el-dropdown-item>
+                    <el-dropdown-item v-else command="enable" divided style="color: #67c23a" v-hasPermi="['fulfiller:pool:enable']">启用账号</el-dropdown-item>
+                    <el-dropdown-item command="violation" v-hasPermi="['fulfiller:pool:violationLog']">违规记录</el-dropdown-item>
+                    <el-dropdown-item command="resetPwd" v-hasPermi="['fulfiller:pool:resetPassword']">重置密码</el-dropdown-item>
                   </el-dropdown-menu>
                 </template>
               </el-dropdown>

+ 3 - 3
src/views/fulfiller/tag/index.vue

@@ -4,7 +4,7 @@
       <template #header>
         <div class="card-header">
           <span class="title">履约者标签管理</span>
-          <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['fulfiller:tag:add']">新增标签</el-button>
+          <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['fulfiller:tagManagement:add']">新增标签</el-button>
         </div>
       </template>
 
@@ -25,8 +25,8 @@
         <el-table-column prop="createTime" label="创建时间" width="180" />
         <el-table-column label="操作" width="150">
           <template #default="scope">
-            <el-button link type="primary" @click="handleEdit(scope.row)" v-hasPermi="['fulfiller:tag:edit']">编辑</el-button>
-            <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['fulfiller:tag:remove']">删除</el-button>
+            <el-button link type="primary" @click="handleEdit(scope.row)" v-hasPermi="['fulfiller:tagManagement:edit']">编辑</el-button>
+            <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['fulfiller:tagManagement:remove']">删除</el-button>
           </template>
         </el-table-column>
       </el-table>

+ 3 - 3
src/views/login.vue

@@ -72,9 +72,9 @@
       </el-form-item>
     </el-form>
     <!--  底部  -->
-    <div class="el-login-footer">
-      <span>Copyright © 2018-2026 疯狂的狮子Li All Rights Reserved.</span>
-    </div>
+<!--    <div class="el-login-footer">-->
+<!--      <span>Copyright © 2018-2026 疯狂的狮子Li All Rights Reserved.</span>-->
+<!--    </div>-->
   </div>
 </template>
 

+ 190 - 98
src/views/monitor/logininfor/index.vue

@@ -1,106 +1,90 @@
 <template>
-  <div class="p-2">
-    <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="ipaddr">
-              <el-input v-model="queryParams.ipaddr" placeholder="请输入登录地址" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <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="status">
-              <el-select v-model="queryParams.status" placeholder="登录状态" clearable>
-                <el-option v-for="dict in sys_common_status" :key="dict.value" :label="dict.label" :value="dict.value" />
-              </el-select>
-            </el-form-item>
-            <el-form-item label="登录时间" style="width: 308px">
-              <el-date-picker
-                v-model="dateRange"
-                value-format="YYYY-MM-DD HH:mm:ss"
-                type="daterange"
-                range-separator="-"
-                start-placeholder="开始日期"
-                end-placeholder="结束日期"
-                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
-              ></el-date-picker>
-            </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">
+  <div class="logininfor-container">
+    <el-card shadow="never" class="main-card">
       <template #header>
-        <el-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['monitor:logininfor:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
-              删除
-            </el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['monitor:logininfor:remove']" type="danger" plain icon="Delete" @click="handleClean">清空</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['monitor:logininfor:unlock']" type="primary" plain icon="Unlock" :disabled="single" @click="handleUnlock">
-              解锁
-            </el-button>
-          </el-col>
-          <el-col :span="1.5">
+        <div class="page-header">
+          <div class="header-left">
+            <span class="title">登录日志中心</span>
+          </div>
+          <div class="header-right">
+            <el-button v-hasPermi="['monitor:logininfor:unlock']" type="primary" plain icon="Unlock" :disabled="single" @click="handleUnlock">解锁</el-button>
             <el-button v-hasPermi="['monitor:logininfor:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
-          </el-col>
-          <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
-        </el-row>
+            <el-button v-hasPermi="['monitor:logininfor:clean']" type="danger" plain icon="Delete" @click="handleClean">清空日志</el-button>
+          </div>
+        </div>
       </template>
 
-      <el-table
-        ref="loginInfoTableRef"
-        v-loading="loading"
-        :data="loginInfoList"
-        :default-sort="defaultSort"
-        border
-        @selection-change="handleSelectionChange"
-        @sort-change="handleSortChange"
-      >
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="访问编号" align="center" prop="infoId" />
-        <el-table-column
-          label="用户名称"
-          align="center"
-          prop="userName"
-          :show-overflow-tooltip="true"
-          sortable="custom"
-          :sort-orders="['descending', 'ascending']"
-        />
-        <el-table-column label="客户端" align="center" prop="clientKey" :show-overflow-tooltip="true" />
-        <el-table-column label="设备类型" align="center">
-          <template #default="scope">
-            <dict-tag :options="sys_device_type" :value="scope.row.deviceType" />
-          </template>
-        </el-table-column>
-        <el-table-column label="地址" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
-        <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
-        <el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
-        <el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
-        <el-table-column label="登录状态" align="center" prop="status">
-          <template #default="scope">
-            <dict-tag :options="sys_common_status" :value="scope.row.status" />
-          </template>
-        </el-table-column>
-        <el-table-column label="描述" align="center" prop="msg" :show-overflow-tooltip="true" />
-        <el-table-column label="访问时间" align="center" prop="loginTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="180">
-          <template #default="scope">
-            <span>{{ proxy.parseTime(scope.row.loginTime) }}</span>
-          </template>
-        </el-table-column>
-      </el-table>
-
-      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+      <!-- Search Section -->
+      <div v-show="showSearch" class="search-section">
+        <el-form ref="queryFormRef" :model="queryParams" :inline="true" class="custom-search">
+          <el-form-item prop="ipaddr">
+            <el-input v-model="queryParams.ipaddr" placeholder="登录地址 (IP)" clearable style="width: 180px" />
+          </el-form-item>
+          <el-form-item prop="userName">
+            <el-input v-model="queryParams.userName" placeholder="用户名称" clearable style="width: 150px" />
+          </el-form-item>
+          <el-form-item prop="status">
+            <el-select v-model="queryParams.status" placeholder="登录状态" clearable style="width: 120px">
+              <el-option v-for="dict in sys_common_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+            </el-select>
+          </el-form-item>
+          <el-form-item style="width: 260px">
+            <el-date-picker v-model="dateRange" value-format="YYYY-MM-DD" type="daterange" range-separator="-"
+              start-placeholder="开始" end-placeholder="结束" size="default" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="handleQuery">查询</el-button>
+            <el-button @click="resetQuery">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <div class="table-container">
+        <el-table ref="loginInfoTableRef" v-loading="loading" :data="loginInfoList" 
+          :default-sort="defaultSort" @selection-change="handleSelectionChange" @sort-change="handleSortChange"
+          :header-cell-style="{ background: '#f8f9fb', color: '#606266', fontWeight: '600' }">
+          <el-table-column type="selection" width="55" align="center" />
+          <el-table-column label="用户名称" prop="userName" min-width="120" sortable="custom" />
+          <el-table-column label="登录状态" width="100" align="center">
+            <template #default="scope">
+              <el-tag :type="scope.row.status === '0' ? 'success' : 'danger'" effect="light" size="small">
+                {{ scope.row.status === '0' ? '成功' : '失败' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="访问时间" prop="loginTime" sortable="custom" width="170">
+            <template #default="scope">
+              <span>{{ proxy.parseTime(scope.row.loginTime) }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="登录地址" min-width="160">
+            <template #default="scope">
+              <div class="ip-info">
+                <span class="ip-addr">{{ scope.row.ipaddr }}</span>
+                <span class="ip-loc">({{ scope.row.loginLocation }})</span>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="终端环境" min-width="200">
+            <template #default="scope">
+              <div class="agent-info">
+                <span class="os">{{ scope.row.os }}</span>
+                <span class="divider">/</span>
+                <span class="browser">{{ scope.row.browser }}</span>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="描述项" prop="msg" :show-overflow-tooltip="true" min-width="120" />
+          <el-table-column label="操作" width="100" align="right" fixed="right">
+            <template #default="scope">
+              <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total"
+        @pagination="getList" />
     </el-card>
   </div>
 </template>
@@ -207,3 +191,111 @@ onMounted(() => {
   getList();
 });
 </script>
+
+<style scoped lang="scss">
+.main-card {
+  margin: 20px;
+  border: none;
+  border-radius: 12px;
+  background-color: #fff;
+  min-height: calc(100vh - 124px);
+
+  :deep(.el-card__header) {
+    padding: 24px;
+    border-bottom: 1px solid #f0f2f5;
+  }
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.header-left {
+  .title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #1d2129;
+    position: relative;
+    padding-left: 12px;
+
+    &::before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 50%;
+      transform: translateY(-50%);
+      width: 4px;
+      height: 18px;
+      background: #409eff;
+      border-radius: 2px;
+    }
+  }
+}
+
+.search-section {
+  padding: 16px 24px;
+  background-color: #fcfcfd;
+}
+
+.custom-search {
+  :deep(.el-form-item) {
+    margin-bottom: 0;
+    margin-right: 12px;
+  }
+
+  :deep(.el-input__wrapper) {
+    box-shadow: 0 0 0 1px #e5e6eb inset;
+    background-color: #fff;
+    &:hover { box-shadow: 0 0 0 1px #409eff inset; }
+  }
+}
+
+.table-container {
+  padding: 0 24px;
+}
+
+.ip-info {
+  display: flex;
+  flex-direction: column;
+  .ip-addr {
+    font-weight: 500;
+    color: #1d2129;
+  }
+  .ip-loc {
+    font-size: 12px;
+    color: #86909c;
+    margin-top: 2px;
+  }
+}
+
+.agent-info {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  color: #4e5969;
+  font-size: 13px;
+
+  .divider {
+    color: #e5e6eb;
+  }
+
+  .os { color: #1d2129; }
+  .browser { color: #86909c; }
+}
+
+:deep(.el-table) {
+  --el-table-border-color: #f0f2f5;
+  
+  .el-table__row {
+    transition: background-color 0.2s;
+    &:hover { background-color: #f5f8ff; }
+  }
+}
+
+:deep(.pagination-container) {
+  padding: 24px;
+  justify-content: flex-end;
+}
+</style>

+ 214 - 120
src/views/monitor/operlog/index.vue

@@ -1,129 +1,98 @@
 <template>
-  <div class="p-2">
-    <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="operIp">
-              <el-input v-model="queryParams.operIp" placeholder="请输入操作地址" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="系统模块" prop="title">
-              <el-input v-model="queryParams.title" placeholder="请输入系统模块" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="操作人员" prop="operName">
-              <el-input v-model="queryParams.operName" placeholder="请输入操作人员" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="类型" prop="businessType">
-              <el-select v-model="queryParams.businessType" placeholder="操作类型" clearable>
-                <el-option v-for="dict in sys_oper_type" :key="dict.value" :label="dict.label" :value="dict.value" />
-              </el-select>
-            </el-form-item>
-            <el-form-item label="状态" prop="status">
-              <el-select v-model="queryParams.status" placeholder="操作状态" clearable>
-                <el-option v-for="dict in sys_common_status" :key="dict.value" :label="dict.label" :value="dict.value" />
-              </el-select>
-            </el-form-item>
-            <el-form-item label="操作时间" style="width: 308px">
-              <el-date-picker
-                v-model="dateRange"
-                value-format="YYYY-MM-DD HH:mm:ss"
-                type="daterange"
-                range-separator="-"
-                start-placeholder="开始日期"
-                end-placeholder="结束日期"
-                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
-              ></el-date-picker>
-            </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">
+  <div class="operlog-container">
+    <el-card shadow="never" class="main-card">
       <template #header>
-        <el-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['monitor:operlog:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
-              删除
-            </el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['monitor:operlog:remove']" type="danger" plain icon="WarnTriangleFilled" @click="handleClean">清空</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['monitor:operlog:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
-          </el-col>
-          <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
-        </el-row>
+        <div class="page-header">
+          <div class="header-left">
+            <span class="title">系统操作中心</span>
+          </div>
+          <div class="header-right">
+            <el-button v-hasPermi="['monitor:operlog:export']" type="warning" plain icon="Download" @click="handleExport">导出记录</el-button>
+            <el-button v-hasPermi="['monitor:operlog:clean']" type="danger" plain icon="Delete" @click="handleClean">清空日志</el-button>
+          </div>
+        </div>
       </template>
 
-      <el-table
-        ref="operLogTableRef"
-        v-loading="loading"
-        :data="operlogList"
-        border
-        :default-sort="defaultSort"
-        @selection-change="handleSelectionChange"
-        @sort-change="handleSortChange"
-      >
-        <el-table-column type="selection" width="50" align="center" />
-        <el-table-column label="日志编号" align="center" prop="operId" />
-        <el-table-column label="系统模块" align="center" prop="title" :show-overflow-tooltip="true" />
-        <el-table-column label="操作类型" align="center" prop="businessType">
-          <template #default="scope">
-            <dict-tag :options="sys_oper_type" :value="scope.row.businessType" />
-          </template>
-        </el-table-column>
-        <el-table-column
-          label="操作人员"
-          align="center"
-          width="110"
-          prop="operName"
-          :show-overflow-tooltip="true"
-          sortable="custom"
-          :sort-orders="['descending', 'ascending']"
-        />
-        <el-table-column label="部门" align="center" prop="deptName" width="130" :show-overflow-tooltip="true" />
-        <el-table-column label="操作地址" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" />
-        <el-table-column label="操作状态" align="center" prop="status">
-          <template #default="scope">
-            <dict-tag :options="sys_common_status" :value="scope.row.status" />
-          </template>
-        </el-table-column>
-        <el-table-column label="操作日期" align="center" prop="operTime" width="180" sortable="custom" :sort-orders="['descending', 'ascending']">
-          <template #default="scope">
-            <span>{{ proxy.parseTime(scope.row.operTime) }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column
-          label="消耗时间"
-          align="center"
-          prop="costTime"
-          width="110"
-          :show-overflow-tooltip="true"
-          sortable="custom"
-          :sort-orders="['descending', 'ascending']"
-        >
-          <template #default="scope">
-            <span>{{ scope.row.costTime }}毫秒</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" fixed="right" align="center" class-name="small-padding fixed-width">
-          <template #default="scope">
-            <el-tooltip content="详细" placement="top">
-              <el-button v-hasPermi="['monitor:operlog:query']" link type="primary" icon="View" @click="handleView(scope.row)"> </el-button>
-            </el-tooltip>
-          </template>
-        </el-table-column>
-      </el-table>
+      <!-- Search Area -->
+      <div v-show="showSearch" class="search-section">
+        <el-form ref="queryFormRef" :model="queryParams" :inline="true" class="custom-search">
+          <el-form-item prop="title">
+            <el-input v-model="queryParams.title" placeholder="系统模块" clearable style="width: 150px" />
+          </el-form-item>
+          <el-form-item prop="operName">
+            <el-input v-model="queryParams.operName" placeholder="操作人员" clearable style="width: 140px" />
+          </el-form-item>
+          <el-form-item prop="businessType">
+            <el-select v-model="queryParams.businessType" placeholder="业务类型" clearable style="width: 120px">
+              <el-option v-for="dict in sys_oper_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+            </el-select>
+          </el-form-item>
+          <el-form-item prop="status">
+            <el-select v-model="queryParams.status" placeholder="操作状态" clearable style="width: 120px">
+              <el-option v-for="dict in sys_common_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+            </el-select>
+          </el-form-item>
+          <el-form-item style="width: 260px">
+            <el-date-picker v-model="dateRange" value-format="YYYY-MM-DD" type="daterange" range-separator="-"
+              start-placeholder="开始" end-placeholder="结束" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="handleQuery">查询</el-button>
+            <el-button @click="resetQuery">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <div class="table-container">
+        <el-table ref="operLogTableRef" v-loading="loading" :data="operlogList" :default-sort="defaultSort"
+          @selection-change="handleSelectionChange" @sort-change="handleSortChange"
+          :header-cell-style="{ background: '#f8f9fb', color: '#606266', fontWeight: '600' }">
+          <el-table-column type="selection" width="55" align="center" />
+          <el-table-column label="模块/类型" min-width="150" :show-overflow-tooltip="true">
+            <template #default="scope">
+              <div class="module-info">
+                <span class="m-title">{{ scope.row.title }}</span>
+                <el-tag size="small" type="info" class="m-tag">{{ typeFormat(scope.row) }}</el-tag>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作人员" min-width="180">
+            <template #default="scope">
+              <div class="user-info">
+                <span class="u-name">{{ scope.row.operName }}</span>
+                <span class="u-ip">{{ scope.row.operIp }} ({{ scope.row.operLocation }})</span>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作状态" width="100" align="center">
+            <template #default="scope">
+              <el-tag :type="scope.row.status === 0 ? 'success' : 'danger'" effect="light" size="small">
+                {{ scope.row.status === 0 ? '成功' : '失败' }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="日期/耗时" width="200" sortable="custom" prop="operTime">
+            <template #default="scope">
+              <div class="time-info">
+                <span class="t-date">{{ proxy.parseTime(scope.row.operTime) }}</span>
+                <span class="t-cost">{{ scope.row.costTime }}ms</span>
+              </div>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="140" align="right" fixed="right">
+            <template #default="scope">
+              <div class="op-btns">
+                <el-button v-hasPermi="['monitor:operlog:query']" link type="primary" @click="handleView(scope.row)">详情</el-button>
+                <el-button v-hasPermi="['monitor:operlog:remove']" link type="danger" @click="handleDelete(scope.row)">删除</el-button>
+              </div>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
 
-      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total"
+        @pagination="getList" />
     </el-card>
-    <!-- 操作日志详细 -->
     <OperInfoDialog ref="operInfoDialogRef" />
   </div>
 </template>
@@ -259,3 +228,128 @@ onMounted(() => {
   getList();
 });
 </script>
+
+<style scoped lang="scss">
+.main-card {
+  margin: 20px;
+  border: none;
+  border-radius: 12px;
+  background-color: #fff;
+  min-height: calc(100vh - 124px);
+
+  :deep(.el-card__header) {
+    padding: 24px;
+    border-bottom: 1px solid #f0f2f5;
+  }
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.header-left {
+  .title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #1d2129;
+    position: relative;
+    padding-left: 12px;
+
+    &::before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 50%;
+      transform: translateY(-50%);
+      width: 4px;
+      height: 18px;
+      background: #409eff;
+      border-radius: 2px;
+    }
+  }
+}
+
+.search-section {
+  padding: 16px 24px;
+  background-color: #fcfcfd;
+}
+
+.custom-search {
+  :deep(.el-form-item) {
+    margin-bottom: 0;
+    margin-right: 12px;
+  }
+
+  :deep(.el-input__wrapper) {
+    box-shadow: 0 0 0 1px #e5e6eb inset;
+    background-color: #fff;
+    &:hover { box-shadow: 0 0 0 1px #409eff inset; }
+  }
+}
+
+.table-container {
+  padding: 0 24px;
+}
+
+.module-info {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  .m-title {
+    font-weight: 500;
+    color: #1d2129;
+  }
+  .m-tag {
+    align-self: flex-start;
+  }
+}
+
+.user-info {
+  display: flex;
+  flex-direction: column;
+  .u-name {
+    font-weight: 500;
+    color: #1d2129;
+  }
+  .u-ip {
+    font-size: 12px;
+    color: #86909c;
+    margin-top: 2px;
+  }
+}
+
+.time-info {
+  display: flex;
+  flex-direction: column;
+  .t-date {
+    color: #1d2129;
+  }
+  .t-cost {
+    font-size: 11px;
+    color: #f59e0b;
+    margin-top: 2px;
+  }
+}
+
+.op-btns {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+
+:deep(.el-table) {
+  --el-table-border-color: #f0f2f5;
+  
+  .el-table__row {
+    transition: background-color 0.2s;
+    &:hover { background-color: #f5f8ff; }
+  }
+}
+
+:deep(.pagination-container) {
+  padding: 24px;
+  justify-content: flex-end;
+}
+</style>

+ 2 - 2
src/views/order/dispatch/components/OrderListPanel.vue

@@ -52,7 +52,7 @@
             <el-tag size="small" :type="getOrderStatusType(order.status)" effect="plain">{{ getOrderStatusText(order.status) }}</el-tag>
             <div class="actions">
               <el-button v-if="order.status === 0" type="primary" size="small" @click.stop="$emit('dispatch', order)"
-                >派单</el-button
+                v-hasPermi="['order:dispatch:dispatch']">派单</el-button
               >
               <el-button
                 v-else-if="[1, 2, 3].includes(order.status)"
@@ -60,7 +60,7 @@
                 size="small"
                 plain
                 @click.stop="$emit('dispatch', order)"
-                >重新派单</el-button
+                v-hasPermi="['order:dispatch:redispatch']">重新派单</el-button
               >
             </div>
           </div>

+ 2 - 3
src/views/order/dispatch/components/RiderListPanel.vue

@@ -63,9 +63,8 @@
               <span class="lbl">待服</span>
               <span class="val warning">{{ rider.todoCount }}</span>
             </div>
-            <el-button link type="primary" size="small" style="margin-top: 4px; padding: 0" @click.stop="$emit('view-orders', rider)"
-              >查看订单</el-button
-            >
+            <!-- <el-button link type="primary" size="small" style="margin-top: 4px; padding: 0" @click.stop="$emit('view-orders', rider)"
+              >查看订单</el-button> -->
           </div>
         </div>
       </el-scrollbar>

+ 95 - 23
src/views/order/dispatch/index.vue

@@ -21,19 +21,33 @@
     <div class="main-content">
       <!-- Left: Real Gaode Map Area -->
       <div class="map-wrapper">
-        <div id="amap-container" class="map-view"></div>
-
-        <!-- Bottom Left: Map Controls & Stats -->
-        <div class="map-controls-panel">
-          <div class="control-group">
-            <div class="c-btn" :class="{ active: activeMapFilter === 'all' }" @click="setMapFilter('all')">全部</div>
-            <div class="c-btn red" :class="{ active: activeMapFilter === 'merchants' }"
-              @click="setMapFilter('merchants')">商家({{ merchantList.length }})</div>
-            <div class="c-btn green" :class="{ active: activeMapFilter === 'fulfillers' }"
-              @click="setMapFilter('fulfillers')">履约者({{ ridersList.length }})</div>
-            <div class="c-btn blue" :class="{ active: activeMapFilter === 'orders' }" @click="setMapFilter('orders')">
-              订单({{ ordersList.length }})</div>
-            <div class="c-btn gray">灰色表示离线</div>
+        <template v-if="checkPermi(['order:dispatch:map'])">
+          <div id="amap-container" class="map-view"></div>
+
+          <!-- Bottom Left: Map Controls & Stats -->
+          <div class="map-controls-panel">
+            <div class="control-group">
+              <div class="c-btn" :class="{ active: activeMapFilter === 'all' }" @click="setMapFilter('all')">全部</div>
+              <div class="c-btn red" :class="{ active: activeMapFilter === 'merchants' }"
+                @click="setMapFilter('merchants')">商家({{ merchantList.length }})</div>
+              <div class="c-btn green" :class="{ active: activeMapFilter === 'fulfillers' }"
+                @click="setMapFilter('fulfillers')">履约者({{ ridersList.length }})</div>
+              <div class="c-btn blue" :class="{ active: activeMapFilter === 'orders' }" @click="setMapFilter('orders')">
+                订单({{ ordersList.length }})</div>
+              <div class="c-btn gray">灰色表示离线</div>
+            </div>
+          </div>
+        </template>
+        <div v-else class="no-auth-map">
+          <div class="no-auth-content">
+            <div class="lock-icon">
+              <!-- Inline SVG for Lock Icon -->
+              <svg viewBox="0 0 1024 1024" width="64" height="64">
+                <path d="M512 64a256 256 0 0 0-256 256v128h-64a64 64 0 0 0-64 64v384a64 64 0 0 0 64 64h640a64 64 0 0 0 64-64V512a64 64 0 0 0-64-64h-64V320a256 256 0 0 0-256-256z m160 384H352V320a160 160 0 1 1 320 0v128z" fill="#E4E7ED"/>
+              </svg>
+            </div>
+            <h3 class="no-auth-title">暂无地图权限</h3>
+            <p class="no-auth-desc">当前角色尚未分配地图查看权限,无法展示实时调度数据</p>
           </div>
         </div>
       </div>
@@ -55,8 +69,9 @@
   </div>
 </template>
 
-<script setup>
-import { ref, computed, reactive, onMounted, watch } from 'vue';
+<script setup lang="ts">
+import { ref, computed, reactive, onMounted, watch, getCurrentInstance, ComponentInternalInstance } from 'vue';
+import { checkPermi } from "@/utils/permission";
 import { ElMessage } from 'element-plus';
 import { listAllService } from '@/api/service/list/index'
 import { listOnStore as listAreaStationOnStore } from '@/api/system/areaStation'
@@ -73,6 +88,8 @@ import DispatchDialog from './components/DispatchDialog.vue';
 import dispatchMockData from '@/mock/dispatch.json';
 import riderOrdersMockData from '@/mock/RiderOrdersDialog.json';
 
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
 // --- Data & State ---
 const filters = reactive({
   orderType: 'all',
@@ -424,14 +441,16 @@ watch([currentOrderTab, currentRiderTab], () => {
 onMounted(async () => {
   getServiceList()
   await getAreaStationList()
-  loadAMapScript()
-    .then(() => {
-      initMap();
-    })
-    .catch((err) => {
-      console.error('Map loading failed', err);
-      ElMessage.error('地图加载失败,请检查网络');
-    });
+  if (checkPermi(['order:dispatch:map'])) {
+    loadAMapScript()
+      .then(() => {
+        initMap();
+      })
+      .catch((err) => {
+        console.error('Map loading failed', err);
+        ElMessage.error('地图加载失败,请检查网络');
+      });
+  }
 });
 
 // Dispatch Dialog State
@@ -623,6 +642,59 @@ const filteredRiders = computed(() => {
   border: 1px solid #d9ecff;
 }
 
+.no-auth-map {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #fcfcfd;
+  background-image: radial-gradient(#e5e7eb 1px, transparent 1px);
+  background-size: 20px 20px;
+}
+
+.no-auth-content {
+  text-align: center;
+  padding: 40px;
+  background: rgba(255, 255, 255, 0.8);
+  backdrop-filter: blur(8px);
+  border-radius: 16px;
+  border: 1px solid rgba(234, 236, 239, 0.8);
+  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.03);
+  max-width: 320px;
+}
+
+.lock-icon {
+  margin-bottom: 24px;
+  display: flex;
+  justify-content: center;
+  opacity: 0.8;
+}
+
+.no-auth-title {
+  margin: 0 0 12px 0;
+  color: #303133;
+  font-size: 20px;
+  font-weight: 600;
+}
+
+.no-auth-desc {
+  margin: 0 0 20px 0;
+  color: #909399;
+  font-size: 14px;
+  line-height: 1.6;
+}
+
+.no-auth-tip {
+  display: inline-block;
+  padding: 4px 12px;
+  background: #f4f4f5;
+  color: #a8abb2;
+  font-size: 12px;
+  border-radius: 4px;
+  font-family: monospace;
+}
+
 /* Right Panel */
 .right-panel {
   width: 440px;

+ 7 - 8
src/views/order/orderList/index.vue

@@ -116,13 +116,13 @@
         <el-table-column label="操作" width="200" fixed="right">
           <template #default="{ row }">
             <div class="op-cell">
-              <el-button link type="primary" size="small" @click="handleDetail(row)">详情</el-button>
+              <el-button link type="primary" size="small" @click="handleDetail(row)" v-hasPermi="['order:orderList:query']">详情</el-button>
               <el-button v-if="row.status === 0" link type="success" size="small"
-                @click="openDispatchDialog(row)">派单</el-button>
+                @click="openDispatchDialog(row)" v-hasPermi="['order:orderList:dispatch']">派单</el-button>
               <el-button v-if="![0, 4].includes(row.status)" link type="warning" size="small"
-                @click="openDispatchDialog(row)">重新派单</el-button>
+                @click="openDispatchDialog(row)" v-hasPermi="['order:orderList:redispatch']">重新派单</el-button>
               <el-button v-if="[0, 1].includes(row.status)" link type="danger" size="small"
-                @click="handleCancel(row)">取消</el-button>
+                @click="handleCancel(row)" v-hasPermi="['order:orderList:cancel']">取消</el-button>
               <el-button v-if="row.fulfiller && ![1].includes(row.status)" link type="warning" size="small"
                 @click="openComplaintDialog(row)">投诉</el-button>
 
@@ -135,11 +135,10 @@
                 </span>
                 <template #dropdown>
                   <el-dropdown-menu>
-                    <el-dropdown-item v-if="row.status === 3" command="complete">确认完成</el-dropdown-item>
                     <el-dropdown-item v-if="row.status === 4 && getServiceMode(row.service) == 0"
-                      command="care_summary">护理小结</el-dropdown-item>
-                    <el-dropdown-item command="reward">奖惩</el-dropdown-item>
-                    <el-dropdown-item command="remark">备注</el-dropdown-item>
+                      command="care_summary" v-hasPermi="['order:orderList:nursingSummary']">护理小结</el-dropdown-item>
+                    <el-dropdown-item command="reward" v-hasPermi="['order:orderList:reward']">奖惩</el-dropdown-item>
+                    <el-dropdown-item command="remark" v-hasPermi="['order:orderList:remark']">备注</el-dropdown-item>
                   </el-dropdown-menu>
                 </template>
               </el-dropdown>

+ 4 - 3
src/views/order/purchase/index.vue

@@ -33,7 +33,7 @@
                         style="display:flex; justify-content:space-between; align-items:center; width:100%; height: 24px;">
                         <span>宠主用户</span>
                         <el-button type="primary" plain size="small" @click="openAddUser" icon="Plus"
-                          style="margin-left: 15px;">添加用户</el-button>
+                          style="margin-left: 15px;" v-hasPermi="['order:purchase:addCustomer']">添加用户</el-button>
                       </div>
                     </template>
                     <PageSelect v-model="form.userId" placeholder="搜索姓名/手机号" size="large" style="width: 100%"
@@ -60,7 +60,7 @@
                   </div>
 
                   <!-- Add Button Card (Last Item in Grid) -->
-                  <div class="pet-card add-card" @click="openAddPet">
+                  <div class="pet-card add-card" @click="openAddPet" v-hasPermi="['order:purchase:addPet']">
                     <el-icon :size="24">
                       <Plus />
                     </el-icon>
@@ -174,7 +174,8 @@
           </div>
 
           <div class="summary-footer">
-            <el-button type="primary" size="large" class="submit-btn" :disabled="!canSubmit" @click="handleSubmit">
+            <el-button type="primary" size="large" class="submit-btn" :disabled="!canSubmit" @click="handleSubmit"
+              v-hasPermi="['order:purchase:create']">
               立即下单
             </el-button>
           </div>

+ 3 - 3
src/views/register.vue

@@ -66,9 +66,9 @@
       </el-form-item>
     </el-form>
     <!--  底部  -->
-    <div class="el-register-footer">
-      <span>Copyright © 2018-2026 疯狂的狮子Li All Rights Reserved.</span>
-    </div>
+<!--    <div class="el-register-footer">-->
+<!--      <span>Copyright © 2018-2026 疯狂的狮子Li All Rights Reserved.</span>-->
+<!--    </div>-->
   </div>
 </template>
 

+ 165 - 132
src/views/service/list/index.vue

@@ -1,81 +1,68 @@
 <template>
-  <div class="p-2">
-    <!--    <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>-->
-    <!--              <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="never">
+  <div class="page-container">
+    <el-card shadow="never" class="table-card">
       <template #header>
-        <el-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['list:list:add']">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
-              v-hasPermi="['list:list:edit']">修改</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"
-              v-hasPermi="['list:list:remove']">删除</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="warning" plain icon="Download" @click="handleExport"
-              v-hasPermi="['list:list:export']">导出</el-button>
-          </el-col>
-          <!--          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>-->
-        </el-row>
+        <div class="card-header">
+          <div class="header-left">
+            <span class="title">服务管理 (列表)</span>
+          </div>
+          <div class="header-right">
+            <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['service:list:add']">新增服务</el-button>
+          </div>
+        </div>
       </template>
 
-      <el-table v-loading="loading" border :data="serviceList" @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="排序" align="center" prop="sort" width="100" />
-        <el-table-column label="服务名称" align="center" prop="name" width="200" />
-        <el-table-column label="服务图标" align="center" prop="iconUrl" width="100">
+      <el-table v-loading="loading" :data="serviceList" style="width: 100%" :header-cell-style="{ background: '#f8f9fb', color: '#606266' }">
+        <el-table-column label="排序权重" align="center" prop="sort" width="100" />
+        <el-table-column label="服务名称" prop="name" min-width="150">
+          <template #default="scope">
+            <span class="service-name-text">{{ scope.row.name }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="服务图标" align="center" width="120">
           <template #default="scope">
-            <image-preview :src="scope.row.iconUrl" :width="50" :height="50" />
+            <div class="icon-wrapper">
+              <image-preview :src="scope.row.iconUrl" :width="40" :height="40" />
+            </div>
           </template>
         </el-table-column>
-        <el-table-column label="服务模式" align="center" prop="mode" width="200">
+        <el-table-column label="服务模式" align="center" width="150">
           <template #default="scope">
-            {{ getModeLabel(scope.row.mode) }}
+            <el-tag :type="getModeTagType(scope.row.mode)" size="small" effect="light" class="mode-tag">
+              {{ getModeLabel(scope.row.mode) }}
+            </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="备注说明" align="center" prop="remark" width="700" show-overflow-tooltip />
-        <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <el-table-column label="备注说明" prop="remark" min-width="250" show-overflow-tooltip />
+        <el-table-column label="创建时间" align="center" width="180">
           <template #default="scope">
-            <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{m}:{s}') }}</span>
+            <span class="time-text">{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{m}:{s}') }}</span>
           </template>
         </el-table-column>
-        <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
+        <el-table-column label="操作" align="right" width="140" fixed="right">
           <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
-                v-hasPermi="['list:list:edit']"></el-button>
-            </el-tooltip>
-            <el-tooltip content="删除" placement="top">
-              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
-                v-hasPermi="['list:list:remove']"></el-button>
-            </el-tooltip>
+            <div class="op-btns">
+              <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['service:list:edit']">编辑</el-button>
+              <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['service:list:remove']">删除</el-button>
+            </div>
           </template>
         </el-table-column>
       </el-table>
 
-      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
-        v-model:limit="queryParams.pageSize" @pagination="getList" />
+      <div class="pagination-container">
+        <pagination
+          v-show="total > 0"
+          v-model:total="total"
+          v-model:page="queryParams.pageNum"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </div>
     </el-card>
+
     <!-- 添加或修改服务项目对话框 -->
     <el-dialog :title="dialog.title" v-model="dialog.visible" width="650px" append-to-body>
-      <el-form ref="serviceFormRef" :model="form" :rules="rules" label-width="80px">
+      <el-form ref="serviceFormRef" :model="form" :rules="rules" label-width="100px">
         <el-form-item label="服务名称" prop="name">
           <el-input v-model="form.name" placeholder="请输入服务名称" />
         </el-form-item>
@@ -89,29 +76,24 @@
             </el-radio>
           </el-radio-group>
         </el-form-item>
-<!--        <el-form-item label="所属品牌" prop="tenantId">-->
-<!--          <PageSelect v-model="form.tenantId" :options="brandOptions" :total="brandTotal" :pageSize="10"-->
-<!--            placeholder="请选择所属品牌" @page-change="handleBrandPageChange"-->
-<!--            @visible-change="handleBrandSelectVisibleChange" />-->
-<!--        </el-form-item>-->
         <el-form-item label="排序权重" prop="sort">
-          <el-input v-model="form.sort" placeholder="请输入排序权重" />
+          <el-input-number v-model="form.sort" :min="0" controls-position="right" style="width: 100%" />
         </el-form-item>
         <el-form-item label="备注说明" prop="remark">
-          <el-input v-model="form.remark" placeholder="请输入备注说明" />
+          <el-input v-model="form.remark" placeholder="请输入备注说明" type="textarea" :rows="2" />
         </el-form-item>
         <el-form-item label="打卡备注" prop="clockInRemark">
           <div class="w-full">
-            <el-card v-for="(item, index) in clockInRemarkList" :key="index" class="mb-2" shadow="never">
+            <el-card v-for="(item, index) in clockInRemarkList" :key="index" class="step-card" shadow="never">
               <template #header>
-                <div style="display: flex; align-items: center; justify-content: space-between;">
+                <div class="step-header">
                   <span>步骤 {{ index + 1 }}</span>
                 </div>
               </template>
-              <el-form-item label="步骤标题" label-width="80px" style="margin-bottom: 12px;">
+              <el-form-item label="步骤标题" label-width="80px" class="inner-form-item">
                 <el-input v-model="item.title" placeholder="请输入步骤标题" />
               </el-form-item>
-              <el-form-item label="打卡备注" label-width="80px" style="margin-bottom: 0;">
+              <el-form-item label="打卡备注" label-width="80px" class="inner-form-item last">
                 <el-input v-model="item.remark" type="textarea" placeholder="请输入当前步骤打卡备注" />
               </el-form-item>
             </el-card>
@@ -120,8 +102,8 @@
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
           <el-button @click="cancel">取 消</el-button>
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
         </div>
       </template>
     </el-dialog>
@@ -133,7 +115,6 @@ import { listService, getService, delService, addService, updateService } from '
 import { ServiceVO, ServiceQuery, ServiceForm } from '@/api/service/list/types';
 import { list as listMode } from '@/api/service/mode';
 import { SysServiceModeVo } from '@/api/service/mode/types';
-import PageSelect from '@/components/PageSelect/index.vue';
 
 interface ClockInRemarkItem {
   step: number;
@@ -142,22 +123,14 @@ interface ClockInRemarkItem {
 }
 
 const clockInRemarkList = ref<ClockInRemarkItem[]>([]);
-
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const serviceList = ref<ServiceVO[]>([]);
 const buttonLoading = ref(false);
 const loading = ref(true);
-const showSearch = ref(true);
-const ids = ref<Array<string | number>>([]);
-const single = ref(true);
-const multiple = ref(true);
 const total = ref(0);
-const modeList = ref<SysServiceModeVo[]>([]); // 服务模式列表
-const brandOptions = ref<any[]>([]); // 品牌选项
-const brandTotal = ref(0); // 品牌总数
+const modeList = ref<SysServiceModeVo[]>([]);
 
-const queryFormRef = ref<ElFormInstance>();
 const serviceFormRef = ref<ElFormInstance>();
 
 const dialog = reactive<DialogOption>({
@@ -175,6 +148,7 @@ const initFormData: ServiceForm = {
   clockInRemark: undefined,
   tenantId: undefined
 };
+
 const data = reactive<PageData<ServiceForm, ServiceQuery>>({
   form: { ...initFormData },
   queryParams: {
@@ -183,7 +157,6 @@ const data = reactive<PageData<ServiceForm, ServiceQuery>>({
     params: {}
   },
   rules: {
-    id: [{ required: true, message: '序号不能为空', trigger: 'blur' }],
     name: [{ required: true, message: '服务名称不能为空', trigger: 'blur' }],
     icon: [{ required: true, message: '服务图标不能为空', trigger: 'blur' }],
     mode: [{ required: true, message: '服务模式不能为空', trigger: 'change' }]
@@ -211,26 +184,18 @@ const getModeList = async () => {
   }
 };
 
+/** 根据服务模式获取 Tag 类型 */
+const getModeTagType = (value: any) => {
+  const map: any = { 'round': 'primary', 'one-way': 'info' };
+  return map[value] || 'primary';
+};
+
 /** 根据服务模式value获取label */
 const getModeLabel = (value: any): string => {
   const mode = modeList.value.find((item) => item.value === value);
   return mode ? mode.label : value;
 };
 
-/** 处理品牌分页 */
-const handleBrandPageChange = (page: number) => {
-  // 这里可以调用API获取对应页的数据
-  console.log('品牌分页切换到:', page);
-};
-
-/** 处理品牌选择框可见性变化 */
-const handleBrandSelectVisibleChange = (visible: boolean) => {
-  console.log('品牌选择框可见性变化:', visible);
-  if (visible) {
-    // 这里可以调用API获取初始数据
-  }
-};
-
 /** 取消按钮 */
 const cancel = () => {
   reset();
@@ -253,25 +218,6 @@ const reset = () => {
   serviceFormRef.value?.resetFields();
 };
 
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.value.pageNum = 1;
-  getList();
-};
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value?.resetFields();
-  handleQuery();
-};
-
-/** 多选框选中数据 */
-const handleSelectionChange = (selection: ServiceVO[]) => {
-  ids.value = selection.map((item) => item.id);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
-};
-
 /** 新增按钮操作 */
 const handleAdd = () => {
   reset();
@@ -280,10 +226,9 @@ const handleAdd = () => {
 };
 
 /** 修改按钮操作 */
-const handleUpdate = async (row?: ServiceVO) => {
+const handleUpdate = async (row: ServiceVO) => {
   reset();
-  const _id = row?.id || ids.value[0];
-  const res = await getService(_id);
+  const res = await getService(row.id);
   Object.assign(form.value, res.data);
   if (form.value.clockInRemark) {
     try {
@@ -291,7 +236,7 @@ const handleUpdate = async (row?: ServiceVO) => {
       if (Array.isArray(parsed)) {
         clockInRemarkList.value = getInitialClockInRemarks().map((item, index) => {
           return parsed[index] ? { ...item, ...parsed[index] } : item;
-        });
+          });
       }
     } catch (e) {
       clockInRemarkList.value = getInitialClockInRemarks();
@@ -320,27 +265,115 @@ const submitForm = () => {
 };
 
 /** 删除按钮操作 */
-const handleDelete = async (row?: ServiceVO) => {
-  const _ids = row?.id || ids.value;
-  await proxy?.$modal.confirm('是否确认删除服务项目编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
-  await delService(_ids);
+const handleDelete = async (row: ServiceVO) => {
+  await proxy?.$modal.confirm(`是否确认删除服务项目"${row.name}"?`);
+  loading.value = true;
+  await delService(row.id).finally(() => (loading.value = false));
   proxy?.$modal.msgSuccess('删除成功');
   await getList();
 };
 
-/** 导出按钮操作 */
-const handleExport = () => {
-  proxy?.download(
-    'service/service/export',
-    {
-      ...queryParams.value
-    },
-    `service_${new Date().getTime()}.xlsx`
-  );
-};
-
 onMounted(() => {
   getList();
   getModeList();
 });
 </script>
+
+<style scoped lang="scss">
+.page-container {
+  padding: 20px;
+  background-color: #f5f7f9;
+  min-height: 100%;
+}
+
+.table-card {
+  border: none;
+  border-radius: 8px;
+  
+  :deep(.el-card__header) {
+    padding: 20px 24px;
+    border-bottom: 1px solid #f0f0f0;
+  }
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.header-left {
+  .title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333;
+  }
+}
+
+.service-name-text {
+  font-weight: 600;
+  color: #333;
+}
+
+.icon-wrapper {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.mode-tag {
+  border: none;
+  font-weight: 500;
+}
+
+.time-text {
+  color: #909399;
+  font-size: 13px;
+}
+
+.op-btns {
+  display: flex;
+  justify-content: flex-end;
+  gap: 16px;
+}
+
+.pagination-container {
+  margin-top: 24px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+.step-card {
+  margin-bottom: 12px;
+  background-color: #f8f9fb;
+  border: 1px solid #ebeef5;
+  
+  .step-header {
+    font-size: 13px;
+    font-weight: 600;
+    color: #606266;
+  }
+  
+  .inner-form-item {
+    margin-bottom: 12px;
+    &.last { margin-bottom: 0; }
+  }
+  
+  :deep(.el-card__header) {
+    padding: 8px 16px;
+    background-color: #f0f2f5;
+  }
+}
+
+:deep(.el-table) {
+  --el-table-border-color: #f0f0f0;
+  
+  th.el-table__cell {
+    font-weight: 600;
+  }
+  
+  td.el-table__cell {
+    padding: 12px 0;
+  }
+}
+</style>

+ 97 - 48
src/views/system/areaStation/index.vue

@@ -13,51 +13,41 @@
     <!--      </div>-->
     <!--    </transition>-->
 
-    <el-card shadow="never">
+    <el-card shadow="never" class="table-card">
       <template #header>
-        <el-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button type="primary" plain icon="Plus" @click="handleAdd()"
-              v-hasPermi="['system:areaStation:add']">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
-          </el-col>
-          <!--          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>-->
-        </el-row>
+        <div class="card-header">
+          <div class="header-left">
+            <span class="title">区域与站点管理</span>
+          </div>
+          <div class="header-right">
+            <el-button type="primary" icon="Plus" @click="handleAdd()" v-hasPermi="['system:areaStation:addCity']">新增城市</el-button>
+          </div>
+        </div>
       </template>
-      <el-table ref="areaStationTableRef" v-loading="loading" :data="areaStationList" row-key="id" border
-        :default-expand-all="isExpandAll" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }">
-        <el-table-column label="区域名称" prop="name" width="300" />
-        <el-table-column label="类型" align="center" prop="type">
+
+      <el-table ref="areaStationTableRef" v-loading="loading" :data="areaStationList" row-key="id" 
+        :default-expand-all="isExpandAll" :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+        style="width: 100%" :header-cell-style="{ background: '#f8f9fb', color: '#606266' }">
+        <el-table-column label="名称" prop="name" width="280" />
+        <el-table-column label="类型" width="100" align="center">
           <template #default="scope">
-            <el-tag :type="getTypeStyle(scope.row.type)">
+            <el-tag :type="getTypeStyle(scope.row.type)" effect="light" size="small">
               {{ getTypeLabel(scope.row.type) }}
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="省市编码" align="center" prop="code" width="200" />
-        <!--        <el-table-column label="排序权重" align="center" prop="sort" />-->
-        <el-table-column label="详细地址" align="center" prop="address" width="400" />
-        <el-table-column label="站长姓名" align="center" prop="leaderName" />
-        <el-table-column label="联系电话" align="center" prop="contactPhone" />
-        <!--        <el-table-column label="经度" align="center" prop="longitude" />-->
-        <!--        <el-table-column label="纬度" align="center" prop="latitude" />-->
-        <!--        <el-table-column label="状态" align="center" prop="status" width="100" />-->
-        <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
+        <el-table-column label="编码" prop="code" width="150" align="center" />
+        <el-table-column label="详细地址 (站点)" prop="address" min-width="240" />
+        <el-table-column label="负责人" prop="leaderName" width="120" align="center" />
+        <el-table-column label="联系电话" prop="contactPhone" width="150" align="center" />
+        <el-table-column label="操作" width="220" align="right" fixed="right">
           <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
-                v-hasPermi="['system:areaStation:edit']" />
-            </el-tooltip>
-            <el-tooltip v-if="scope.row.type !== 2" content="新增" placement="top">
-              <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)"
-                v-hasPermi="['system:areaStation:add']" />
-            </el-tooltip>
-            <el-tooltip content="删除" placement="top">
-              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
-                v-hasPermi="['system:areaStation:remove']" />
-            </el-tooltip>
+            <div class="op-btns">
+              <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['system:areaStation:edit']">编辑</el-button>
+              <el-button v-if="scope.row.type === 0" link type="success" @click="handleAdd(scope.row)" v-hasPermi="['system:areaStation:addArea']">新增区域</el-button>
+              <el-button v-if="scope.row.type === 1" link type="success" @click="handleAdd(scope.row)" v-hasPermi="['system:areaStation:addStation']">新增站点</el-button>
+              <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['system:areaStation:remove']">删除</el-button>
+            </div>
           </template>
         </el-table-column>
       </el-table>
@@ -68,7 +58,8 @@
         <el-form-item label="区域名称" prop="name">
           <el-input v-model="form.name" placeholder="请输入区域名称" />
         </el-form-item>
-        <el-form-item label="区域类型" prop="type">
+        <!-- 区域类型固定由入口决定,无需展示 -->
+        <el-form-item v-if="false" label="区域类型" prop="type">
           <el-radio-group v-model="form.type" :disabled="!!form.id">
             <el-radio v-for="type in filteredTypeList" :key="type.value" :label="type.value">
               <el-tag :type="type.style">
@@ -250,21 +241,15 @@ const handleAdd = (row?: AreaStationVO) => {
   if (row != null && row.id) {
     form.value.parentId = row.id;
     if (row.type === 0) {
-      // 城市底下只能新增区域
-      filteredTypeList.value = typeList.value.filter((item) => item.value === 1);
+      // 城市底下新增区域
       form.value.type = 1;
     } else if (row.type === 1) {
-      // 区域底下可以新增区域或者站点
-      filteredTypeList.value = typeList.value.filter((item) => item.value === 1 || item.value === 2);
-      form.value.type = 1; // 默认选中区域
-    } else {
-      // 站点底下不能新增,理论上按钮已隐藏
-      filteredTypeList.value = [];
+      // 区域底下新增站点
+      form.value.type = 2;
     }
   } else {
-    // 头部新增只能新增城市
+    // 头部操作新增城市
     form.value.parentId = 0;
-    filteredTypeList.value = typeList.value.filter((item) => item.value === 0);
     form.value.type = 0;
   }
   dialog.visible = true;
@@ -331,3 +316,67 @@ onMounted(() => {
   getTypeList();
 });
 </script>
+
+<style scoped lang="scss">
+.p-2 {
+  padding: 20px;
+  background-color: #f5f7f9;
+  min-height: 100vh;
+}
+
+.table-card {
+  border: none;
+  border-radius: 8px;
+  
+  :deep(.el-card__header) {
+    padding: 20px 24px;
+    border-bottom: 1px solid #f0f0f0;
+  }
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.header-left {
+  .title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333;
+  }
+}
+
+.header-right {
+  display: flex;
+  gap: 12px;
+}
+
+.op-btns {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  
+  .el-button {
+    font-weight: 400;
+    padding: 0;
+    
+    &.el-button--primary { color: #409eff; }
+    &.el-button--success { color: #67c23a; }
+    &.el-button--danger { color: #f56c6c; }
+  }
+}
+
+:deep(.el-table) {
+  --el-table-border-color: #f0f0f0;
+  
+  th.el-table__cell {
+    font-weight: 600;
+  }
+
+  .el-tag {
+    border: none;
+  }
+}
+</style>

+ 6 - 6
src/views/system/dict/index.vue

@@ -7,7 +7,7 @@
           <template #header>
             <div class="card-header">
               <span class="title">字典类型</span>
-              <el-button link type="primary" icon="Plus" @click="handleAddType()">新增</el-button>
+              <el-button link type="primary" icon="Plus" @click="handleAddType()" v-hasPermi="['system:dict:addType']">新增</el-button>
             </div>
             <div class="search-box">
               <el-input v-model="typeSearch" placeholder="搜索类型名称/编码" prefix-icon="Search" clearable />
@@ -21,8 +21,8 @@
                 <div class="type-code">{{ item.dictType }}</div>
               </div>
               <div class="type-ops">
-                <el-button link type="primary" icon="Edit" @click.stop="handleUpdateType(item)"></el-button>
-                <el-button link type="danger" icon="Delete" @click.stop="handleDeleteType(item)"></el-button>
+                <el-button link type="primary" icon="Edit" @click.stop="handleUpdateType(item)" v-hasPermi="['system:dict:editType']"></el-button>
+                <el-button link type="danger" icon="Delete" @click.stop="handleDeleteType(item)" v-hasPermi="['system:dict:removeType']"></el-button>
               </div>
             </div>
           </el-scrollbar>
@@ -39,7 +39,7 @@
                 <el-tag size="small" type="info" class="ml8">{{ activeType?.dictType }}</el-tag>
               </div>
               <div class="header-right">
-                <el-button type="primary" icon="Plus" @click="handleAddData()">新增数据</el-button>
+                <el-button type="primary" icon="Plus" @click="handleAddData()" v-hasPermi="['system:dict:addData']">新增数据</el-button>
               </div>
             </div>
           </template>
@@ -52,8 +52,8 @@
             <el-table-column label="操作" width="160" align="right" fixed="right">
               <template #default="scope">
                 <div class="op-btns">
-                  <el-button link type="primary" @click="handleUpdateData(scope.row)">编辑</el-button>
-                  <el-button link type="danger" @click="handleDeleteData(scope.row)">删除</el-button>
+                  <el-button link type="primary" @click="handleUpdateData(scope.row)" v-hasPermi="['system:dict:editData']">编辑</el-button>
+                  <el-button link type="danger" @click="handleDeleteData(scope.row)" v-hasPermi="['system:dict:removeData']">删除</el-button>
                 </div>
               </template>
             </el-table-column>

+ 247 - 326
src/views/system/menu/index.vue

@@ -1,190 +1,112 @@
 <template>
-  <div class="p-2">
-    <!-- 统一卡片容器 -->
-    <el-card shadow="hover">
-      <!-- 顶部选项卡 -->
-      <el-tabs v-model="platformId" @tab-click="handlePlatformChange" type="border-card" class="mb-[15px]">
-        <el-tab-pane label="管理后台" :name="0">
-          <!-- 管理后台内容 -->
-          <div class="tab-content">
-            <el-row :gutter="10" class="mb-[10px]">
-              <el-col :span="1.5">
-                <el-button v-hasPermi="['system:menu:add']" type="primary" plain icon="Plus"
-                           @click="handleAdd()">新增</el-button>
-              </el-col>
-              <el-col :span="1.5">
-                <el-button v-hasPermi="['system:menu:remove']" type="danger" plain icon="Delete"
-                           @click="handleCascadeDelete" :loading="deleteLoading">级联删除</el-button>
-              </el-col>
-              <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
-            </el-row>
-
-            <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
-                        :leave-active-class="proxy?.animate.searchAnimate.leave">
-              <div v-show="showSearch" class="mb-[10px]">
-                <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-                  <el-form-item label="菜单名称" prop="menuName">
-                    <el-input v-model="queryParams.menuName" placeholder="请输入菜单名称" clearable
-                              @keyup.enter="handleQuery" />
-                  </el-form-item>
-                  <el-form-item label="状态" prop="status">
-                    <el-select v-model="queryParams.status" placeholder="菜单状态" clearable>
-                      <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label"
-                                 :value="dict.value" />
-                    </el-select>
-                  </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>
-              </div>
-            </transition>
-
-            <el-table ref="menuTableRef" v-loading="loading" :data="menuList" row-key="menuId" border
-                      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" :default-expand-all="false" lazy
-                      :load="getChildrenList" :expand-change="expandMenuHandle">
-              <el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
-              <el-table-column prop="icon" label="图标" align="center" width="100">
-                <template #default="scope">
-                  <svg-icon :icon-class="scope.row.icon" />
-                </template>
-              </el-table-column>
-              <el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
-              <el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
-              <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
-              <el-table-column prop="status" label="状态" width="80">
-                <template #default="scope">
-                  <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
-                </template>
-              </el-table-column>
-              <el-table-column label="创建时间" align="center" prop="createTime">
-                <template #default="scope">
-                  <span>{{ scope.row.createTime }}</span>
-                </template>
-              </el-table-column>
-              <el-table-column fixed="right" label="操作" width="180">
-                <template #default="scope">
-                  <el-tooltip content="修改" placement="top">
-                    <el-button v-hasPermi="['system:menu:edit']" link type="primary" icon="Edit"
-                               @click="handleUpdate(scope.row)" />
-                  </el-tooltip>
-                  <el-tooltip content="新增" placement="top">
-                    <el-button v-hasPermi="['system:menu:add']" link type="primary" icon="Plus"
-                               @click="handleAdd(scope.row)" />
-                  </el-tooltip>
-                  <el-tooltip content="删除" placement="top">
-                    <el-button v-hasPermi="['system:menu:remove']" link type="primary" icon="Delete"
-                               @click="handleDelete(scope.row)" />
-                  </el-tooltip>
-                </template>
-              </el-table-column>
-            </el-table>
+  <div class="page-container">
+    <el-card shadow="never" class="table-card">
+      <template #header>
+        <div class="card-header">
+          <div class="header-left">
+            <span class="title">菜单管理</span>
           </div>
-        </el-tab-pane>
-        <el-tab-pane label="商户后台" :name="1">
-          <!-- 商户后台内容 -->
-          <div class="tab-content">
-            <el-row :gutter="10" class="mb-[10px]">
-              <el-col :span="1.5">
-                <el-button v-hasPermi="['system:menu:add']" type="primary" plain icon="Plus"
-                           @click="handleAdd()">新增</el-button>
-              </el-col>
-              <el-col :span="1.5">
-                <el-button v-hasPermi="['system:menu:remove']" type="danger" plain icon="Delete"
-                           @click="handleCascadeDelete" :loading="deleteLoading">级联删除</el-button>
-              </el-col>
-              <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
-            </el-row>
-
-            <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
-                        :leave-active-class="proxy?.animate.searchAnimate.leave">
-              <div v-show="showSearch" class="mb-[10px]">
-                <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-                  <el-form-item label="菜单名称" prop="menuName">
-                    <el-input v-model="queryParams.menuName" placeholder="请输入菜单名称" clearable
-                              @keyup.enter="handleQuery" />
-                  </el-form-item>
-                  <el-form-item label="状态" prop="status">
-                    <el-select v-model="queryParams.status" placeholder="菜单状态" clearable>
-                      <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label"
-                                 :value="dict.value" />
-                    </el-select>
-                  </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>
-              </div>
-            </transition>
-
-            <el-table ref="menuTableRef" v-loading="loading" :data="menuList" row-key="menuId" border
-                      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" :default-expand-all="false" lazy
-                      :load="getChildrenList" :expand-change="expandMenuHandle">
-              <el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
-              <el-table-column prop="icon" label="图标" align="center" width="100">
-                <template #default="scope">
-                  <svg-icon :icon-class="scope.row.icon" />
-                </template>
-              </el-table-column>
-              <el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
-              <el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
-              <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
-              <el-table-column prop="status" label="状态" width="80">
-                <template #default="scope">
-                  <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
-                </template>
-              </el-table-column>
-              <el-table-column label="创建时间" align="center" prop="createTime">
-                <template #default="scope">
-                  <span>{{ scope.row.createTime }}</span>
-                </template>
-              </el-table-column>
-              <el-table-column fixed="right" label="操作" width="180">
-                <template #default="scope">
-                  <el-tooltip content="修改" placement="top">
-                    <el-button v-hasPermi="['system:menu:edit']" link type="primary" icon="Edit"
-                               @click="handleUpdate(scope.row)" />
-                  </el-tooltip>
-                  <el-tooltip content="新增" placement="top">
-                    <el-button v-hasPermi="['system:menu:add']" link type="primary" icon="Plus"
-                               @click="handleAdd(scope.row)" />
-                  </el-tooltip>
-                  <el-tooltip content="删除" placement="top">
-                    <el-button v-hasPermi="['system:menu:remove']" link type="primary" icon="Delete"
-                               @click="handleDelete(scope.row)" />
-                  </el-tooltip>
-                </template>
-              </el-table-column>
-            </el-table>
+          <div class="header-right">
+            <el-input
+              v-model="queryParams.menuName"
+              placeholder="搜索菜单名称"
+              class="search-input"
+              prefix-icon="Search"
+              clearable
+              @keyup.enter="handleQuery"
+            />
+            <el-select v-model="queryParams.status" placeholder="状态" class="status-select" clearable @change="handleQuery">
+              <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+            </el-select>
+            <el-button type="primary" icon="Plus" @click="handleAdd()">新增菜单</el-button>
+            <el-button type="danger" plain icon="Delete" @click="handleCascadeDelete" :loading="deleteLoading">级联删除</el-button>
           </div>
-        </el-tab-pane>
+        </div>
+      </template>
+
+      <el-tabs v-model="platformId" @tab-change="handlePlatformChange" class="custom-tabs">
+        <el-tab-pane label="管理后台" :name="0" />
+        <el-tab-pane label="商户后台" :name="1" />
       </el-tabs>
+
+      <el-table
+        ref="menuTableRef"
+        v-loading="loading"
+        :data="menuList"
+        row-key="menuId"
+        style="width: 100%"
+        :header-cell-style="{ background: '#f8f9fb', color: '#606266' }"
+        :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+        lazy
+        :load="getChildrenList"
+        @expand-change="expandMenuHandle"
+      >
+        <el-table-column prop="menuName" label="菜单名称" min-width="180" :show-overflow-tooltip="true">
+          <template #default="scope">
+            <span class="menu-name-text">{{ scope.row.menuName }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="icon" label="图标" align="center" width="80">
+          <template #default="scope">
+            <div class="icon-wrapper">
+              <svg-icon :icon-class="scope.row.icon" />
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="orderNum" label="排序" align="center" width="80" />
+        <el-table-column prop="perms" label="权限标识" min-width="150" :show-overflow-tooltip="true">
+          <template #default="scope">
+            <code class="perms-code" v-if="scope.row.perms">{{ scope.row.perms }}</code>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="component" label="组件路径" min-width="180" :show-overflow-tooltip="true">
+          <template #default="scope">
+            <span class="path-text">{{ scope.row.component || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="status" label="状态" align="center" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.status === '0' ? 'success' : 'danger'" size="small" effect="light" class="status-tag">
+              {{ scope.row.status === '0' ? '正常' : '停用' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" align="center" width="180" prop="createTime" />
+        <el-table-column label="操作" align="right" width="180" fixed="right">
+          <template #default="scope">
+            <div class="op-btns">
+              <el-button link type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
+              <el-button link type="primary" @click="handleAdd(scope.row)">新增</el-button>
+              <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
     </el-card>
 
-    <el-dialog v-model="dialog.visible" :title="dialog.title" destroy-on-close append-to-bod width="750px">
+    <!-- 添加或修改菜单对话框 -->
+    <el-dialog v-model="dialog.visible" :title="dialog.title" destroy-on-close append-to-body width="750px">
       <el-form ref="menuFormRef" :model="form" :rules="rules" label-width="100px">
         <el-row>
           <el-col :span="24">
             <el-form-item label="上级菜单">
               <el-tree-select v-model="form.parentId" :data="menuOptions"
                               :props="{ value: 'menuId', label: 'menuName', children: 'children' } as any" value-key="menuId"
-                              placeholder="选择上级菜单" check-strictly />
+                              placeholder="选择上级菜单" check-strictly style="width: 100%" />
             </el-form-item>
           </el-col>
           <el-col :span="24">
             <el-form-item label="菜单类型" prop="menuType">
               <el-radio-group v-model="form.menuType">
-                <el-radio value="M">目录</el-radio>
-                <el-radio value="C">菜单</el-radio>
-                <el-radio value="F">按钮</el-radio>
+                <el-radio-button value="M">目录</el-radio-button>
+                <el-radio-button value="C">菜单</el-radio-button>
+                <el-radio-button value="F">按钮</el-radio-button>
               </el-radio-group>
             </el-form-item>
           </el-col>
           <el-col v-if="form.menuType !== 'F'" :span="24">
             <el-form-item label="菜单图标" prop="icon">
-              <!-- 图标选择器 -->
               <icon-select v-model="form.icon" />
             </el-form-item>
           </el-col>
@@ -195,19 +117,11 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="显示排序" prop="orderNum">
-              <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
+              <el-input-number v-model="form.orderNum" controls-position="right" :min="0" style="width: 100%" />
             </el-form-item>
           </el-col>
           <el-col v-if="form.menuType !== 'F'" :span="12">
-            <el-form-item>
-              <template #label>
-                <span>
-                  <el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
-                    <el-icon>
-                      <question-filled />
-                    </el-icon> </el-tooltip>是否外链
-                </span>
-              </template>
+            <el-form-item label="是否外链">
               <el-radio-group v-model="form.isFrame">
                 <el-radio value="0">是</el-radio>
                 <el-radio value="1">否</el-radio>
@@ -219,11 +133,8 @@
               <template #label>
                 <span>
                   <el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
-                    <el-icon>
-                      <question-filled />
-                    </el-icon>
-                  </el-tooltip>
-                  路由地址
+                    <el-icon><question-filled /></el-icon>
+                  </el-tooltip> 路由地址
                 </span>
               </template>
               <el-input v-model="form.path" placeholder="请输入路由地址" />
@@ -234,58 +145,20 @@
               <template #label>
                 <span>
                   <el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
-                    <el-icon>
-                      <question-filled />
-                    </el-icon>
-                  </el-tooltip>
-                  组件路径
+                    <el-icon><question-filled /></el-icon>
+                  </el-tooltip> 组件路径
                 </span>
               </template>
               <el-input v-model="form.component" placeholder="请输入组件路径" />
             </el-form-item>
           </el-col>
           <el-col v-if="form.menuType !== 'M'" :span="12">
-            <el-form-item>
+            <el-form-item label="权限字符">
               <el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
-              <template #label>
-                <span>
-                  <el-tooltip content="控制器中定义的权限字符,如:@SaCheckPermission('system:user:list')" placement="top">
-                    <el-icon>
-                      <question-filled />
-                    </el-icon>
-                  </el-tooltip>
-                  权限字符
-                </span>
-              </template>
             </el-form-item>
           </el-col>
           <el-col v-if="form.menuType === 'C'" :span="12">
-            <el-form-item>
-              <el-input v-model="form.queryParam" placeholder="请输入路由参数" maxlength="255" />
-              <template #label>
-                <span>
-                  <el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
-                    <el-icon>
-                      <question-filled />
-                    </el-icon>
-                  </el-tooltip>
-                  路由参数
-                </span>
-              </template>
-            </el-form-item>
-          </el-col>
-          <el-col v-if="form.menuType === 'C'" :span="12">
-            <el-form-item>
-              <template #label>
-                <span>
-                  <el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
-                    <el-icon>
-                      <question-filled />
-                    </el-icon>
-                  </el-tooltip>
-                  是否缓存
-                </span>
-              </template>
+            <el-form-item label="是否缓存">
               <el-radio-group v-model="form.isCache">
                 <el-radio value="0">缓存</el-radio>
                 <el-radio value="1">不缓存</el-radio>
@@ -293,75 +166,40 @@
             </el-form-item>
           </el-col>
           <el-col v-if="form.menuType !== 'F'" :span="12">
-            <el-form-item>
-              <template #label>
-                <span>
-                  <el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
-                    <el-icon>
-                      <question-filled />
-                    </el-icon>
-                  </el-tooltip>
-                  显示状态
-                </span>
-              </template>
+            <el-form-item label="显示状态">
               <el-radio-group v-model="form.visible">
-                <el-radio v-for="dict in sys_show_hide" :key="dict.value" :value="dict.value">{{ dict.label }}
-                </el-radio>
+                <el-radio v-for="dict in sys_show_hide" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
               </el-radio-group>
             </el-form-item>
           </el-col>
           <el-col :span="12">
-            <el-form-item>
-              <template #label>
-                <span>
-                  <el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
-                    <el-icon>
-                      <question-filled />
-                    </el-icon>
-                  </el-tooltip>
-                  菜单状态
-                </span>
-              </template>
+            <el-form-item label="菜单状态">
               <el-radio-group v-model="form.status">
-                <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">
-                  {{ dict.label }}
-                </el-radio>
+                <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
               </el-radio-group>
             </el-form-item>
           </el-col>
-          <el-col v-if="form.visible !== '0'" :span="12">
-            <el-form-item label="激活路径" prop="form.remark">
-              <template #label>
-                <span>
-                  <el-tooltip content="隐藏菜单填写默认激活路由,比如激活父菜单的路由 /system/user" placement="top">
-                    <el-icon>
-                      <question-filled />
-                    </el-icon>
-                  </el-tooltip>
-                  激活路由
-                </span>
-              </template>
-              <el-input v-model="form.remark" placeholder="请输入激活路径" />
-            </el-form-item>
-          </el-col>
         </el-row>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">确 定</el-button>
           <el-button @click="cancel">取 消</el-button>
+          <el-button type="primary" @click="submitForm">确 定</el-button>
         </div>
       </template>
     </el-dialog>
 
-    <el-dialog v-model="deleteDialog.visible" :title="deleteDialog.title" destroy-on-close append-to-bod width="750px">
-      <el-tree ref="menuTreeRef" class="tree-border" :data="menuOptions" show-checkbox node-key="menuId"
-               :check-strictly="false" empty-text="加载中,请稍候" :default-expanded-keys="[0]"
-               :props="{ value: 'menuId', label: 'menuName', children: 'children' } as any" />
+    <!-- 级联删除对话框 -->
+    <el-dialog v-model="deleteDialog.visible" :title="deleteDialog.title" destroy-on-close append-to-body width="600px">
+      <div class="delete-tree-container">
+        <el-tree ref="menuTreeRef" class="tree-border" :data="menuOptions" show-checkbox node-key="menuId"
+                 :check-strictly="false" empty-text="加载中,请稍候" :default-expanded-keys="[0]"
+                 :props="{ value: 'menuId', label: 'menuName', children: 'children' } as any" />
+      </div>
       <template #footer>
         <div class="dialog-footer">
-          <el-button type="primary" @click="submitDeleteForm" :loading="deleteLoading">确 定</el-button>
           <el-button @click="cancelCascade">取 消</el-button>
+          <el-button type="primary" @click="submitDeleteForm" :loading="deleteLoading">确 定</el-button>
         </div>
       </template>
     </el-dialog>
@@ -382,12 +220,11 @@ interface MenuOptionsType {
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { sys_show_hide, sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_show_hide', 'sys_normal_disable'));
 
-const platformId = ref(0); // 默认选中管理后台,使用数字类型
+const platformId = ref(0);
 const menuList = ref<MenuVO[]>([]);
 const menuChildrenListMap = ref({});
 const menuExpandMap = ref({});
 const loading = ref(true);
-const showSearch = ref(true);
 const menuOptions = ref<MenuOptionsType[]>([]);
 
 const dialog = reactive<DialogOption>({
@@ -395,7 +232,6 @@ const dialog = reactive<DialogOption>({
   title: ''
 });
 
-const queryFormRef = ref<ElFormInstance>();
 const menuFormRef = ref<ElFormInstance>();
 const initFormData = {
   path: '',
@@ -424,29 +260,24 @@ const data = reactive<PageData<MenuForm, MenuQuery>>({
 });
 
 const menuTableRef = ref<ElTableInstance>();
-
 const { queryParams, form, rules } = toRefs<PageData<MenuForm, MenuQuery>>(data);
 
 /** 获取子菜单列表 */
 const getChildrenList = async (row: any, treeNode: unknown, resolve: (data: any[]) => void) => {
   menuExpandMap.value[row.menuId] = { row, treeNode, resolve };
   const children = menuChildrenListMap.value[row.menuId] || [];
-  // 菜单的子菜单清空后关闭展开
   if (children.length == 0) {
-    // fix: 处理当菜单只有一个子菜单并被删除,需要将父菜单的展开状态关闭
     menuTableRef.value?.updateKeyChildren(row.menuId, children);
   }
   resolve(children);
 };
 
-/** 收起菜单时从menuExpandMap中删除对应菜单id数据 */
 const expandMenuHandle = async (row: any, expanded: boolean) => {
   if (!expanded) {
     menuExpandMap.value[row.menuId] = undefined;
   }
 };
 
-/** 刷新展开的菜单数据 */
 const refreshLoadTree = (parentId: string | number) => {
   if (menuExpandMap.value[parentId]) {
     const { row, treeNode, resolve } = menuExpandMap.value[parentId];
@@ -454,52 +285,41 @@ const refreshLoadTree = (parentId: string | number) => {
       getChildrenList(row, treeNode, resolve);
       if (row.parentId) {
         const grandpaMenu = menuExpandMap.value[row.parentId];
-        getChildrenList(grandpaMenu.row, grandpaMenu.treeNode, grandpaMenu.resolve);
+        if (grandpaMenu) getChildrenList(grandpaMenu.row, grandpaMenu.treeNode, grandpaMenu.resolve);
       }
     }
   }
 };
 
-/** 重新加载所有已展开的菜单的数据 */
 const refreshAllExpandMenuData = () => {
   for (const menuId in menuExpandMap.value) {
     refreshLoadTree(menuId);
   }
 };
 
-/** 查询菜单列表 */
 const getList = async () => {
   loading.value = true;
-  const params = {
-    ...queryParams.value,
-    platformId: platformId.value
-  };
+  const params = { ...queryParams.value, platformId: platformId.value };
   const res = await listMenu(params);
 
   const tempMap = {};
-  // 存储 父菜单:子菜单列表
   for (const menu of res.data) {
     const parentId = menu.parentId;
-    if (!tempMap[parentId]) {
-      tempMap[parentId] = [];
-    }
+    if (!tempMap[parentId]) tempMap[parentId] = [];
     tempMap[parentId].push(menu);
   }
-  // 创建一个当前所有 menuId 的 Set,用于查找父菜单是否存在于当前数据中
+  
   const menuIdSet = new Set();
-  // 设置有没有子菜单
   for (const menu of res.data) {
     menu['hasChildren'] = tempMap[menu.menuId]?.length > 0;
     menuIdSet.add(menu.menuId);
   }
   menuChildrenListMap.value = tempMap;
-  // 找出所有父ID不在当前菜单ID集合中的菜单项,作为新的顶层菜单
   menuList.value = res.data.filter((menu) => !menuIdSet.has(menu.parentId));
-  // 根据新数据重新加载子菜单数据
   refreshAllExpandMenuData();
   loading.value = false;
 };
-/** 查询菜单下拉树结构 */
+
 const getTreeselect = async () => {
   menuOptions.value = [];
   const response = await listMenu({ platformId: platformId.value });
@@ -507,43 +327,55 @@ const getTreeselect = async () => {
   menu.children = proxy?.handleTree<MenuOptionsType>(response.data, 'menuId');
   menuOptions.value.push(menu);
 };
-/** 取消按钮 */
+
 const cancel = () => {
   reset();
   dialog.visible = false;
 };
-/** 表单重置 */
+
 const reset = () => {
   form.value = { ...initFormData };
   menuFormRef.value?.resetFields();
 };
 
-/** 平台切换操作 */
-const handlePlatformChange = (tab: any) => {
-  // 确保platformId的值与当前选中的选项卡一致
-  platformId.value = Number(tab.props.name);
+const handlePlatformChange = () => {
   getList();
 };
 
-/** 搜索按钮操作 */
 const handleQuery = () => {
   getList();
 };
 
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value?.resetFields();
-  handleQuery();
-};
 /** 新增按钮操作 */
 const handleAdd = (row?: MenuVO) => {
   reset();
   getTreeselect();
-  row && row.menuId ? (form.value.parentId = row.menuId) : (form.value.parentId = 0);
+  if (row && row.menuId) {
+    form.value.parentId = row.menuId;
+
+    // 1. 自动填充权限标识前缀(提取父级权限的前两段)
+    if (row.perms) {
+      const parts = row.perms.split(':');
+      if (parts.length >= 2) {
+        form.value.perms = parts[0] + ':' + parts[1] + ':';
+      }
+    }
+
+    // 2. 自动计算排序(父级下子项最大排序值 + 1)
+    const children = menuChildrenListMap.value[row.menuId] || [];
+    if (children.length > 0) {
+      const maxOrder = Math.max(...children.map((c: any) => c.orderNum || 0));
+      form.value.orderNum = maxOrder + 1;
+    } else {
+      form.value.orderNum = 1;
+    }
+  } else {
+    form.value.parentId = 0;
+  }
   dialog.visible = true;
   dialog.title = '添加菜单';
 };
-/** 修改按钮操作 */
+
 const handleUpdate = async (row: MenuVO) => {
   reset();
   await getTreeselect();
@@ -554,14 +386,11 @@ const handleUpdate = async (row: MenuVO) => {
   dialog.visible = true;
   dialog.title = '修改菜单';
 };
-/** 提交按钮 */
+
 const submitForm = () => {
   menuFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
-      const menuData = {
-        ...form.value,
-        platformId: platformId.value
-      };
+      const menuData = { ...form.value, platformId: platformId.value };
       form.value.menuId ? await updateMenu(menuData) : await addMenu(menuData);
       proxy?.$modal.msgSuccess('操作成功');
       dialog.visible = false;
@@ -569,9 +398,9 @@ const submitForm = () => {
     }
   });
 };
-/** 删除按钮操作 */
+
 const handleDelete = async (row: MenuVO) => {
-  await proxy?.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?');
+  await proxy?.$modal.confirm(`是否确认删除名称为"${row.menuName}"的数据项?`);
   await delMenu(row.menuId, platformId.value);
   await getList();
   proxy?.$modal.msgSuccess('删除成功');
@@ -579,33 +408,26 @@ const handleDelete = async (row: MenuVO) => {
 
 const deleteLoading = ref<boolean>(false);
 const menuTreeRef = ref<ElTreeInstance>();
-
 const deleteDialog = reactive<DialogOption>({
   visible: false,
   title: '级联删除菜单'
 });
 
-/** 级联删除按钮操作 */
 const handleCascadeDelete = () => {
-  menuTreeRef.value?.setCheckedKeys([]);
   getTreeselect();
   deleteDialog.visible = true;
 };
 
-/** 取消按钮 */
 const cancelCascade = () => {
-  menuTreeRef.value?.setCheckedKeys([]);
   deleteDialog.visible = false;
 };
 
-/** 删除提交按钮 */
 const submitDeleteForm = async () => {
   const menuIds = menuTreeRef.value?.getCheckedKeys();
-  if (menuIds.length < 0) {
+  if (!menuIds || menuIds.length === 0) {
     proxy?.$modal.msgWarning('请选择要删除的菜单');
     return;
   }
-
   deleteLoading.value = true;
   await cascadeDelMenu(menuIds, platformId.value).finally(() => (deleteLoading.value = false));
   await getList();
@@ -619,8 +441,107 @@ onMounted(() => {
 </script>
 
 <style scoped lang="scss">
-.tree-border {
-  height: 300px;
-  overflow: auto;
+.page-container {
+  padding: 20px;
+  background-color: #f5f7f9;
+  min-height: 100%;
+}
+
+.table-card {
+  border: none;
+  border-radius: 8px;
+  
+  :deep(.el-card__header) {
+    padding: 20px 24px;
+    border-bottom: 1px solid #f0f0f0;
+  }
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.header-left {
+  .title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333;
+  }
+}
+
+.header-right {
+  display: flex;
+  gap: 12px;
+  
+  .search-input { width: 220px; }
+  .status-select { width: 120px; }
+  
+  :deep(.el-input__wrapper) {
+    background-color: #f4f5f7;
+    box-shadow: none;
+    border: 1px solid transparent;
+    
+    &:hover, &.is-focus {
+      border-color: #409eff;
+      background-color: #fff;
+    }
+  }
+}
+
+.custom-tabs {
+  margin-bottom: 16px;
+  :deep(.el-tabs__nav-wrap::after) { height: 1px; }
+  :deep(.el-tabs__header) { margin-bottom: 0; }
+}
+
+.menu-name-text {
+  font-weight: 600;
+  color: #333;
+}
+
+.icon-wrapper {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 18px;
+  color: #606266;
+}
+
+.perms-code {
+  background-color: #f0f2f5;
+  padding: 2px 6px;
+  border-radius: 4px;
+  font-family: monospace;
+  font-size: 12px;
+  color: #e6a23c;
+}
+
+.path-text {
+  color: #909399;
+  font-size: 13px;
+}
+
+.status-tag { border: none; font-weight: 500; }
+
+.op-btns {
+  display: flex;
+  justify-content: flex-end;
+  gap: 16px;
+}
+
+.delete-tree-container {
+  max-height: 450px;
+  overflow-y: auto;
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  padding: 10px;
+}
+
+:deep(.el-table) {
+  --el-table-border-color: #f0f0f0;
+  th.el-table__cell { font-weight: 600; }
+  td.el-table__cell { padding: 12px 0; }
 }
 </style>

+ 239 - 294
src/views/system/oss/config.vue

@@ -1,186 +1,96 @@
 <template>
-  <div class="p-2">
-    <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="配置key" prop="configKey">
-              <el-input v-model="queryParams.configKey" placeholder="配置key" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="桶名称" prop="bucketName">
-              <el-input v-model="queryParams.bucketName" placeholder="请输入桶名称" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="是否默认" prop="status">
-              <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
-                <el-option key="0" label="是" value="0" />
-                <el-option key="1" label="否" value="1" />
-              </el-select>
-            </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-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:ossConfig:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:ossConfig:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
-              >修改</el-button
-            >
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:ossConfig:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
-              删除
-            </el-button>
-          </el-col>
-          <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
-        </el-row>
-      </template>
-
-      <el-table v-loading="loading" border :data="ossConfigList" @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column v-if="columns[0].visible" label="主建" align="center" prop="ossConfigId" />
-        <el-table-column v-if="columns[1].visible" label="配置key" align="center" prop="configKey" />
-        <el-table-column v-if="columns[2].visible" label="访问站点" align="center" prop="endpoint" width="200" />
-        <el-table-column v-if="columns[3].visible" label="自定义域名" align="center" prop="domain" width="200" />
-        <el-table-column v-if="columns[4].visible" label="桶名称" align="center" prop="bucketName" />
-        <el-table-column v-if="columns[5].visible" label="前缀" align="center" prop="prefix" />
-        <el-table-column v-if="columns[6].visible" label="域" align="center" prop="region" />
-        <el-table-column v-if="columns[7].visible" label="桶权限类型" align="center" prop="accessPolicy">
-          <template #default="scope">
-            <el-tag v-if="scope.row.accessPolicy === '0'" type="warning">private</el-tag>
-            <el-tag v-if="scope.row.accessPolicy === '1'" type="success">public</el-tag>
-            <el-tag v-if="scope.row.accessPolicy === '2'" type="info">custom</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column v-if="columns[8].visible" label="是否默认" align="center" prop="status">
-          <template #default="scope">
-            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" fixed="right" align="center" width="150" class-name="small-padding">
-          <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button v-hasPermi="['system:ossConfig:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
-            </el-tooltip>
-            <el-tooltip content="删除" placement="top">
-              <el-button v-hasPermi="['system:ossConfig:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
-            </el-tooltip>
-          </template>
-        </el-table-column>
-      </el-table>
-
-      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
-    </el-card>
-    <!-- 添加或修改对象存储配置对话框 -->
-    <el-dialog v-model="dialog.visible" :title="dialog.title" width="800px" append-to-body>
-      <el-form ref="ossConfigFormRef" :model="form" :rules="rules" label-width="120px">
-        <el-form-item label="配置key" prop="configKey">
-          <el-input v-model="form.configKey" placeholder="请输入配置key" />
-        </el-form-item>
-        <el-form-item label="访问站点" prop="endpoint">
-          <el-input v-model="form.endpoint" placeholder="请输入访问站点">
-            <template #prefix>
-              <span style="color: #999">{{ protocol }}</span>
-            </template>
-          </el-input>
+  <div class="oss-config-setting">
+    <!-- 顶部业务提醒 -->
+    <div class="setting-hint">
+      <el-alert :closable="false" type="info" class="custom-alert">
+        <template #title>
+          <div class="alert-content">
+            <p>1. 上线后更换请慎重!存储在哪一方迁移都是很费时间的;</p>
+            <p>2. 将 public 下面的 static 中的文件全部上传到存储空间;</p>
+            <p>3. 本地存储需要将图片域名绑定到 public/upload 目录,否则无法上传。</p>
+          </div>
+        </template>
+      </el-alert>
+    </div>
+
+    <!-- 存储配置表单 -->
+    <div class="setting-body">
+      <el-form ref="ossFormRef" :model="form" :rules="rules" label-width="140px" class="premium-setting-form">
+        
+        <!-- 存储设置选择 -->
+        <el-form-item label="存储设置:">
+          <el-radio-group v-model="form.configKey" @change="handleTypeChange" class="custom-radio-group">
+            <el-radio label="local" border>本地存储</el-radio>
+            <el-radio label="qiniu" border>七牛云</el-radio>
+            <el-radio label="aliyun" border>阿里云</el-radio>
+            <el-radio label="tencent" border>腾讯云</el-radio>
+          </el-radio-group>
         </el-form-item>
-        <el-form-item label="自定义域名" prop="domain">
-          <el-input v-model="form.domain" placeholder="请输入自定义域名">
+
+        <!-- 通用字段:存储域名 -->
+        <el-form-item label="存储域名:" prop="domain">
+          <el-input v-model="form.domain" placeholder="格式 https://img.tpidea.cn" class="config-input">
             <template #prefix>
-              <span style="color: #999">{{ protocol }}</span>
+              <span class="protocol-prefix">{{ protocol }}</span>
             </template>
           </el-input>
+          <div class="form-tip" v-if="form.configKey === 'local'">对应服务器 public/upload 目录绑定的域名</div>
         </el-form-item>
-        <el-form-item label="accessKey" prop="accessKey">
-          <el-input v-model="form.accessKey" placeholder="请输入accessKey" />
-        </el-form-item>
-        <el-form-item label="secretKey" prop="secretKey">
-          <el-input v-model="form.secretKey" placeholder="请输入秘钥" show-password />
-        </el-form-item>
-        <el-form-item label="桶名称" prop="bucketName">
-          <el-input v-model="form.bucketName" placeholder="请输入桶名称" />
-        </el-form-item>
-        <el-form-item label="前缀" prop="prefix">
-          <el-input v-model="form.prefix" placeholder="请输入前缀" />
-        </el-form-item>
-        <el-form-item label="是否HTTPS">
-          <el-radio-group v-model="form.isHttps">
-            <el-radio v-for="dict in sys_yes_no" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="桶权限类型">
-          <el-radio-group v-model="form.accessPolicy">
-            <el-radio value="0">private</el-radio>
-            <el-radio value="1">public</el-radio>
-            <el-radio value="2">custom</el-radio>
-          </el-radio-group>
+
+        <!-- 云端特有字段 -->
+        <template v-if="form.configKey !== 'local'">
+          <el-form-item label="AccessKey:" prop="accessKey">
+            <el-input v-model="form.accessKey" placeholder="请输入云端存储 AccessKey" class="config-input" />
+          </el-form-item>
+
+          <el-form-item label="SecretKey:" prop="secretKey">
+            <el-input v-model="form.secretKey" type="password" show-password placeholder="请输入云端存储 SecretKey" class="config-input" />
+          </el-form-item>
+
+          <el-form-item label="桶名称:" prop="bucketName">
+            <el-input v-model="form.bucketName" placeholder="请输入存储桶(Bucket)名称" class="config-input" />
+          </el-form-item>
+
+          <el-form-item label="存储区域:" prop="region">
+            <el-input v-model="form.region" placeholder="如:cn-hangzhou, oss-cn-beijing" class="config-input" />
+          </el-form-item>
+
+          <el-form-item label="访问站点:" prop="endpoint">
+            <el-input v-model="form.endpoint" placeholder="请输入访问域名或站点 Endpoint" class="config-input" />
+          </el-form-item>
+        </template>
+
+        <!-- 其他配置 -->
+        <el-form-item label="前缀路径:" prop="prefix">
+          <el-input v-model="form.prefix" placeholder="如:upload, static" class="config-input" />
+          <div class="form-tip">选填,存储文件时的路径前缀</div>
         </el-form-item>
-        <el-form-item label="域" prop="region">
-          <el-input v-model="form.region" placeholder="请输入域" />
+
+        <el-form-item label="HTTPS 访问:" v-if="form.configKey !== 'local'">
+          <el-switch v-model="form.isHttps" active-value="Y" inactive-value="N" active-text="开启" inactive-text="关闭" />
         </el-form-item>
-        <el-form-item label="备注" prop="remark">
-          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+
+        <!-- 保存操作 -->
+        <el-form-item class="action-item">
+          <el-button type="primary" class="save-btn" :loading="buttonLoading" @click="submitForm">保存设置</el-button>
         </el-form-item>
       </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
-          <el-button @click="cancel">取 消</el-button>
-        </div>
-      </template>
-    </el-dialog>
+    </div>
   </div>
 </template>
 
 <script setup name="OssConfig" lang="ts">
-import { listOssConfig, getOssConfig, delOssConfig, addOssConfig, updateOssConfig, changeOssConfigStatus } from '@/api/system/ossConfig';
-import { OssConfigForm, OssConfigQuery, OssConfigVO } from '@/api/system/ossConfig/types';
+import { listOssConfig, addOssConfig, updateOssConfig } from '@/api/system/ossConfig';
+import { OssConfigForm } from '@/api/system/ossConfig/types';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { sys_yes_no } = toRefs<any>(proxy?.useDict('sys_yes_no'));
 
-const ossConfigList = ref<OssConfigVO[]>([]);
 const buttonLoading = ref(false);
-const loading = ref(true);
-const showSearch = ref(true);
-const ids = ref<Array<number | string>>([]);
-const single = ref(true);
-const multiple = ref(true);
-const total = ref(0);
-
-const queryFormRef = ref<ElFormInstance>();
-const ossConfigFormRef = ref<ElFormInstance>();
-
-const dialog = reactive<DialogOption>({
-  visible: false,
-  title: ''
-});
+const ossFormRef = ref<ElFormInstance>();
 
-// 列显隐信息
-const columns = ref<FieldOption[]>([
-  { key: 0, label: `主建`, visible: false },
-  { key: 1, label: `配置key`, visible: true },
-  { key: 2, label: `访问站点`, visible: true },
-  { key: 3, label: `自定义域名`, visible: true },
-  { key: 4, label: `桶名称`, visible: true },
-  { key: 5, label: `前缀`, visible: true },
-  { key: 6, label: `域`, visible: true },
-  { key: 7, label: `桶权限类型`, visible: true },
-  { key: 8, label: `状态`, visible: true }
-]);
-
-const initFormData: OssConfigForm = {
+const form = ref<OssConfigForm>({
   ossConfigId: undefined,
-  configKey: '',
+  configKey: 'local',
   accessKey: '',
   secretKey: '',
   bucketName: '',
@@ -190,155 +100,190 @@ const initFormData: OssConfigForm = {
   isHttps: 'N',
   accessPolicy: '1',
   region: '',
-  status: '1',
+  status: '0', // 默认设为启用/默认状态
   remark: ''
-};
-const data = reactive<PageData<OssConfigForm, OssConfigQuery>>({
-  form: { ...initFormData },
-  // 查询参数
-  queryParams: {
-    pageNum: 1,
-    pageSize: 10,
-    configKey: '',
-    bucketName: '',
-    status: ''
-  },
-  rules: {
-    configKey: [{ required: true, message: 'configKey不能为空', trigger: 'blur' }],
-    accessKey: [
-      { required: true, message: 'accessKey不能为空', trigger: 'blur' },
-      {
-        min: 2,
-        max: 200,
-        message: 'accessKey长度必须介于 2 和 100 之间',
-        trigger: 'blur'
-      }
-    ],
-    secretKey: [
-      { required: true, message: 'secretKey不能为空', trigger: 'blur' },
-      {
-        min: 2,
-        max: 100,
-        message: 'secretKey长度必须介于 2 和 100 之间',
-        trigger: 'blur'
-      }
-    ],
-    bucketName: [
-      { required: true, message: 'bucketName不能为空', trigger: 'blur' },
-      {
-        min: 2,
-        max: 100,
-        message: 'bucketName长度必须介于 2 和 100 之间',
-        trigger: 'blur'
-      }
-    ],
-    endpoint: [
-      { required: true, message: 'endpoint不能为空', trigger: 'blur' },
-      {
-        min: 2,
-        max: 100,
-        message: 'endpoint名称长度必须介于 2 和 100 之间',
-        trigger: 'blur'
-      }
-    ],
-    accessPolicy: [{ required: true, message: 'accessPolicy不能为空', trigger: 'blur' }]
-  }
 });
 
-const { queryParams, form, rules } = toRefs(data);
+const rules = {
+  domain: [{ required: true, message: "存储域名不能为空", trigger: "blur" }],
+  accessKey: [{ required: true, message: "AccessKey 不能为空", trigger: "blur" }],
+  secretKey: [{ required: true, message: "SecretKey 不能为空", trigger: "blur" }],
+  bucketName: [{ required: true, message: "桶名称不能为空", trigger: "blur" }],
+  endpoint: [{ required: true, message: "访问站点不能为空", trigger: "blur" }],
+};
 
 const protocol = computed(() => (form.value.isHttps === 'Y' ? 'https://' : 'http://'));
 
-/** 查询对象存储配置列表 */
-const getList = async () => {
-  loading.value = true;
-  const res = await listOssConfig(queryParams.value);
-  ossConfigList.value = res.rows;
-  total.value = res.total;
-  loading.value = false;
-};
-/** 取消按钮 */
-const cancel = () => {
-  dialog.visible = false;
-  reset();
-};
-/** 表单重置 */
-const reset = () => {
-  form.value = { ...initFormData };
-  ossConfigFormRef.value?.resetFields();
-};
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.value.pageNum = 1;
-  getList();
-};
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value?.resetFields();
-  handleQuery();
-};
-/** 选择条数  */
-const handleSelectionChange = (selection: OssConfigVO[]) => {
-  ids.value = selection.map((item) => item.ossConfigId);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
-};
-/** 新增按钮操作 */
-const handleAdd = () => {
-  reset();
-  dialog.visible = true;
-  dialog.title = '添加对象存储配置';
+/** 加载对应类型的配置 */
+const loadTypeConfig = async (configKey: string) => {
+  const res = await listOssConfig({ configKey } as any);
+  if (res.rows && res.rows.length > 0) {
+    Object.assign(form.value, res.rows[0]);
+  } else {
+    // 重置表单(保留当前 key)
+    const currentKey = form.value.configKey;
+    form.value = {
+      ossConfigId: undefined,
+      configKey: currentKey,
+      accessKey: '',
+      secretKey: '',
+      bucketName: '',
+      prefix: '',
+      endpoint: '',
+      domain: '',
+      isHttps: 'N',
+      accessPolicy: '1',
+      region: '',
+      status: '0',
+      remark: ''
+    };
+  }
 };
-/** 修改按钮操作 */
-const handleUpdate = async (row?: OssConfigVO) => {
-  reset();
-  const ossConfigId = row?.ossConfigId || ids.value[0];
-  const res = await getOssConfig(ossConfigId);
-  Object.assign(form.value, res.data);
-  dialog.visible = true;
-  dialog.title = '修改对象存储配置';
+
+/** 切换存储类型 */
+const handleTypeChange = (val: any) => {
+  loadTypeConfig(val);
 };
-/** 提交按钮 */
+
+/** 提交表单 */
 const submitForm = () => {
-  ossConfigFormRef.value?.validate(async (valid: boolean) => {
+  ossFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
       buttonLoading.value = true;
-      if (form.value.ossConfigId) {
-        await updateOssConfig(form.value).finally(() => (buttonLoading.value = false));
-      } else {
-        await addOssConfig(form.value).finally(() => (buttonLoading.value = false));
+      try {
+        // 保存并强制设为默认状态
+        form.value.status = '0';
+        if (form.value.ossConfigId) {
+          await updateOssConfig(form.value);
+        } else {
+          await addOssConfig(form.value);
+        }
+        proxy?.$modal.msgSuccess("存储配置已保存并激活");
+        await loadTypeConfig(form.value.configKey);
+      } finally {
+        buttonLoading.value = false;
       }
-      proxy?.$modal.msgSuccess('新增成功');
-      dialog.visible = false;
-      await getList();
     }
   });
 };
-/** 状态修改  */
-const handleStatusChange = async (row: OssConfigVO) => {
-  const text = row.status === '0' ? '启用' : '停用';
-  try {
-    await proxy?.$modal.confirm('确认要"' + text + '""' + row.configKey + '"配置吗?');
-    await changeOssConfigStatus(row.ossConfigId, row.status, row.configKey);
-    await getList();
-    proxy?.$modal.msgSuccess(text + '成功');
-  } catch {
-    return;
-  } finally {
-    row.status = row.status === '0' ? '1' : '0';
+
+/** 初始化:加载已激活的配置项 */
+const initOssConfig = async () => {
+  const res = await listOssConfig({} as any);
+  if (res.rows && res.rows.length > 0) {
+    const activeItem = res.rows.find((item: any) => item.status === '0');
+    if (activeItem) {
+      form.value.configKey = activeItem.configKey;
+      Object.assign(form.value, activeItem);
+    } else {
+      loadTypeConfig('local');
+    }
+  } else {
+    loadTypeConfig('local');
   }
 };
-/** 删除按钮操作 */
-const handleDelete = async (row?: OssConfigVO) => {
-  const ossConfigIds = row?.ossConfigId || ids.value;
-  await proxy?.$modal.confirm('是否确认删除OSS配置编号为"' + ossConfigIds + '"的数据项?');
-  loading.value = true;
-  await delOssConfig(ossConfigIds).finally(() => (loading.value = false));
-  await getList();
-  proxy?.$modal.msgSuccess('删除成功');
-};
 
 onMounted(() => {
-  getList();
+  initOssConfig();
 });
 </script>
+
+<style scoped lang="scss">
+.oss-config-setting {
+  padding: 8px 0;
+}
+
+.setting-hint {
+  margin-bottom: 32px;
+  
+  .custom-alert {
+    background-color: #f0faff;
+    border: 1px solid #c2eaff;
+    border-radius: 8px;
+    padding: 12px 16px;
+
+    .alert-content {
+      color: #0089ff;
+      font-size: 14px;
+      line-height: 1.8;
+      
+      p {
+        margin: 0;
+      }
+    }
+  }
+}
+
+.setting-body {
+  padding-left: 20px;
+}
+
+.premium-setting-form {
+  :deep(.el-form-item__label) {
+    font-weight: 500;
+    color: #4e5969;
+    padding-right: 24px;
+  }
+
+  .config-input {
+    max-width: 520px;
+    
+    .protocol-prefix {
+      color: #909399;
+      font-size: 13px;
+    }
+
+    :deep(.el-input__wrapper) {
+      box-shadow: 0 0 0 1px #e5e6eb inset;
+      padding: 4px 12px;
+      border-radius: 6px;
+      
+      &:hover {
+        box-shadow: 0 0 0 1px #409eff inset;
+      }
+      
+      &.is-focus {
+        box-shadow: 0 0 0 1px #409eff inset;
+      }
+    }
+  }
+
+  .form-tip {
+    font-size: 13px;
+    color: #86909c;
+    margin-top: 8px;
+    line-height: 1.4;
+  }
+
+  .custom-radio-group {
+    :deep(.el-radio) {
+      margin-right: 16px;
+      border-radius: 6px;
+      padding: 0 20px;
+      height: 36px;
+      
+      &.is-bordered.is-checked {
+        border-color: #409eff;
+        background-color: rgba(64, 158, 255, 0.04);
+      }
+      
+      .el-radio__label {
+        font-size: 14px;
+      }
+    }
+  }
+
+  .action-item {
+    margin-top: 40px;
+  }
+
+  .save-btn {
+    padding: 12px 32px;
+    height: auto;
+    font-size: 15px;
+    font-weight: 500;
+    border-radius: 6px;
+    box-shadow: 0 4px 10px rgba(64, 158, 255, 0.2);
+  }
+}
+</style>

+ 3 - 3
src/views/system/role/authUser.vue

@@ -20,10 +20,10 @@
       <template #header>
         <el-row :gutter="10">
           <el-col :span="1.5">
-            <el-button v-hasPermi="['system:role:add']" type="primary" plain icon="Plus" @click="openSelectUser">添加用户</el-button>
+            <el-button type="primary" plain icon="Plus" @click="openSelectUser">添加用户</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button v-hasPermi="['system:role:remove']" type="danger" plain icon="CircleClose" :disabled="multiple" @click="cancelAuthUserAll">
+            <el-button type="danger" plain icon="CircleClose" :disabled="multiple" @click="cancelAuthUserAll">
               批量取消授权
             </el-button>
           </el-col>
@@ -52,7 +52,7 @@
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
           <template #default="scope">
             <el-tooltip content="取消授权" placement="top">
-              <el-button v-hasPermi="['system:role:remove']" link type="primary" icon="CircleClose" @click="cancelAuthUser(scope.row)"> </el-button>
+              <el-button link type="primary" icon="CircleClose" @click="cancelAuthUser(scope.row)"> </el-button>
             </el-tooltip>
           </template>
         </el-table-column>

+ 21 - 6
src/views/system/role/index.vue

@@ -7,7 +7,7 @@
             <span class="title">角色与权限管理</span>
           </div>
           <div class="header-right">
-            <el-button type="primary" icon="Plus" @click="handleAdd()">新增角色</el-button>
+            <el-button type="primary" icon="Plus" @click="handleAdd()" v-hasPermi="['system:role:add']">新增角色</el-button>
           </div>
         </div>
       </template>
@@ -17,14 +17,14 @@
         <el-table-column label="描述" prop="remark" min-width="300" :show-overflow-tooltip="true" />
         <el-table-column label="状态" width="100" align="center">
           <template #default="scope">
-            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
+            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)" v-hasPermi="['system:role:changeStatus']"></el-switch>
           </template>
         </el-table-column>
         <el-table-column label="操作" width="160" align="right" fixed="right">
           <template #default="scope">
             <div class="op-btns" v-if="scope.row.roleId !== 1">
-              <el-button v-hasPermi="['system:role:edit']" link type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
-              <el-button v-hasPermi="['system:role:remove']" link type="danger" @click="handleDelete(scope.row)">删除</el-button>
+              <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['system:role:edit']">编辑</el-button>
+              <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['system:role:remove']">删除</el-button>
             </div>
           </template>
         </el-table-column>
@@ -237,10 +237,25 @@ const handleAuthUser = (row: RoleVO) => {
   router.push('/system/role-auth/user/' + row.roleId);
 };
 
+/** 过滤不可见菜单 */
+const filterVisibleMenus = (menus: MenuTreeOption[]): MenuTreeOption[] => {
+  return menus
+    .filter((item) => item.visible !== '1')
+    .map((item) => {
+      if (item.children && item.children.length > 0) {
+        return {
+          ...item,
+          children: filterVisibleMenus(item.children)
+        };
+      }
+      return item;
+    });
+};
+
 /** 查询菜单树结构 */
 const getMenuTreeselect = async () => {
   const res = await menuTreeselect();
-  menuOptions.value = res.data;
+  menuOptions.value = filterVisibleMenus(res.data);
 };
 /** 所有部门节点数据 */
 const getDeptAllCheckedKeys = (): any => {
@@ -289,7 +304,7 @@ const handleUpdate = async (row: RoleVO) => {
 /** 根据角色ID查询菜单树结构 */
 const getRoleMenuTreeselect = (roleId: string | number) => {
   return roleMenuTreeselect(roleId).then((res): RoleMenuTree => {
-    menuOptions.value = res.data.menus;
+    menuOptions.value = filterVisibleMenus(res.data.menus);
     return res.data;
   });
 };

+ 316 - 350
src/views/system/store/index.vue

@@ -1,38 +1,47 @@
 <template>
-  <div class="p-2">
-    <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
-      :leave-active-class="proxy?.animate.searchAnimate.leave">
-      <div v-show="showSearch" class="mb-[10px] bg-white p-[20px] rounded-[4px] flex justify-between items-center shadow-sm" style="background-color: #fff; padding: 20px; border-radius: 4px; box-shadow: 0 1px 4px rgba(0,21,41,.08);">
-        <div class="text-[18px] font-bold text-[#303133]" style="font-size: 18px; font-weight: bold; color: #303133;">门店管理</div>
-        <div class="flex items-center gap-[10px]" style="display: flex; gap: 10px; align-items: center;">
-          <el-input v-model="queryParams.storeOrContact" placeholder="搜索门店名称/联系人" prefix-icon="Search"
-            style="width: 250px" clearable @keyup.enter="handleQuery" @clear="handleQuery" />
-          <el-cascader v-model="searchRegionValue" :options="areaOptions" placeholder="所属城市"
-            style="width: 150px" clearable @change="handleSearchAreaChange" />
-          <el-select v-model="queryParams.station" placeholder="所属站点" style="width: 150px" clearable @change="handleQuery">
-            <el-option v-for="site in searchSiteOptions" :key="site.value" :label="site.label" :value="site.value" />
-          </el-select>
-          <el-select v-model="queryParams.status" placeholder="状态" style="width: 120px" clearable @change="handleQuery">
-            <el-option v-for="item in statusList" :key="item.value" :label="item.label" :value="item.value" />
-          </el-select>
-          <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['system:store:add']">新增门店</el-button>
+  <div class="page-container">
+    <el-card shadow="never" class="table-card">
+      <template #header>
+        <div class="card-header">
+          <div class="header-left">
+            <span class="title">门店管理</span>
+          </div>
+          <div class="header-right">
+            <el-input
+              v-model="queryParams.storeOrContact"
+              placeholder="搜索门店名称/联系人"
+              class="search-input"
+              prefix-icon="Search"
+              clearable
+              @keyup.enter="handleQuery"
+            />
+            <el-cascader
+              v-model="searchRegionValue"
+              :options="areaOptions"
+              placeholder="所属城市"
+              class="region-select"
+              clearable
+              @change="handleSearchAreaChange"
+            />
+            <el-select v-model="queryParams.station" placeholder="所属站点" class="station-select" clearable @change="handleQuery" :disabled="!queryParams.area">
+              <el-option v-for="site in searchSiteOptions" :key="site.value" :label="site.label" :value="site.value" />
+            </el-select>
+            <el-select v-model="queryParams.status" placeholder="状态" class="status-select" clearable @change="handleQuery">
+              <el-option v-for="item in statusList" :key="item.value" :label="item.label" :value="item.value" />
+            </el-select>
+            <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['system:store:add']">新增门店</el-button>
+          </div>
         </div>
-      </div>
-    </transition>
-
-    <el-card shadow="never">
+      </template>
 
-      <el-table v-loading="loading" border :data="storeList" @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="门店信息" align="left" width="300">
+      <el-table v-loading="loading" :data="storeList" style="width: 100%" :header-cell-style="{ background: '#f8f9fb', color: '#606266' }">
+        <el-table-column label="门店信息" min-width="240">
           <template #default="scope">
-            <div class="store-info" style="display: flex; align-items: center; gap: 10px;">
-              <div class="store-logo">
-                <image-preview :src="scope.row.logoUrl" :width="50" :height="50" />
-              </div>
-              <div class="store-details" style="display: flex; flex-direction: column; gap: 6px;">
-                <div class="store-name" style="font-size: 14px; font-weight: 500; color: #303133;">{{ scope.row.name }}</div>
-                <div class="store-categories" style="display: flex; gap: 6px;">
+            <div class="store-info-box">
+              <image-preview :src="scope.row.logoUrl" :width="50" :height="50" class="store-logo" />
+              <div class="store-desc">
+                <div class="name">{{ scope.row.name }}</div>
+                <div class="tags">
                   <el-tag size="small" type="warning" effect="plain" v-if="scope.row.tenantName">{{ scope.row.tenantName }}</el-tag>
                   <el-tag size="small" type="success" effect="plain" v-if="scope.row.tenantCatergoriesName">{{ scope.row.tenantCatergoriesName }}</el-tag>
                 </div>
@@ -40,94 +49,106 @@
             </div>
           </template>
         </el-table-column>
-        <el-table-column label="资质认证" align="center" width="100">
-          <template #default="scope">
-            <image-preview v-if="scope.row.businessLicenseUrl" :src="scope.row.businessLicenseUrl" :width="40" :height="40" />
-            <span v-else>-</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="服务项目" align="center" width="200">
+
+        <el-table-column label="服务项目" min-width="180">
           <template #default="scope">
-            <div class="services">
-              <el-tag v-for="service in scope.row.services" :key="service" size="small"
-                style="margin-right: 5px; margin-bottom: 5px">
+            <div class="service-tags">
+              <el-tag v-for="service in scope.row.services" :key="service" size="small" effect="light" class="service-tag">
                 {{ getServiceName(service) }}
               </el-tag>
             </div>
           </template>
         </el-table-column>
-        <el-table-column label="归属站点" align="center" width="150">
-          <template #default="scope">
-            <div>{{ scope.row.siteName }}</div>
-          </template>
-        </el-table-column>
-        <el-table-column label="服务单" align="center" width="150">
+
+        <el-table-column label="资质认证" align="center" width="100">
           <template #default="scope">
-            <div>{{ scope.row.serviceOrder }}</div>
+            <image-preview v-if="scope.row.businessLicenseUrl" :src="scope.row.businessLicenseUrl" :width="40" :height="40" />
+            <span v-else>-</span>
           </template>
         </el-table-column>
-        <el-table-column label="营业时间" align="center" width="150">
+
+        <el-table-column label="归属区域/站点" min-width="150">
           <template #default="scope">
-            <div>{{ formatTime(scope.row.startBusinessTime) }}-{{ formatTime(scope.row.endBusinessTime) }}</div>
+            <div class="region-info">
+              <div class="region-name">{{ getRegionNameBySite(scope.row.site) }}</div>
+              <div class="site-name">
+                <el-icon><Location /></el-icon>
+                <span>{{ scope.row.siteName }}</span>
+              </div>
+            </div>
           </template>
         </el-table-column>
-        <el-table-column label="门店地址" align="center" width="300">
+
+        <el-table-column label="服务单" align="center" width="100" prop="serviceOrder" sortable />
+
+        <el-table-column label="营业时间" align="center" width="140">
           <template #default="scope">
-            <div>{{ getFullAddress(scope.row) }}</div>
+            <span class="time-text">{{ formatTime(scope.row.startBusinessTime) }},{{ formatTime(scope.row.endBusinessTime) }}</span>
           </template>
         </el-table-column>
-        <el-table-column label="联系方式" align="left" width="180">
+
+        <el-table-column label="联系方式" min-width="160">
           <template #default="scope">
-            <div style="display: flex; flex-direction: column; gap: 6px;">
-              <div style="display: flex; align-items: center; gap: 6px; color: #606266; font-size: 14px;">
-                <el-icon size="16"><User /></el-icon>
-                <span>{{ scope.row.contact }}</span>
+            <div class="contact-info">
+              <div class="contact-item">
+                <el-icon><User /></el-icon><span>{{ scope.row.contact }}</span>
               </div>
-              <div style="display: flex; align-items: center; gap: 6px; color: #409eff; font-size: 14px;">
-                <el-icon size="16"><Phone /></el-icon>
-                <span>{{ scope.row.contactNumber }}</span>
+              <div class="contact-item phone">
+                <el-icon><Phone /></el-icon><span>{{ scope.row.contactNumber }}</span>
               </div>
             </div>
           </template>
         </el-table-column>
-        <el-table-column label="有效期至" align="center" width="120">
+
+        <el-table-column label="门店地址" prop="detailAddress" min-width="200" show-overflow-tooltip>
           <template #default="scope">
-            <div>{{ parseTime(scope.row.validity, '{y}-{m}-{d}') }}</div>
+            {{ getFullAddress(scope.row) }}
           </template>
         </el-table-column>
-        <el-table-column label="状态" align="center" width="100">
+
+        <el-table-column label="状态" align="center" width="90">
           <template #default="scope">
             <template v-for="item in statusList" :key="item.value">
-              <el-tag v-if="scope.row.status === item.value" :type="item.style">{{ item.label }}</el-tag>
+              <el-tag v-if="scope.row.status === item.value" :type="item.style" size="small" effect="light">
+                {{ item.label }}
+              </el-tag>
             </template>
           </template>
         </el-table-column>
-        <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
+
+        <el-table-column label="操作" align="right" width="180" fixed="right">
           <template #default="scope">
-            <el-tooltip content="详情" placement="top">
-              <el-button link type="primary" icon="View" @click="handleDetail(scope.row)"></el-button>
-            </el-tooltip>
-            <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
-                v-hasPermi="['system:store:edit']"></el-button>
-            </el-tooltip>
-            <el-dropdown @command="(command: string) => handleCommand(command, scope.row)" v-hasPermi="['system:store:edit']">
-              <el-button link type="primary" icon="More"></el-button>
-              <template #dropdown>
-                <el-dropdown-menu>
-                  <el-dropdown-item command="handleRenew" icon="Calendar">续期</el-dropdown-item>
-                  <el-dropdown-item command="handleBan" icon="CircleClose" v-if="scope.row.status === 1">禁用</el-dropdown-item>
-                  <el-dropdown-item command="handleEnable" icon="CircleCheck" v-if="scope.row.status === 2">启用</el-dropdown-item>
-                </el-dropdown-menu>
-              </template>
-            </el-dropdown>
+            <div class="op-btns">
+              <el-button link type="primary" @click="handleDetail(scope.row)" v-hasPermi="['system:store:query']">详情</el-button>
+              <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['system:store:edit']">编辑</el-button>
+              <el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, scope.row)">
+                <el-button link type="primary" class="more-btn">
+                  更多<el-icon class="el-icon--right"><arrow-down /></el-icon>
+                </el-button>
+                <template #dropdown>
+                  <el-dropdown-menu>
+                    <el-dropdown-item command="handleRenew" v-hasPermi="['system:store:renew']">续期</el-dropdown-item>
+                    <el-dropdown-item v-if="scope.row.status === 1" command="handleBan" class="delete-item" v-hasPermi="['system:store:disable']">禁用</el-dropdown-item>
+                    <el-dropdown-item v-if="scope.row.status === 2" command="handleEnable" v-hasPermi="['system:store:enable']">启用</el-dropdown-item>
+                  </el-dropdown-menu>
+                </template>
+              </el-dropdown>
+            </div>
           </template>
         </el-table-column>
       </el-table>
 
-      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
-        v-model:limit="queryParams.pageSize" @pagination="getList" />
+      <div class="pagination-container">
+        <pagination
+          v-show="total > 0"
+          v-model:total="total"
+          v-model:page="queryParams.pageNum"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </div>
     </el-card>
+
     <!-- 添加或修改门店管理对话框 -->
     <el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
       <el-form ref="storeFormRef" :model="form" :rules="rules" label-width="120px">
@@ -148,31 +169,35 @@
           </el-checkbox-group>
         </el-form-item>
         <el-form-item label="商户分类" prop="tenantCatergories">
-          <PageSelect v-model="form.tenantCatergories"
+          <PageSelect
+            v-model="form.tenantCatergories"
             :options="tenantCategoriesList.map(item => ({ value: item.id, label: item.name }))"
-            :total="tenantCategoriesTotal" :pageSize="10" placeholder="请选择商户分类"
-            @page-change="handleTenantCategoriesPageChange" @visible-change="handleTenantCategoriesVisibleChange" />
+            :total="tenantCategoriesTotal"
+            :pageSize="10"
+            placeholder="请选择商户分类"
+            @page-change="handleTenantCategoriesPageChange"
+            @visible-change="handleTenantCategoriesVisibleChange"
+          />
         </el-form-item>
         <el-form-item label="所属品牌" prop="tenantId">
-          <PageSelect v-model="form.tenantId"
-            :options="brandList.map(item => ({ value: item.tenantId, label: item.name }))" :total="brandTotal"
-            :pageSize="10" placeholder="请选择所属品牌" @page-change="handleBrandPageChange"
-            @visible-change="handleBrandSelectVisibleChange" />
+          <PageSelect
+            v-model="form.tenantId"
+            :options="brandList.map(item => ({ value: item.tenantId, label: item.name }))"
+            :total="brandTotal"
+            :pageSize="10"
+            placeholder="请选择所属品牌"
+            @page-change="handleBrandPageChange"
+            @visible-change="handleBrandSelectVisibleChange"
+          />
         </el-form-item>
         <el-form-item label="营业时间" prop="startBusinessTime">
           <el-row :gutter="10">
             <el-col :span="10">
-              <el-time-picker clearable v-model="form.startBusinessTime" format="HH:mm" value-format="HH:mm" placeholder="开始时间"
-                style="width: 100%">
-              </el-time-picker>
-            </el-col>
-            <el-col :span="4" style="text-align: center; line-height: 40px">
-              至
+              <el-time-picker clearable v-model="form.startBusinessTime" format="HH:mm" value-format="HH:mm" placeholder="开始时间" style="width: 100%" />
             </el-col>
+            <el-col :span="4" style="text-align: center; line-height: 32px">至</el-col>
             <el-col :span="10">
-              <el-time-picker clearable v-model="form.endBusinessTime" format="HH:mm" value-format="HH:mm" placeholder="结束时间"
-                style="width: 100%">
-              </el-time-picker>
+              <el-time-picker clearable v-model="form.endBusinessTime" format="HH:mm" value-format="HH:mm" placeholder="结束时间" style="width: 100%" />
             </el-col>
           </el-row>
         </el-form-item>
@@ -183,20 +208,17 @@
           <el-input v-model="form.contactNumber" placeholder="请输入联系电话" />
         </el-form-item>
         <el-form-item label="有效期至" prop="validity">
-          <el-date-picker clearable v-model="form.validity" type="date" value-format="YYYY-MM-DD" placeholder="请选择有效期至"
-            style="width: 100%">
-          </el-date-picker>
+          <el-date-picker clearable v-model="form.validity" type="date" value-format="YYYY-MM-DD" placeholder="请选择有效期至" style="width: 100%" />
         </el-form-item>
         <el-row :gutter="10">
           <el-col :span="12">
             <el-form-item label="所在区域" prop="regionId">
-              <el-cascader v-model="regionValue" :options="areaOptions" placeholder="选择区域" style="width: 100%"
-                @change="handleAreaChange" />
+              <el-cascader v-model="regionValue" :options="areaOptions" placeholder="选择区域" style="width: 100%" @change="handleAreaChange" />
             </el-form-item>
           </el-col>
           <el-col :span="12">
             <el-form-item label="归属站点" prop="site">
-              <el-select v-model="form.site" placeholder="选择站点" :disabled="!form.regionId">
+              <el-select v-model="form.site" placeholder="选择站点" :disabled="!form.regionId" style="width: 100%">
                 <el-option v-for="site in siteOptions" :key="site.value" :label="site.label" :value="site.value" />
               </el-select>
             </el-form-item>
@@ -205,8 +227,7 @@
         <el-form-item label="详细地址">
           <el-row :gutter="10" style="margin-bottom: 10px">
             <el-col :span="24">
-              <el-cascader v-model="addressCascaderValue" :options="regionData" placeholder="选择省市区"
-                style="width: 100%" />
+              <el-cascader v-model="addressCascaderValue" :options="regionData" placeholder="选择省市区" style="width: 100%" />
             </el-col>
           </el-row>
           <el-input v-model="form.detailAddress" type="textarea" placeholder="输入详细地址" rows="3" style="width: 100%" />
@@ -229,8 +250,8 @@
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
           <el-button @click="cancel">取 消</el-button>
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
         </div>
       </template>
     </el-dialog>
@@ -302,8 +323,8 @@
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button type="primary" @click="submitRenew" :loading="renewLoading">确 定</el-button>
           <el-button @click="renewDialog.visible = false">取 消</el-button>
+          <el-button type="primary" @click="submitRenew" :loading="renewLoading">确 定</el-button>
         </div>
       </template>
     </el-dialog>
@@ -327,22 +348,15 @@ const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const storeList = ref<StoreVO[]>([]);
 const buttonLoading = ref(false);
 const loading = ref(true);
-const showSearch = ref(true);
-const ids = ref<Array<string | number>>([]);
-const single = ref(true);
-const multiple = ref(true);
 const total = ref(0);
 
-const queryFormRef = ref<ElFormInstance>();
 const storeFormRef = ref<ElFormInstance>();
-const brandSelectRef = ref<any>(null);
 
 const searchRegionValue = ref<any[]>([]); // 搜索的区域值
 const searchSiteOptions = ref<any[]>([]); // 搜索的站点选项
 
 /** 处理搜索区域选择变化 */
 const handleSearchAreaChange = (value: any[]) => {
-  // 清空级联站点
   queryParams.value.station = undefined;
 
   if (value && value.length > 0) {
@@ -358,10 +372,9 @@ const handleSearchAreaChange = (value: any[]) => {
     queryParams.value.area = undefined;
     searchSiteOptions.value = [];
   }
-  handleQuery();
+  // 移除 handleQuery(); 只选城市不发送请求
 };
 
-// 新增的响应式变量
 const regionValue = ref<any[]>([]);
 const province = ref('');
 const city = ref('');
@@ -381,8 +394,6 @@ const areaStationList = ref<SysAreaStationOnStoreVo[]>([]); // 区域站点列
 const areaOptions = ref<any[]>([]); // 所在区域树形选项
 const siteOptions = ref<any[]>([]); // 归属站点选项
 
-
-
 const dialog = reactive<DialogOption>({
   visible: false,
   title: ''
@@ -428,7 +439,6 @@ const getOrderList = async () => {
 /** 详情按钮操作 */
 const handleDetail = async (row: StoreVO) => {
   const res = await getStore(row.id);
-  // 合并列表里的关联数据,以便能够展示名称等额外字段
   detailData.value = { ...row, ...res.data };
   activeTab.value = 'basic';
   orderQueryParams.storeId = row.id;
@@ -467,7 +477,7 @@ const handleCommand = (command: string, row: StoreVO) => {
 /** 续期按钮操作 */
 const handleRenew = (row: StoreVO) => {
   renewForm.id = row.id;
-  renewForm.to = ''; // 清空之前的选择
+  renewForm.to = '';
   renewDialog.visible = true;
 };
 
@@ -500,7 +510,6 @@ const handleBan = async (row: StoreVO) => {
     proxy?.$modal.msgSuccess("禁用成功");
     getList();
   } catch (err) {
-    // 取消确认或请求失败
   } finally {
     loading.value = false;
   }
@@ -515,7 +524,6 @@ const handleEnable = async (row: StoreVO) => {
     proxy?.$modal.msgSuccess("启用成功");
     getList();
   } catch (err) {
-    // 取消确认或请求失败
   } finally {
     loading.value = false;
   }
@@ -551,46 +559,20 @@ const data = reactive<PageData<StoreForm, SysStorePageBo>>({
     area: undefined,
     station: undefined,
     status: undefined,
-    params: {
-    }
+    params: {}
   },
   rules: {
-    id: [
-      { required: true, message: "序号不能为空", trigger: "blur" }
-    ],
-    businessLicense: [
-      { required: true, message: "营业执照不能为空", trigger: "blur" }
-    ],
-    name: [
-      { required: true, message: "门店名称不能为空", trigger: "blur" }
-    ],
-    tenantCatergories: [
-      { required: true, message: "商户分类不能为空", trigger: "change" }
-    ],
-    startBusinessTime: [
-      { required: true, message: "开始营业时间不能为空", trigger: "blur" }
-    ],
-    endBusinessTime: [
-      { required: true, message: "结束营业时间不能为空", trigger: "blur" }
-    ],
-    contact: [
-      { required: true, message: "联系人不能为空", trigger: "blur" }
-    ],
-    contactNumber: [
-      { required: true, message: "联系电话不能为空", trigger: "blur" }
-    ],
-    validity: [
-      { required: true, message: "有效期至不能为空", trigger: "blur" }
-    ],
-    tenantId: [
-      { required: true, message: "租户编号不能为空", trigger: "change" }
-    ],
-    regionId: [
-      { required: true, message: "所在区域不能为空", trigger: "change" }
-    ],
-    site: [
-      { required: true, message: "归属站点不能为空", trigger: "change" }
-    ],
+    businessLicense: [{ required: true, message: "营业执照不能为空", trigger: "blur" }],
+    name: [{ required: true, message: "门店名称不能为空", trigger: "blur" }],
+    tenantCatergories: [{ required: true, message: "商户分类不能为空", trigger: "change" }],
+    startBusinessTime: [{ required: true, message: "开始营业时间不能为空", trigger: "blur" }],
+    endBusinessTime: [{ required: true, message: "结束营业时间不能为空", trigger: "blur" }],
+    contact: [{ required: true, message: "联系人不能为空", trigger: "blur" }],
+    contactNumber: [{ required: true, message: "联系电话不能为空", trigger: "blur" }],
+    validity: [{ required: true, message: "有效期至不能为空", trigger: "blur" }],
+    tenantId: [{ required: true, message: "所属品牌不能为空", trigger: "change" }],
+    regionId: [{ required: true, message: "所在区域不能为空", trigger: "change" }],
+    site: [{ required: true, message: "归属站点不能为空", trigger: "change" }],
   }
 });
 
@@ -614,7 +596,6 @@ const cancel = () => {
 /** 表单重置 */
 const reset = () => {
   form.value = { ...initFormData };
-  // 重置新增的变量
   regionValue.value = [];
   province.value = '';
   city.value = '';
@@ -633,24 +614,6 @@ const handleQuery = () => {
   getList();
 }
 
-/** 重置按钮操作 */
-const resetQuery = () => {
-  searchRegionValue.value = [];
-  searchSiteOptions.value = [];
-  queryParams.value.storeOrContact = undefined;
-  queryParams.value.area = undefined;
-  queryParams.value.station = undefined;
-  queryParams.value.status = undefined;
-  handleQuery();
-}
-
-/** 多选框选中数据 */
-const handleSelectionChange = (selection: StoreVO[]) => {
-  ids.value = selection.map(item => item.id);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
-}
-
 /** 新增按钮操作 */
 const handleAdd = () => {
   reset();
@@ -659,13 +622,11 @@ const handleAdd = () => {
 }
 
 /** 修改按钮操作 */
-const handleUpdate = async (row?: StoreVO) => {
+const handleUpdate = async (row: StoreVO) => {
   reset();
-  const _id = row?.id || ids.value[0]
-  const res = await getStore(_id);
+  const res = await getStore(row.id);
   Object.assign(form.value, res.data);
 
-  // 预填商户分类名,防止PageSelect组件在点击前显示为空或ID
   if (res.data.tenantCatergories && (res.data as any).tenantCatergoriesName) {
     tenantCategoriesList.value = [{
       id: res.data.tenantCatergories,
@@ -673,7 +634,6 @@ const handleUpdate = async (row?: StoreVO) => {
     }];
     tenantCategoriesTotal.value = 1;
   }
-  // 预填品牌名
   if (res.data.tenantId && (res.data as any).tenantName) {
     brandList.value = [{
       tenantId: res.data.tenantId,
@@ -739,22 +699,6 @@ const submitForm = () => {
   });
 }
 
-/** 删除按钮操作 */
-const handleDelete = async (row?: StoreVO) => {
-  const _ids = row?.id || ids.value;
-  await proxy?.$modal.confirm('是否确认删除门店管理编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
-  await delStore(_ids);
-  proxy?.$modal.msgSuccess("删除成功");
-  await getList();
-}
-
-/** 导出按钮操作 */
-const handleExport = () => {
-  proxy?.download('system/store/export', {
-    ...queryParams.value
-  }, `store_${new Date().getTime()}.xlsx`)
-}
-
 /** 获取经纬度 */
 const getGeolocation = () => {
   if ('geolocation' in navigator) {
@@ -767,18 +711,10 @@ const getGeolocation = () => {
       (error) => {
         let errorMessage = '获取位置失败';
         switch (error.code) {
-          case error.PERMISSION_DENIED:
-            errorMessage = '用户拒绝了地理定位请求';
-            break;
-          case error.POSITION_UNAVAILABLE:
-            errorMessage = '位置信息不可用';
-            break;
-          case error.TIMEOUT:
-            errorMessage = '获取位置超时';
-            break;
-          case error.UNKNOWN_ERROR:
-            errorMessage = '未知错误';
-            break;
+          case error.PERMISSION_DENIED: errorMessage = '用户拒绝了地理定位请求'; break;
+          case error.POSITION_UNAVAILABLE: errorMessage = '位置信息不可用'; break;
+          case error.TIMEOUT: errorMessage = '获取位置超时'; break;
+          case error.UNKNOWN_ERROR: errorMessage = '未知错误'; break;
         }
         proxy?.$modal.msgError(errorMessage);
       }
@@ -791,19 +727,10 @@ const getGeolocation = () => {
 /** 获取品牌列表 */
 const getBrandList = async (pageNum = 1, keyword = '', append = false) => {
   brandLoading.value = true;
-  // 确保参数格式正确,直接传递数字类型的pageNum
-  const res = await listOnStore({ pageNum: pageNum, pageSize: 10 });
+  const res = await listOnStore({ pageNum, pageSize: 10 });
   if (res.code === 200) {
-    if (append) {
-      // 追加模式,用于分页加载
-      brandList.value = [...brandList.value, ...res.rows];
-    } else {
-      // 替换模式,用于初始加载或搜索
-      brandList.value = res.rows;
-    }
-    // 存储总数
+    brandList.value = append ? [...brandList.value, ...res.rows] : res.rows;
     brandTotal.value = res.total || 0;
-    console.log('总数', brandTotal.value);
   }
   brandLoading.value = false;
 };
@@ -812,7 +739,6 @@ const getBrandList = async (pageNum = 1, keyword = '', append = false) => {
 const getServiceList = async () => {
   try {
     const res = await listAllService();
-    // 转换数据格式,适配checkbox组件
     serviceList.value = res.data || res;
   } catch (error) {
     console.error('获取服务项目列表失败:', error);
@@ -825,13 +751,7 @@ const getAreaStationList = async () => {
     const res = await listAreaStationOnStore();
     const data = res.data || res;
     areaStationList.value = data;
-
-    // 分离所在区域数据(type为0或1)
-    const areaData = data.filter((item: any) => item.type === 0 || item.type === 1);
-    // 构建树形结构
-    areaOptions.value = buildTree(areaData, 0);
-
-    // 初始化站点数据为空
+    areaOptions.value = buildTree(data.filter((item: any) => item.type === 0 || item.type === 1), 0);
     siteOptions.value = [];
   } catch (error) {
     console.error('获取区域站点列表失败:', error);
@@ -851,15 +771,10 @@ const buildTree = (data: any[], parentId: any): any[] => {
 
 /** 处理所在区域选择变化 */
 const handleAreaChange = (value: any[]) => {
-  // 清空归属站点选择
   form.value.site = undefined;
-
   if (value && value.length > 0) {
-    // 获取最后一级的id
     const areaId = value[value.length - 1];
-    // 更新regionId
     form.value.regionId = areaId;
-    // 过滤出parentId等于areaId的站点
     siteOptions.value = areaStationList.value
       .filter((item: any) => item.type === 2 && String(item.parentId) === String(areaId))
       .map((item: any) => ({
@@ -867,7 +782,6 @@ const handleAreaChange = (value: any[]) => {
         label: item.name
       }));
   } else {
-    // 如果没有选择区域,清空站点选项和regionId
     form.value.regionId = undefined;
     siteOptions.value = [];
   }
@@ -886,109 +800,44 @@ const getTenantCategoriesList = async (pageNum = 1) => {
   }
 };
 
-/** 处理品牌页面切换 */
-const handleBrandPageChange = (page: number) => {
-  // 确保page是数字类型
-  const pageNum = Number(page);
-  currentPage.value = pageNum;
-  getBrandList(pageNum, brandKeyword.value, false);
-};
-
-/** 处理商户分类分页 */
-const handleTenantCategoriesPageChange = (page: number) => {
-  // 确保page是数字类型
-  const pageNum = Number(page);
-  getTenantCategoriesList(pageNum);
-};
-
-/** 处理商户分类选择框可见性变化 */
-const handleTenantCategoriesVisibleChange = (visible: boolean) => {
-  if (visible) {
-    getTenantCategoriesList(1);
-  }
-};
-
-/** 远程搜索方法 */
-const remoteMethod = (query: string) => {
-  brandKeyword.value = query;
-  currentPage.value = 1;
-  getBrandList(1, query, false);
-};
-
-/** 处理品牌选择框显示状态变化 */
+const handleBrandPageChange = (page: number) => { getBrandList(Number(page), brandKeyword.value, false); };
+const handleTenantCategoriesPageChange = (page: number) => { getTenantCategoriesList(Number(page)); };
+const handleTenantCategoriesVisibleChange = (visible: boolean) => { if (visible) getTenantCategoriesList(1); };
 const handleBrandSelectVisibleChange = (visible: boolean) => {
   brandSelectVisible.value = visible;
   if (visible) {
-    // 选择框显示时,重置页码并重新加载数据
     currentPage.value = 1;
     getBrandList(1, brandKeyword.value, false);
   }
 };
 
-// 监听省市区选择变化,不追加到详细地址,直接存储到区域编码中
-watch(
-  addressCascaderValue,
-  (newValue) => {
-    if (newValue && newValue.length > 0) {
-      form.value.areaCode = newValue.join(',');
-    } else {
-      form.value.areaCode = undefined;
-    }
-  },
-  { deep: true }
-);
+watch(addressCascaderValue, (newValue) => {
+  form.value.areaCode = newValue && newValue.length > 0 ? newValue.join(',') : undefined;
+}, { deep: true });
 
-/** 获取服务项目名称 */
 const getServiceName = (serviceId: number): string => {
   const service = serviceList.value.find(item => item.id === serviceId);
   return service ? service.name : String(serviceId);
 };
 
-/** 获取状态列表 */
 const getStatusList = async () => {
   try {
     const res: any = await listStoreStatus();
-    // 兼容可能的不同响应体结构
     statusList.value = res.data || res.rows || res;
   } catch (error) {
     console.error('获取状态列表失败:', error);
   }
 };
 
-/** 格式化时间为时分 */
 const formatTime = (time: string | number): string => {
   if (!time) return '';
-
-  // 如果已经是 HH:mm 格式,直接返回
-  if (typeof time === 'string' && /^\d{2}:\d{2}$/.test(time)) {
-    return time;
-  }
-
-  // 如果是 HH:mm:ss 格式,截取前5位
-  if (typeof time === 'string' && /^\d{2}:\d{2}:\d{2}$/.test(time)) {
-    return time.substring(0, 5);
-  }
-
-  // 处理时间戳或日期字符串
-  let date: Date;
-  if (typeof time === 'string' && !time.includes('-') && !time.includes('T')) {
-    // 尝试补全日期以使 Date 能够解析某些纯时间字符串,或者如果上面正则没匹配到则直接返回
-    return time;
-  } else {
-    date = new Date(time);
-  }
-
-  // 检查是否是有效日期
-  if (isNaN(date.getTime())) return String(time);
-
-  // 格式化为 HH:mm
-  const hours = date.getHours().toString().padStart(2, '0');
-  const minutes = date.getMinutes().toString().padStart(2, '0');
-
-  return `${hours}:${minutes}`;
+  if (typeof time === 'string' && /^\d{2}:\d{2}$/.test(time)) return time;
+  if (typeof time === 'string' && /^\d{2}:\d{2}:\d{2}$/.test(time)) return time.substring(0, 5);
+  let date = (typeof time === 'string' && !time.includes('-') && !time.includes('T')) ? null : new Date(time);
+  if (!date || isNaN(date.getTime())) return String(time);
+  return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
 };
 
-/** 获取完整门店地址:{省}{市}{区} {详细地址} */
 const getFullAddress = (row: any): string => {
   let areaText = '';
   if (row.areaCode) {
@@ -998,6 +847,30 @@ const getFullAddress = (row: any): string => {
   return areaText ? `${areaText} ${row.detailAddress || ''}` : (row.detailAddress || '');
 };
 
+/** 根据站点ID获取区域全称(向上遍历树形关系) */
+const getRegionNameBySite = (siteId: string | number): string => {
+  if (!siteId || areaStationList.value.length === 0) return '正在加载...';
+
+  const site = areaStationList.value.find(item => String(item.id) === String(siteId));
+  if (!site) return '未知区域';
+
+  let parentNames: string[] = [];
+  let currentParentId = site.parentId;
+
+  // 向上遍历直到父级ID为0或没找到父级
+  while (currentParentId && String(currentParentId) !== '0') {
+    const parent = areaStationList.value.find(item => String(item.id) === String(currentParentId));
+    if (parent) {
+      parentNames.unshift(parent.name);
+      currentParentId = parent.parentId;
+    } else {
+      break;
+    }
+  }
+
+  return parentNames.length > 0 ? parentNames.join('/') : '顶级区域';
+};
+
 onMounted(() => {
   getList();
   getBrandList();
@@ -1007,58 +880,151 @@ onMounted(() => {
 });
 </script>
 
-<style scoped>
-.brand-pagination {
-  margin-top: 10px;
-  padding-top: 10px;
-  border-top: 1px solid #ebeef5;
-  text-align: center;
+<style scoped lang="scss">
+.page-container {
+  padding: 20px;
+  background-color: #f5f7f9;
+  min-height: 100%;
+}
+
+.table-card {
+  border: none;
+  border-radius: 8px;
+  
+  :deep(.el-card__header) {
+    padding: 20px 24px;
+    border-bottom: 1px solid #f0f0f0;
+  }
 }
 
-.custom-pagination {
+.card-header {
   display: flex;
+  justify-content: space-between;
   align-items: center;
-  justify-content: center;
-  gap: 10px;
-  font-size: 14px;
 }
 
-.page-arrow {
-  cursor: pointer;
-  color: #606266;
-  user-select: none;
-  padding: 2px 8px;
-  transition: color 0.3s;
+.header-left {
+  .title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333;
+  }
+}
+
+.header-right {
+  display: flex;
+  gap: 12px;
+  
+  .search-input { width: 200px; }
+  .region-select, .station-select, .status-select { width: 140px; }
+  
+  :deep(.el-input__wrapper) {
+    background-color: #f4f5f7;
+    box-shadow: none;
+    border: 1px solid transparent;
+    
+    &:hover, &.is-focus {
+      border-color: #409eff;
+      background-color: #fff;
+    }
+  }
+}
+
+.store-info-box {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  
+  .store-desc {
+    .name {
+      font-weight: 600;
+      color: #333;
+      margin-bottom: 4px;
+    }
+    .tags {
+      display: flex;
+      gap: 4px;
+    }
+  }
+}
+
+.service-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
 }
 
-.page-arrow:hover:not(.disabled) {
-  color: #409eff;
+.region-info {
+  .region-name {
+    font-weight: 500;
+    color: #333;
+  }
+  .site-name {
+    font-size: 12px;
+    color: #909399;
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    margin-top: 2px;
+  }
 }
 
-.page-arrow.disabled {
-  color: #c0c4cc;
-  cursor: not-allowed;
+.contact-info {
+  .contact-item {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    font-size: 13px;
+    color: #606266;
+    
+    &.phone {
+      color: #409eff;
+      margin-top: 2px;
+    }
+    
+    .el-icon { font-size: 14px; }
+  }
 }
 
-.page-number {
-  cursor: pointer;
+.time-text, .count-text {
+  font-weight: 500;
   color: #606266;
-  padding: 2px 8px;
-  transition: all 0.3s;
 }
 
-.page-number:hover {
-  color: #409eff;
+.status-tag {
+  border: none;
+  font-weight: 500;
 }
 
-.page-number.active {
-  color: #409eff;
-  font-weight: bold;
+.op-btns {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
 }
 
-.total-text {
-  margin-left: 15px;
-  font-size: 12px;
-  color: #909399;
+.delete-item {
+  color: #f56c6c !important;
+  &:hover {
+    color: #f56c6c !important;
+    background-color: #fef0f0 !important;
+  }
+}
+
+.pagination-container {
+  margin-top: 24px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+:deep(.el-table) {
+  --el-table-border-color: #f0f0f0;
+  
+  th.el-table__cell {
+    font-weight: 600;
+  }
+  
+  td.el-table__cell {
+    padding: 12px 0;
+  }
 }
 </style>

+ 181 - 215
src/views/system/tenant/index.vue

@@ -1,111 +1,84 @@
 <template>
-  <div class="p-2">
-    <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="tenantId">
-              <el-input v-model="queryParams.tenantId" placeholder="请输入品牌编号" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="联系人" prop="contactUserName">
-              <el-input v-model="queryParams.contactUserName" placeholder="请输入联系人" clearable
-                @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="联系电话" prop="contactPhone">
-              <el-input v-model="queryParams.contactPhone" placeholder="请输入联系电话" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="企业名称" prop="companyName">
-              <el-input v-model="queryParams.companyName" 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">
+  <div class="page-container">
+    <el-card shadow="never" class="table-card">
       <template #header>
-        <el-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:tenant:add']" type="primary" plain icon="Plus"
-              @click="handleAdd">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:tenant:edit']" type="success" plain icon="Edit" :disabled="single"
-              @click="handleUpdate()">修改</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:tenant:remove']" type="danger" plain icon="Delete" :disabled="multiple"
-              @click="handleDelete()">
-              删除
-            </el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:tenant:export']" type="warning" plain icon="Download"
-              @click="handleExport">导出</el-button>
-          </el-col>
-          <!--          <el-col :span="1.5">-->
-          <!--            <el-button v-if="userId === 1" type="success" plain icon="Refresh" @click="handleSyncTenantDict">同步品牌字典</el-button>-->
-          <!--          </el-col>-->
-          <!--          <el-col :span="1.5">-->
-          <!--            <el-button v-if="userId === 1" type="success" plain icon="Refresh" @click="handleSyncTenantConfig">同步品牌参数配置</el-button>-->
-          <!--          </el-col>-->
-          <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
-        </el-row>
+        <div class="card-header">
+          <div class="header-left">
+            <span class="title">连锁品牌管理</span>
+          </div>
+          <div class="header-right">
+            <el-input
+              v-model="queryParams.companyName"
+              placeholder="品牌名称/联系人"
+              class="search-input"
+              prefix-icon="Search"
+              clearable
+              @keyup.enter="handleQuery"
+            />
+            <el-input
+              v-model="queryParams.address"
+              placeholder="搜索地址"
+              class="search-input"
+              prefix-icon="Location"
+              clearable
+              @keyup.enter="handleQuery"
+            />
+            <el-select v-model="queryParams.status" placeholder="状态" class="status-select" clearable @change="handleQuery">
+              <el-option label="正常" value="0" />
+              <el-option label="停用" value="1" />
+            </el-select>
+            <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['system:tenant:add']">新增品牌</el-button>
+          </div>
+        </div>
       </template>
 
-      <el-table v-loading="loading" border :data="tenantList" @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column v-if="false" label="id" align="center" prop="id" />
-        <!--        <el-table-column label="品牌编号" align="center" prop="tenantId" />-->
-        <el-table-column label="LOGO" align="center" prop="logoUrl" width="100">
+      <el-table v-loading="loading" :data="tenantList" style="width: 100%" :header-cell-style="{ background: '#f8f9fb', color: '#606266' }">
+        <el-table-column label="Logo" align="center" width="100">
           <template #default="scope">
-            <image-preview :src="scope.row.logoUrl" :width="50" :height="50" />
+            <div class="logo-wrapper">
+              <image-preview :src="scope.row.logoUrl" :width="40" :height="40" />
+            </div>
           </template>
         </el-table-column>
-        <el-table-column label="品牌名称" align="center" prop="companyName" />
-        <el-table-column label="管理员账号" align="center" prop="username" />
-        <el-table-column label="联系人" align="center" prop="contactUserName" />
-        <el-table-column label="联系电话" align="center" prop="contactPhone" />
-        <el-table-column label="总部地址" align="center" prop="address" />
-        <!--        <el-table-column label="社会信用代码" align="center" prop="licenseNumber" />-->
-        <!--        <el-table-column label="过期时间" align="center" prop="expireTime" width="180">-->
-        <!--          <template #default="scope">-->
-        <!--            <span>{{ proxy.parseTime(scope.row.expireTime, '{y}-{m}-{d}') }}</span>-->
-        <!--          </template>-->
-        <!--        </el-table-column>-->
-        <el-table-column label="状态" align="center" prop="status">
+        <el-table-column label="品牌名称" prop="companyName" min-width="150" />
+        <el-table-column label="商户账号" prop="username" min-width="120" />
+        <el-table-column label="联系人" prop="contactUserName" width="100" />
+        <el-table-column label="联系电话" prop="contactPhone" width="130" />
+<!--        <el-table-column label="旗下门店数" align="center" width="120" sortable>-->
+<!--          <template #default="scope">-->
+<!--            <span class="count-text">{{ scope.row.storeCount || 0 }}</span>-->
+<!--          </template>-->
+<!--        </el-table-column>-->
+        <el-table-column label="总部地址" prop="address" min-width="180" show-overflow-tooltip />
+        <el-table-column label="状态" align="center" width="100">
           <template #default="scope">
-            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1"
-              @change="handleStatusChange(scope.row)"></el-switch>
+            <el-tag :type="scope.row.status === '0' ? 'success' : 'danger'" size="small" effect="light" class="status-tag">
+              {{ scope.row.status === '0' ? '正常' : '停用' }}
+            </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="品牌简介" align="center" prop="intro" />
-        <el-table-column width="150" label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
+        <el-table-column label="品牌简介" prop="intro" min-width="150" show-overflow-tooltip />
+        <el-table-column label="操作" align="right" width="140" fixed="right">
           <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button v-hasPermi="['system:tenant:edit']" link type="primary" icon="Edit"
-                @click="handleUpdate(scope.row)"></el-button>
-            </el-tooltip>
-            <!--            <el-tooltip content="同步套餐" placement="top">-->
-            <!--              <el-button v-hasPermi="['system:tenant:edit']" link type="primary" icon="Refresh" @click="handleSyncTenantPackage(scope.row)">-->
-            <!--              </el-button>-->
-            <!--            </el-tooltip>-->
-            <el-tooltip content="删除" placement="top">
-              <el-button v-hasPermi="['system:tenant:remove']" link type="primary" icon="Delete"
-                @click="handleDelete(scope.row)"></el-button>
-            </el-tooltip>
+            <div class="op-btns">
+              <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['system:tenant:edit']">编辑</el-button>
+              <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['system:tenant:remove']">删除</el-button>
+            </div>
           </template>
         </el-table-column>
       </el-table>
 
-      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
-        :total="total" @pagination="getList" />
+      <div class="pagination-container">
+        <pagination
+          v-show="total > 0"
+          v-model:total="total"
+          v-model:page="queryParams.pageNum"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </div>
     </el-card>
+
     <!-- 添加或修改品牌对话框 -->
     <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
       <el-form ref="tenantFormRef" :model="form" :rules="rules" label-width="100px">
@@ -124,41 +97,20 @@
         <el-form-item v-if="!form.id" label="管理员密码" prop="password">
           <el-input v-model="form.password" type="password" placeholder="请输入管理员密码" maxlength="20" />
         </el-form-item>
-        <!--        <el-form-item label="品牌套餐" prop="packageId">-->
-        <!--          <el-select v-model="form.packageId" :disabled="!!form.tenantId" placeholder="请选择品牌套餐" clearable style="width: 100%">-->
-        <!--            <el-option v-for="item in packageList" :key="item.packageId" :label="item.packageName" :value="item.packageId" />-->
-        <!--          </el-select>-->
-        <!--        </el-form-item>-->
-        <!--        <el-form-item label="过期时间" prop="expireTime">-->
-        <!--          <el-date-picker v-model="form.expireTime" clearable type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择过期时间">-->
-        <!--          </el-date-picker>-->
-        <!--        </el-form-item>-->
-        <!--        <el-form-item label="用户数量" prop="accountCount">-->
-        <!--          <el-input v-model="form.accountCount" placeholder="请输入用户数量" />-->
-        <!--        </el-form-item>-->
-        <!--        <el-form-item label="绑定域名" prop="domain">-->
-        <!--          <el-input v-model="form.domain" 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="licenseNumber">-->
-        <!--          <el-input v-model="form.licenseNumber" placeholder="请输入统一社会信用代码" />-->
-        <!--        </el-form-item>-->
         <el-form-item label="简介" prop="intro">
           <el-input v-model="form.intro" type="textarea" placeholder="请输入简介" />
         </el-form-item>
         <el-form-item label="LOGO" prop="logo">
           <image-upload v-model="form.logo" :limit="1" />
         </el-form-item>
-        <!--        <el-form-item label="备注" prop="remark">-->
-        <!--          <el-input v-model="form.remark" placeholder="请输入备注" />-->
-        <!--        </el-form-item>-->
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
           <el-button @click="cancel">取 消</el-button>
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
         </div>
       </template>
     </el-dialog>
@@ -172,10 +124,7 @@ import {
   delTenant,
   addTenant,
   updateTenant,
-  changeTenantStatus,
-  syncTenantPackage,
-  syncTenantDict,
-  syncTenantConfig
+  changeTenantStatus
 } from '@/api/system/tenant';
 import { selectTenantPackage } from '@/api/system/tenantPackage';
 import { useUserStore } from '@/store/modules/user';
@@ -183,17 +132,12 @@ import { TenantForm, TenantQuery, TenantVO } from '@/api/system/tenant/types';
 import { TenantPkgVO } from '@/api/system/tenantPackage/types';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-
 const userStore = useUserStore();
-const userId = ref(userStore.userId);
+
 const tenantList = ref<TenantVO[]>([]);
 const packageList = ref<TenantPkgVO[]>([]);
 const buttonLoading = ref(false);
 const loading = ref(true);
-const showSearch = ref(true);
-const ids = ref<Array<string | number>>([]);
-const single = ref(true);
-const multiple = ref(true);
 const total = ref(0);
 
 const queryFormRef = ref<ElFormInstance>();
@@ -228,14 +172,11 @@ const data = reactive<PageData<TenantForm, TenantQuery>>({
   queryParams: {
     pageNum: 1,
     pageSize: 10,
-    tenantId: '',
-    contactUserName: '',
-    contactPhone: '',
-    companyName: ''
+    companyName: '',
+    address: '',
+    status: undefined
   },
   rules: {
-    id: [{ required: true, message: 'id不能为空', trigger: 'blur' }],
-    tenantId: [{ required: true, message: '品牌编号不能为空', trigger: 'blur' }],
     contactUserName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
     contactPhone: [{ required: true, message: '联系电话不能为空', trigger: 'blur' }],
     companyName: [{ required: true, message: '企业名称不能为空', trigger: 'blur' }],
@@ -252,12 +193,6 @@ const data = reactive<PageData<TenantForm, TenantQuery>>({
 
 const { queryParams, form, rules } = toRefs(data);
 
-/** 查询所有品牌套餐 */
-const getTenantPackage = async () => {
-  const res = await selectTenantPackage();
-  packageList.value = res.data;
-};
-
 /** 查询品牌列表 */
 const getList = async () => {
   loading.value = true;
@@ -267,63 +202,35 @@ const getList = async () => {
   loading.value = false;
 };
 
-// 品牌套餐状态修改
-const handleStatusChange = async (row: TenantVO) => {
-  const text = row.status === '0' ? '启用' : '停用';
-  try {
-    await proxy?.$modal.confirm('确认要"' + text + '""' + row.companyName + '"品牌吗?');
-    await changeTenantStatus(row.id, row.tenantId, row.status);
-    proxy?.$modal.msgSuccess(text + '成功');
-  } catch {
-    row.status = row.status === '0' ? '1' : '0';
-  }
+/** 搜索及重置逻辑由 handleQuery 统一处理 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
 };
 
-// 取消按钮
+/** 取消按钮 */
 const cancel = () => {
   reset();
   dialog.visible = false;
 };
 
-// 表单重置
+/** 表单重置 */
 const reset = () => {
   form.value = { ...initFormData };
   tenantFormRef.value?.resetFields();
 };
 
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.value.pageNum = 1;
-  getList();
-};
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value?.resetFields();
-  handleQuery();
-};
-
-// 多选框选中数据
-const handleSelectionChange = (selection: TenantVO[]) => {
-  ids.value = selection.map((item) => item.id);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
-};
-
 /** 新增按钮操作 */
 const handleAdd = () => {
   reset();
-  getTenantPackage();
   dialog.visible = true;
   dialog.title = '添加品牌';
 };
 
 /** 修改按钮操作 */
-const handleUpdate = async (row?: TenantVO) => {
+const handleUpdate = async (row: TenantVO) => {
   reset();
-  await getTenantPackage();
-  const _id = row?.id || ids.value[0];
-  const res = await getTenant(_id);
+  const res = await getTenant(row.id);
   Object.assign(form.value, res.data);
   dialog.visible = true;
   dialog.title = '修改品牌';
@@ -347,56 +254,115 @@ const submitForm = () => {
 };
 
 /** 删除按钮操作 */
-const handleDelete = async (row?: TenantVO) => {
-  const _ids = row?.id || ids.value;
-  await proxy?.$modal.confirm('是否确认删除品牌编号为"' + _ids + '"的数据项?');
+const handleDelete = async (row: TenantVO) => {
+  await proxy?.$modal.confirm(`是否确认删除品牌"${row.companyName}"?`);
   loading.value = true;
-  await delTenant(_ids).finally(() => (loading.value = false));
+  await delTenant(row.id).finally(() => (loading.value = false));
   await getList();
   proxy?.$modal.msgSuccess('删除成功');
 };
 
-/** 同步品牌套餐按钮操作 */
-const handleSyncTenantPackage = async (row: TenantVO) => {
-  try {
-    await proxy?.$modal.confirm('是否确认同步品牌套餐品牌编号为"' + row.tenantId + '"的数据项?');
-    loading.value = true;
-    await syncTenantPackage(row.tenantId, row.packageId);
-    await getList();
-    proxy?.$modal.msgSuccess('同步成功');
-  } catch {
-    return;
-  } finally {
-    loading.value = false;
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped lang="scss">
+.page-container {
+  padding: 20px;
+  background-color: #f5f7f9;
+  min-height: 100%;
+}
+
+.table-card {
+  border: none;
+  border-radius: 8px;
+
+  :deep(.el-card__header) {
+    padding: 20px 24px;
+    border-bottom: 1px solid #f0f0f0;
   }
-};
+}
 
-/** 导出按钮操作 */
-const handleExport = () => {
-  proxy?.download(
-    'system/tenant/export',
-    {
-      ...queryParams.value
-    },
-    `tenant_${new Date().getTime()}.xlsx`
-  );
-};
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
 
-/**同步品牌字典*/
-const handleSyncTenantDict = async () => {
-  await proxy?.$modal.confirm('确认要同步所有品牌字典吗?');
-  const res = await syncTenantDict();
-  proxy?.$modal.msgSuccess(res.msg);
-};
+.header-left {
+  .title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333;
+  }
+}
 
-/**同步品牌参数配置*/
-const handleSyncTenantConfig = async () => {
-  await proxy?.$modal.confirm('确认要同步所有品牌参数配置吗?');
-  const res = await syncTenantConfig();
-  proxy?.$modal.msgSuccess(res.msg);
-};
+.header-right {
+  display: flex;
+  gap: 12px;
 
-onMounted(() => {
-  getList();
-});
-</script>
+  .search-input {
+    width: 200px;
+
+    :deep(.el-input__wrapper) {
+      background-color: #f4f5f7;
+      box-shadow: none;
+      border: 1px solid transparent;
+
+      &:hover, &.is-focus {
+        border-color: #409eff;
+        background-color: #fff;
+      }
+    }
+  }
+
+  .status-select {
+    width: 120px;
+    :deep(.el-input__wrapper) {
+      background-color: #f4f5f7;
+      box-shadow: none;
+    }
+  }
+}
+
+.logo-wrapper {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.count-text {
+  font-weight: 600;
+  color: #606266;
+}
+
+.status-tag {
+  border: none;
+  font-weight: 500;
+}
+
+.op-btns {
+  display: flex;
+  justify-content: flex-end;
+  gap: 16px;
+}
+
+.pagination-container {
+  margin-top: 24px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+:deep(.el-table) {
+  --el-table-border-color: #f0f0f0;
+
+  th.el-table__cell {
+    font-weight: 600;
+  }
+
+  td.el-table__cell {
+    padding: 12px 0;
+  }
+}
+</style>

+ 159 - 138
src/views/system/tenantCategories/index.vue

@@ -1,114 +1,63 @@
 <template>
-  <div class="p-2">
-    <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="name">
-              <el-input v-model="queryParams.name" placeholder="请输入分类名称" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="创建时间" style="width: 308px">
-              <el-date-picker
-                v-model="dateRangeCreateTime"
-                value-format="YYYY-MM-DD HH:mm:ss"
-                type="daterange"
-                range-separator="-"
-                start-placeholder="开始日期"
-                end-placeholder="结束日期"
-                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
-              />
-            </el-form-item>
-            <el-form-item label="更新时间" style="width: 308px">
-              <el-date-picker
-                v-model="dateRangeUpdateTime"
-                value-format="YYYY-MM-DD HH:mm:ss"
-                type="daterange"
-                range-separator="-"
-                start-placeholder="开始日期"
-                end-placeholder="结束日期"
-                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
-              />
-            </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="never">
+  <div class="page-container">
+    <el-card shadow="never" class="table-card">
       <template #header>
-        <el-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:tenantCategories:add']">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:tenantCategories:edit']"
-              >修改</el-button
-            >
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:tenantCategories:remove']"
-              >删除</el-button
-            >
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:tenantCategories:export']">导出</el-button>
-          </el-col>
-          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
-        </el-row>
+        <div class="card-header">
+          <div class="header-left">
+            <span class="title">商户分类管理</span>
+          </div>
+          <div class="header-right">
+            <el-input
+              v-model="queryParams.name"
+              placeholder="搜索分类名称"
+              class="search-input"
+              prefix-icon="Search"
+              clearable
+              @keyup.enter="handleQuery"
+            />
+            <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['system:tenantCategories:add']">新增分类</el-button>
+          </div>
+        </div>
       </template>
 
-      <el-table v-loading="loading" border :data="tenantCategoriesList" @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="分类名称"  prop="name" width="1000" />
-        <el-table-column label="图标" align="center" prop="iconUrl" width="100">
+      <el-table v-loading="loading" :data="tenantCategoriesList" style="width: 100%" :header-cell-style="{ background: '#f8f9fb', color: '#606266' }">
+        <el-table-column label="分类名称" prop="name" min-width="200" />
+        <el-table-column label="图标" align="center" width="120">
           <template #default="scope">
-            <image-preview :src="scope.row.iconUrl" :width="50" :height="50" />
+            <div class="icon-wrapper">
+              <image-preview :src="scope.row.iconUrl" :width="40" :height="40" />
+            </div>
           </template>
         </el-table-column>
-        <el-table-column label="排序" align="center" prop="sort" width="100" />
-        <el-table-column label="状态" align="center" prop="status" width="100">
+        <el-table-column label="排序" align="center" prop="sort" width="120" />
+        <el-table-column label="状态" align="center" width="120">
           <template #default="scope">
-            <el-tag :type="scope.row.status ? 'success' : 'danger'">
+            <el-tag :type="scope.row.status ? 'success' : 'danger'" size="small" effect="light" class="status-tag">
               {{ scope.row.status ? '启用' : '禁用' }}
             </el-tag>
           </template>
         </el-table-column>
-        <!--        <el-table-column label="创建者" align="center" prop="createBy" />-->
-        <!--        <el-table-column label="创建时间" align="center" prop="createTime" width="180">-->
-        <!--          <template #default="scope">-->
-        <!--            <span>{{ parseTime(scope.row.createTime) }}</span>-->
-        <!--          </template>-->
-        <!--        </el-table-column>-->
-        <!--        <el-table-column label="更新者" align="center" prop="updateBy" />-->
-        <!--        <el-table-column label="更新时间" align="center" prop="updateTime" width="180">-->
-        <!--          <template #default="scope">-->
-        <!--            <span>{{ parseTime(scope.row.updateTime) }}</span>-->
-        <!--          </template>-->
-        <!--        </el-table-column>-->
-        <el-table-column label="操作" align="center" fixed="right" class-name="small-padding">
+        <el-table-column label="操作" align="right" width="140" fixed="right">
           <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:tenantCategories:edit']"></el-button>
-            </el-tooltip>
-            <el-tooltip content="删除" placement="top">
-              <el-button
-                link
-                type="primary"
-                icon="Delete"
-                @click="handleDelete(scope.row)"
-                v-hasPermi="['system:tenantCategories:remove']"
-              ></el-button>
-            </el-tooltip>
+            <div class="op-btns">
+              <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['system:tenantCategories:edit']">编辑</el-button>
+              <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['system:tenantCategories:remove']">删除</el-button>
+            </div>
           </template>
         </el-table-column>
       </el-table>
 
-      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+      <div class="pagination-container">
+        <pagination
+          v-show="total > 0"
+          v-model:total="total"
+          v-model:page="queryParams.pageNum"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </div>
     </el-card>
+
     <!-- 添加或修改商户分类对话框 -->
     <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
       <el-form ref="tenantCategoriesFormRef" :model="form" :rules="rules" label-width="80px">
@@ -116,7 +65,7 @@
           <el-input v-model="form.name" placeholder="请输入分类名称" />
         </el-form-item>
         <el-form-item label="排序" prop="sort">
-          <el-input v-model="form.sort" placeholder="请输入排序" />
+          <el-input-number v-model="form.sort" :min="0" controls-position="right" style="width: 100%" />
         </el-form-item>
         <el-form-item label="状态" prop="status">
           <el-switch v-model="form.status" />
@@ -127,8 +76,8 @@
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
           <el-button @click="cancel">取 消</el-button>
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
         </div>
       </template>
     </el-dialog>
@@ -150,13 +99,7 @@ const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const tenantCategoriesList = ref<TenantCategoriesVO[]>([]);
 const buttonLoading = ref(false);
 const loading = ref(true);
-const showSearch = ref(true);
-const ids = ref<Array<string | number>>([]);
-const single = ref(true);
-const multiple = ref(true);
 const total = ref(0);
-const dateRangeCreateTime = ref<[DateModelType, DateModelType]>(['', '']);
-const dateRangeUpdateTime = ref<[DateModelType, DateModelType]>(['', '']);
 
 const queryFormRef = ref<ElFormInstance>();
 const tenantCategoriesFormRef = ref<ElFormInstance>();
@@ -186,7 +129,6 @@ const data = reactive<PageData<TenantCategoriesForm, TenantCategoriesQuery>>({
     }
   },
   rules: {
-    id: [{ required: true, message: '序号不能为空', trigger: 'blur' }],
     name: [{ required: true, message: '分类名称不能为空', trigger: 'blur' }],
     status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
   }
@@ -197,15 +139,19 @@ const { queryParams, form, rules } = toRefs(data);
 /** 查询商户分类列表 */
 const getList = async () => {
   loading.value = true;
-  queryParams.value.params = {};
-  proxy?.addDateRange(queryParams.value, dateRangeCreateTime.value, 'CreateTime');
-  proxy?.addDateRange(queryParams.value, dateRangeUpdateTime.value, 'UpdateTime');
   const res = await listTenantCategories(queryParams.value);
   tenantCategoriesList.value = res.rows;
   total.value = res.total;
   loading.value = false;
 };
 
+/** 处理更多菜单命令 */
+const handleCommand = (command: string, row: TenantCategoriesVO) => {
+  if (command === 'delete') {
+    handleDelete(row);
+  }
+};
+
 /** 取消按钮 */
 const cancel = () => {
   reset();
@@ -224,21 +170,6 @@ const handleQuery = () => {
   getList();
 };
 
-/** 重置按钮操作 */
-const resetQuery = () => {
-  dateRangeCreateTime.value = ['', ''];
-  dateRangeUpdateTime.value = ['', ''];
-  queryFormRef.value?.resetFields();
-  handleQuery();
-};
-
-/** 多选框选中数据 */
-const handleSelectionChange = (selection: TenantCategoriesVO[]) => {
-  ids.value = selection.map((item) => item.id);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
-};
-
 /** 新增按钮操作 */
 const handleAdd = () => {
   reset();
@@ -247,10 +178,9 @@ const handleAdd = () => {
 };
 
 /** 修改按钮操作 */
-const handleUpdate = async (row?: TenantCategoriesVO) => {
+const handleUpdate = async (row: TenantCategoriesVO) => {
   reset();
-  const _id = row?.id || ids.value[0];
-  const res = await getTenantCategories(_id);
+  const res = await getTenantCategories(row.id);
   Object.keys(initFormData).forEach((key) => {
     form.value[key] = res.data[key];
   });
@@ -276,26 +206,117 @@ const submitForm = () => {
 };
 
 /** 删除按钮操作 */
-const handleDelete = async (row?: TenantCategoriesVO) => {
-  const _ids = row?.id || ids.value;
-  await proxy?.$modal.confirm('是否确认删除商户分类编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
-  await delTenantCategories(_ids);
+const handleDelete = async (row: TenantCategoriesVO) => {
+  await proxy?.$modal.confirm(`是否确认删除分类"${row.name}"?`);
+  loading.value = true;
+  await delTenantCategories(row.id).finally(() => (loading.value = false));
   proxy?.$modal.msgSuccess('删除成功');
   await getList();
 };
 
-/** 导出按钮操作 */
-const handleExport = () => {
-  proxy?.download(
-    'system/tenantCategories/export',
-    {
-      ...queryParams.value
-    },
-    `tenantCategories_${new Date().getTime()}.xlsx`
-  );
-};
-
 onMounted(() => {
   getList();
 });
 </script>
+
+<style scoped lang="scss">
+.page-container {
+  padding: 20px;
+  background-color: #f5f7f9;
+  min-height: 100%;
+}
+
+.table-card {
+  border: none;
+  border-radius: 8px;
+  
+  :deep(.el-card__header) {
+    padding: 20px 24px;
+    border-bottom: 1px solid #f0f0f0;
+  }
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.header-left {
+  .title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333;
+  }
+}
+
+.header-right {
+  display: flex;
+  gap: 12px;
+  
+  .search-input {
+    width: 240px;
+    
+    :deep(.el-input__wrapper) {
+      background-color: #f4f5f7;
+      box-shadow: none;
+      border: 1px solid transparent;
+      
+      &:hover, &.is-focus {
+        border-color: #409eff;
+        background-color: #fff;
+      }
+    }
+  }
+}
+
+.icon-wrapper {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.status-tag {
+  border: none;
+  font-weight: 500;
+}
+
+.op-btns {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 16px;
+
+  .more-btn {
+    display: flex;
+    align-items: center;
+    color: #409eff;
+  }
+}
+
+.delete-item {
+  color: #f56c6c !important;
+  &:hover {
+    color: #f56c6c !important;
+    background-color: #fef0f0 !important;
+  }
+}
+
+.pagination-container {
+  margin-top: 24px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+:deep(.el-table) {
+  --el-table-border-color: #f0f0f0;
+  
+  th.el-table__cell {
+    font-weight: 600;
+  }
+  
+  td.el-table__cell {
+    padding: 12px 0;
+  }
+}
+</style>

+ 7 - 7
src/views/system/tenantPackage/index.vue

@@ -20,20 +20,20 @@
       <template #header>
         <el-row :gutter="10" class="mb8">
           <el-col :span="1.5">
-            <el-button v-hasPermi="['system:tenantPackage:add']" type="primary" plain icon="Plus" @click="handleAdd"> 新增 </el-button>
+            <el-button type="primary" plain icon="Plus" @click="handleAdd"> 新增 </el-button>
           </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:tenantPackage:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">
+           <el-col :span="1.5">
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">
               修改
             </el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button v-hasPermi="['system:tenantPackage:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
               删除
             </el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button v-hasPermi="['system:tenantPackage:export']" type="warning" plain icon="Download" @click="handleExport">导出 </el-button>
+            <el-button type="warning" plain icon="Download" @click="handleExport">导出 </el-button>
           </el-col>
           <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
         </el-row>
@@ -52,10 +52,10 @@
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
           <template #default="scope">
             <el-tooltip content="修改" placement="top">
-              <el-button v-hasPermi="['system:tenantPackage:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
             </el-tooltip>
             <el-tooltip content="删除" placement="top">
-              <el-button v-hasPermi="['system:tenantPackage:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
             </el-tooltip>
           </template>
         </el-table-column>

+ 78 - 7
src/views/system/user/index.vue

@@ -15,7 +15,7 @@
               clearable
               @keyup.enter="handleQuery"
             />
-            <el-button type="primary" icon="Plus" @click="handleAdd()">新增账号</el-button>
+            <el-button type="primary" icon="Plus" @click="handleAdd()" v-hasPermi="['system:user:add']">新增账号</el-button>
           </div>
         </div>
       </template>
@@ -42,18 +42,22 @@
             <span v-else>-</span>
           </template>
         </el-table-column>
-        <el-table-column key="deptName" label="所属区域/站点" prop="deptName" min-width="200" />
+        <el-table-column label="管理区域/站点" min-width="250">
+          <template #default="scope">
+            <span class="station-path">{{ formatAreaStations(scope.row.areaStations) }}</span>
+          </template>
+        </el-table-column>
         <el-table-column label="状态" width="100" align="center">
           <template #default="scope">
-            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
+            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)" v-hasPermi="['system:user:changeStatus']"></el-switch>
           </template>
         </el-table-column>
         <el-table-column label="操作" width="220" align="right" fixed="right">
           <template #default="scope">
             <div class="op-btns" v-if="scope.row.userId !== 1">
-              <el-button v-hasPermi="['system:user:edit']" link type="primary" @click="handleUpdate(scope.row)">编辑</el-button>
-              <el-button v-hasPermi="['system:user:resetPwd']" link type="warning" @click="handleResetPwd(scope.row)">重置密码</el-button>
-              <el-button v-hasPermi="['system:user:remove']" link type="danger" @click="handleDelete(scope.row)">删除</el-button>
+              <el-button link type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['system:user:edit']">编辑</el-button>
+              <el-button link type="warning" @click="handleResetPwd(scope.row)" v-hasPermi="['system:user:resetPassword']">重置密码</el-button>
+              <el-button link type="danger" @click="handleDelete(scope.row)" v-hasPermi="['system:user:remove']">删除</el-button>
             </div>
           </template>
         </el-table-column>
@@ -175,6 +179,14 @@
             </el-col>
           </template>
         </el-row>
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="管理站点" prop="areaStations">
+              <el-checkbox v-model="allAreaStations" style="margin-bottom: 4px;">全部站点</el-checkbox>
+              <PermiSelect v-if="!allAreaStations" v-model:selectedKeys="form.areaStations" :data="areaStationOptions" />
+            </el-form-item>
+          </el-col>
+        </el-row>
         <el-row>
           <el-col :span="24">
             <el-form-item label="备注">
@@ -234,6 +246,9 @@ import { DeptTreeVO, DeptVO } from '@/api/system/dept/types';
 import { RoleVO } from '@/api/system/role/types';
 import { PostVO } from '@/api/system/post/types';
 import { globalHeaders } from '@/utils/request';
+import { listAreaStation } from '@/api/system/areaStation';
+import { AreaStationVO } from '@/api/system/areaStation/types';
+import PermiSelect from '@/components/PermiSelect/index.vue';
 import { to } from 'await-to-js';
 import { optionselect } from '@/api/system/post';
 import { checkPermi } from '@/utils/permission';
@@ -254,6 +269,8 @@ const enabledDeptOptions = ref<DeptTreeVO[]>([]);
 const initPassword = ref<string>('');
 const postOptions = ref<PostVO[]>([]);
 const roleOptions = ref<RoleVO[]>([]);
+const areaStationList = ref<AreaStationVO[]>([]);
+const areaStationOptions = ref<AreaStationVO[]>([]);
 /*** 用户导入参数 */
 const upload = reactive<ImportOption>({
   // 是否显示弹出层(用户导入)
@@ -302,7 +319,8 @@ const initFormData: UserForm = {
   status: '0',
   remark: '',
   postIds: [],
-  roleIds: []
+  roleIds: [],
+  areaStations: []
 };
 
 const initData: PageData<UserForm, UserQuery> = {
@@ -359,6 +377,18 @@ const data = reactive<PageData<UserForm, UserQuery>>(initData);
 
 const { queryParams, form, rules } = toRefs<PageData<UserForm, UserQuery>>(data);
 
+/** 计算并处理“全部站点”勾选逻辑 */
+const allAreaStations = computed({
+  get: () => form.value.areaStations?.includes(0) || form.value.areaStations?.includes('0'),
+  set: (val) => {
+    if (val) {
+      form.value.areaStations = [0];
+    } else {
+      form.value.areaStations = [];
+    }
+  }
+});
+
 
 /** 查询用户列表 */
 const getList = async () => {
@@ -536,6 +566,7 @@ const handleUpdate = async (row?: UserForm) => {
   );
   form.value.postIds = data.postIds;
   form.value.roleIds = data.roleIds;
+  form.value.areaStations = data.user.areaStations || [];
   form.value.password = '';
 };
 
@@ -578,8 +609,43 @@ const resetForm = () => {
 
   form.value.id = undefined;
   form.value.status = '1';
+  form.value.areaStations = [];
+};
+/** 查询区域站点列表 */
+const getAreaStationList = async () => {
+  const res = (await listAreaStation()) as any;
+  areaStationList.value = res.data;
+  areaStationOptions.value = proxy?.handleTree<AreaStationVO>(res.data, 'id', 'parentId') || [];
+};
+
+/** 格式化显示区域站点路径 */
+const formatAreaStations = (areaStations: (string | number)[]) => {
+  if (!areaStations || areaStations.length === 0) return '-';
+  if (areaStations.includes(0) || areaStations.includes('0')) return '全站点';
+  
+  const labels = areaStations.map(id => {
+    const path = [];
+    let currentId: string | number | undefined = id;
+    
+    // 递归向上查找父级构建完整路径
+    let safetyCounter = 0; // 防止无限循环
+    while (currentId !== undefined && currentId !== null && currentId !== 0 && currentId !== '0' && safetyCounter < 10) {
+      const station = areaStationList.value.find(s => String(s.id) === String(currentId));
+      if (station) {
+        path.unshift(station.name);
+        currentId = station.parentId;
+      } else {
+        break;
+      }
+      safetyCounter++;
+    }
+    return path.join('/');
+  });
+  
+  return labels.join(', ');
 };
 onMounted(() => {
+  getAreaStationList();
   getDeptTree(); // 初始化部门数据
   getList(); // 初始化列表数据
   proxy?.getConfigKey('sys.user.initPassword').then((response) => {
@@ -650,6 +716,11 @@ async function handleDeptChange(value: number | string) {
   color: #333;
 }
 
+.station-path {
+  font-size: 13px;
+  color: #606266;
+}
+
 .role-tag {
   margin-right: 4px;
   margin-bottom: 4px;

+ 165 - 79
src/views/systemConfig/index.vue

@@ -1,37 +1,61 @@
 <template>
   <div class="system-config-container">
-    <div class="menu-tabs">
-      <div 
-        v-for="(item, index) in menuList" 
-        :key="index"
-        :class="['menu-item', { active: activeTab === item.key }]"
-        @click="activeTab = item.key"
-      >
-        {{ item.label }}
+    <el-card shadow="never" class="main-card">
+      <template #header>
+        <div class="page-header">
+          <div class="header-left">
+            <span class="title">系统设置中心</span>
+          </div>
+          <div class="header-right">
+            <div class="custom-tabs">
+              <div v-for="(item, index) in menuList" :key="index"
+                :class="['tab-item', { active: activeTab === item.key }]" @click="activeTab = item.key">
+                {{ item.label }}
+              </div>
+            </div>
+          </div>
+        </div>
+      </template>
+
+      <div class="content-body">
+        <transition name="fade-transform" mode="out-in">
+          <div :key="activeTab" class="content-wrapper">
+            <!-- 网站设置 -->
+            <div v-if="activeTab === 'website'" class="empty-state">
+              <div class="empty-content">
+                <el-icon class="empty-icon"><Setting /></el-icon>
+                <div class="empty-text">网站设置</div>
+                <div class="empty-desc">该模块内容正在深度集成中,敬请期待</div>
+              </div>
+            </div>
+
+            <!-- 平台配置 -->
+            <div v-if="activeTab === 'platform'" class="empty-state">
+              <div class="empty-content">
+                <el-icon class="empty-icon"><Platform /></el-icon>
+                <div class="empty-text">平台配置</div>
+                <div class="empty-desc">参数调优功能即将上线</div>
+              </div>
+            </div>
+
+            <!-- 文件存储配置 -->
+            <OssConfig v-if="activeTab === 'storage'" />
+
+            <!-- 短信配置 -->
+            <SmsConfig v-if="activeTab === 'sms'" />
+
+            <!-- 协议配置 -->
+            <div v-if="activeTab === 'protocol'" class="empty-state">
+              <div class="empty-content">
+                <el-icon class="empty-icon"><Document /></el-icon>
+                <div class="empty-text">协议配置</div>
+                <div class="empty-desc">站点与用户协议管理正在开发中</div>
+              </div>
+            </div>
+          </div>
+        </transition>
       </div>
-    </div>
-    
-    <div class="page-title">{{ activeTitle }}</div>
-
-    <div class="content-container">
-      <!-- 网站设置组件预留 -->
-      <!-- <WebsiteConfig v-if="activeTab === 'website'" /> -->
-      <div v-if="activeTab === 'website'" class="placeholder">{{ activeTitle }}页面开发中...</div>
-
-      <!-- 平台配置组件预留 -->
-      <!-- <PlatformConfig v-if="activeTab === 'platform'" /> -->
-      <div v-if="activeTab === 'platform'" class="placeholder">{{ activeTitle }}页面开发中...</div>
-      
-      <!-- 文件存储配置组件引入 -->
-      <OssConfig v-if="activeTab === 'storage'" />
-
-      <!-- 短信配置组件引入 -->
-      <SmsConfig v-if="activeTab === 'sms'" />
-
-      <!-- 协议配置组件预留 -->
-      <!-- <ProtocolConfig v-if="activeTab === 'protocol'" /> -->
-      <div v-if="activeTab === 'protocol'" class="placeholder">{{ activeTitle }}页面开发中...</div>
-    </div>
+    </el-card>
   </div>
 </template>
 
@@ -39,82 +63,144 @@
 import { ref, computed } from 'vue';
 import SmsConfig from '@/views/systemConfig/sms/index.vue';
 import OssConfig from '@/views/system/oss/config.vue';
+import { Setting, Platform, Document } from '@element-plus/icons-vue';
 
 const activeTab = ref('website');
 
 const menuList = [
   { label: '网站设置', key: 'website' },
   { label: '平台配置', key: 'platform' },
-  { label: '文件存储配置', key: 'storage' },
+  { label: '存储配置', key: 'storage' },
   { label: '短信配置', key: 'sms' },
   { label: '协议配置', key: 'protocol' }
 ];
-
-const activeTitle = computed(() => {
-  const find = menuList.find(item => item.key === activeTab.value);
-  return find ? find.label : '';
-});
 </script>
 
-<style scoped>
+<style scoped lang="scss">
 .system-config-container {
   padding: 20px;
-  background-color: #fff;
+  background-color: #f5f7f9;
   min-height: calc(100vh - 84px);
-  border-radius: 4px;
 }
 
-.menu-tabs {
+.main-card {
+  border: none;
+  border-radius: 12px;
+  background-color: #fff;
+  min-height: calc(100vh - 124px);
+
+  :deep(.el-card__header) {
+    padding: 24px;
+    border-bottom: 1px solid #f0f2f5;
+  }
+}
+
+.page-header {
   display: flex;
-  margin-bottom: 30px;
-  border-bottom: 1px solid #e4e7ed;
+  justify-content: space-between;
+  align-items: center;
 }
 
-.menu-item {
-  padding: 12px 24px;
-  font-size: 14px;
-  color: #606266;
-  cursor: pointer;
-  background-color: transparent;
-  border-top: 1px solid transparent;
-  border-left: 1px solid transparent;
-  border-right: 1px solid transparent;
-  border-bottom: 2px solid transparent;
-  border-radius: 4px 4px 0 0;
-  margin-bottom: -1px;
-  transition: all 0.3s;
+.header-left {
+  .title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #1d2129;
+    position: relative;
+    padding-left: 12px;
+
+    &::before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 50%;
+      transform: translateY(-50%);
+      width: 4px;
+      height: 18px;
+      background: #409eff;
+      border-radius: 2px;
+    }
+  }
 }
 
-.menu-item:hover {
-  color: #409eff;
+.custom-tabs {
+  display: flex;
+  background-color: #f4f5f8;
+  padding: 4px;
+  border-radius: 8px;
+  gap: 4px;
+
+  .tab-item {
+    padding: 6px 16px;
+    font-size: 14px;
+    color: #4e5969;
+    cursor: pointer;
+    border-radius: 6px;
+    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+    user-select: none;
+
+    &:hover {
+      color: #1d2129;
+      background-color: #fff;
+    }
+
+    &.active {
+      background-color: #fff;
+      color: #409eff;
+      font-weight: 600;
+      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+    }
+  }
 }
 
-.menu-item.active {
-  color: #409eff;
-  border-top-color: #e4e7ed;
-  border-left-color: #e4e7ed;
-  border-right-color: #e4e7ed;
-  border-bottom-color: #fff;
-  background-color: #fff;
+.content-body {
+  padding: 24px;
+}
+
+.empty-state {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 400px;
+
+  .empty-content {
+    text-align: center;
+    max-width: 320px;
+  }
+
+  .empty-icon {
+    font-size: 64px;
+    color: #e5e6eb;
+    margin-bottom: 16px;
+  }
+
+  .empty-text {
+    font-size: 18px;
+    font-weight: 600;
+    color: #1d2129;
+    margin-bottom: 8px;
+  }
+
+  .empty-desc {
+    font-size: 14px;
+    color: #86909c;
+    line-height: 1.6;
+  }
 }
 
-.page-title {
-  font-size: 16px;
-  font-weight: bold;
-  color: #303133;
-  margin-bottom: 20px;
+/* Tab 切换动画 */
+.fade-transform-enter-active,
+.fade-transform-leave-active {
+  transition: all 0.3s;
 }
 
-.placeholder {
-  padding: 40px;
-  text-align: center;
-  color: #909399;
-  background-color: #f8f9fa;
-  border-radius: 4px;
+.fade-transform-enter-from {
+  opacity: 0;
+  transform: translateX(-15px);
 }
 
-/* 如果SmsConfig组件自带了padding,这里可以通过深度选择器稍做调整以防多余内边距 */
-:deep(.content-container > .p-2) {
-  padding: 0;
+.fade-transform-leave-to {
+  opacity: 0;
+  transform: translateX(15px);
 }
 </style>

+ 211 - 255
src/views/systemConfig/sms/index.vue

@@ -1,294 +1,250 @@
 <template>
-  <div class="p-2">
-<!--    <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="supplier">-->
-<!--              <el-input v-model="queryParams.supplier" placeholder="请输入供应商" clearable @keyup.enter="handleQuery" />-->
-<!--            </el-form-item>-->
-<!--            <el-form-item label="AccessKey" prop="accessKeyId">-->
-<!--              <el-input v-model="queryParams.accessKeyId" placeholder="请输入AccessKey" clearable @keyup.enter="handleQuery" />-->
-<!--            </el-form-item>-->
-<!--            <el-form-item label="AccessKeySecret" prop="accessKeySecret">-->
-<!--              <el-input v-model="queryParams.accessKeySecret" placeholder="请输入AccessKeySecret" clearable @keyup.enter="handleQuery" />-->
-<!--            </el-form-item>-->
-<!--            <el-form-item label="短信签名" prop="signature">-->
-<!--              <el-input v-model="queryParams.signature" placeholder="请输入短信签名" clearable @keyup.enter="handleQuery" />-->
-<!--            </el-form-item>-->
-<!--            <el-form-item label="sdk-app-id" prop="sdkAppId">-->
-<!--              <el-input v-model="queryParams.sdkAppId" placeholder="请输入sdk-app-id" clearable @keyup.enter="handleQuery" />-->
-<!--            </el-form-item>-->
-<!--            <el-form-item label="模板ID" prop="templateId">-->
-<!--              <el-input v-model="queryParams.templateId" placeholder="请输入模板ID" 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="never">
-      <template #header>
-        <el-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['resource:smsConfig:add']">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['resource:smsConfig:edit']">修改</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['resource:smsConfig:remove']">删除</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['resource:smsConfig:export']">导出</el-button>
-          </el-col>
-<!--          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>-->
-        </el-row>
-      </template>
-
-      <el-table v-loading="loading" border :data="smsConfigList" @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-<!--        <el-table-column label="序号" align="center" prop="id" v-if="true" />-->
-        <el-table-column label="供应商" align="center" prop="supplier" />
-        <el-table-column label="AccessKey" align="center" prop="accessKeyId" />
-        <el-table-column label="AccessKeySecret" align="center" prop="accessKeySecret" />
-        <el-table-column label="短信签名" align="center" prop="signature" />
-        <el-table-column label="sdk-app-id" align="center" prop="sdkAppId" />
-        <el-table-column label="模板ID" align="center" prop="templateId" />
-        <el-table-column label="状态" align="center" prop="status">
-          <template #default="scope">
-            <el-switch v-model="scope.row.status" :active-value="true" :inactive-value="false" @change="handleStatusChange(scope.row)"></el-switch>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
-          <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['resource:smsConfig:edit']"></el-button>
-            </el-tooltip>
-            <el-tooltip content="删除" placement="top">
-              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['resource:smsConfig:remove']"></el-button>
-            </el-tooltip>
-          </template>
-        </el-table-column>
-      </el-table>
-
-      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
-    </el-card>
-    <!-- 添加或修改短信配置对话框 -->
-    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
-      <el-form ref="smsConfigFormRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="供应商" prop="supplier">
-          <el-input v-model="form.supplier" placeholder="请输入供应商" />
+  <div class="sms-config-setting">
+    <!-- 顶部提示信息 -->
+    <div class="setting-hint">
+      <el-alert :closable="false" type="info" class="custom-alert">
+        <template #title>
+          <div class="alert-content">
+            <el-icon class="info-icon"><InfoFilled /></el-icon>
+            <span>短信目前仅支持 3 家,绑定哪一家后要将模板提交到对方平台审核回填模板 ID;模板中的变量请按照对应平台的标准更改。</span>
+          </div>
+        </template>
+      </el-alert>
+    </div>
+
+    <!-- 配置表单区 -->
+    <div class="setting-body">
+      <el-form ref="smsFormRef" :model="form" :rules="rules" label-width="140px" label-position="right" class="premium-setting-form">
+        
+        <!-- 供应商选择 -->
+        <el-form-item label="短信设置:">
+          <el-radio-group v-model="form.supplier" @change="handleSupplierChange" class="custom-radio-group">
+            <el-radio label="alibaba" border>阿里云</el-radio>
+            <el-radio label="qiniu" border>七牛云</el-radio>
+            <el-radio label="tencent" border>腾讯云</el-radio>
+          </el-radio-group>
         </el-form-item>
-        <el-form-item label="AccessKey" prop="accessKeyId">
-          <el-input v-model="form.accessKeyId" placeholder="请输入AccessKey" />
+
+        <!-- 模板 ID -->
+        <el-form-item label="验证码模板 ID:" prop="templateId">
+          <el-input v-model="form.templateId" placeholder="请输入在对应平台申请的验证码短信模板 ID" class="config-input" />
+          <div class="form-tip">请复制短信模版去申请:尊敬的用户您的短信验证码是${code},10分钟内有效!</div>
         </el-form-item>
-        <el-form-item label="AccessKeySecret" prop="accessKeySecret">
-          <el-input v-model="form.accessKeySecret" placeholder="请输入AccessKeySecret" />
+
+        <!-- AccessKey ID -->
+        <el-form-item label="AccessKey_id:" prop="accessKeyId">
+          <el-input v-model="form.accessKeyId" placeholder="请输入密钥 AccessKey_id" class="config-input" />
         </el-form-item>
-        <el-form-item label="短信签名" prop="signature">
-          <el-input v-model="form.signature" placeholder="请输入短信签名" />
+
+        <!-- AccessKey Secret -->
+        <el-form-item label="AccessKey_Secret:" prop="accessKeySecret">
+          <el-input v-model="form.accessKeySecret" type="password" show-password placeholder="请输入密钥 AccessKey_Secret" class="config-input" />
         </el-form-item>
-        <el-form-item label="sdk-app-id" prop="sdkAppId">
-          <el-input v-model="form.sdkAppId" placeholder="请输入sdk-app-id" />
+
+        <!-- 短信签名 -->
+        <el-form-item label="短信签名:" prop="signature">
+          <el-input v-model="form.signature" placeholder="请输入短信签名" class="config-input" />
+          <div class="form-tip">需要对方平台审核通过</div>
         </el-form-item>
-        <el-form-item label="模板ID" prop="templateId">
-          <el-input v-model="form.templateId" placeholder="请输入模板ID" />
+
+        <!-- 保存按钮 -->
+        <el-form-item class="action-item">
+          <el-button type="primary" class="save-btn" :loading="buttonLoading" @click="submitForm">保存设置</el-button>
         </el-form-item>
       </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
-          <el-button @click="cancel">取 消</el-button>
-        </div>
-      </template>
-    </el-dialog>
+    </div>
   </div>
 </template>
 
 <script setup name="SmsConfig" lang="ts">
-import { listSmsConfig, getSmsConfig, delSmsConfig, addSmsConfig, updateSmsConfig, changeSmsConfigStatus } from '@/api/resource/smsConfig';
-import { SmsConfigVO, SmsConfigQuery, SmsConfigForm } from '@/api/resource/smsConfig/types';
+import { listSmsConfig, addSmsConfig, updateSmsConfig } from '@/api/resource/smsConfig';
+import { SmsConfigForm } from '@/api/resource/smsConfig/types';
+import { InfoFilled } from '@element-plus/icons-vue';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
-const smsConfigList = ref<SmsConfigVO[]>([]);
 const buttonLoading = ref(false);
-const loading = ref(true);
-const showSearch = ref(true);
-const ids = ref<Array<string | number>>([]);
-const single = ref(true);
-const multiple = ref(true);
-const total = ref(0);
+const smsFormRef = ref<ElFormInstance>();
 
-const queryFormRef = ref<ElFormInstance>();
-const smsConfigFormRef = ref<ElFormInstance>();
-
-const dialog = reactive<DialogOption>({
-  visible: false,
-  title: ''
-});
-
-const initFormData: SmsConfigForm = {
+const form = ref<SmsConfigForm>({
   id: undefined,
-  supplier: undefined,
-  accessKeyId: undefined,
-  accessKeySecret: undefined,
-  signature: undefined,
-  sdkAppId: undefined,
-  templateId: undefined,
-  status: undefined,
-}
-const data = reactive<PageData<SmsConfigForm, SmsConfigQuery>>({
-  form: {...initFormData},
-  queryParams: {
-    pageNum: 1,
-    pageSize: 10,
-    supplier: undefined,
-    accessKeyId: undefined,
-    accessKeySecret: undefined,
-    signature: undefined,
-    sdkAppId: undefined,
-    templateId: undefined,
-    status: undefined,
-    params: {
-    }
-  },
-  rules: {
-    id: [
-      { required: true, message: "序号不能为空", trigger: "blur" }
-    ],
-    supplier: [
-      { required: true, message: "供应商不能为空", trigger: "blur" }
-    ],
-    accessKeyId: [
-      { required: true, message: "AccessKey不能为空", trigger: "blur" }
-    ],
-    accessKeySecret: [
-      { required: true, message: "AccessKeySecret不能为空", trigger: "blur" }
-    ],
-    signature: [
-      { required: true, message: "短信签名不能为空", trigger: "blur" }
-    ],
-    sdkAppId: [
-      { required: true, message: "sdk-app-id不能为空", trigger: "blur" }
-    ],
-    templateId: [
-      { required: true, message: "模板ID不能为空", trigger: "blur" }
-    ]
-  }
+  supplier: 'alibaba',
+  accessKeyId: '',
+  accessKeySecret: '',
+  signature: '',
+  sdkAppId: '',
+  templateId: '',
+  status: true,
 });
 
-const { queryParams, form, rules } = toRefs(data);
+const rules = {
+  templateId: [{ required: true, message: "验证码模板 ID 不能为空", trigger: "blur" }],
+  accessKeyId: [{ required: true, message: "AccessKey_id 不能为空", trigger: "blur" }],
+  accessKeySecret: [{ required: true, message: "AccessKey_Secret 不能为空", trigger: "blur" }],
+  signature: [{ required: true, message: "短信签名 不能为空", trigger: "blur" }],
+};
 
-/** 查询短信配置列表 */
-const getList = async () => {
-  loading.value = true;
-  const res = await listSmsConfig(queryParams.value);
-  smsConfigList.value = res.rows;
-  total.value = res.total;
-  loading.value = false;
-}
+/** 加载对应供应商配置 */
+const loadSupplierConfig = async (supplier: string) => {
+  const res = await listSmsConfig({ supplier } as any);
+  if (res.rows && res.rows.length > 0) {
+    Object.assign(form.value, res.rows[0]);
+  } else {
+    // 若无配置,重置表单(保留当前选中的供应商)
+    const currentSupplier = form.value.supplier;
+    form.value = {
+      id: undefined,
+      supplier: currentSupplier,
+      accessKeyId: '',
+      accessKeySecret: '',
+      signature: '',
+      sdkAppId: '',
+      templateId: '',
+      status: true,
+    };
+  }
+};
 
-/** 取消按钮 */
-const cancel = () => {
-  reset();
-  dialog.visible = false;
-}
+/** 切换供应商触发表单刷新 */
+const handleSupplierChange = (val: any) => {
+  loadSupplierConfig(val);
+};
 
-/** 表单重置 */
-const reset = () => {
-  form.value = {...initFormData};
-  smsConfigFormRef.value?.resetFields();
-}
+/** 提交保存 */
+const submitForm = () => {
+  smsFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      try {
+        // 保存时强制确保 status 为启用
+        form.value.status = true;
+        if (form.value.id) {
+          await updateSmsConfig(form.value);
+        } else {
+          await addSmsConfig(form.value);
+        }
+        proxy?.$modal.msgSuccess("配置已保存并启用");
+        await loadSupplierConfig(form.value.supplier as string);
+      } finally {
+        buttonLoading.value = false;
+      }
+    }
+  });
+};
 
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.value.pageNum = 1;
-  getList();
-}
+/** 初始化:加载当前已启用的配置 */
+const initSmsConfig = async () => {
+  // 先查所有配置,找到 status 为 true 的那一项
+  const res = await listSmsConfig({} as any);
+  if (res.rows && res.rows.length > 0) {
+    const activeItem = res.rows.find((item: any) => item.status === true);
+    if (activeItem) {
+      form.value.supplier = activeItem.supplier;
+      Object.assign(form.value, activeItem);
+    } else {
+      // 没配置就默认第一个阿里
+      loadSupplierConfig('alibaba');
+    }
+  } else {
+    loadSupplierConfig('alibaba');
+  }
+};
 
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value?.resetFields();
-  handleQuery();
-}
+onMounted(() => {
+  initSmsConfig();
+});
+</script>
 
-/** 多选框选中数据 */
-const handleSelectionChange = (selection: SmsConfigVO[]) => {
-  ids.value = selection.map(item => item.id);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
+<style scoped lang="scss">
+.sms-config-setting {
+  padding: 8px 0;
 }
 
-/** 新增按钮操作 */
-const handleAdd = () => {
-  reset();
-  dialog.visible = true;
-  dialog.title = "添加短信配置";
+.setting-hint {
+  margin-bottom: 32px;
+  
+  .custom-alert {
+    background-color: #e8f4ff;
+    border: none;
+    border-radius: 8px;
+    padding: 12px 16px;
+
+    .alert-content {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      color: #1890ff;
+      font-size: 14px;
+      line-height: 1.5;
+
+      .info-icon {
+        font-size: 18px;
+      }
+    }
+  }
 }
 
-/** 修改按钮操作 */
-const handleUpdate = async (row?: SmsConfigVO) => {
-  reset();
-  const _id = row?.id || ids.value[0]
-  const res = await getSmsConfig(_id);
-  Object.assign(form.value, res.data);
-  dialog.visible = true;
-  dialog.title = "修改短信配置";
+.setting-body {
+  padding-left: 20px;
 }
 
-/** 提交按钮 */
-const submitForm = () => {
-  smsConfigFormRef.value?.validate(async (valid: boolean) => {
-    if (valid) {
-      buttonLoading.value = true;
-      if (form.value.id) {
-        await updateSmsConfig(form.value).finally(() =>  buttonLoading.value = false);
-      } else {
-        await addSmsConfig(form.value).finally(() =>  buttonLoading.value = false);
+.premium-setting-form {
+  :deep(.el-form-item__label) {
+    font-weight: 500;
+    color: #4e5969;
+    padding-right: 24px;
+  }
+
+  .config-input {
+    max-width: 520px;
+    
+    :deep(.el-input__wrapper) {
+      box-shadow: 0 0 0 1px #e5e6eb inset;
+      padding: 4px 12px;
+      border-radius: 6px;
+      
+      &.is-focus {
+        box-shadow: 0 0 0 1px #409eff inset;
       }
-      proxy?.$modal.msgSuccess("操作成功");
-      dialog.visible = false;
-      await getList();
     }
-  });
-}
+  }
 
-/** 状态修改  */
-const handleStatusChange = async (row: SmsConfigVO) => {
-  const text = row.status ? '启用' : '停用';
-  try {
-    await proxy?.$modal.confirm('确认要"' + text + '""' + row.supplier + '"配置吗?');
-    await changeSmsConfigStatus(row.id, row.status);
-    proxy?.$modal.msgSuccess(text + '成功');
-    await getList();
-  } catch {
-    row.status = !row.status;
+  .form-tip {
+    font-size: 13px;
+    color: #86909c;
+    margin-top: 8px;
+    line-height: 1.4;
   }
-};
 
-/** 删除按钮操作 */
-const handleDelete = async (row?: SmsConfigVO) => {
-  const _ids = row?.id || ids.value;
-  await proxy?.$modal.confirm('是否确认删除短信配置编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
-  await delSmsConfig(_ids);
-  proxy?.$modal.msgSuccess("删除成功");
-  await getList();
-}
+  .custom-radio-group {
+    :deep(.el-radio) {
+      margin-right: 16px;
+      border-radius: 6px;
+      padding: 0 20px;
+      height: 36px;
+      
+      &.is-bordered.is-checked {
+        border-color: #409eff;
+        background-color: rgba(64, 158, 255, 0.04);
+      }
+      
+      .el-radio__label {
+        font-size: 14px;
+      }
+    }
+  }
 
-/** 导出按钮操作 */
-const handleExport = () => {
-  proxy?.download('resource/smsConfig/export', {
-    ...queryParams.value
-  }, `smsConfig_${new Date().getTime()}.xlsx`)
-}
+  .action-item {
+    margin-top: 40px;
+  }
 
-onMounted(() => {
-  getList();
-});
-</script>
+  .save-btn {
+    padding: 12px 32px;
+    height: auto;
+    font-size: 15px;
+    font-weight: 500;
+    border-radius: 6px;
+    box-shadow: 0 4px 10px rgba(64, 158, 255, 0.2);
+  }
+}
+</style>