Explorar el Código

1-19-zl-我的工作台

林小张 hace 2 meses
padre
commit
8af8daf85b

+ 2 - 0
src/components.d.ts

@@ -37,6 +37,7 @@ declare module 'vue' {
     ElTree: typeof import('element-plus/es')['ElTree']
     ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
     ElUpload: typeof import('element-plus/es')['ElUpload']
+    EmptyState: typeof import('./components/EmptyState/index.vue')['default']
     HeaderBar: typeof import('./components/HeaderBar/index.vue')['default']
     PageTitle: typeof import('./components/PageTitle/index.vue')['default']
     ProductCard: typeof import('./components/ProductCard/index.vue')['default']
@@ -46,6 +47,7 @@ declare module 'vue' {
     SearchBar: typeof import('./components/SearchBar/index.vue')['default']
     StatCards: typeof import('./components/StatCards/index.vue')['default']
     StatusTabs: typeof import('./components/StatusTabs/index.vue')['default']
+    TableActions: typeof import('./components/TableActions/index.vue')['default']
     TablePagination: typeof import('./components/TablePagination/index.vue')['default']
   }
 }

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

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

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

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

+ 5 - 1
src/components/index.ts

@@ -7,6 +7,8 @@ import SearchBar from './SearchBar/index.vue'
 import StatCards from './StatCards/index.vue'
 import TablePagination from './TablePagination/index.vue'
 import HeaderBar from './HeaderBar/index.vue'
+import EmptyState from './EmptyState/index.vue'
+import TableActions from './TableActions/index.vue'
 
 export {
   PageTitle,
@@ -16,5 +18,7 @@ export {
   SearchBar,
   StatCards,
   TablePagination,
-  HeaderBar
+  HeaderBar,
+  EmptyState,
+  TableActions
 }

+ 30 - 0
src/router/index.ts

@@ -300,4 +300,34 @@ const router = createRouter({
   routes
 })
 
+// 白名单路由(不需要登录)
+const whiteList = ['/login', '/register', '/404']
+
+// 路由守卫
+router.beforeEach(async (to, from, next) => {
+  // 设置页面标题
+  const title = to.meta.title as string
+  document.title = title ? `${title} - 优易商城` : '优易商城'
+
+  const token = localStorage.getItem('token')
+
+  if (token) {
+    // 已登录
+    if (to.path === '/login') {
+      next('/')
+    } else {
+      next()
+    }
+  } else {
+    // 未登录
+    if (whiteList.includes(to.path)) {
+      next()
+    } else {
+      // TODO: 暂时不拦截,等登录页面做好后再启用
+      // next(`/login?redirect=${to.path}`)
+      next()
+    }
+  }
+})
+
 export default router

+ 34 - 0
src/stores/app.ts

@@ -0,0 +1,34 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useAppStore = defineStore('app', () => {
+  // 页面加载状态
+  const loading = ref(false)
+
+  // 侧边栏折叠状态
+  const sidebarCollapsed = ref(false)
+
+  // 当前展开的菜单
+  const openedMenus = ref<string[]>([])
+
+  const setLoading = (value: boolean) => {
+    loading.value = value
+  }
+
+  const toggleSidebar = () => {
+    sidebarCollapsed.value = !sidebarCollapsed.value
+  }
+
+  const setOpenedMenus = (menus: string[]) => {
+    openedMenus.value = menus
+  }
+
+  return {
+    loading,
+    sidebarCollapsed,
+    openedMenus,
+    setLoading,
+    toggleSidebar,
+    setOpenedMenus
+  }
+})

+ 93 - 0
src/stores/cart.ts

@@ -0,0 +1,93 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+
+export interface CartItem {
+  id: number
+  productId: number
+  productName: string
+  productImg: string
+  spec: string
+  price: number
+  quantity: number
+  checked: boolean
+}
+
+export const useCartStore = defineStore('cart', () => {
+  const cartList = ref<CartItem[]>([])
+
+  // 购物车数量
+  const cartCount = computed(() => cartList.value.reduce((sum, item) => sum + item.quantity, 0))
+
+  // 选中的商品
+  const checkedItems = computed(() => cartList.value.filter(item => item.checked))
+
+  // 选中商品总价
+  const checkedTotal = computed(() => 
+    checkedItems.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
+  )
+
+  // 添加到购物车
+  const addToCart = (product: Omit<CartItem, 'id' | 'checked'>) => {
+    const existItem = cartList.value.find(
+      item => item.productId === product.productId && item.spec === product.spec
+    )
+    if (existItem) {
+      existItem.quantity += product.quantity
+    } else {
+      cartList.value.push({
+        ...product,
+        id: Date.now(),
+        checked: true
+      })
+    }
+  }
+
+  // 更新数量
+  const updateQuantity = (id: number, quantity: number) => {
+    const item = cartList.value.find(item => item.id === id)
+    if (item) {
+      item.quantity = Math.max(1, quantity)
+    }
+  }
+
+  // 删除商品
+  const removeFromCart = (id: number) => {
+    const index = cartList.value.findIndex(item => item.id === id)
+    if (index > -1) {
+      cartList.value.splice(index, 1)
+    }
+  }
+
+  // 切换选中状态
+  const toggleChecked = (id: number) => {
+    const item = cartList.value.find(item => item.id === id)
+    if (item) {
+      item.checked = !item.checked
+    }
+  }
+
+  // 全选/取消全选
+  const toggleAllChecked = (checked: boolean) => {
+    cartList.value.forEach(item => {
+      item.checked = checked
+    })
+  }
+
+  // 清空购物车
+  const clearCart = () => {
+    cartList.value = []
+  }
+
+  return {
+    cartList,
+    cartCount,
+    checkedItems,
+    checkedTotal,
+    addToCart,
+    updateQuantity,
+    removeFromCart,
+    toggleChecked,
+    toggleAllChecked,
+    clearCart
+  }
+})

+ 9 - 0
src/stores/index.ts

@@ -0,0 +1,9 @@
+import { createPinia } from 'pinia'
+
+const pinia = createPinia()
+
+export default pinia
+
+export * from './user'
+export * from './cart'
+export * from './app'

+ 65 - 0
src/stores/user.ts

@@ -0,0 +1,65 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+
+export interface UserInfo {
+  id: number
+  username: string
+  nickname: string
+  avatar: string
+  phone: string
+  companyName: string
+  deptName: string
+  role: string
+}
+
+export const useUserStore = defineStore('user', () => {
+  const token = ref(localStorage.getItem('token') || '')
+  const userInfo = ref<UserInfo | null>(null)
+
+  const isLoggedIn = computed(() => !!token.value)
+
+  // 模拟用户信息
+  const mockUserInfo: UserInfo = {
+    id: 1,
+    username: 'admin',
+    nickname: '张三',
+    avatar: '',
+    phone: '138****8888',
+    companyName: '优易科技有限公司',
+    deptName: '采购部',
+    role: '采购负责人'
+  }
+
+  // 登录
+  const login = async (username: string, password: string) => {
+    // TODO: 调用登录接口
+    token.value = 'mock_token_' + Date.now()
+    localStorage.setItem('token', token.value)
+    userInfo.value = mockUserInfo
+    return true
+  }
+
+  // 获取用户信息
+  const getUserInfo = async () => {
+    if (!token.value) return null
+    // TODO: 调用获取用户信息接口
+    userInfo.value = mockUserInfo
+    return userInfo.value
+  }
+
+  // 退出登录
+  const logout = () => {
+    token.value = ''
+    userInfo.value = null
+    localStorage.removeItem('token')
+  }
+
+  return {
+    token,
+    userInfo,
+    isLoggedIn,
+    login,
+    getUserInfo,
+    logout
+  }
+})

+ 46 - 0
src/types/common.ts

@@ -0,0 +1,46 @@
+// 通用类型定义
+
+// 分页参数
+export interface PageParams {
+  pageNum: number
+  pageSize: number
+}
+
+// 分页响应
+export interface PageResult<T> {
+  list: T[]
+  total: number
+  pageNum: number
+  pageSize: number
+}
+
+// API 响应
+export interface ApiResponse<T = any> {
+  code: number
+  msg: string
+  data: T
+}
+
+// 下拉选项
+export interface SelectOption {
+  label: string
+  value: string | number
+}
+
+// 树形结构
+export interface TreeNode {
+  id: number
+  name: string
+  parentId?: number
+  children?: TreeNode[]
+}
+
+// 表格列配置
+export interface TableColumn {
+  prop: string
+  label: string
+  width?: number | string
+  minWidth?: number | string
+  align?: 'left' | 'center' | 'right'
+  fixed?: 'left' | 'right' | boolean
+}

+ 68 - 0
src/types/enterprise.ts

@@ -0,0 +1,68 @@
+// 企业模块类型定义
+
+// 企业信息
+export interface CompanyInfo {
+  id: number
+  companyName: string
+  creditCode: string
+  legalPerson: string
+  address: string
+  phone: string
+  email: string
+  status: string
+}
+
+// 收货地址
+export interface Address {
+  id: number
+  name: string
+  phone: string
+  province: string
+  city: string
+  district: string
+  detail: string
+  isDefault: boolean
+}
+
+// 发票抬头
+export interface InvoiceTitle {
+  id: number
+  type: 'personal' | 'company'
+  title: string
+  taxNo?: string
+  bankName?: string
+  bankAccount?: string
+  address?: string
+  phone?: string
+  isDefault: boolean
+}
+
+// 商品
+export interface Product {
+  id: number
+  name: string
+  image: string
+  price: number
+  originalPrice?: number
+  spec: string
+  category: string
+  brand: string
+  stock: number
+  sales: number
+}
+
+// 收藏商品
+export interface CollectionItem {
+  id: number
+  productId: number
+  product: Product
+  createTime: string
+}
+
+// 浏览足迹
+export interface FootprintItem {
+  id: number
+  productId: number
+  product: Product
+  viewTime: string
+}

+ 5 - 0
src/types/index.ts

@@ -0,0 +1,5 @@
+// 统一导出类型
+export * from './common'
+export * from './enterprise'
+export * from './trade'
+export * from './organization'

+ 70 - 0
src/types/organization.ts

@@ -0,0 +1,70 @@
+// 组织管理模块类型定义
+
+// 部门
+export interface Department {
+  id: number
+  name: string
+  parentId: number
+  sort: number
+  status: string
+  children?: Department[]
+}
+
+// 员工
+export interface Staff {
+  id: number
+  name: string
+  phone: string
+  email?: string
+  deptId: number
+  deptName: string
+  roleId: number
+  roleName: string
+  status: string
+  createTime: string
+}
+
+// 角色
+export interface Role {
+  id: number
+  name: string
+  code: string
+  description?: string
+  status: string
+  permissions?: string[]
+}
+
+// 审批流程
+export interface ApprovalFlow {
+  id: number
+  name: string
+  type: string
+  description?: string
+  nodes: ApprovalNode[]
+  status: string
+  createTime: string
+}
+
+// 审批节点
+export interface ApprovalNode {
+  id: number
+  name: string
+  type: 'start' | 'approval' | 'condition' | 'end'
+  approverType: 'user' | 'role' | 'dept'
+  approverId: number
+  approverName: string
+  nextNodeId?: number
+}
+
+// 关联企业
+export interface RelatedEnterprise {
+  id: number
+  companyName: string
+  contact: string
+  phone: string
+  monthLimit: number
+  yearLimit: number
+  creditLimit: number
+  remainLimit: number
+  purchaseStatus: string
+}

+ 60 - 0
src/types/trade.ts

@@ -0,0 +1,60 @@
+// 交易模块类型定义
+
+// 订单状态
+export type OrderStatus = 'pending' | 'paid' | 'shipped' | 'received' | 'completed' | 'cancelled'
+
+// 订单商品
+export interface OrderProduct {
+  id: number
+  productId: number
+  productName: string
+  productImg: string
+  spec: string
+  price: number
+  quantity: number
+}
+
+// 订单
+export interface Order {
+  id: number
+  orderNo: string
+  status: OrderStatus
+  statusText: string
+  products: OrderProduct[]
+  totalAmount: number
+  payAmount: number
+  freight: number
+  createTime: string
+  payTime?: string
+  shipTime?: string
+  receiveTime?: string
+  address: {
+    name: string
+    phone: string
+    address: string
+  }
+  remark?: string
+}
+
+// 售后申请
+export interface AfterSale {
+  id: number
+  orderNo: string
+  type: 'refund' | 'return' | 'exchange'
+  reason: string
+  amount: number
+  status: string
+  createTime: string
+}
+
+// 订单评价
+export interface OrderEvaluation {
+  id: number
+  orderNo: string
+  productId: number
+  productName: string
+  rating: number
+  content: string
+  images?: string[]
+  createTime: string
+}

+ 53 - 0
src/utils/confirm.ts

@@ -0,0 +1,53 @@
+import { ElMessageBox, ElMessage } from 'element-plus'
+
+/**
+ * 删除确认弹窗
+ * @param name 要删除的名称
+ * @param onConfirm 确认回调
+ */
+export const confirmDelete = (name: string, onConfirm: () => void | Promise<void>) => {
+  ElMessageBox.confirm(`确定要删除"${name}"吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    await onConfirm()
+    ElMessage.success('删除成功')
+  }).catch(() => {})
+}
+
+/**
+ * 批量删除确认弹窗
+ * @param count 要删除的数量
+ * @param onConfirm 确认回调
+ */
+export const confirmBatchDelete = (count: number, onConfirm: () => void | Promise<void>) => {
+  ElMessageBox.confirm(`确定要删除选中的 ${count} 条数据吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    await onConfirm()
+    ElMessage.success('删除成功')
+  }).catch(() => {})
+}
+
+/**
+ * 通用确认弹窗
+ * @param message 提示信息
+ * @param onConfirm 确认回调
+ * @param title 标题
+ */
+export const confirm = (
+  message: string, 
+  onConfirm: () => void | Promise<void>,
+  title = '提示'
+) => {
+  ElMessageBox.confirm(message, title, {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    await onConfirm()
+  }).catch(() => {})
+}

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

@@ -188,6 +188,8 @@ const handleTabClick = (tabKey: string) => {
   activeTab.value = tabKey
   if (tabKey === 'security') router.push('/enterprise/securitySetting')
   else if (tabKey === 'purchaseHabit') router.push('/enterprise/purchaseHabit')
+  else if (tabKey === 'invoice') router.push('/enterprise/invoiceManage')
+  else if (tabKey === 'creditApply') router.push('/cost/quotaControl/apply')
 }
 </script>