refactor(developer-config): 移除开发者配置页面相关代码和文档
- 删除应用配置页面及相关组件,重构路由为 /developer/config/[id].vue - 移除开发者文档页面及其导航与样式实现 - 清理开发者侧功能完善工作日志文件 - 删除全局.gitignore配置文件,清理无用忽略规则 - 优化应用配置页面的参数读取和路由结构,解决刷新404问题 - 解决数据库配置唯一键冲突,调整保存逻辑避免重复插入 - 移除对后端配置加密字段的 secret 标记,修正加密异常问题
This commit is contained in:
247
app/pages/admin/account.vue
Normal file
247
app/pages/admin/account.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
const { user } = useUser()
|
||||
|
||||
// 个人信息数据
|
||||
const profile = reactive({
|
||||
username: 'admin',
|
||||
nickname: '系统管理员',
|
||||
email: 'admin@company.com',
|
||||
phone: '138****8888',
|
||||
department: '信息技术部',
|
||||
position: '系统管理员',
|
||||
joinDate: '2024-01-15',
|
||||
lastLogin: '2026-04-09 07:00',
|
||||
avatar: '',
|
||||
})
|
||||
|
||||
// 安全设置
|
||||
const securitySettings = reactive({
|
||||
emailVerified: true,
|
||||
phoneVerified: true,
|
||||
twoFactorEnabled: false,
|
||||
loginPwdChanged: '2026-03-15',
|
||||
})
|
||||
|
||||
// 修改密码表单
|
||||
const passwordForm = reactive({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
|
||||
const passwordRules = {
|
||||
newPassword: [{ required: true, message: '请输入新密码', trigger: 'blur' }],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule: any, value: string) => {
|
||||
if (value !== passwordForm.newPassword) {
|
||||
return Promise.reject('两次输入的密码不一致')
|
||||
}
|
||||
return Promise.resolve()
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// 操作日志
|
||||
const loginHistory = ref([
|
||||
{ time: '2026-04-09 07:00:00', ip: '192.168.1.100', device: 'Chrome / Windows 11', location: '广东深圳', status: '成功' },
|
||||
{ time: '2026-04-08 18:30:00', ip: '192.168.1.100', device: 'Chrome / macOS', location: '广东深圳', status: '成功' },
|
||||
{ time: '2026-04-08 09:15:00', ip: '192.168.1.101', device: 'Safari / iOS', location: '广东广州', status: '成功' },
|
||||
{ time: '2026-04-07 16:45:00', ip: '10.0.0.1', device: 'Firefox / Ubuntu', location: '广东深圳', status: '成功' },
|
||||
{ time: '2026-04-07 08:00:00', ip: '192.168.1.100', device: 'Chrome / Windows 11', location: '广东深圳', status: '成功' },
|
||||
])
|
||||
|
||||
const activeTab = ref('profile')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="account-page">
|
||||
<a-tabs v-model:activeKey="activeTab" class="account-tabs">
|
||||
<!-- 基本信息 -->
|
||||
<a-tab-pane key="profile" tab="基本信息">
|
||||
<a-row :gutter="24">
|
||||
<a-col :xs="24" :lg="16">
|
||||
<a-card title="个人信息" class="info-card">
|
||||
<a-descriptions :column="{ xs: 1, sm: 2 }" bordered>
|
||||
<a-descriptions-item label="用户名">{{ profile.username }}</a-descriptions-item>
|
||||
<a-descriptions-item label="昵称">{{ profile.nickname }}</a-descriptions-item>
|
||||
<a-descriptions-item label="邮箱">
|
||||
{{ profile.email }}
|
||||
<a-tag v-if="securitySettings.emailVerified" color="success" size="small">已认证</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="手机号">
|
||||
{{ profile.phone }}
|
||||
<a-tag v-if="securitySettings.phoneVerified" color="success" size="small">已认证</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="部门">{{ profile.department }}</a-descriptions-item>
|
||||
<a-descriptions-item label="职位">{{ profile.position }}</a-descriptions-item>
|
||||
<a-descriptions-item label="入职日期">{{ profile.joinDate }}</a-descriptions-item>
|
||||
<a-descriptions-item label="上次登录">{{ profile.lastLogin }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<div class="mt-4">
|
||||
<a-button type="primary">编辑资料</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="头像设置" class="avatar-card">
|
||||
<div class="avatar-upload">
|
||||
<a-avatar :size="100" :src="profile.avatar">
|
||||
<template #icon><UserOutlined /></template>
|
||||
</a-avatar>
|
||||
<div class="mt-4">
|
||||
<a-button size="small">更换头像</a-button>
|
||||
<p class="text-xs text-gray-400 mt-2">支持 JPG、PNG 格式,文件小于 2MB</p>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 账号安全 -->
|
||||
<a-tab-pane key="security" tab="账号安全">
|
||||
<a-row :gutter="24">
|
||||
<a-col :xs="24" :lg="16">
|
||||
<!-- 登录密码 -->
|
||||
<a-card title="登录密码" class="security-card mb-4">
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<div class="security-title">登录密码</div>
|
||||
<div class="security-desc">上次修改于 {{ securitySettings.loginPwdChanged }}</div>
|
||||
</div>
|
||||
<a-button>修改密码</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 邮箱绑定 -->
|
||||
<a-card title="邮箱绑定" class="security-card mb-4">
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<div class="security-title">{{ profile.email }}</div>
|
||||
<div class="security-desc">
|
||||
<a-tag v-if="securitySettings.emailVerified" color="success" size="small">已验证</a-tag>
|
||||
<span v-else class="text-orange-500">未验证</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-button type="primary" ghost>更换邮箱</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 手机绑定 -->
|
||||
<a-card title="手机绑定" class="security-card mb-4">
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<div class="security-title">{{ profile.phone }}</div>
|
||||
<div class="security-desc">
|
||||
<a-tag v-if="securitySettings.phoneVerified" color="success" size="small">已验证</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<a-button type="primary" ghost>更换手机</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 两步验证 -->
|
||||
<a-card title="两步验证" class="security-card">
|
||||
<div class="security-item">
|
||||
<div class="security-info">
|
||||
<div class="security-title">开启两步验证</div>
|
||||
<div class="security-desc">启用后登录需输入手机验证码,提升账号安全</div>
|
||||
</div>
|
||||
<a-switch v-model:checked="securitySettings.twoFactorEnabled" />
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 登录历史 -->
|
||||
<a-tab-pane key="history" tab="登录历史">
|
||||
<a-card title="最近登录记录">
|
||||
<a-table :dataSource="loginHistory" :pagination="false" rowKey="time" size="small">
|
||||
<a-table-column title="登录时间" dataIndex="time" width="180" />
|
||||
<a-table-column title="IP 地址" dataIndex="ip" width="140" />
|
||||
<a-table-column title="设备" dataIndex="device" />
|
||||
<a-table-column title="位置" dataIndex="location" width="100" />
|
||||
<a-table-column title="状态" dataIndex="status" width="80" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="text === '成功' ? 'success' : 'error'">{{ text }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.account-page {
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.account-tabs :deep(.ant-tabs-nav) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-card,
|
||||
.security-card,
|
||||
.avatar-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.avatar-upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.security-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.security-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.security-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.security-desc {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.text-orange-500 {
|
||||
color: #f97316;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,415 +1,234 @@
|
||||
<template>
|
||||
<div class="developers-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2 class="page-title">🧑💻 开发者管理</h2>
|
||||
<p class="page-desc">管理平台上有应用发布记录的开发者账号</p>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button @click="loadData" :loading="loading">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<a-card :bordered="false">
|
||||
<template #title>开发者管理</template>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="12" :md="6" v-for="stat in stats" :key="stat.label">
|
||||
<div class="stat-card" :class="stat.color">
|
||||
<div class="stat-icon">{{ stat.icon }}</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
<a-tabs v-model:activeKey="activeTab">
|
||||
<!-- 开发者申请 -->
|
||||
<a-tab-pane key="apply" tab="开发者申请">
|
||||
<div class="filter-bar">
|
||||
<a-input-search v-model:value="searchKeyword" placeholder="搜索申请人、企业..." style="width: 280px" allow-clear />
|
||||
<a-select v-model:value="filterType" placeholder="申请类型" style="width: 150px" allow-clear>
|
||||
<a-select-option value="api">API 开发者</a-select-option>
|
||||
<a-select-option value="plugin">插件开发者</a-select-option>
|
||||
<a-select-option value="template">模板开发者</a-select-option>
|
||||
</a-select>
|
||||
<a-button @click="resetFilter">重置</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="filter-bar">
|
||||
<a-radio-group v-model:value="filterType" button-style="solid" @change="handleFilterChange">
|
||||
<a-radio-button :value="2">开发者用户</a-radio-button>
|
||||
<a-radio-button :value="null">全部用户</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索用户名/昵称/手机号"
|
||||
style="width: 240px"
|
||||
allow-clear
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 开发者列表 -->
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">🧑💻 用户列表</span>
|
||||
<a-tag color="blue">共 {{ pagination.total }} 人</a-tag>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="developers"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="userId"
|
||||
@change="handleTableChange"
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 开发者信息 -->
|
||||
<template v-if="column.key === 'devInfo'">
|
||||
<div class="dev-info-cell">
|
||||
<a-avatar :size="38" :src="record.avatar || record.avatarUrl">
|
||||
<template #icon><UserOutlined /></template>
|
||||
</a-avatar>
|
||||
<div class="dev-info-text">
|
||||
<div class="dev-name">{{ record.nickname || record.username || '-' }}</div>
|
||||
<div class="dev-sub" v-if="record.username">@{{ record.username }}</div>
|
||||
<div class="dev-sub" v-if="record.phone || record.mobile">
|
||||
📱 {{ record.phone || record.mobile }}
|
||||
<a-table :columns="applyColumns" :data-source="applyData" row-key="id">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'applicant'">
|
||||
<div class="user-cell">
|
||||
<a-avatar style="background: linear-gradient(135deg, #11998e, #38ef7d)" :size="36">{{ record.name[0] }}</a-avatar>
|
||||
<div>
|
||||
<p class="name-text">{{ record.name }}</p>
|
||||
<p class="sub-text">{{ record.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'type'">
|
||||
<a-tag :color="typeColor[record.type]">{{ typeMap[record.type] }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-badge :status="statusBadge[record.status]" :text="statusText[record.status]" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button v-if="record.status === 'pending'" type="primary" size="small" @click="handleApprove(record)">审核</a-button>
|
||||
<a-button type="link" size="small" @click="handleViewApply(record)">详情</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 用户类型 -->
|
||||
<template v-if="column.key === 'userType'">
|
||||
<a-tag v-if="record.type === 2" color="purple">开发者</a-tag>
|
||||
<a-tag v-else-if="record.type === 1" color="blue">企业用户</a-tag>
|
||||
<a-tag v-else color="default">普通用户</a-tag>
|
||||
</template>
|
||||
<!-- 权限审核 -->
|
||||
<a-tab-pane key="audit" tab="权限审核">
|
||||
<div class="filter-bar">
|
||||
<a-select v-model:value="auditFilterType" placeholder="权限类型" style="width: 150px" allow-clear>
|
||||
<a-select-option value="api">API 权限</a-select-option>
|
||||
<a-select-option value="plugin">插件权限</a-select-option>
|
||||
<a-select-option value="admin">管理权限</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="auditModalVisible = true">新增审核</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 应用数量 -->
|
||||
<template v-if="column.key === 'appCount'">
|
||||
<a-space>
|
||||
<a-tag color="blue">{{ appCountMap[record.userId!] ?? 0 }} 个应用</a-tag>
|
||||
<a-tag color="success" v-if="publishedCountMap[record.userId!]">
|
||||
{{ publishedCountMap[record.userId!] }} 已上架
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-table :columns="auditColumns" :data-source="auditData" row-key="id">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="auditTypeColor[record.type]">{{ auditTypeMap[record.type] }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'level'">
|
||||
<a-tag :color="levelColor[record.level]">{{ levelMap[record.level] }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-badge :status="statusBadge[record.status]" :text="statusText[record.status]" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button v-if="record.status === 'pending'" type="primary" size="small" @click="handleAudit(record)">审核</a-button>
|
||||
<a-button type="link" size="small" @click="handleViewAudit(record)">详情</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
|
||||
<!-- 注册时间 -->
|
||||
<template v-if="column.key === 'createTime'">
|
||||
<span class="text-sm text-gray">{{ record.createTime?.substring(0, 10) || '-' }}</span>
|
||||
</template>
|
||||
|
||||
<!-- 状态 -->
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 0 ? 'success' : 'error'">
|
||||
{{ record.status === 0 ? '正常' : '已冻结' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleViewDev(record)">查看应用</a-button>
|
||||
<a-popconfirm
|
||||
v-if="record.type !== 2"
|
||||
title="确认将该用户设为开发者?"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="handleSetDeveloper(record, 2)"
|
||||
>
|
||||
<a-button type="link" size="small">设为开发者</a-button>
|
||||
</a-popconfirm>
|
||||
<a-popconfirm
|
||||
v-else
|
||||
title="确认取消该用户的开发者资质?"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@confirm="handleSetDeveloper(record, 0)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>取消资质</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 开发者应用列表弹窗 -->
|
||||
<!-- 审核弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showAppsModal"
|
||||
:title="`${currentDev?.nickname || currentDev?.username || '开发者'} 的应用`"
|
||||
width="780px"
|
||||
:footer="null"
|
||||
v-model:open="auditModalVisible"
|
||||
:title="editingApply ? '审核开发者申请' : '权限审核'"
|
||||
width="560px"
|
||||
:confirm-loading="submitting"
|
||||
@ok="handleSubmitAudit"
|
||||
@cancel="auditModalVisible = false"
|
||||
>
|
||||
<div v-if="loadingApps" class="modal-spin">
|
||||
<a-spin />
|
||||
</div>
|
||||
<template v-else>
|
||||
<a-empty v-if="devApps.length === 0" description="该开发者暂无应用" />
|
||||
<div v-else class="dev-apps-grid">
|
||||
<div v-for="app in devApps" :key="app.productId" class="dev-app-card">
|
||||
<div class="dev-app-header">
|
||||
<img v-if="app.icon" :src="app.icon" class="dev-app-icon" />
|
||||
<div v-else class="dev-app-icon-placeholder" :style="{ background: iconBgColor(app.productName) }">
|
||||
{{ (app.productName || 'A').charAt(0).toUpperCase() }}
|
||||
</div>
|
||||
<div class="dev-app-info">
|
||||
<div class="dev-app-name">
|
||||
{{ app.productName }}
|
||||
<a-tag color="blue" style="margin-left:6px;font-size:11px">{{ APP_TYPE_NAME[app.appType ?? 10] || '网站' }}</a-tag>
|
||||
</div>
|
||||
<div class="dev-app-code">{{ app.productCode }}</div>
|
||||
</div>
|
||||
<a-tag :color="pubStatusColor(app.publishStatus)" style="margin-left:auto">
|
||||
{{ pubStatusText(app.publishStatus) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="dev-app-desc">{{ app.description || '暂无简介' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-alert v-if="editingApply" :message="`正在审核:${editingApply.name} (${editingApply.email})`" type="info" show-icon class="mb-4" />
|
||||
|
||||
<a-form :model="auditForm" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form-item label="申请人">
|
||||
<a-input v-model:value="auditForm.name" />
|
||||
</a-form-item>
|
||||
<a-form-item label="权限类型">
|
||||
<a-select v-model:value="auditForm.type">
|
||||
<a-select-option value="api">API 权限</a-select-option>
|
||||
<a-select-option value="plugin">插件权限</a-select-option>
|
||||
<a-select-option value="admin">管理权限</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="权限等级">
|
||||
<a-select v-model:value="auditForm.level">
|
||||
<a-select-option value="basic">基础</a-select-option>
|
||||
<a-select-option value="standard">标准</a-select-option>
|
||||
<a-select-option value="advance">高级</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="审核结果">
|
||||
<a-radio-group v-model:value="auditForm.result">
|
||||
<a-radio value="approved">通过</a-radio>
|
||||
<a-radio value="rejected">拒绝</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="审核备注">
|
||||
<a-textarea v-model:value="auditForm.remark" :rows="3" placeholder="选填" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ReloadOutlined, UserOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { getUserAppStats, pageAppProductAll } from '@/api/app/appProduct'
|
||||
import { pageUsers, updateUser } from '@/api/system/user/index'
|
||||
import type { AppProduct } from '@/api/app/appProduct/model'
|
||||
import { APP_TYPE_NAME } from '@/api/app/appProduct/model'
|
||||
import type { User } from '@/api/system/user/model'
|
||||
|
||||
definePageMeta({ layout: 'admin' })
|
||||
useHead({ title: '开发者管理 - 平台管理' })
|
||||
|
||||
const loading = ref(false)
|
||||
const loadingApps = ref(false)
|
||||
const developers = ref<User[]>([])
|
||||
const activeTab = ref('apply')
|
||||
const searchKeyword = ref('')
|
||||
const filterType = ref<number | null>(2) // 默认只看开发者
|
||||
const filterType = ref<string | undefined>()
|
||||
const auditFilterType = ref<string | undefined>()
|
||||
const auditModalVisible = ref(false)
|
||||
const submitting = ref(false)
|
||||
const editingApply = ref<any>(null)
|
||||
|
||||
// 应用数量映射 userId -> count
|
||||
const appCountMap = ref<Record<number, number>>({})
|
||||
const publishedCountMap = ref<Record<number, number>>({})
|
||||
|
||||
const showAppsModal = ref(false)
|
||||
const currentDev = ref<User | null>(null)
|
||||
const devApps = ref<AppProduct[]>([])
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
const auditForm = reactive({
|
||||
name: '',
|
||||
type: 'api',
|
||||
level: 'basic',
|
||||
result: 'approved',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
const stats = reactive([
|
||||
{ icon: '🧑💻', label: '开发者总数', value: 0, color: 'blue' },
|
||||
{ icon: '📦', label: '应用总数', value: 0, color: 'green' },
|
||||
{ icon: '✅', label: '已上架应用', value: 0, color: 'orange' },
|
||||
{ icon: '⏳', label: '待审核', value: 0, color: 'red' },
|
||||
])
|
||||
const typeMap: Record<string, string> = { api: 'API 开发者', plugin: '插件开发者', template: '模板开发者' }
|
||||
const typeColor: Record<string, string> = { api: 'blue', plugin: 'green', template: 'purple' }
|
||||
const statusText: Record<string, string> = { pending: '待审核', approved: '已通过', rejected: '已拒绝' }
|
||||
const statusBadge: Record<string, any> = { pending: 'warning', approved: 'success', rejected: 'error' }
|
||||
const auditTypeMap: Record<string, string> = { api: 'API 权限', plugin: '插件权限', admin: '管理权限' }
|
||||
const auditTypeColor: Record<string, string> = { api: 'blue', plugin: 'green', admin: 'purple' }
|
||||
const levelMap: Record<string, string> = { basic: '基础', standard: '标准', advance: '高级' }
|
||||
const levelColor: Record<string, string> = { basic: 'default', standard: 'blue', advance: 'red' }
|
||||
|
||||
const columns = [
|
||||
{ title: '用户', key: 'devInfo', width: 240 },
|
||||
{ title: '类型', key: 'userType', width: 100 },
|
||||
{ title: '应用数量', key: 'appCount', width: 180 },
|
||||
{ title: '注册时间', key: 'createTime', width: 120 },
|
||||
{ title: '状态', key: 'status', width: 90 },
|
||||
{ title: '操作', key: 'action', width: 160 },
|
||||
const applyColumns = [
|
||||
{ title: '申请人', key: 'applicant', width: 220 },
|
||||
{ title: '企业', dataIndex: 'enterprise', key: 'enterprise', width: 160 },
|
||||
{ title: '申请类型', key: 'type', width: 110 },
|
||||
{ title: '申请理由', dataIndex: 'reason', key: 'reason', ellipsis: true },
|
||||
{ title: '申请时间', dataIndex: 'date', key: 'date', width: 120 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '操作', key: 'actions', width: 140, fixed: 'right' },
|
||||
]
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await pageUsers({
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize,
|
||||
keywords: searchKeyword.value || undefined,
|
||||
type: filterType.value ?? undefined,
|
||||
})
|
||||
developers.value = res?.list || []
|
||||
pagination.total = res?.count || 0
|
||||
stats[0].value = pagination.total
|
||||
// 加载完用户后,单次请求批量加载应用数量
|
||||
loadAppCounts()
|
||||
} catch {
|
||||
message.error('加载用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const applyData = ref([
|
||||
{ id: 1, name: '陈志远', email: 'chenzy@example.com', enterprise: '腾云科技', type: 'api', reason: '需要调用平台 API 实现数据对接功能', date: '2026-04-08', status: 'pending' },
|
||||
{ id: 2, name: '林晓东', email: 'linxd@example.com', enterprise: '华创数据', type: 'plugin', reason: '希望开发企业级插件产品', date: '2026-04-07', status: 'approved' },
|
||||
{ id: 3, name: '周文博', email: 'zhouwb@example.com', enterprise: '云智科技', type: 'api', reason: '进行系统集成开发', date: '2026-04-06', status: 'approved' },
|
||||
{ id: 4, name: '吴浩宇', email: 'wuhao@example.com', enterprise: '数智科技', type: 'template', reason: '发布企业模板到应用市场', date: '2026-04-05', status: 'pending' },
|
||||
{ id: 5, name: '郑海峰', email: 'zhenghf@example.com', enterprise: '腾云科技', type: 'api', reason: '需要高级 API 权限进行批量操作', date: '2026-04-04', status: 'approved' },
|
||||
])
|
||||
|
||||
async function loadAppCounts() {
|
||||
if (!developers.value.length) return
|
||||
try {
|
||||
// 单次 POST 请求,一条 SQL 批量统计所有用户的应用数
|
||||
const userIds = developers.value.map(u => u.userId!).filter(Boolean)
|
||||
const rows = await getUserAppStats(userIds)
|
||||
const countMap: Record<number, number> = {}
|
||||
const pubMap: Record<number, number> = {}
|
||||
let totalApps = 0
|
||||
let totalPublished = 0
|
||||
for (const row of rows) {
|
||||
const uid = Number(row.userId)
|
||||
const total = Number(row.totalCount) || 0
|
||||
const pub = Number(row.publishedCount) || 0
|
||||
countMap[uid] = total
|
||||
if (pub > 0) pubMap[uid] = pub
|
||||
totalApps += total
|
||||
totalPublished += pub
|
||||
}
|
||||
appCountMap.value = countMap
|
||||
publishedCountMap.value = pubMap
|
||||
stats[1].value = totalApps
|
||||
stats[2].value = totalPublished
|
||||
} catch { /* ignore */ }
|
||||
// 异步加载全局统计(待审核数)
|
||||
loadPendingCount()
|
||||
}
|
||||
const auditColumns = [
|
||||
{ title: '申请人', dataIndex: 'name', key: 'name', width: 140 },
|
||||
{ title: '企业', dataIndex: 'enterprise', key: 'enterprise', width: 140 },
|
||||
{ title: '权限类型', key: 'type', width: 110 },
|
||||
{ title: '权限等级', key: 'level', width: 100 },
|
||||
{ title: '申请时间', dataIndex: 'date', key: 'date', width: 120 },
|
||||
{ title: '审核人', dataIndex: 'auditor', key: 'auditor', width: 100 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '操作', key: 'actions', width: 140, fixed: 'right' },
|
||||
]
|
||||
|
||||
async function loadPendingCount() {
|
||||
try {
|
||||
const res = await pageAppProductAll({ current: 1, size: 1, publishStatus: 'pending_review' })
|
||||
stats[3].value = res?.count ?? 0
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
const auditData = ref([
|
||||
{ id: 1, name: '陈志远', enterprise: '腾云科技', type: 'api', level: 'standard', date: '2026-04-08', auditor: '李明', status: 'pending' },
|
||||
{ id: 2, name: '吴浩宇', enterprise: '数智科技', type: 'plugin', level: 'advance', date: '2026-04-05', auditor: '李明', status: 'pending' },
|
||||
{ id: 3, name: '林晓东', enterprise: '华创数据', type: 'plugin', level: 'standard', date: '2026-04-07', auditor: '李明', status: 'approved' },
|
||||
{ id: 4, name: '周文博', enterprise: '云智科技', type: 'api', level: 'basic', date: '2026-04-06', auditor: '李明', status: 'approved' },
|
||||
])
|
||||
|
||||
function handleSearch() {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
const resetFilter = () => { searchKeyword.value = ''; filterType.value = undefined }
|
||||
const handleViewApply = (r: any) => message.info('查看申请:' + r.name)
|
||||
const handleViewAudit = (r: any) => message.info('查看审核:' + r.name)
|
||||
const handleApprove = (r: any) => { editingApply.value = r; Object.assign(auditForm, { name: r.name, type: r.type }); auditModalVisible.value = true }
|
||||
const handleAudit = (r: any) => { editingApply.value = r; Object.assign(auditForm, { name: r.name, type: r.type }); auditModalVisible.value = true }
|
||||
const handleSubmitAudit = async () => {
|
||||
submitting.value = true
|
||||
await new Promise((r) => setTimeout(r, 800))
|
||||
message.success('审核提交成功')
|
||||
auditModalVisible.value = false
|
||||
submitting.value = false
|
||||
editingApply.value = null
|
||||
}
|
||||
|
||||
function handleFilterChange() {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function handleSetDeveloper(record: User, type: number) {
|
||||
try {
|
||||
await updateUser({ userId: record.userId, type })
|
||||
record.type = type
|
||||
message.success(type === 2 ? '已设为开发者用户' : '已取消开发者资质')
|
||||
// 如果当前只展示开发者,取消后刷新列表
|
||||
if (filterType.value === 2 && type !== 2) loadData()
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewDev(record: User) {
|
||||
currentDev.value = record
|
||||
showAppsModal.value = true
|
||||
loadingApps.value = true
|
||||
try {
|
||||
const res = await pageAppProductAll({
|
||||
current: 1,
|
||||
size: 100,
|
||||
userId: record.userId,
|
||||
})
|
||||
devApps.value = res?.list || []
|
||||
} catch {
|
||||
message.error('加载应用列表失败')
|
||||
} finally {
|
||||
loadingApps.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function pubStatusColor(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
developing: 'default', pending_review: 'orange',
|
||||
published: 'success', rejected: 'error', deprecated: 'default',
|
||||
}
|
||||
return map[status || ''] || 'default'
|
||||
}
|
||||
|
||||
function pubStatusText(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
developing: '开发中', pending_review: '待审核',
|
||||
published: '已上架', rejected: '已拒绝', deprecated: '已下架',
|
||||
}
|
||||
return map[status || ''] || '-'
|
||||
}
|
||||
|
||||
const PALETTE = ['#4e6ef2', '#f4a261', '#e76f51', '#2a9d8f', '#e9c46a', '#457b9d']
|
||||
function iconBgColor(name?: string) {
|
||||
if (!name) return PALETTE[0]
|
||||
let h = 0
|
||||
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) & 0xffffffff
|
||||
return PALETTE[Math.abs(h) % PALETTE.length]
|
||||
}
|
||||
|
||||
onMounted(() => loadData())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.developers-page { min-height: 100%; }
|
||||
|
||||
.page-header {
|
||||
display: flex; align-items: center;
|
||||
justify-content: space-between; margin-bottom: 20px;
|
||||
}
|
||||
.page-title { font-size: 18px; font-weight: 700; color: #1f2937; margin: 0; }
|
||||
.page-desc { font-size: 13px; color: #9ca3af; margin: 2px 0 0; }
|
||||
|
||||
.stat-card {
|
||||
display: flex; align-items: center;
|
||||
gap: 12px; padding: 16px;
|
||||
border-radius: 12px; border: 2px solid transparent; transition: all 0.2s;
|
||||
}
|
||||
.stat-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
|
||||
.stat-card.blue { background: #eff6ff; border-color: #dbeafe; }
|
||||
.stat-card.green { background: #f0fdf4; border-color: #bbf7d0; }
|
||||
.stat-card.orange { background: #fff7ed; border-color: #fed7aa; }
|
||||
.stat-card.red { background: #fff1f2; border-color: #fecdd3; }
|
||||
.stat-icon { font-size: 28px; flex-shrink: 0; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; color: rgba(0,0,0,0.85); line-height: 1.2; }
|
||||
.stat-label { font-size: 12px; color: rgba(0,0,0,0.45); margin-top: 2px; }
|
||||
|
||||
.panel { background: #fff; border: 1px solid #f0f0f0; border-radius: 12px; overflow: hidden; }
|
||||
.panel-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 14px 18px; border-bottom: 1px solid #f5f5f5; flex-wrap: wrap; gap: 10px;
|
||||
}
|
||||
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); }
|
||||
|
||||
.filter-bar {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
flex-wrap: wrap; gap: 12px; margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dev-info-cell { display: flex; align-items: center; gap: 10px; }
|
||||
.dev-info-text { flex: 1; min-width: 0; }
|
||||
.dev-name { font-size: 14px; font-weight: 500; color: rgba(0,0,0,0.85); }
|
||||
.dev-sub { font-size: 12px; color: rgba(0,0,0,0.45); margin-top: 1px; }
|
||||
|
||||
/* 应用弹窗卡片 */
|
||||
.dev-apps-grid { display: flex; flex-direction: column; gap: 12px; max-height: 520px; overflow-y: auto; padding-right: 4px; }
|
||||
.dev-app-card {
|
||||
border: 1px solid #f0f0f0; border-radius: 10px; padding: 14px;
|
||||
transition: all 0.15s;
|
||||
.user-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.dev-app-card:hover { border-color: #d0d0ff; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
||||
.dev-app-header { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
|
||||
.dev-app-icon { width: 40px; height: 40px; border-radius: 8px; object-fit: cover; }
|
||||
.dev-app-icon-placeholder {
|
||||
width: 40px; height: 40px; border-radius: 8px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 16px; font-weight: 600; color: #fff; flex-shrink: 0;
|
||||
}
|
||||
.dev-app-info { flex: 1; }
|
||||
.dev-app-name { font-size: 14px; font-weight: 500; color: rgba(0,0,0,0.85); }
|
||||
.dev-app-code { font-size: 12px; color: rgba(0,0,0,0.45); }
|
||||
.dev-app-desc { font-size: 12px; color: rgba(0,0,0,0.45); padding-left: 52px; }
|
||||
|
||||
.modal-spin { display: flex; align-items: center; justify-content: center; min-height: 200px; }
|
||||
.text-sm { font-size: 12px; }
|
||||
.text-gray { color: rgba(0,0,0,0.45); }
|
||||
.mb-6 { margin-bottom: 24px; }
|
||||
.name-text {
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
119
app/pages/admin/developers/audit.vue
Normal file
119
app/pages/admin/developers/audit.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="developer-audit-page">
|
||||
<a-card :bordered="false" title="权限审核">
|
||||
<div class="filter-bar">
|
||||
<a-select v-model:value="filterType" placeholder="权限类型" style="width: 150px" allow-clear>
|
||||
<a-select-option value="api">API 权限</a-select-option>
|
||||
<a-select-option value="plugin">插件权限</a-select-option>
|
||||
<a-select-option value="admin">管理权限</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="filterStatus" placeholder="审核状态" style="width: 140px" allow-clear>
|
||||
<a-select-option value="pending">待审核</a-select-option>
|
||||
<a-select-option value="approved">已通过</a-select-option>
|
||||
<a-select-option value="rejected">已拒绝</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="auditModalVisible = true">新增审核</a-button>
|
||||
</div>
|
||||
|
||||
<a-table :columns="columns" :data-source="data" row-key="id">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="typeColor[record.type]">{{ typeMap[record.type] }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'level'">
|
||||
<a-tag :color="levelColor[record.level]">{{ levelMap[record.level] }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-badge :status="statusBadge[record.status]" :text="statusText[record.status]" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button v-if="record.status === 'pending'" type="primary" size="small" @click="handleAudit(record)">审核</a-button>
|
||||
<a-button type="link" size="small" @click="handleView(record)">详情</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-modal v-model:open="auditModalVisible" title="权限审核" width="520px" @ok="handleSubmit">
|
||||
<a-form :model="form" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form-item label="申请人">
|
||||
<a-input v-model:value="form.name" />
|
||||
</a-form-item>
|
||||
<a-form-item label="权限类型">
|
||||
<a-select v-model:value="form.type">
|
||||
<a-select-option value="api">API 权限</a-select-option>
|
||||
<a-select-option value="plugin">插件权限</a-select-option>
|
||||
<a-select-option value="admin">管理权限</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="权限等级">
|
||||
<a-select v-model:value="form.level">
|
||||
<a-select-option value="basic">基础</a-select-option>
|
||||
<a-select-option value="standard">标准</a-select-option>
|
||||
<a-select-option value="advance">高级</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="审核结果">
|
||||
<a-radio-group v-model:value="form.result">
|
||||
<a-radio value="approved">通过</a-radio>
|
||||
<a-radio value="rejected">拒绝</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="审核备注">
|
||||
<a-textarea v-model:value="form.remark" :rows="3" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
const filterType = ref<string | undefined>()
|
||||
const filterStatus = ref<string | undefined>()
|
||||
const auditModalVisible = ref(false)
|
||||
const form = reactive({ name: '', type: 'api', level: 'basic', result: 'approved', remark: '' })
|
||||
|
||||
const typeMap: Record<string, string> = { api: 'API 权限', plugin: '插件权限', admin: '管理权限' }
|
||||
const typeColor: Record<string, string> = { api: 'blue', plugin: 'green', admin: 'purple' }
|
||||
const levelMap: Record<string, string> = { basic: '基础', standard: '标准', advance: '高级' }
|
||||
const levelColor: Record<string, string> = { basic: 'default', standard: 'blue', advance: 'red' }
|
||||
const statusText: Record<string, string> = { pending: '待审核', approved: '已通过', rejected: '已拒绝' }
|
||||
const statusBadge: Record<string, any> = { pending: 'warning', approved: 'success', rejected: 'error' }
|
||||
|
||||
const columns = [
|
||||
{ title: '申请人', dataIndex: 'name', key: 'name', width: 140 },
|
||||
{ title: '企业', dataIndex: 'enterprise', key: 'enterprise', width: 160 },
|
||||
{ title: '权限类型', key: 'type', width: 110 },
|
||||
{ title: '权限等级', key: 'level', width: 100 },
|
||||
{ title: '申请时间', dataIndex: 'date', key: 'date', width: 120 },
|
||||
{ title: '审核人', dataIndex: 'auditor', key: 'auditor', width: 100 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '操作', key: 'actions', width: 140 },
|
||||
]
|
||||
|
||||
const data = ref([
|
||||
{ id: 1, name: '陈志远', enterprise: '腾云科技', type: 'api', level: 'standard', date: '2026-04-08', auditor: '李明', status: 'pending' },
|
||||
{ id: 2, name: '吴浩宇', enterprise: '数智科技', type: 'plugin', level: 'advance', date: '2026-04-05', auditor: '李明', status: 'pending' },
|
||||
{ id: 3, name: '林晓东', enterprise: '华创数据', type: 'plugin', level: 'standard', date: '2026-04-07', auditor: '李明', status: 'approved' },
|
||||
{ id: 4, name: '周文博', enterprise: '云智科技', type: 'api', level: 'basic', date: '2026-04-06', auditor: '李明', status: 'approved' },
|
||||
])
|
||||
|
||||
const handleAudit = (r: any) => { Object.assign(form, { name: r.name, type: r.type }); auditModalVisible.value = true }
|
||||
const handleView = (r: any) => message.info('查看:' + r.name)
|
||||
const handleSubmit = () => { auditModalVisible.value = false; message.success('审核成功') }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
301
app/pages/admin/enterprises.vue
Normal file
301
app/pages/admin/enterprises.vue
Normal file
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<div class="enterprises-page">
|
||||
<a-card :bordered="false">
|
||||
<template #title>
|
||||
<div class="card-title">
|
||||
<span>企业管理</span>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增企业
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="filter-bar">
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索企业名称、联系人..."
|
||||
style="width: 280px"
|
||||
allow-clear
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<a-select
|
||||
v-model:value="filterStatus"
|
||||
placeholder="状态筛选"
|
||||
style="width: 140px"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="active">已认证</a-select-option>
|
||||
<a-select-option value="pending">待审核</a-select-option>
|
||||
<a-select-option value="suspended">已停用</a-select-option>
|
||||
</a-select>
|
||||
<a-range-picker
|
||||
v-model:value="dateRange"
|
||||
style="width: 260px"
|
||||
/>
|
||||
<a-button @click="resetFilter">重置</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<div class="enterprise-cell">
|
||||
<a-avatar
|
||||
style="background: linear-gradient(135deg, #667eea, #764ba2); flex-shrink: 0"
|
||||
:size="36"
|
||||
>
|
||||
{{ record.name[0] }}
|
||||
</a-avatar>
|
||||
<div>
|
||||
<p class="name-text">{{ record.name }}</p>
|
||||
<p class="sub-text">ID: {{ record.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-badge
|
||||
:status="record.status === 'active' ? 'success' : record.status === 'pending' ? 'warning' : 'error'"
|
||||
:text="statusMap[record.status]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'plan'">
|
||||
<a-tag :color="planColor[record.plan]">{{ planMap[record.plan] }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleView(record)">详情</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-dropdown>
|
||||
<a-button type="link" size="small">更多</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="suspend" @click="handleSuspend(record)">
|
||||
{{ record.status === 'suspended' ? '启用' : '停用' }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="reset">重置密码</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="delete" style="color: #ff4d4f">删除</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="editingId ? '编辑企业' : '新增企业'"
|
||||
width="640px"
|
||||
:confirm-loading="submitting"
|
||||
@ok="handleSubmit"
|
||||
@cancel="modalVisible = false"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 16 }"
|
||||
>
|
||||
<a-form-item label="企业名称" name="name" :rules="[{ required: true, message: '请输入企业名称' }]">
|
||||
<a-input v-model:value="formState.name" placeholder="请输入企业名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="联系人" name="contact" :rules="[{ required: true, message: '请输入联系人' }]">
|
||||
<a-input v-model:value="formState.contact" placeholder="请输入联系人姓名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="联系电话" name="phone">
|
||||
<a-input v-model:value="formState.phone" placeholder="请输入联系电话" />
|
||||
</a-form-item>
|
||||
<a-form-item label="企业邮箱" name="email">
|
||||
<a-input v-model:value="formState.email" placeholder="请输入企业邮箱" />
|
||||
</a-form-item>
|
||||
<a-form-item label="套餐等级" name="plan">
|
||||
<a-select v-model:value="formState.plan">
|
||||
<a-select-option value="basic">基础版</a-select-option>
|
||||
<a-select-option value="standard">标准版</a-select-option>
|
||||
<a-select-option value="enterprise">企业版</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="formState.status">
|
||||
<a-select-option value="active">已认证</a-select-option>
|
||||
<a-select-option value="pending">待审核</a-select-option>
|
||||
<a-select-option value="suspended">已停用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="formState.remark" :rows="3" placeholder="选填" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import type { TableProps } from 'ant-design-vue'
|
||||
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
const searchKeyword = ref('')
|
||||
const filterStatus = ref<string | undefined>()
|
||||
const dateRange = ref<[any, any] | null>(null)
|
||||
const loading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
const formState = reactive({
|
||||
name: '',
|
||||
contact: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
plan: 'basic',
|
||||
status: 'pending',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
active: '已认证',
|
||||
pending: '待审核',
|
||||
suspended: '已停用',
|
||||
}
|
||||
|
||||
const planMap: Record<string, string> = {
|
||||
basic: '基础版',
|
||||
standard: '标准版',
|
||||
enterprise: '企业版',
|
||||
}
|
||||
|
||||
const planColor: Record<string, string> = {
|
||||
basic: 'default',
|
||||
standard: 'blue',
|
||||
enterprise: 'purple',
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: '企业信息', key: 'name', width: 260 },
|
||||
{ title: '联系人', dataIndex: 'contact', key: 'contact', width: 120 },
|
||||
{ title: '联系电话', dataIndex: 'phone', key: 'phone', width: 140 },
|
||||
{ title: '套餐', key: 'plan', width: 100 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '注册时间', dataIndex: 'createdAt', key: 'createdAt', width: 120 },
|
||||
{ title: '操作', key: 'actions', width: 200, fixed: 'right' },
|
||||
]
|
||||
|
||||
const tableData = ref([
|
||||
{ id: 1001, name: '深圳市腾云科技有限公司', contact: '李明', phone: '13800138001', plan: 'enterprise', status: 'pending', createdAt: '2026-04-08' },
|
||||
{ id: 1002, name: '杭州智联网络技术有限公司', contact: '王芳', phone: '13800138002', plan: 'standard', status: 'active', createdAt: '2026-04-07' },
|
||||
{ id: 1003, name: '北京华创数据服务有限公司', contact: '张伟', phone: '13800138003', plan: 'enterprise', status: 'active', createdAt: '2026-04-06' },
|
||||
{ id: 1004, name: '广州云智科技有限公司', contact: '陈静', phone: '13800138004', plan: 'basic', status: 'pending', createdAt: '2026-04-05' },
|
||||
{ id: 1005, name: '上海数智科技有限公司', contact: '刘强', phone: '13800138005', plan: 'standard', status: 'active', createdAt: '2026-04-04' },
|
||||
{ id: 1006, name: '成都万物互联有限公司', contact: '赵丽', phone: '13800138006', plan: 'basic', status: 'suspended', createdAt: '2026-04-03' },
|
||||
{ id: 1007, name: '武汉云帆科技有限公司', contact: '孙磊', phone: '13800138007', plan: 'standard', status: 'active', createdAt: '2026-04-02' },
|
||||
])
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 7,
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
message.info('搜索:' + searchKeyword.value)
|
||||
}
|
||||
|
||||
const resetFilter = () => {
|
||||
searchKeyword.value = ''
|
||||
filterStatus.value = undefined
|
||||
dateRange.value = null
|
||||
}
|
||||
|
||||
const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
pagination.current = pag.current || 1
|
||||
pagination.pageSize = pag.pageSize || 10
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
editingId.value = null
|
||||
Object.assign(formState, { name: '', contact: '', phone: '', email: '', plan: 'basic', status: 'pending', remark: '' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
editingId.value = record.id
|
||||
Object.assign(formState, { ...record })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleView = (record: any) => {
|
||||
message.info('查看企业详情:' + record.name)
|
||||
}
|
||||
|
||||
const handleSuspend = (record: any) => {
|
||||
const action = record.status === 'suspended' ? '启用' : '停用'
|
||||
message.success(`${action}成功`)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
submitting.value = true
|
||||
await new Promise((r) => setTimeout(r, 800))
|
||||
message.success(editingId.value ? '编辑成功' : '新增成功')
|
||||
modalVisible.value = false
|
||||
submitting.value = false
|
||||
} catch {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.enterprise-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.name-text {
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
78
app/pages/admin/enterprises/detail.vue
Normal file
78
app/pages/admin/enterprises/detail.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="enterprise-detail">
|
||||
<a-card :bordered="false">
|
||||
<template #title>
|
||||
<a-space>
|
||||
<a-button type="text" @click="navigateTo('/admin/enterprises')">
|
||||
<LeftOutlined /> 返回
|
||||
</a-button>
|
||||
<span>企业详情</span>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-descriptions bordered :column="2">
|
||||
<a-descriptions-item label="企业名称">深圳市腾云科技有限公司</a-descriptions-item>
|
||||
<a-descriptions-item label="企业ID">ENT-20260408-001</a-descriptions-item>
|
||||
<a-descriptions-item label="联系人">李明</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">138****8001</a-descriptions-item>
|
||||
<a-descriptions-item label="企业邮箱">liming@tengyun.com</a-descriptions-item>
|
||||
<a-descriptions-item label="套餐等级">
|
||||
<a-tag color="purple">企业版</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="认证状态">
|
||||
<a-badge status="success" text="已认证" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="注册时间">2026-04-08 10:30</a-descriptions-item>
|
||||
<a-descriptions-item label="营业执照">
|
||||
<a-image :width="120" src="https://via.placeholder.com/120x80?text=执照" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="备注">-</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
|
||||
<a-row :gutter="[16, 16]" class="mt-4">
|
||||
<a-col :xs="24" :xl="12">
|
||||
<a-card title="成员信息" :bordered="false">
|
||||
<a-list :data-source="members">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #title>{{ item.name }}</template>
|
||||
<template #description>{{ item.role }} · {{ item.email }}</template>
|
||||
<template #avatar>
|
||||
<a-avatar>{{ item.name[0] }}</a-avatar>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :xl="12">
|
||||
<a-card title="账单信息" :bordered="false">
|
||||
<a-descriptions :column="1" size="small">
|
||||
<a-descriptions-item label="账户余额">¥ 15,680.00</a-descriptions-item>
|
||||
<a-descriptions-item label="本月消费">¥ 12,800.00</a-descriptions-item>
|
||||
<a-descriptions-item label="累计充值">¥ 100,000.00</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LeftOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
const members = [
|
||||
{ name: '李明', role: '管理员', email: 'liming@tengyun.com' },
|
||||
{ name: '王芳', role: '财务', email: 'wangfang@tengyun.com' },
|
||||
{ name: '张伟', role: '开发', email: 'zhangwei@tengyun.com' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mt-4 { margin-top: 16px; }
|
||||
</style>
|
||||
267
app/pages/admin/finance.vue
Normal file
267
app/pages/admin/finance.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<div class="finance-page">
|
||||
<!-- 统计概览 -->
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="24" :sm="12" :xl="6">
|
||||
<a-card class="fin-stat-card" :bordered="false">
|
||||
<div class="fin-stat-inner">
|
||||
<div>
|
||||
<p class="fin-label">本月收入</p>
|
||||
<p class="fin-value">¥ 287.5 万</p>
|
||||
<p class="fin-trend up"><ArrowUpOutlined /> 15.3%</p>
|
||||
</div>
|
||||
<div class="fin-icon green"><AccountBookOutlined /></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :xl="6">
|
||||
<a-card class="fin-stat-card" :bordered="false">
|
||||
<div class="fin-stat-inner">
|
||||
<div>
|
||||
<p class="fin-label">待结算</p>
|
||||
<p class="fin-value">¥ 43.2 万</p>
|
||||
<p class="fin-trend neutral">平稳</p>
|
||||
</div>
|
||||
<div class="fin-icon blue"><DollarOutlined /></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :xl="6">
|
||||
<a-card class="fin-stat-card" :bordered="false">
|
||||
<div class="fin-stat-inner">
|
||||
<div>
|
||||
<p class="fin-label">充值总额</p>
|
||||
<p class="fin-value">¥ 1,856.3 万</p>
|
||||
<p class="fin-trend up"><ArrowUpOutlined /> 8.6%</p>
|
||||
</div>
|
||||
<div class="fin-icon purple"><PayCircleOutlined /></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :xl="6">
|
||||
<a-card class="fin-stat-card" :bordered="false">
|
||||
<div class="fin-stat-inner">
|
||||
<div>
|
||||
<p class="fin-label">退款笔数</p>
|
||||
<p class="fin-value">12 笔</p>
|
||||
<p class="fin-trend down"><ArrowDownOutlined /> 2 笔</p>
|
||||
</div>
|
||||
<div class="fin-icon orange"><ExclamationCircleOutlined /></div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<template #title>账单管理</template>
|
||||
|
||||
<a-tabs v-model:activeKey="activeTab">
|
||||
<!-- 账单列表 -->
|
||||
<a-tab-pane key="bills" tab="账单列表">
|
||||
<div class="filter-bar">
|
||||
<a-input-search v-model:value="searchKeyword" placeholder="企业名称、订单号..." style="width: 280px" allow-clear @search="handleSearch" />
|
||||
<a-select v-model:value="filterBillType" placeholder="账单类型" style="width: 140px" allow-clear>
|
||||
<a-select-option value="recharge">充值</a-select-option>
|
||||
<a-select-option value="consume">消费</a-select-option>
|
||||
<a-select-option value="refund">退款</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="filterBillStatus" placeholder="支付状态" style="width: 140px" allow-clear>
|
||||
<a-select-option value="paid">已支付</a-select-option>
|
||||
<a-select-option value="pending">待支付</a-select-option>
|
||||
<a-select-option value="failed">已失败</a-select-option>
|
||||
</a-select>
|
||||
<a-range-picker v-model:value="dateRange" style="width: 260px" />
|
||||
<a-button @click="resetFilter">重置</a-button>
|
||||
</div>
|
||||
|
||||
<a-table :columns="billColumns" :data-source="billData" row-key="id" :pagination="pagination">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'enterprise'">
|
||||
<div class="enterprise-cell">
|
||||
<a-avatar style="background: linear-gradient(135deg, #667eea, #764ba2); flex-shrink: 0" :size="32">{{ record.enterprise[0] }}</a-avatar>
|
||||
<span>{{ record.enterprise }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'amount'">
|
||||
<span :class="record.type === 'refund' ? 'refund-amount' : record.type === 'consume' ? 'consume-amount' : ''">
|
||||
{{ record.type === 'refund' ? '-' : record.type === 'consume' ? '-' : '+' }}¥ {{ record.amount.toLocaleString() }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'billType'">
|
||||
<a-tag :color="billTypeColor[record.billType]">{{ billTypeMap[record.billType] }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-badge :status="billStatusBadge[record.status]" :text="billStatusMap[record.status]" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleViewBill(record)">详情</a-button>
|
||||
<a-button v-if="record.status === 'pending'" type="link" size="small" @click="handleRemind(record)">催款</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 充值记录 -->
|
||||
<a-tab-pane key="recharge" tab="充值管理">
|
||||
<div class="filter-bar">
|
||||
<a-input-search v-model:value="rechargeKeyword" placeholder="企业名称、充值账号..." style="width: 280px" allow-clear />
|
||||
<a-select v-model:value="rechargeChannel" placeholder="支付渠道" style="width: 140px" allow-clear>
|
||||
<a-select-option value="alipay">支付宝</a-select-option>
|
||||
<a-select-option value="wechat">微信支付</a-select-option>
|
||||
<a-select-option value="bank">银行转账</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="rechargeModalVisible = true">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
手动充值
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table :columns="rechargeColumns" :data-source="rechargeData" row-key="id">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'enterprise'">
|
||||
<span>{{ record.enterprise }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'amount'">
|
||||
<span class="income-amount">+ ¥ {{ record.amount.toLocaleString() }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'channel'">
|
||||
<a-tag>{{ channelMap[record.channel] }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-badge :status="billStatusBadge[record.status]" :text="billStatusMap[record.status]" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-button type="link" size="small" @click="handleViewRecharge(record)">凭证</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
|
||||
<!-- 手动充值弹窗 -->
|
||||
<a-modal v-model:open="rechargeModalVisible" title="手动充值" width="480px" @ok="handleRechargeSubmit">
|
||||
<a-form :model="rechargeForm" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form-item label="企业名称" :rules="[{ required: true, message: '请输入企业名称' }]">
|
||||
<a-input v-model:value="rechargeForm.enterprise" placeholder="请输入企业名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="充值金额" :rules="[{ required: true, message: '请输入充值金额' }]">
|
||||
<a-input-number v-model:value="rechargeForm.amount" :min="0" :precision="2" style="width: 100%" placeholder="请输入金额" />
|
||||
</a-form-item>
|
||||
<a-form-item label="支付渠道">
|
||||
<a-select v-model:value="rechargeForm.channel">
|
||||
<a-select-option value="manual">手动充值</a-select-option>
|
||||
<a-select-option value="bank">银行转账</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注">
|
||||
<a-textarea v-model:value="rechargeForm.remark" :rows="2" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowUpOutlined, ArrowDownOutlined, AccountBookOutlined, DollarOutlined, PlusOutlined, ExclamationCircleOutlined, PayCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
const activeTab = ref('bills')
|
||||
const searchKeyword = ref('')
|
||||
const rechargeKeyword = ref('')
|
||||
const filterBillType = ref<string | undefined>()
|
||||
const filterBillStatus = ref<string | undefined>()
|
||||
const rechargeChannel = ref<string | undefined>()
|
||||
const dateRange = ref<[any, any] | null>(null)
|
||||
const rechargeModalVisible = ref(false)
|
||||
|
||||
const rechargeForm = reactive({ enterprise: '', amount: 0, channel: 'manual', remark: '' })
|
||||
|
||||
const billTypeMap: Record<string, string> = { recharge: '充值', consume: '消费', refund: '退款' }
|
||||
const billTypeColor: Record<string, string> = { recharge: 'green', consume: 'blue', refund: 'orange' }
|
||||
const billStatusMap: Record<string, string> = { paid: '已支付', pending: '待支付', failed: '已失败' }
|
||||
const billStatusBadge: Record<string, any> = { paid: 'success', pending: 'warning', failed: 'error' }
|
||||
const channelMap: Record<string, string> = { alipay: '支付宝', wechat: '微信支付', bank: '银行转账', manual: '手动' }
|
||||
|
||||
const billColumns = [
|
||||
{ title: '企业', key: 'enterprise', width: 200 },
|
||||
{ title: '账单类型', key: 'billType', width: 100 },
|
||||
{ title: '金额', key: 'amount', width: 140 },
|
||||
{ title: '订单号', dataIndex: 'orderNo', key: 'orderNo', width: 180 },
|
||||
{ title: '支付状态', key: 'status', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 160 },
|
||||
{ title: '操作', key: 'actions', width: 120, fixed: 'right' },
|
||||
]
|
||||
|
||||
const billData = ref([
|
||||
{ id: 1, enterprise: '腾云科技有限公司', type: 'consume', billType: 'consume', amount: 12800, orderNo: 'ORD20260408001', status: 'paid', createdAt: '2026-04-08 10:30' },
|
||||
{ id: 2, enterprise: '华创数据服务有限公司', type: 'recharge', billType: 'recharge', amount: 50000, orderNo: 'ORD20260408002', status: 'paid', createdAt: '2026-04-08 09:15' },
|
||||
{ id: 3, enterprise: '云智科技有限公司', type: 'consume', billType: 'consume', amount: 6800, orderNo: 'ORD20260407003', status: 'pending', createdAt: '2026-04-07 16:20' },
|
||||
{ id: 4, enterprise: '数智科技有限公司', type: 'refund', billType: 'refund', amount: 3200, orderNo: 'ORD20260406004', status: 'paid', createdAt: '2026-04-06 14:00' },
|
||||
{ id: 5, enterprise: '万物互联有限公司', type: 'recharge', billType: 'recharge', amount: 100000, orderNo: 'ORD20260405005', status: 'paid', createdAt: '2026-04-05 11:00' },
|
||||
{ id: 6, enterprise: '云帆科技有限公司', type: 'consume', billType: 'consume', amount: 9200, orderNo: 'ORD20260403006', status: 'failed', createdAt: '2026-04-03 09:30' },
|
||||
])
|
||||
|
||||
const rechargeColumns = [
|
||||
{ title: '企业', key: 'enterprise', width: 180 },
|
||||
{ title: '充值金额', key: 'amount', width: 140 },
|
||||
{ title: '支付渠道', key: 'channel', width: 110 },
|
||||
{ title: '交易流水', dataIndex: 'flowNo', key: 'flowNo', width: 180 },
|
||||
{ title: '充值时间', dataIndex: 'createdAt', key: 'createdAt', width: 160 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '操作', key: 'actions', width: 100 },
|
||||
]
|
||||
|
||||
const rechargeData = ref([
|
||||
{ id: 1, enterprise: '腾云科技有限公司', amount: 50000, channel: 'alipay', flowNo: 'ZF20260408001', status: 'paid', createdAt: '2026-04-08 09:15' },
|
||||
{ id: 2, enterprise: '华创数据服务有限公司', amount: 100000, channel: 'bank', flowNo: 'BK20260405002', status: 'paid', createdAt: '2026-04-05 11:00' },
|
||||
{ id: 3, enterprise: '云智科技有限公司', amount: 20000, channel: 'wechat', flowNo: 'WX20260403003', status: 'paid', createdAt: '2026-04-03 14:30' },
|
||||
{ id: 4, enterprise: '数智科技有限公司', amount: 30000, channel: 'manual', flowNo: 'MN20260402004', status: 'paid', createdAt: '2026-04-02 10:00' },
|
||||
{ id: 5, enterprise: '万物互联有限公司', amount: 50000, channel: 'alipay', flowNo: 'ZF20260328005', status: 'paid', createdAt: '2026-03-28 16:20' },
|
||||
])
|
||||
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 6 })
|
||||
|
||||
const handleSearch = () => message.info('搜索:' + searchKeyword.value)
|
||||
const resetFilter = () => { searchKeyword.value = ''; filterBillType.value = undefined; filterBillStatus.value = undefined; dateRange.value = null }
|
||||
const handleViewBill = (r: any) => message.info('查看账单:' + r.orderNo)
|
||||
const handleRemind = (r: any) => message.success('已发送催款通知')
|
||||
const handleViewRecharge = (r: any) => message.info('查看充值凭证')
|
||||
const handleRechargeSubmit = () => { rechargeModalVisible.value = false; message.success('充值成功') }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb-6 { margin-bottom: 20px; }
|
||||
|
||||
.fin-stat-card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
.fin-stat-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.fin-label { font-size: 13px; color: #6b7280; margin: 0 0 4px; }
|
||||
.fin-value { font-size: 24px; font-weight: 700; color: #111827; margin: 0 0 4px; }
|
||||
.fin-trend { font-size: 12px; margin: 0; }
|
||||
.fin-trend.up { color: #22c55e; }
|
||||
.fin-trend.down { color: #ef4444; }
|
||||
.fin-trend.neutral { color: #6b7280; }
|
||||
.fin-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0; }
|
||||
.fin-icon.green { background: #ecfdf5; color: #10b981; }
|
||||
.fin-icon.blue { background: #eff6ff; color: #3b82f6; }
|
||||
.fin-icon.purple { background: #f5f3ff; color: #8b5cf6; }
|
||||
.fin-icon.orange { background: #fff7ed; color: #f59e0b; }
|
||||
|
||||
.filter-bar {
|
||||
display: flex; align-items: center; gap: 12px; margin-bottom: 16px; flex-wrap: wrap;
|
||||
}
|
||||
.enterprise-cell { display: flex; align-items: center; gap: 8px; }
|
||||
.income-amount { color: #22c55e; font-weight: 600; }
|
||||
.consume-amount { color: #ef4444; font-weight: 600; }
|
||||
.refund-amount { color: #f59e0b; font-weight: 600; }
|
||||
</style>
|
||||
89
app/pages/admin/finance/recharge.vue
Normal file
89
app/pages/admin/finance/recharge.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="recharge-page">
|
||||
<a-card :bordered="false" title="充值管理">
|
||||
<div class="filter-bar">
|
||||
<a-input-search v-model:value="keyword" placeholder="企业名称..." style="width: 280px" allow-clear />
|
||||
<a-select v-model:value="channel" placeholder="支付渠道" style="width: 140px" allow-clear>
|
||||
<a-select-option value="alipay">支付宝</a-select-option>
|
||||
<a-select-option value="wechat">微信支付</a-select-option>
|
||||
<a-select-option value="bank">银行转账</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="modalVisible = true">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
手动充值
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table :columns="columns" :data-source="data" row-key="id">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'amount'">
|
||||
<span style="color: #22c55e; font-weight: 600">+ ¥ {{ record.amount.toLocaleString() }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'channel'">
|
||||
<a-tag>{{ channelMap[record.channel] }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-badge :status="statusBadge[record.status]" :text="statusMap[record.status]" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-button type="link" size="small">凭证</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-modal v-model:open="modalVisible" title="手动充值" width="480px" @ok="handleSubmit">
|
||||
<a-form :model="form" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form-item label="企业名称">
|
||||
<a-input v-model:value="form.enterprise" />
|
||||
</a-form-item>
|
||||
<a-form-item label="充值金额">
|
||||
<a-input-number v-model:value="form.amount" :min="0" :precision="2" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注">
|
||||
<a-textarea v-model:value="form.remark" :rows="2" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
const keyword = ref('')
|
||||
const channel = ref<string | undefined>()
|
||||
const modalVisible = ref(false)
|
||||
const form = reactive({ enterprise: '', amount: 0, remark: '' })
|
||||
|
||||
const channelMap: Record<string, string> = { alipay: '支付宝', wechat: '微信支付', bank: '银行转账', manual: '手动' }
|
||||
const statusMap: Record<string, string> = { paid: '已支付', pending: '待支付', failed: '已失败' }
|
||||
const statusBadge: Record<string, any> = { paid: 'success', pending: 'warning', failed: 'error' }
|
||||
|
||||
const columns = [
|
||||
{ title: '企业', dataIndex: 'enterprise', key: 'enterprise', width: 180 },
|
||||
{ title: '充值金额', key: 'amount', width: 140 },
|
||||
{ title: '支付渠道', key: 'channel', width: 110 },
|
||||
{ title: '交易流水', dataIndex: 'flowNo', key: 'flowNo', width: 180 },
|
||||
{ title: '充值时间', dataIndex: 'createdAt', key: 'createdAt', width: 160 },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '操作', key: 'actions', width: 100 },
|
||||
]
|
||||
|
||||
const data = ref([
|
||||
{ id: 1, enterprise: '腾云科技有限公司', amount: 50000, channel: 'alipay', flowNo: 'ZF20260408001', status: 'paid', createdAt: '2026-04-08 09:15' },
|
||||
{ id: 2, enterprise: '华创数据服务有限公司', amount: 100000, channel: 'bank', flowNo: 'BK20260405002', status: 'paid', createdAt: '2026-04-05 11:00' },
|
||||
{ id: 3, enterprise: '云智科技有限公司', amount: 20000, channel: 'wechat', flowNo: 'WX20260403003', status: 'paid', createdAt: '2026-04-03 14:30' },
|
||||
])
|
||||
|
||||
const handleSubmit = () => { modalVisible.value = false; message.success('充值成功') }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-bar {
|
||||
display: flex; align-items: center; gap: 12px; margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,89 +1,157 @@
|
||||
<template>
|
||||
<div class="admin-home">
|
||||
<!-- 欢迎横幅 -->
|
||||
<div class="welcome-banner">
|
||||
<div class="welcome-left">
|
||||
<h2 class="welcome-title">🎛️ 平台管理中心</h2>
|
||||
<p class="welcome-sub">欢迎回来,{{ adminName }},今日数据已更新</p>
|
||||
</div>
|
||||
<div class="welcome-right">
|
||||
<a-space>
|
||||
<a-tag color="red" style="font-size:13px;padding:4px 12px">超级管理员</a-tag>
|
||||
<a-button size="small" @click="loadStats" :loading="loadingStats">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新数据
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
<!-- 核心数据统计 -->
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="12" :sm="12" :md="6" v-for="stat in coreStats" :key="stat.label">
|
||||
<div class="stat-block" :class="stat.color" @click="navigateTo(stat.to)" :style="{ cursor: stat.to ? 'pointer' : 'default' }">
|
||||
<div class="stat-block-header">
|
||||
<span class="stat-block-icon">{{ stat.icon }}</span>
|
||||
<span class="stat-block-label">{{ stat.label }}</span>
|
||||
const { activeTab } = useNav()
|
||||
|
||||
const stats = ref([
|
||||
{ label: '生产工单', value: 128, unit: '单', change: '+12%', up: true, icon: '📋', color: '#6366f1' },
|
||||
{ label: '在制品数量', value: 3, unit: '万', change: '+8%', up: true, icon: '🏭', color: '#10b981' },
|
||||
{ label: '设备利用率', value: 87, unit: '%', change: '+3%', up: true, icon: '⚙️', color: '#f59e0b' },
|
||||
{ label: '订单交付率', value: 96, unit: '%', change: '+1%', up: true, icon: '🚚', color: '#3b82f6' },
|
||||
])
|
||||
|
||||
const quickActions = ref([
|
||||
{ label: '计划排程', icon: '📅', color: 'from-indigo-500 to-purple-500', path: '/admin/production/schedule' },
|
||||
{ label: '生产管控', icon: '🎛️', color: 'from-emerald-500 to-teal-500', path: '/admin/production/control' },
|
||||
{ label: '质量检测', icon: '🔍', color: 'from-amber-500 to-orange-500', path: '/admin/production/quality' },
|
||||
{ label: '设备监控', icon: '📊', color: 'from-blue-500 to-cyan-500', path: '/admin/production/equipment' },
|
||||
{ label: '采购申请', icon: '🛒', color: 'from-pink-500 to-rose-500', path: '/admin/supply/purchase' },
|
||||
{ label: '库存查询', icon: '📦', color: 'from-violet-500 to-purple-500', path: '/admin/supply/warehouse' },
|
||||
])
|
||||
|
||||
const recentOrders = ref([
|
||||
{ id: 'WO2026040901', product: '精密轴承组件 A型', quantity: 500, status: '生产中', progress: 65, startDate: '2026-04-09' },
|
||||
{ id: 'WO2026040802', product: '液压缸体 B型', quantity: 200, status: '待排产', progress: 0, startDate: '2026-04-08' },
|
||||
{ id: 'WO2026040801', product: '传动齿轮组 C型', quantity: 1000, status: '已完成', progress: 100, startDate: '2026-04-07' },
|
||||
{ id: 'WO2026040703', product: '密封圈组件 D型', quantity: 3000, status: '已完成', progress: 100, startDate: '2026-04-06' },
|
||||
{ id: 'WO2026040702', product: '弹簧组件 E型', quantity: 800, status: '已取消', progress: 30, startDate: '2026-04-05' },
|
||||
])
|
||||
|
||||
const qualityAlerts = ref([
|
||||
{ level: 'warning', title: '质检异常:批次 B-20260408-03', desc: '尺寸超出公差范围,已自动隔离', time: '10分钟前' },
|
||||
{ level: 'info', title: '质检报告生成', desc: '批次 A-20260409-01 质检完成', time: '30分钟前' },
|
||||
{ level: 'warning', title: '设备报警:CNC-03', desc: '主轴温度异常,请及时处理', time: '1小时前' },
|
||||
])
|
||||
|
||||
const equipmentStatus = ref([
|
||||
{ name: 'CNC-01', status: '运行中', utilization: 92 },
|
||||
{ name: 'CNC-02', status: '运行中', utilization: 88 },
|
||||
{ name: 'CNC-03', status: '告警', utilization: 0 },
|
||||
{ name: '铣床-01', status: '待机', utilization: 0 },
|
||||
{ name: '铣床-02', status: '运行中', utilization: 75 },
|
||||
])
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
'生产中': 'processing',
|
||||
'待排产': 'warning',
|
||||
'已完成': 'success',
|
||||
'已取消': 'default',
|
||||
}
|
||||
const equipStatusMap: Record<string, string> = {
|
||||
'运行中': 'success',
|
||||
'告警': 'error',
|
||||
'待机': 'default',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="[20, 20]" class="mb-6">
|
||||
<a-col :xs="24" :sm="12" :xl="6" v-for="stat in stats" :key="stat.label">
|
||||
<div class="stat-card" :style="{ '--accent': stat.color }">
|
||||
<div class="stat-header">
|
||||
<span class="stat-icon">{{ stat.icon }}</span>
|
||||
<span :class="['stat-change', stat.up ? 'up' : 'down']">{{ stat.change }}</span>
|
||||
</div>
|
||||
<div class="stat-block-value">
|
||||
<template v-if="loadingStats">
|
||||
<a-skeleton-input :active="true" size="small" style="width:60px" />
|
||||
</template>
|
||||
<template v-else>{{ stat.value }}</template>
|
||||
</div>
|
||||
<div class="stat-block-desc">{{ stat.desc }}</div>
|
||||
<div class="stat-value">{{ stat.value }}<span class="stat-unit">{{ stat.unit }}</span></div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 待办事项 + 快速入口 -->
|
||||
<a-row :gutter="[16, 16]">
|
||||
<!-- 待处理事项 -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">🔔 待处理事项</span>
|
||||
</div>
|
||||
<div class="todo-list">
|
||||
<div
|
||||
v-for="todo in todoItems"
|
||||
:key="todo.label"
|
||||
class="todo-item"
|
||||
:class="{ 'todo-item-urgent': todo.urgent }"
|
||||
@click="navigateTo(todo.to)"
|
||||
<a-row :gutter="[20, 20]">
|
||||
<!-- 左侧主体 -->
|
||||
<a-col :xs="24" :xl="16">
|
||||
<!-- 快捷入口 -->
|
||||
<div class="card mb-6">
|
||||
<div class="card-title">快捷入口</div>
|
||||
<div class="quick-grid">
|
||||
<NuxtLink
|
||||
v-for="action in quickActions"
|
||||
:key="action.label"
|
||||
:to="action.path"
|
||||
class="quick-item"
|
||||
>
|
||||
<div class="todo-dot" :class="todo.dotColor"></div>
|
||||
<div class="todo-content">
|
||||
<span class="todo-label">{{ todo.label }}</span>
|
||||
<a-tag :color="todo.tagColor" style="margin-left:8px">
|
||||
<template v-if="loadingStats">...</template>
|
||||
<template v-else>{{ todo.value }}</template>
|
||||
</a-tag>
|
||||
</div>
|
||||
<RightOutlined class="todo-arrow" />
|
||||
</div>
|
||||
<div v-if="!loadingStats && todoItems.every(t => t.value === 0)" class="todo-empty">
|
||||
🎉 暂无待处理事项,一切正常!
|
||||
</div>
|
||||
<div class="quick-icon" :class="action.color">{{ action.icon }}</div>
|
||||
<span class="quick-label">{{ action.label }}</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工单列表 -->
|
||||
<div class="card">
|
||||
<div class="card-title">近期工单</div>
|
||||
<a-table
|
||||
:dataSource="recentOrders"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
rowKey="id"
|
||||
:scroll="{ x: 600 }"
|
||||
>
|
||||
<a-table-column title="工单编号" dataIndex="id" width="140" />
|
||||
<a-table-column title="产品名称" dataIndex="product" />
|
||||
<a-table-column title="数量" dataIndex="quantity" width="80" align="center" />
|
||||
<a-table-column title="状态" dataIndex="status" width="100" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="statusMap[text] === 'success' ? 'success' : statusMap[text] === 'processing' ? 'processing' : statusMap[text] === 'warning' ? 'warning' : 'default'">
|
||||
{{ text }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="进度" dataIndex="progress" width="140" align="center">
|
||||
<template #default="{ record }">
|
||||
<a-progress
|
||||
:percent="record.progress"
|
||||
:status="record.progress === 100 ? 'success' : 'active'"
|
||||
:showInfo="false"
|
||||
size="small"
|
||||
/>
|
||||
<span class="text-xs text-gray-400 ml-2">{{ record.progress }}%</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="开始日期" dataIndex="startDate" width="120" />
|
||||
</a-table>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 快速导航 -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">⚡ 快速入口</span>
|
||||
<!-- 右侧 -->
|
||||
<a-col :xs="24" :xl="8">
|
||||
<!-- 质量告警 -->
|
||||
<div class="card mb-6">
|
||||
<div class="card-title">质量告警</div>
|
||||
<div class="alert-list">
|
||||
<div v-for="(alert, idx) in qualityAlerts" :key="idx" class="alert-item" :class="alert.level">
|
||||
<div class="alert-header">
|
||||
<span class="alert-dot" :class="alert.level"></span>
|
||||
<span class="alert-title">{{ alert.title }}</span>
|
||||
</div>
|
||||
<div class="alert-desc">{{ alert.desc }}</div>
|
||||
<div class="alert-time">{{ alert.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-grid">
|
||||
<div
|
||||
v-for="item in quickLinks"
|
||||
:key="item.to"
|
||||
class="quick-card"
|
||||
@click="navigateTo(item.to)"
|
||||
>
|
||||
<div class="quick-icon" :style="{ background: item.bg }">{{ item.icon }}</div>
|
||||
<div class="quick-label">{{ item.label }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备状态 -->
|
||||
<div class="card">
|
||||
<div class="card-title">设备状态</div>
|
||||
<div class="equip-list">
|
||||
<div v-for="equip in equipmentStatus" :key="equip.name" class="equip-item">
|
||||
<span class="equip-name">{{ equip.name }}</span>
|
||||
<a-tag :color="equipStatusMap[equip.status] === 'success' ? 'success' : equipStatusMap[equip.status] === 'error' ? 'error' : 'default'" size="small">
|
||||
{{ equip.status }}
|
||||
</a-tag>
|
||||
<span class="equip-util">{{ equip.utilization }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,180 +160,220 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ReloadOutlined, RightOutlined } from '@ant-design/icons-vue'
|
||||
import { getUserInfo } from '@/api/layout'
|
||||
import { getToken } from '@/utils/token-util'
|
||||
import { pageAppProductAll } from '@/api/app/appProduct'
|
||||
import { pageUsers } from '@/api/system/user/index'
|
||||
import { listAppArticle as listCmsArticle } from '@/api/app/article'
|
||||
import { pageGitAccounts } from '@/api/developer'
|
||||
|
||||
definePageMeta({ layout: 'admin' })
|
||||
useHead({ title: '平台管理首页' })
|
||||
|
||||
const adminName = ref('管理员')
|
||||
const loadingStats = ref(false)
|
||||
|
||||
const coreStats = reactive([
|
||||
{ icon: '📦', label: '应用总数', value: 0, desc: '全平台应用', color: 'blue', to: '/admin/apps' },
|
||||
{ icon: '👥', label: '用户总数', value: 0, desc: '注册用户', color: 'green', to: '/admin/users' },
|
||||
{ icon: '⏳', label: '待审核应用', value: 0, desc: '等待审核中', color: 'orange', to: '/admin/app-review' },
|
||||
{ icon: '🛒', label: '上架应用', value: 0, desc: '市场在售', color: 'purple', to: '/admin/market' },
|
||||
])
|
||||
|
||||
const todoItems = reactive([
|
||||
{ label: '待审核应用', value: 0, to: '/admin/app-review', tagColor: 'orange', dotColor: 'dot-orange', urgent: false },
|
||||
{ label: '待审核Git账号', value: 0, to: '/admin/git-review', tagColor: 'cyan', dotColor: 'dot-cyan', urgent: false },
|
||||
{ label: '草稿文章', value: 0, to: '/admin/articles', tagColor: 'blue', dotColor: 'dot-blue', urgent: false },
|
||||
{ label: '冻结用户', value: 0, to: '/admin/users', tagColor: 'red', dotColor: 'dot-red', urgent: false },
|
||||
])
|
||||
|
||||
const ANNOUNCE_MODEL = 'announcement'
|
||||
|
||||
const quickLinks = [
|
||||
{ to: '/admin/app-review', icon: '🔍', label: '应用审核', bg: '#fff7ed' },
|
||||
{ to: '/admin/git-review', icon: '🔧', label: 'Git 审核', bg: '#ecfdf5' },
|
||||
{ to: '/admin/apps', icon: '📦', label: '应用管理', bg: '#eff6ff' },
|
||||
{ to: '/admin/market', icon: '🛒', label: '应用市场', bg: '#faf5ff' },
|
||||
{ to: '/admin/users', icon: '👥', label: '用户管理', bg: '#f0fdf4' },
|
||||
{ to: '/admin/developers', icon: '🧑💻', label: '开发者', bg: '#f0f9ff' },
|
||||
{ to: '/admin/tickets', icon: '🎫', label: '工单处理', bg: '#fdf4ff' },
|
||||
{ to: '/admin/articles', icon: '📝', label: '文章管理', bg: '#fefce8' },
|
||||
{ to: '/admin/article-categories', icon: '🗂️', label: '文章分类', bg: '#ecfeff' },
|
||||
{ to: '/admin/announcements', icon: '📢', label: '公告管理', bg: '#fff1f2' },
|
||||
{ to: '/admin/settings', icon: '⚙️', label: '平台设置', bg: '#f9fafb' },
|
||||
]
|
||||
|
||||
async function loadStats() {
|
||||
loadingStats.value = true
|
||||
try {
|
||||
const [appsRes, usersRes, pendingRes, marketRes, draftRes, frozenRes, gitPendingRes] = await Promise.allSettled([
|
||||
pageAppProductAll({ current: 1, size: 1 }),
|
||||
pageUsers({ page: 1, limit: 1 }),
|
||||
pageAppProductAll({ current: 1, size: 1, publishStatus: 'pending_review' }),
|
||||
pageAppProductAll({ current: 1, size: 1, publishStatus: 'published' }),
|
||||
listCmsArticle({ status: 1 }),
|
||||
pageUsers({ page: 1, limit: 1, status: 1 }),
|
||||
pageGitAccounts({ page: 1, size: 1, status: 'pending' }),
|
||||
])
|
||||
|
||||
coreStats[0].value = appsRes.status === 'fulfilled' ? appsRes.value?.count || 0 : 0
|
||||
coreStats[1].value = usersRes.status === 'fulfilled' ? usersRes.value?.count || 0 : 0
|
||||
coreStats[2].value = pendingRes.status === 'fulfilled' ? pendingRes.value?.count || 0 : 0
|
||||
coreStats[3].value = marketRes.status === 'fulfilled' ? marketRes.value?.count || 0 : 0
|
||||
|
||||
const pendingCount = pendingRes.status === 'fulfilled' ? pendingRes.value?.count || 0 : 0
|
||||
const draftCount = draftRes.status === 'fulfilled'
|
||||
? (draftRes.value || []).filter(item => (item.model || '').trim() !== ANNOUNCE_MODEL).length
|
||||
: 0
|
||||
const frozenCount = frozenRes.status === 'fulfilled' ? frozenRes.value?.count || 0 : 0
|
||||
const gitPendingCount = gitPendingRes.status === 'fulfilled' ? (gitPendingRes.value as any)?.data?.data?.total || 0 : 0
|
||||
|
||||
todoItems[0].value = pendingCount
|
||||
todoItems[0].urgent = pendingCount > 0
|
||||
todoItems[1].value = gitPendingCount
|
||||
todoItems[1].urgent = gitPendingCount > 0
|
||||
todoItems[2].value = draftCount
|
||||
todoItems[3].value = frozenCount
|
||||
} catch { /* ignore */ } finally {
|
||||
loadingStats.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const token = getToken()
|
||||
if (!token) return
|
||||
// 并发加载用户信息和统计数据
|
||||
Promise.allSettled([
|
||||
getUserInfo().then(me => {
|
||||
adminName.value = me?.nickname?.trim() || me?.username?.trim() || '管理员'
|
||||
}),
|
||||
loadStats(),
|
||||
])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-home {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
.dashboard {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* 欢迎横幅 */
|
||||
.welcome-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(135deg, #1a0f0f 0%, #3d1515 100%);
|
||||
border-radius: 14px;
|
||||
padding: 24px 28px;
|
||||
color: #fff;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.welcome-title { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 6px; }
|
||||
.welcome-sub { font-size: 14px; color: rgba(255,255,255,0.7); margin: 0; }
|
||||
|
||||
/* 核心统计块 */
|
||||
.stat-block {
|
||||
padding: 18px 20px;
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||
border: 1px solid #f0f0f0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.stat-block:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.08); }
|
||||
.stat-block.blue { background: #eff6ff; border-color: #dbeafe; }
|
||||
.stat-block.green { background: #f0fdf4; border-color: #bbf7d0; }
|
||||
.stat-block.orange { background: #fff7ed; border-color: #fed7aa; }
|
||||
.stat-block.purple { background: #faf5ff; border-color: #e9d5ff; }
|
||||
|
||||
.stat-block-header { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
|
||||
.stat-block-icon { font-size: 18px; }
|
||||
.stat-block-label { font-size: 13px; color: rgba(0,0,0,0.55); }
|
||||
.stat-block-value { font-size: 32px; font-weight: 800; color: rgba(0,0,0,0.85); line-height: 1.1; margin-bottom: 4px; }
|
||||
.stat-block-desc { font-size: 12px; color: rgba(0,0,0,0.4); }
|
||||
|
||||
/* Panel */
|
||||
.panel { background: #fff; border: 1px solid #f0f0f0; border-radius: 12px; overflow: hidden; }
|
||||
.panel-header { padding: 14px 18px; border-bottom: 1px solid #f5f5f5; }
|
||||
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); }
|
||||
|
||||
/* 待办 */
|
||||
.todo-list { padding: 8px 0; }
|
||||
.todo-item {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 12px 18px; cursor: pointer; transition: background 0.15s;
|
||||
.stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.stat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stat-change.up {
|
||||
color: #10b981;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.stat-change.down {
|
||||
color: #ef4444;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-unit {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.todo-item:hover { background: #f9fafb; }
|
||||
.todo-item-urgent .todo-label { font-weight: 600; }
|
||||
.todo-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.dot-orange { background: #f97316; }
|
||||
.dot-blue { background: #3b82f6; }
|
||||
.dot-cyan { background: #06b6d4; }
|
||||
.dot-red { background: #ef4444; }
|
||||
.todo-content { flex: 1; display: flex; align-items: center; }
|
||||
.todo-label { font-size: 14px; color: rgba(0,0,0,0.75); }
|
||||
.todo-arrow { font-size: 11px; color: rgba(0,0,0,0.3); }
|
||||
.todo-empty { text-align: center; padding: 20px 0; color: rgba(0,0,0,0.4); font-size: 14px; }
|
||||
|
||||
/* 快速入口九宫格 */
|
||||
.quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1px;
|
||||
background: #f5f5f5;
|
||||
gap: 12px;
|
||||
}
|
||||
.quick-card {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
gap: 8px; padding: 18px 12px; background: #fff;
|
||||
cursor: pointer; transition: background 0.15s;
|
||||
|
||||
.quick-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 8px;
|
||||
border-radius: 10px;
|
||||
background: #fafafa;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.quick-card:hover { background: #f9fafb; }
|
||||
|
||||
.quick-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
width: 44px; height: 44px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center; font-size: 22px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(168,85,247,0.1));
|
||||
}
|
||||
|
||||
.quick-label {
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.alert-item.warning {
|
||||
background: #fffbeb;
|
||||
border-left: 3px solid #f59e0b;
|
||||
}
|
||||
|
||||
.alert-item.info {
|
||||
background: #eff6ff;
|
||||
border-left: 3px solid #3b82f6;
|
||||
}
|
||||
|
||||
.alert-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.alert-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.alert-dot.warning {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.alert-dot.info {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.alert-desc {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.alert-time {
|
||||
font-size: 11px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.equip-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.equip-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.equip-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.equip-util {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
.quick-label { font-size: 13px; color: rgba(0,0,0,0.75); font-weight: 500; }
|
||||
</style>
|
||||
|
||||
144
app/pages/admin/management/decision.vue
Normal file
144
app/pages/admin/management/decision.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'management-decision'
|
||||
|
||||
const kpis = ref([
|
||||
{ name: '设备综合效率 OEE', value: 78.5, target: 85, unit: '%', trend: '+2.3%', color: '#6366f1' },
|
||||
{ name: '订单准时交付率', value: 96.2, target: 98, unit: '%', trend: '+1.5%', color: '#10b981' },
|
||||
{ name: '产品一次合格率', value: 97.8, target: 98.5, unit: '%', trend: '+0.3%', color: '#3b82f6' },
|
||||
{ name: '人均产值', value: 8.5, target: 9.0, unit: '万/月', trend: '+0.3', color: '#f59e0b' },
|
||||
])
|
||||
|
||||
const analysis = ref([
|
||||
{ title: '生产效能分析', desc: '本月产能利用率达85%,较上月提升5个百分点', type: 'production', icon: '📊' },
|
||||
{ title: '质量趋势分析', desc: '近30天良品率稳定在97%以上,呈上升趋势', type: 'quality', icon: '✅' },
|
||||
{ title: '成本分析报告', desc: '本月生产成本控制良好,材料利用率提升3%', type: 'cost', icon: '💰' },
|
||||
{ title: '库存周转分析', desc: '库存周转天数28天,处于健康水平', type: 'inventory', icon: '📦' },
|
||||
])
|
||||
|
||||
const typeColor: Record<string, string> = {
|
||||
production: 'indigo',
|
||||
quality: 'green',
|
||||
cost: 'orange',
|
||||
inventory: 'blue',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">决策支持</h2>
|
||||
<a-button @click="() => {}">导出报告</a-button>
|
||||
</div>
|
||||
|
||||
<!-- KPI 指标卡 -->
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="24" :sm="12" :xl="6" v-for="kpi in kpis" :key="kpi.name">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-header">
|
||||
<span class="kpi-name">{{ kpi.name }}</span>
|
||||
<span class="kpi-trend">{{ kpi.trend }}</span>
|
||||
</div>
|
||||
<div class="kpi-value" :style="{ color: kpi.color }">{{ kpi.value }}<span class="kpi-unit">{{ kpi.unit }}</span></div>
|
||||
<div class="kpi-bar">
|
||||
<a-progress :percent="Math.round(kpi.value / kpi.target * 100)" :showInfo="false" :strokeColor="kpi.color" size="small" />
|
||||
<span class="kpi-target">目标: {{ kpi.target }}{{ kpi.unit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="[20, 20]">
|
||||
<a-col :xs="24" :xl="16">
|
||||
<div class="card">
|
||||
<div class="card-title">经营分析报告</div>
|
||||
<div class="analysis-list">
|
||||
<div v-for="item in analysis" :key="item.title" class="analysis-item">
|
||||
<div class="analysis-icon" :class="item.type">{{ item.icon }}</div>
|
||||
<div class="analysis-body">
|
||||
<div class="analysis-title">{{ item.title }}</div>
|
||||
<div class="analysis-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
<a-button type="link" size="small">查看详情</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :xl="8">
|
||||
<div class="card">
|
||||
<div class="card-title">数据趋势</div>
|
||||
<div class="trend-list">
|
||||
<div class="trend-item">
|
||||
<div class="trend-label">营收趋势</div>
|
||||
<div class="trend-bar">
|
||||
<div class="trend-fill" style="width: 72%; background: #10b981;"></div>
|
||||
</div>
|
||||
<div class="trend-val">72%</div>
|
||||
</div>
|
||||
<div class="trend-item">
|
||||
<div class="trend-label">利润趋势</div>
|
||||
<div class="trend-bar">
|
||||
<div class="trend-fill" style="width: 65%; background: #6366f1;"></div>
|
||||
</div>
|
||||
<div class="trend-val">65%</div>
|
||||
</div>
|
||||
<div class="trend-item">
|
||||
<div class="trend-label">产能趋势</div>
|
||||
<div class="trend-bar">
|
||||
<div class="trend-fill" style="width: 85%; background: #f59e0b;"></div>
|
||||
</div>
|
||||
<div class="trend-val">85%</div>
|
||||
</div>
|
||||
<div class="trend-item">
|
||||
<div class="trend-label">质量趋势</div>
|
||||
<div class="trend-bar">
|
||||
<div class="trend-fill" style="width: 78%; background: #3b82f6;"></div>
|
||||
</div>
|
||||
<div class="trend-val">78%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
|
||||
|
||||
.kpi-card { background: white; border-radius: 12px; padding: 18px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
|
||||
.kpi-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||
.kpi-name { font-size: 13px; color: #6b7280; }
|
||||
.kpi-trend { font-size: 12px; color: #10b981; font-weight: 600; }
|
||||
.kpi-value { font-size: 30px; font-weight: 700; }
|
||||
.kpi-unit { font-size: 13px; font-weight: 400; color: #9ca3af; margin-left: 2px; }
|
||||
.kpi-bar { margin-top: 10px; }
|
||||
.kpi-target { font-size: 11px; color: #9ca3af; margin-top: 4px; display: block; }
|
||||
|
||||
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
|
||||
.card-title { font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 16px; }
|
||||
|
||||
.analysis-list { display: flex; flex-direction: column; gap: 14px; }
|
||||
.analysis-item { display: flex; align-items: center; gap: 14px; padding: 14px; background: #fafafa; border-radius: 10px; }
|
||||
.analysis-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 20px; background: #f0f0f0; }
|
||||
.analysis-icon.production { background: #eef2ff; }
|
||||
.analysis-icon.quality { background: #ecfdf5; }
|
||||
.analysis-icon.cost { background: #fffbeb; }
|
||||
.analysis-icon.inventory { background: #eff6ff; }
|
||||
.analysis-body { flex: 1; }
|
||||
.analysis-title { font-size: 14px; font-weight: 600; color: #374151; margin-bottom: 4px; }
|
||||
.analysis-desc { font-size: 12px; color: #9ca3af; }
|
||||
|
||||
.trend-list { display: flex; flex-direction: column; gap: 16px; }
|
||||
.trend-item { display: flex; align-items: center; gap: 10px; }
|
||||
.trend-label { width: 80px; font-size: 13px; color: #6b7280; }
|
||||
.trend-bar { flex: 1; height: 8px; background: #f0f0f0; border-radius: 4px; overflow: hidden; }
|
||||
.trend-fill { height: 100%; border-radius: 4px; }
|
||||
.trend-val { width: 40px; text-align: right; font-size: 13px; font-weight: 600; color: #374151; }
|
||||
|
||||
.mb-6 { margin-bottom: 20px; }
|
||||
</style>
|
||||
383
app/pages/admin/management/finance.vue
Normal file
383
app/pages/admin/management/finance.vue
Normal file
@@ -0,0 +1,383 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
// 财务统计
|
||||
const financeStats = ref([
|
||||
{ label: '本月收入', value: '128.5', unit: '万', icon: 'fa-arrow-down', gradient: 'from-green-500 to-teal-500', change: '+18%', up: true },
|
||||
{ label: '本月支出', value: '89.5', unit: '万', icon: 'fa-arrow-up', gradient: 'from-red-500 to-pink-500', change: '+12%', up: false },
|
||||
{ label: '本月利润', value: '39.0', unit: '万', icon: 'fa-chart-line', gradient: 'from-blue-500 to-purple-500', change: '+25%', up: true },
|
||||
{ label: '应收账款', value: '256.8', unit: '万', icon: 'fa-wallet', gradient: 'from-orange-500 to-yellow-500', change: '-5%', up: true },
|
||||
])
|
||||
|
||||
// 收支记录
|
||||
const transactions = ref([
|
||||
{ id: 'TX-2026040901', type: 'income', category: '销售收款', amount: 58000, customer: '深圳市精密机械有限公司', date: '2026-04-09', status: 'completed' },
|
||||
{ id: 'TX-2026040802', type: 'expense', category: '采购付款', amount: 36000, supplier: '上海五金工具厂', date: '2026-04-08', status: 'completed' },
|
||||
{ id: 'TX-2026040801', type: 'expense', category: '工资支出', amount: 125000, supplier: '人力资源部', date: '2026-04-08', status: 'completed' },
|
||||
{ id: 'TX-2026040703', type: 'income', category: '销售收款', amount: 42000, customer: '东莞市金属材料公司', date: '2026-04-07', status: 'completed' },
|
||||
{ id: 'TX-2026040702', type: 'expense', category: '水电费', amount: 8500, supplier: '电力公司', date: '2026-04-07', status: 'completed' },
|
||||
{ id: 'TX-2026040601', type: 'income', category: '销售收款', amount: 75000, customer: '苏州液压设备厂', date: '2026-04-06', status: 'completed' },
|
||||
{ id: 'TX-2026040501', type: 'expense', category: '设备维修', amount: 12000, supplier: '设备维修部', date: '2026-04-05', status: 'completed' },
|
||||
{ id: 'TX-2026040502', type: 'expense', category: '办公费用', amount: 3200, supplier: '办公用品供应商', date: '2026-04-05', status: 'completed' },
|
||||
])
|
||||
|
||||
// 应收应付
|
||||
const receivables = ref([
|
||||
{ customer: '深圳市精密机械有限公司', amount: 85000, dueDate: '2026-04-15', status: 'pending' },
|
||||
{ customer: '东莞市金属材料公司', amount: 68000, dueDate: '2026-04-20', status: 'pending' },
|
||||
{ customer: '苏州液压设备厂', amount: 42000, dueDate: '2026-04-25', status: 'overdue' },
|
||||
{ customer: '广州电子科技有限公司', amount: 95000, dueDate: '2026-05-01', status: 'pending' },
|
||||
])
|
||||
|
||||
const typeFilter = ref('all')
|
||||
const searchKeyword = ref('')
|
||||
const activeTab = ref('transactions')
|
||||
|
||||
const filteredTransactions = computed(() => {
|
||||
return transactions.value.filter((item) => {
|
||||
const matchType = typeFilter.value === 'all' || item.type === typeFilter.value
|
||||
const matchSearch = !searchKeyword.value ||
|
||||
item.id.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
item.category.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
(item.customer && item.customer.toLowerCase().includes(searchKeyword.value.toLowerCase()))
|
||||
return matchType && matchSearch
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="finance-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">财务管理</h2>
|
||||
<p class="text-gray-500 mt-1">监控收支状况,管理应收账款</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<a-button>
|
||||
<template #icon><i class="fas fa-download mr-1"></i></template>
|
||||
导出报表
|
||||
</a-button>
|
||||
<a-button type="primary">
|
||||
<template #icon><i class="fas fa-plus mr-1"></i></template>
|
||||
记账
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-4 gap-6 mb-6">
|
||||
<div
|
||||
v-for="stat in financeStats"
|
||||
:key="stat.label"
|
||||
class="glass rounded-2xl p-6 card-hover cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
|
||||
:class="`bg-gradient-to-br ${stat.gradient}`"
|
||||
>
|
||||
<i :class="`fas ${stat.icon}`"></i>
|
||||
</div>
|
||||
<span
|
||||
class="text-sm font-medium"
|
||||
:class="stat.up ? 'text-green-500' : 'text-red-500'"
|
||||
>
|
||||
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
|
||||
{{ stat.change }}
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-gray-800 mb-1">
|
||||
<span class="text-base text-gray-500">¥</span>{{ stat.value }}<span class="text-base text-gray-500 ml-1">{{ stat.unit }}</span>
|
||||
</h3>
|
||||
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab切换 -->
|
||||
<div class="glass rounded-2xl p-4 mb-6">
|
||||
<a-radio-group v-model:value="activeTab" button-style="solid">
|
||||
<a-radio-button value="transactions">
|
||||
<i class="fas fa-list mr-1"></i>收支记录
|
||||
</a-radio-button>
|
||||
<a-radio-button value="receivables">
|
||||
<i class="fas fa-hand-holding-usd mr-1"></i>应收账款
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 收支记录 -->
|
||||
<div v-show="activeTab === 'transactions'" class="glass rounded-2xl p-6">
|
||||
<!-- 筛选 -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-gray-600 font-medium">类型筛选:</span>
|
||||
<a-radio-group v-model:value="typeFilter" button-style="solid">
|
||||
<a-radio-button value="all">全部</a-radio-button>
|
||||
<a-radio-button value="income">收入</a-radio-button>
|
||||
<a-radio-button value="expense">支出</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索单号、类别、对象..."
|
||||
style="width: 280px"
|
||||
allow-clear
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:dataSource="filteredTransactions"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
rowKey="id"
|
||||
:scroll="{ x: 1000 }"
|
||||
>
|
||||
<a-table-column title="单号" dataIndex="id" width="140" />
|
||||
<a-table-column title="类型" dataIndex="type" width="80" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="text === 'income' ? 'success' : 'error'">
|
||||
{{ text === 'income' ? '收入' : '支出' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="类别" dataIndex="category" width="120" />
|
||||
<a-table-column title="金额(元)" dataIndex="amount" width="130" align="right">
|
||||
<template #default="{ record, text }">
|
||||
<span class="font-medium" :class="record.type === 'income' ? 'text-green-600' : 'text-red-600'">
|
||||
{{ record.type === 'income' ? '+' : '-' }}¥{{ text.toLocaleString() }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="对方" width="200">
|
||||
<template #default="{ record }">
|
||||
<span class="font-medium">{{ record.customer || record.supplier || '-' }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="日期" dataIndex="date" width="120" />
|
||||
<a-table-column title="状态" width="100" align="center">
|
||||
<template #default>
|
||||
<a-tag color="success">已完成</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" width="100" align="center" fixed="right">
|
||||
<template #default>
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<a-button type="link" size="small" title="详情">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 应收账款 -->
|
||||
<div v-show="activeTab === 'receivables'" class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-hand-holding-usd text-orange-500 mr-2"></i>
|
||||
应收账款列表
|
||||
</h3>
|
||||
<a-button type="primary">
|
||||
<template #icon><i class="fas fa-plus mr-1"></i></template>
|
||||
登记收款
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:dataSource="receivables"
|
||||
:pagination="false"
|
||||
rowKey="customer"
|
||||
>
|
||||
<a-table-column title="客户名称" dataIndex="customer">
|
||||
<template #default="{ text }">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white text-xs">
|
||||
<i class="fas fa-building"></i>
|
||||
</div>
|
||||
<span class="font-medium">{{ text }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="应收金额(元)" dataIndex="amount" width="150" align="right">
|
||||
<template #default="{ text }">
|
||||
<span class="font-medium text-orange-600">¥{{ text.toLocaleString() }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="到期日期" dataIndex="dueDate" width="140" />
|
||||
<a-table-column title="状态" dataIndex="status" width="100" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag v-if="text === 'pending'" color="processing">待收款</a-tag>
|
||||
<a-tag v-else-if="text === 'overdue'" color="error">已逾期</a-tag>
|
||||
<a-tag v-else color="success">已收款</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" width="160" align="center" fixed="right">
|
||||
<template #default>
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<a-button type="primary" size="small" ghost>催款</a-button>
|
||||
<a-button type="link" size="small">详情</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.finance-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-base {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rounded-2xl {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.gap-6 {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text-gray-800 {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.text-red-600 {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.text-orange-600 {
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
453
app/pages/admin/management/hr.vue
Normal file
453
app/pages/admin/management/hr.vue
Normal file
@@ -0,0 +1,453 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
// 人事统计
|
||||
const hrStats = ref([
|
||||
{ label: '员工总数', value: 56, icon: 'fa-users', gradient: 'from-blue-500 to-cyan-500', change: '+3', up: true },
|
||||
{ label: '在职', value: 52, icon: 'fa-user-check', gradient: 'from-green-500 to-teal-500', change: '+2', up: true },
|
||||
{ label: '本月入职', value: 3, icon: 'fa-user-plus', gradient: 'from-purple-500 to-pink-500', change: '+2', up: true },
|
||||
{ label: '本月离职', value: 1, icon: 'fa-user-minus', gradient: 'from-orange-500 to-red-500', change: '-1', up: true },
|
||||
])
|
||||
|
||||
// 员工列表
|
||||
const employees = ref([
|
||||
{ id: 'EMP-001', name: '张三', department: '生产部', position: '生产主管', phone: '138****1234', email: 'zhangsan@company.com', status: 'active', joinDate: '2020-03-15' },
|
||||
{ id: 'EMP-002', name: '李四', department: '技术部', position: '技术工程师', phone: '139****5678', email: 'lisi@company.com', status: 'active', joinDate: '2021-06-20' },
|
||||
{ id: 'EMP-003', name: '王五', department: '采购部', position: '采购专员', phone: '137****9012', email: 'wangwu@company.com', status: 'active', joinDate: '2022-01-10' },
|
||||
{ id: 'EMP-004', name: '赵六', department: '财务部', position: '财务经理', phone: '136****3456', email: 'zhaoliu@company.com', status: 'active', joinDate: '2019-08-25' },
|
||||
{ id: 'EMP-005', name: '孙七', department: '人事部', position: '人事专员', phone: '135****7890', email: 'sunqi@company.com', status: 'active', joinDate: '2023-02-15' },
|
||||
{ id: 'EMP-006', name: '周八', department: '生产部', position: '操作工', phone: '134****2345', email: 'zhouba@company.com', status: 'active', joinDate: '2024-01-08' },
|
||||
])
|
||||
|
||||
// 考勤记录
|
||||
const attendanceRecords = ref([
|
||||
{ date: '2026-04-09', total: 56, present: 54, absent: 0, late: 2, leave: 0, off: 0 },
|
||||
{ date: '2026-04-08', total: 56, present: 55, absent: 0, late: 1, leave: 0, off: 0 },
|
||||
{ date: '2026-04-07', total: 56, present: 56, absent: 0, late: 0, leave: 0, off: 0 },
|
||||
{ date: '2026-04-06', total: 56, present: 48, absent: 0, late: 0, leave: 3, off: 5 },
|
||||
])
|
||||
|
||||
// 部门分布
|
||||
const departmentStats = ref([
|
||||
{ name: '生产部', count: 28, percentage: 50 },
|
||||
{ name: '技术部', count: 10, percentage: 18 },
|
||||
{ name: '采购部', count: 6, percentage: 11 },
|
||||
{ name: '财务部', count: 5, percentage: 9 },
|
||||
{ name: '人事部', count: 4, percentage: 7 },
|
||||
{ name: '其他', count: 3, percentage: 5 },
|
||||
])
|
||||
|
||||
const activeTab = ref('employees')
|
||||
const searchKeyword = ref('')
|
||||
|
||||
const filteredEmployees = computed(() => {
|
||||
return employees.value.filter((emp) => {
|
||||
return !searchKeyword.value ||
|
||||
emp.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
emp.id.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
emp.department.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hr-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">人力资源</h2>
|
||||
<p class="text-gray-500 mt-1">管理员工信息、考勤记录和薪资发放</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<a-button>
|
||||
<template #icon><i class="fas fa-calendar mr-1"></i></template>
|
||||
考勤统计
|
||||
</a-button>
|
||||
<a-button type="primary">
|
||||
<template #icon><i class="fas fa-plus mr-1"></i></template>
|
||||
新增员工
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-4 gap-6 mb-6">
|
||||
<div
|
||||
v-for="stat in hrStats"
|
||||
:key="stat.label"
|
||||
class="glass rounded-2xl p-6 card-hover cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
|
||||
:class="`bg-gradient-to-br ${stat.gradient}`"
|
||||
>
|
||||
<i :class="`fas ${stat.icon}`"></i>
|
||||
</div>
|
||||
<span
|
||||
class="text-sm font-medium"
|
||||
:class="stat.up ? 'text-green-500' : 'text-red-500'"
|
||||
>
|
||||
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
|
||||
{{ stat.change }}
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-gray-800 mb-1">{{ stat.value }}</h3>
|
||||
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab切换 -->
|
||||
<div class="glass rounded-2xl p-4 mb-6">
|
||||
<a-radio-group v-model:value="activeTab" button-style="solid">
|
||||
<a-radio-button value="employees">
|
||||
<i class="fas fa-users mr-1"></i>员工管理
|
||||
</a-radio-button>
|
||||
<a-radio-button value="attendance">
|
||||
<i class="fas fa-calendar-check mr-1"></i>考勤记录
|
||||
</a-radio-button>
|
||||
<a-radio-button value="salary">
|
||||
<i class="fas fa-money-bill mr-1"></i>薪资管理
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索员工姓名、工号、部门..."
|
||||
style="width: 280px; float: right"
|
||||
allow-clear
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 员工列表 -->
|
||||
<div v-show="activeTab === 'employees'" class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-users text-blue-500 mr-2"></i>
|
||||
员工列表
|
||||
</h3>
|
||||
<span class="text-sm text-gray-500">共 {{ filteredEmployees.length }} 名员工</span>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:dataSource="filteredEmployees"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
rowKey="id"
|
||||
:scroll="{ x: 1000 }"
|
||||
>
|
||||
<a-table-column title="工号" dataIndex="id" width="100" />
|
||||
<a-table-column title="姓名" dataIndex="name" width="100">
|
||||
<template #default="{ text }">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white text-sm font-medium">
|
||||
{{ text.charAt(0) }}
|
||||
</div>
|
||||
<span class="font-medium">{{ text }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="部门" dataIndex="department" width="100" />
|
||||
<a-table-column title="职位" dataIndex="position" width="120" />
|
||||
<a-table-column title="手机号" dataIndex="phone" width="130" />
|
||||
<a-table-column title="邮箱" dataIndex="email" width="180" />
|
||||
<a-table-column title="入职日期" dataIndex="joinDate" width="120" />
|
||||
<a-table-column title="状态" dataIndex="status" width="80" align="center">
|
||||
<template #default>
|
||||
<a-tag color="success">在职</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" width="120" align="center" fixed="right">
|
||||
<template #default>
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<a-button type="link" size="small" title="详情">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a-button>
|
||||
<a-button type="link" size="small" title="编辑">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 考勤记录 -->
|
||||
<div v-show="activeTab === 'attendance'" class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-calendar-check text-green-500 mr-2"></i>
|
||||
考勤记录
|
||||
</h3>
|
||||
<a-button>导出考勤表</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 今日考勤统计 -->
|
||||
<a-row :gutter="16" class="mb-6">
|
||||
<a-col :span="6">
|
||||
<div class="stat-mini glass rounded-xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-blue-600">{{ attendanceRecords[0].total }}</div>
|
||||
<div class="text-sm text-gray-500">应到人数</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div class="stat-mini glass rounded-xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-green-600">{{ attendanceRecords[0].present }}</div>
|
||||
<div class="text-sm text-gray-500">实际出勤</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div class="stat-mini glass rounded-xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-orange-600">{{ attendanceRecords[0].late }}</div>
|
||||
<div class="text-sm text-gray-500">迟到</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div class="stat-mini glass rounded-xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-purple-600">{{ attendanceRecords[0].leave + attendanceRecords[0].off }}</div>
|
||||
<div class="text-sm text-gray-500">请假/休息</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-table
|
||||
:dataSource="attendanceRecords"
|
||||
:pagination="false"
|
||||
rowKey="date"
|
||||
>
|
||||
<a-table-column title="日期" dataIndex="date" width="120" />
|
||||
<a-table-column title="应到人数" dataIndex="total" width="100" align="center" />
|
||||
<a-table-column title="实际出勤" dataIndex="present" width="100" align="center">
|
||||
<template #default="{ text, record }">
|
||||
<span class="text-green-600 font-medium">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="缺勤" dataIndex="absent" width="80" align="center">
|
||||
<template #default="{ text }">
|
||||
<span :class="text > 0 ? 'text-red-600 font-medium' : 'text-gray-400'">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="迟到" dataIndex="late" width="80" align="center">
|
||||
<template #default="{ text }">
|
||||
<span :class="text > 0 ? 'text-orange-600 font-medium' : 'text-gray-400'">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="请假" dataIndex="leave" width="80" align="center" />
|
||||
<a-table-column title="休息" dataIndex="off" width="80" align="center" />
|
||||
<a-table-column title="出勤率" width="120">
|
||||
<template #default="{ record }">
|
||||
<div class="flex items-center gap-2">
|
||||
<a-progress
|
||||
:percent="Math.round((record.present / record.total) * 100)"
|
||||
:showInfo="false"
|
||||
size="small"
|
||||
style="width: 60px"
|
||||
/>
|
||||
<span class="text-sm">{{ Math.round((record.present / record.total) * 100) }}%</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
|
||||
<!-- 部门分布 -->
|
||||
<div class="mt-6">
|
||||
<h4 class="font-bold text-gray-800 mb-4">部门人数分布</h4>
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :span="8" v-for="dept in departmentStats" :key="dept.name">
|
||||
<div class="glass rounded-xl p-4">
|
||||
<div class="flex justify-between mb-2">
|
||||
<span class="font-medium">{{ dept.name }}</span>
|
||||
<span class="text-blue-600 font-medium">{{ dept.count }}人</span>
|
||||
</div>
|
||||
<a-progress :percent="dept.percentage" :showInfo="false" stroke-color="#6366f1" />
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 薪资管理 -->
|
||||
<div v-show="activeTab === 'salary'" class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-money-bill text-green-500 mr-2"></i>
|
||||
薪资发放记录
|
||||
</h3>
|
||||
<a-button type="primary">
|
||||
<template #icon><i class="fas fa-plus mr-1"></i></template>
|
||||
生成工资单
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-empty description="薪资数据将在每月固定日期生成" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.hr-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-mini {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.rounded-2xl {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.gap-6 {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gap-16 {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text-gray-800 {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.text-orange-600 {
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.text-purple-600 {
|
||||
color: #9333ea;
|
||||
}
|
||||
|
||||
.text-red-600 {
|
||||
color: #dc2626;
|
||||
}
|
||||
</style>
|
||||
477
app/pages/admin/management/office.vue
Normal file
477
app/pages/admin/management/office.vue
Normal file
@@ -0,0 +1,477 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
// 协同办公统计
|
||||
const officeStats = ref([
|
||||
{ label: '公告通知', value: 12, icon: 'fa-bullhorn', gradient: 'from-red-500 to-pink-500', change: '+3', up: true },
|
||||
{ label: '待审批', value: 8, icon: 'fa-clock', gradient: 'from-orange-500 to-yellow-500', change: '-2', up: true },
|
||||
{ label: '已完成', value: 156, icon: 'fa-check-circle', gradient: 'from-green-500 to-teal-500', change: '+25', up: true },
|
||||
{ label: '会议预约', value: 5, icon: 'fa-video', gradient: 'from-blue-500 to-purple-500', change: '+1', up: false },
|
||||
])
|
||||
|
||||
// 公告列表
|
||||
const announcements = ref([
|
||||
{ id: 'NOTICE-001', title: '关于清明节放假安排的通知', author: '人事行政部', date: '2026-04-01', views: 1258, important: true, content: '清明节放假时间为4月4日至4月6日,共3天...' },
|
||||
{ id: 'NOTICE-002', title: '2026年第一季度财报公告', author: '财务部', date: '2026-03-28', views: 986, important: true, content: '公司2026年第一季度营收同比增长18%...' },
|
||||
{ id: 'NOTICE-003', title: '新版本系统功能更新说明', author: '技术部', date: '2026-03-25', views: 756, important: false, content: '本次更新新增设备管理模块...' },
|
||||
{ id: 'NOTICE-004', title: '生产车间设备维护通知', author: '生产部', date: '2026-03-20', views: 423, important: false, content: '2号车间将于本周六进行设备维护...' },
|
||||
])
|
||||
|
||||
// 审批列表
|
||||
const approvals = ref([
|
||||
{ id: 'APPR-001', type: 'leave', title: '张三年假申请', applicant: '张三', department: '生产部', amount: '5天', status: 'pending', date: '2026-04-09' },
|
||||
{ id: 'APPR-002', type: 'reimburse', title: '李四差旅费报销', applicant: '李四', department: '技术部', amount: '¥2,580', status: 'pending', date: '2026-04-09' },
|
||||
{ id: 'APPR-003', type: 'purchase', title: '王五办公用品采购', applicant: '王五', department: '采购部', amount: '¥3,200', status: 'pending', date: '2026-04-08' },
|
||||
{ id: 'APPR-004', type: 'leave', title: '赵六病假申请', applicant: '赵六', department: '财务部', amount: '2天', status: 'approved', date: '2026-04-08' },
|
||||
{ id: 'APPR-005', type: 'overtime', title: '孙七加班申请', applicant: '孙七', department: '生产部', amount: '8小时', status: 'approved', date: '2026-04-07' },
|
||||
])
|
||||
|
||||
const typeMap: Record<string, { label: string; color: string }> = {
|
||||
leave: { label: '请假', color: 'blue' },
|
||||
reimburse: { label: '报销', color: 'orange' },
|
||||
purchase: { label: '采购', color: 'purple' },
|
||||
overtime: { label: '加班', color: 'cyan' },
|
||||
}
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: '待审批', color: 'processing' },
|
||||
approved: { label: '已通过', color: 'success' },
|
||||
rejected: { label: '已驳回', color: 'error' },
|
||||
}
|
||||
|
||||
const activeTab = ref('announcements')
|
||||
const addAnnouncementVisible = ref(false)
|
||||
const addAnnouncementForm = reactive({
|
||||
title: '',
|
||||
content: '',
|
||||
important: false,
|
||||
})
|
||||
|
||||
const approveVisible = ref(false)
|
||||
const selectedApproval = ref<typeof approvals.value[0] | null>(null)
|
||||
|
||||
function showApproveDetail(item: typeof approvals.value[0]) {
|
||||
selectedApproval.value = item
|
||||
approveVisible.value = true
|
||||
}
|
||||
|
||||
function handleApprove(status: string) {
|
||||
approveVisible.value = false
|
||||
message.success(status === 'approved' ? '已通过审批' : '已驳回申请')
|
||||
}
|
||||
|
||||
function handleAddAnnouncement() {
|
||||
addAnnouncementVisible.value = false
|
||||
message.success('公告发布成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="office-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">协同办公</h2>
|
||||
<p class="text-gray-500 mt-1">发布公告、处理审批、管理会议</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<a-button @click="addAnnouncementVisible = true">
|
||||
<template #icon><i class="fas fa-bullhorn mr-1"></i></template>
|
||||
发布公告
|
||||
</a-button>
|
||||
<a-button type="primary">
|
||||
<template #icon><i class="fas fa-plus mr-1"></i></template>
|
||||
新建审批
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-4 gap-6 mb-6">
|
||||
<div
|
||||
v-for="stat in officeStats"
|
||||
:key="stat.label"
|
||||
class="glass rounded-2xl p-6 card-hover cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
|
||||
:class="`bg-gradient-to-br ${stat.gradient}`"
|
||||
>
|
||||
<i :class="`fas ${stat.icon}`"></i>
|
||||
</div>
|
||||
<span
|
||||
class="text-sm font-medium"
|
||||
:class="stat.up ? 'text-green-500' : 'text-red-500'"
|
||||
>
|
||||
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
|
||||
{{ stat.change }}
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-gray-800 mb-1">{{ stat.value }}</h3>
|
||||
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab切换 -->
|
||||
<div class="glass rounded-2xl p-4 mb-6">
|
||||
<a-radio-group v-model:value="activeTab" button-style="solid">
|
||||
<a-radio-button value="announcements">
|
||||
<i class="fas fa-bullhorn mr-1"></i>公告通知
|
||||
</a-radio-button>
|
||||
<a-radio-button value="approvals">
|
||||
<i class="fas fa-tasks mr-1"></i>审批流程
|
||||
</a-radio-button>
|
||||
<a-radio-button value="meetings">
|
||||
<i class="fas fa-video mr-1"></i>会议管理
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 公告通知 -->
|
||||
<div v-show="activeTab === 'announcements'" class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-bullhorn text-red-500 mr-2"></i>
|
||||
最新公告
|
||||
</h3>
|
||||
<a-button type="link">全部公告</a-button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="notice in announcements"
|
||||
:key="notice.id"
|
||||
class="notice-item glass rounded-xl p-5 card-hover cursor-pointer"
|
||||
:class="notice.important ? 'border-l-4 border-red-500' : 'border-l-4 border-blue-500'"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<a-tag v-if="notice.important" color="red">重要</a-tag>
|
||||
<h4 class="font-bold text-gray-800">{{ notice.title }}</h4>
|
||||
</div>
|
||||
<p class="text-gray-500 text-sm mb-3 line-clamp-2">{{ notice.content }}</p>
|
||||
<div class="flex items-center gap-4 text-xs text-gray-400">
|
||||
<span><i class="fas fa-user mr-1"></i>{{ notice.author }}</span>
|
||||
<span><i class="fas fa-clock mr-1"></i>{{ notice.date }}</span>
|
||||
<span><i class="fas fa-eye mr-1"></i>{{ notice.views }} 阅读</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ml-4">
|
||||
<a-button type="link" size="small">查看</a-button>
|
||||
<a-button type="link" size="small">编辑</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批流程 -->
|
||||
<div v-show="activeTab === 'approvals'" class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-tasks text-orange-500 mr-2"></i>
|
||||
待审批列表
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:dataSource="approvals"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
rowKey="id"
|
||||
:scroll="{ x: 900 }"
|
||||
>
|
||||
<a-table-column title="类型" dataIndex="type" width="100" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="typeMap[text]?.color">{{ typeMap[text]?.label }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="标题" dataIndex="title" width="200">
|
||||
<template #default="{ text }">
|
||||
<span class="font-medium">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="申请人" dataIndex="applicant" width="100" />
|
||||
<a-table-column title="部门" dataIndex="department" width="100" />
|
||||
<a-table-column title="金额/时长" dataIndex="amount" width="100" align="right">
|
||||
<template #default="{ text }">
|
||||
<span class="font-medium text-orange-600">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="状态" dataIndex="status" width="100" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="statusMap[text]?.color">{{ statusMap[text]?.label }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="申请日期" dataIndex="date" width="120" />
|
||||
<a-table-column title="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ record }">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<a-button type="primary" size="small" ghost @click="showApproveDetail(record)">
|
||||
审批
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 会议管理 -->
|
||||
<div v-show="activeTab === 'meetings'" class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-video text-blue-500 mr-2"></i>
|
||||
会议预约
|
||||
</h3>
|
||||
<a-button type="primary">
|
||||
<template #icon><i class="fas fa-plus mr-1"></i></template>
|
||||
预约会议
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-empty description="暂无会议安排" />
|
||||
</div>
|
||||
|
||||
<!-- 发布公告弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="addAnnouncementVisible"
|
||||
title="发布公告"
|
||||
@ok="handleAddAnnouncement"
|
||||
ok-text="立即发布"
|
||||
cancel-text="取消"
|
||||
width="600px"
|
||||
>
|
||||
<a-form :model="addAnnouncementForm" layout="vertical">
|
||||
<a-form-item label="公告标题" required>
|
||||
<a-input v-model:value="addAnnouncementForm.title" placeholder="请输入公告标题" />
|
||||
</a-form-item>
|
||||
<a-form-item label="公告内容" required>
|
||||
<a-textarea v-model:value="addAnnouncementForm.content" :rows="5" placeholder="请输入公告内容" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="addAnnouncementForm.important">设为重要公告</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 审批详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="approveVisible"
|
||||
:title="`审批 - ${selectedApproval?.title}`"
|
||||
width="500px"
|
||||
:footer="selectedApproval?.status === 'pending' ? [
|
||||
h('a-button', { type: 'primary', onClick: () => handleApprove('approved') }, '批准'),
|
||||
h('a-button', { danger: true, onClick: () => handleApprove('rejected') }, '驳回'),
|
||||
h('a-button', { onClick: () => approveVisible = false }, '取消'),
|
||||
] : null"
|
||||
>
|
||||
<template v-if="selectedApproval">
|
||||
<a-descriptions :column="2" bordered size="small">
|
||||
<a-descriptions-item label="申请类型" :span="2">
|
||||
<a-tag :color="typeMap[selectedApproval.type]?.color">{{ typeMap[selectedApproval.type]?.label }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="申请人">{{ selectedApproval.applicant }}</a-descriptions-item>
|
||||
<a-descriptions-item label="部门">{{ selectedApproval.department }}</a-descriptions-item>
|
||||
<a-descriptions-item label="申请金额/时长">{{ selectedApproval.amount }}</a-descriptions-item>
|
||||
<a-descriptions-item label="申请日期">{{ selectedApproval.date }}</a-descriptions-item>
|
||||
<a-descriptions-item label="当前状态" :span="2">
|
||||
<a-tag :color="statusMap[selectedApproval.status]?.color">{{ statusMap[selectedApproval.status]?.label }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</template>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.office-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.notice-item {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.space-y-4 > * + * {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.ml-4 {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rounded-2xl {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.p-5 {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.gap-6 {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text-gray-800 {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.text-orange-600 {
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.border-l-4 {
|
||||
border-left-width: 4px;
|
||||
}
|
||||
|
||||
.border-red-500 {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.border-blue-500 {
|
||||
border-left-color: #3b82f6;
|
||||
}
|
||||
</style>
|
||||
328
app/pages/admin/members.vue
Normal file
328
app/pages/admin/members.vue
Normal file
@@ -0,0 +1,328 @@
|
||||
<template>
|
||||
<div class="members-page">
|
||||
<a-card :bordered="false">
|
||||
<template #title>成员管理</template>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<a-tabs v-model:activeKey="activeTab">
|
||||
<a-tab-pane key="users" tab="用户列表">
|
||||
<!-- 筛选栏 -->
|
||||
<div class="filter-bar">
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索用户名、邮箱、手机号..."
|
||||
style="width: 300px"
|
||||
allow-clear
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<a-select v-model:value="filterRole" placeholder="角色筛选" style="width: 140px" allow-clear>
|
||||
<a-select-option value="super_admin">超级管理员</a-select-option>
|
||||
<a-select-option value="admin">企业管理员</a-select-option>
|
||||
<a-select-option value="member">普通成员</a-select-option>
|
||||
<a-select-option value="developer">开发者</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="filterStatus" placeholder="状态筛选" style="width: 140px" allow-clear>
|
||||
<a-select-option value="active">正常</a-select-option>
|
||||
<a-select-option value="disabled">已禁用</a-select-option>
|
||||
</a-select>
|
||||
<a-button @click="resetFilter">重置</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="userColumns"
|
||||
:data-source="userData"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1100 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'user'">
|
||||
<div class="user-cell">
|
||||
<a-avatar :src="record.avatar" :size="36">
|
||||
{{ record.name[0] }}
|
||||
</a-avatar>
|
||||
<div>
|
||||
<p class="name-text">{{ record.name }}</p>
|
||||
<p class="sub-text">{{ record.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'role'">
|
||||
<a-tag :color="roleColor[record.role]">{{ roleMap[record.role] }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'enterprise'">
|
||||
{{ record.enterprise || '-' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-badge :status="record.status === 'active' ? 'success' : 'error'" :text="record.status === 'active' ? '正常' : '已禁用'" />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleViewUser(record)">详情</a-button>
|
||||
<a-button type="link" size="small" @click="handleEditUser(record)">编辑</a-button>
|
||||
<a-dropdown>
|
||||
<a-button type="link" size="small">更多</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="role">调整角色</a-menu-item>
|
||||
<a-menu-item key="disable">{{ record.status === 'active' ? '禁用账户' : '启用账户' }}</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="delete" style="color: #ff4d4f">删除</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="roles" tab="角色权限">
|
||||
<div class="roles-section">
|
||||
<div class="roles-header">
|
||||
<a-button type="primary" @click="roleModalVisible = true">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增角色
|
||||
</a-button>
|
||||
</div>
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :md="12" :xl="8" v-for="role in roles" :key="role.key">
|
||||
<a-card class="role-card" :bordered="false">
|
||||
<div class="role-card-header">
|
||||
<a-tag :color="role.color">{{ role.count }} 人</a-tag>
|
||||
<span class="role-name">{{ role.label }}</span>
|
||||
</div>
|
||||
<p class="role-desc">{{ role.desc }}</p>
|
||||
<div class="role-permissions">
|
||||
<a-tag v-for="p in role.permissions" :key="p" size="small">{{ p }}</a-tag>
|
||||
</div>
|
||||
<div class="role-actions">
|
||||
<a-button type="link" size="small" @click="handleEditRole(role)">编辑</a-button>
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
|
||||
<!-- 用户编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="userModalVisible"
|
||||
title="编辑用户"
|
||||
width="540px"
|
||||
@ok="saveUser"
|
||||
@cancel="userModalVisible = false"
|
||||
>
|
||||
<a-form :model="userForm" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form-item label="用户名">
|
||||
<a-input v-model:value="userForm.name" />
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号">
|
||||
<a-input v-model:value="userForm.phone" />
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱">
|
||||
<a-input v-model:value="userForm.email" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色">
|
||||
<a-select v-model:value="userForm.role">
|
||||
<a-select-option value="super_admin">超级管理员</a-select-option>
|
||||
<a-select-option value="admin">企业管理员</a-select-option>
|
||||
<a-select-option value="member">普通成员</a-select-option>
|
||||
<a-select-option value="developer">开发者</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select v-model:value="userForm.status">
|
||||
<a-select-option value="active">正常</a-select-option>
|
||||
<a-select-option value="disabled">已禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 角色编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="roleModalVisible"
|
||||
title="新增角色"
|
||||
width="540px"
|
||||
@ok="saveRole"
|
||||
@cancel="roleModalVisible = false"
|
||||
>
|
||||
<a-form :model="roleForm" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form-item label="角色名称" :rules="[{ required: true, message: '请输入角色名称' }]">
|
||||
<a-input v-model:value="roleForm.label" placeholder="如:财务管理员" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色描述">
|
||||
<a-textarea v-model:value="roleForm.desc" :rows="2" />
|
||||
</a-form-item>
|
||||
<a-form-item label="权限配置">
|
||||
<a-checkbox-group v-model:value="roleForm.permissions">
|
||||
<a-row>
|
||||
<a-col :span="12" v-for="perm in allPermissions" :key="perm.key">
|
||||
<a-checkbox :value="perm.key">{{ perm.label }}</a-checkbox>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
const activeTab = ref('users')
|
||||
const searchKeyword = ref('')
|
||||
const filterRole = ref<string | undefined>()
|
||||
const filterStatus = ref<string | undefined>()
|
||||
|
||||
const userModalVisible = ref(false)
|
||||
const roleModalVisible = ref(false)
|
||||
|
||||
const userForm = reactive({ id: null as number | null, name: '', phone: '', email: '', role: 'member', status: 'active' })
|
||||
const roleForm = reactive({ label: '', desc: '', permissions: [] as string[] })
|
||||
|
||||
const roleMap: Record<string, string> = {
|
||||
super_admin: '超级管理员',
|
||||
admin: '企业管理员',
|
||||
member: '普通成员',
|
||||
developer: '开发者',
|
||||
}
|
||||
|
||||
const roleColor: Record<string, string> = {
|
||||
super_admin: 'red',
|
||||
admin: 'purple',
|
||||
member: 'blue',
|
||||
developer: 'green',
|
||||
}
|
||||
|
||||
const userColumns = [
|
||||
{ title: '用户', key: 'user', width: 240 },
|
||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
|
||||
{ title: '角色', key: 'role', width: 110 },
|
||||
{ title: '所属企业', key: 'enterprise', width: 160 },
|
||||
{ title: '注册时间', dataIndex: 'createdAt', key: 'createdAt', width: 120 },
|
||||
{ title: '最后登录', dataIndex: 'lastLogin', key: 'lastLogin', width: 140 },
|
||||
{ title: '状态', key: 'status', width: 90 },
|
||||
{ title: '操作', key: 'actions', width: 180, fixed: 'right' },
|
||||
]
|
||||
|
||||
const userData = ref([
|
||||
{ id: 1, name: '李明', email: 'liming@example.com', phone: '138****8001', role: 'super_admin', enterprise: '平台', status: 'active', createdAt: '2025-01-01', lastLogin: '2026-04-08 10:23' },
|
||||
{ id: 2, name: '王芳', email: 'wangfang@example.com', phone: '138****8002', role: 'admin', enterprise: '腾云科技', status: 'active', createdAt: '2025-03-15', lastLogin: '2026-04-08 09:15' },
|
||||
{ id: 3, name: '张伟', email: 'zhangwei@example.com', phone: '138****8003', role: 'member', enterprise: '腾云科技', status: 'active', createdAt: '2025-06-20', lastLogin: '2026-04-07 18:30' },
|
||||
{ id: 4, name: '陈静', email: 'chenjing@example.com', phone: '138****8004', role: 'developer', enterprise: '华创数据', status: 'active', createdAt: '2025-09-10', lastLogin: '2026-04-08 11:00' },
|
||||
{ id: 5, name: '刘强', email: 'liuqiang@example.com', phone: '138****8005', role: 'member', enterprise: '华创数据', status: 'disabled', createdAt: '2025-11-05', lastLogin: '2026-03-01 14:00' },
|
||||
])
|
||||
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 5 })
|
||||
|
||||
const roles = [
|
||||
{ key: 'super_admin', label: '超级管理员', desc: '平台最高权限,可管理所有模块', count: 1, color: 'red', permissions: ['全部权限'] },
|
||||
{ key: 'admin', label: '企业管理员', desc: '管理本企业内的用户、配置、账单', count: 12, color: 'purple', permissions: ['用户管理', '账单查看', '应用管理'] },
|
||||
{ key: 'member', label: '普通成员', desc: '使用平台基础功能,无管理权限', count: 845, color: 'blue', permissions: ['功能使用'] },
|
||||
{ key: 'developer', label: '开发者', desc: '拥有开发者权限,可创建和管理应用', count: 342, color: 'green', permissions: ['API调用', '插件开发', '模板发布'] },
|
||||
]
|
||||
|
||||
const allPermissions = [
|
||||
{ key: 'user_manage', label: '用户管理' },
|
||||
{ key: 'app_manage', label: '应用管理' },
|
||||
{ key: 'finance_view', label: '账单查看' },
|
||||
{ key: 'finance_pay', label: '充值缴费' },
|
||||
{ key: 'developer_api', label: 'API 调用' },
|
||||
{ key: 'developer_plugin', label: '插件开发' },
|
||||
{ key: 'setting_base', label: '基础配置' },
|
||||
{ key: 'setting_advance', label: '高级配置' },
|
||||
]
|
||||
|
||||
const handleSearch = () => message.info('搜索:' + searchKeyword.value)
|
||||
const resetFilter = () => { searchKeyword.value = ''; filterRole.value = undefined; filterStatus.value = undefined }
|
||||
const handleViewUser = (u: any) => message.info('查看用户:' + u.name)
|
||||
const handleEditUser = (u: any) => { Object.assign(userForm, u); userModalVisible.value = true }
|
||||
const handleEditRole = (r: any) => { Object.assign(roleForm, r); roleModalVisible.value = true }
|
||||
const saveUser = () => { userModalVisible.value = false; message.success('保存成功') }
|
||||
const saveRole = () => { roleModalVisible.value = false; message.success('保存成功') }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.user-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.name-text {
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.roles-section {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.roles-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.role-card {
|
||||
border-radius: 10px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.role-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.role-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.role-desc {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.role-permissions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.role-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
||||
96
app/pages/admin/members/roles.vue
Normal file
96
app/pages/admin/members/roles.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="roles-page">
|
||||
<a-card :bordered="false" title="角色权限管理">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="modalVisible = true">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增角色
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :md="12" :xl="8" v-for="role in roles" :key="role.key">
|
||||
<a-card class="role-card" :bordered="false">
|
||||
<div class="role-header">
|
||||
<span class="role-name">{{ role.label }}</span>
|
||||
<a-tag :color="role.color">{{ role.count }} 人</a-tag>
|
||||
</div>
|
||||
<p class="role-desc">{{ role.desc }}</p>
|
||||
<div class="role-perms">
|
||||
<a-tag v-for="p in role.permissions" :key="p" size="small">{{ p }}</a-tag>
|
||||
</div>
|
||||
<div class="role-actions">
|
||||
<a-button type="link" size="small" @click="handleEdit(role)">编辑</a-button>
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<a-modal v-model:open="modalVisible" :title="editingRole ? '编辑角色' : '新增角色'" width="540px" @ok="handleSubmit">
|
||||
<a-form :model="form" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
|
||||
<a-form-item label="角色名称">
|
||||
<a-input v-model:value="form.label" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色描述">
|
||||
<a-textarea v-model:value="form.desc" :rows="2" />
|
||||
</a-form-item>
|
||||
<a-form-item label="权限配置">
|
||||
<a-checkbox-group v-model:value="form.permissions">
|
||||
<a-row>
|
||||
<a-col :span="12" v-for="perm in allPermissions" :key="perm.key">
|
||||
<a-checkbox :value="perm.key">{{ perm.label }}</a-checkbox>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-checkbox-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const editingRole = ref<any>(null)
|
||||
const form = reactive({ label: '', desc: '', permissions: [] as string[] })
|
||||
|
||||
const roles = [
|
||||
{ key: 'super_admin', label: '超级管理员', desc: '平台最高权限,可管理所有模块', count: 1, color: 'red', permissions: ['全部权限'] },
|
||||
{ key: 'admin', label: '企业管理员', desc: '管理本企业内的用户、配置、账单', count: 12, color: 'purple', permissions: ['用户管理', '账单查看', '应用管理'] },
|
||||
{ key: 'member', label: '普通成员', desc: '使用平台基础功能,无管理权限', count: 845, color: 'blue', permissions: ['功能使用'] },
|
||||
{ key: 'developer', label: '开发者', desc: '拥有开发者权限,可创建和管理应用', count: 342, color: 'green', permissions: ['API调用', '插件开发', '模板发布'] },
|
||||
]
|
||||
|
||||
const allPermissions = [
|
||||
{ key: 'user_manage', label: '用户管理' },
|
||||
{ key: 'app_manage', label: '应用管理' },
|
||||
{ key: 'finance_view', label: '账单查看' },
|
||||
{ key: 'finance_pay', label: '充值缴费' },
|
||||
{ key: 'developer_api', label: 'API 调用' },
|
||||
{ key: 'developer_plugin', label: '插件开发' },
|
||||
{ key: 'setting_base', label: '基础配置' },
|
||||
{ key: 'setting_advance', label: '高级配置' },
|
||||
]
|
||||
|
||||
const handleEdit = (r: any) => { editingRole.value = r; Object.assign(form, r); modalVisible.value = true }
|
||||
const handleSubmit = () => { modalVisible.value = false; message.success(editingRole.value ? '编辑成功' : '新增成功') }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-card {
|
||||
border-radius: 10px;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.role-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.08); }
|
||||
.role-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
||||
.role-name { font-weight: 600; font-size: 15px; color: #111827; }
|
||||
.role-desc { font-size: 13px; color: #6b7280; margin: 0 0 10px; }
|
||||
.role-perms { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; }
|
||||
.role-actions { display: flex; gap: 4px; }
|
||||
</style>
|
||||
352
app/pages/admin/product/design.vue
Normal file
352
app/pages/admin/product/design.vue
Normal file
@@ -0,0 +1,352 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'product-design'
|
||||
|
||||
// 产品数据
|
||||
const products = ref([
|
||||
{ id: 'PD-001', name: '精密轴承组件 A型', code: 'BA-A-001', category: '轴承类', version: 'V2.1', status: '设计中', progress: 75, designer: '张工', updateTime: '2026-04-08' },
|
||||
{ id: 'PD-002', name: '液压缸体 B型', code: 'HA-B-002', category: '液压类', version: 'V1.5', status: '评审中', progress: 90, designer: '李工', updateTime: '2026-04-07' },
|
||||
{ id: 'PD-003', name: '传动齿轮组 C型', code: 'GA-C-003', category: '传动类', version: 'V3.0', status: '已发布', progress: 100, designer: '王工', updateTime: '2026-04-01' },
|
||||
{ id: 'PD-004', name: '密封圈组件 D型', code: 'SA-D-004', category: '密封类', version: 'V1.2', status: '设计中', progress: 45, designer: '赵工', updateTime: '2026-04-09' },
|
||||
{ id: 'PD-005', name: '弹簧组件 E型', code: 'SA-E-005', category: '弹簧类', version: 'V2.0', status: '已发布', progress: 100, designer: '张工', updateTime: '2026-03-28' },
|
||||
])
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
'设计中': 'processing',
|
||||
'评审中': 'warning',
|
||||
'已发布': 'success',
|
||||
}
|
||||
|
||||
// 设计任务
|
||||
const designTasks = ref([
|
||||
{ id: 1, task: '优化轴承组件公差设计', assignee: '张工', deadline: '2026-04-15', priority: 'high' },
|
||||
{ id: 2, task: '完成缸体3D建模', assignee: '李工', deadline: '2026-04-12', priority: 'medium' },
|
||||
{ id: 3, task: '齿轮强度校核报告', assignee: '王工', deadline: '2026-04-10', priority: 'high' },
|
||||
{ id: 4, task: '密封圈材料选型', assignee: '赵工', deadline: '2026-04-18', priority: 'low' },
|
||||
])
|
||||
|
||||
const priorityMap: Record<string, string> = {
|
||||
high: 'error',
|
||||
medium: 'warning',
|
||||
low: 'default',
|
||||
}
|
||||
|
||||
// BOM 清单
|
||||
const bomItems = ref([
|
||||
{ no: 1, code: 'BA-A-001-01', name: '外圈', material: 'GCr15', qty: 2, unit: '件', supplier: '洛阳轴承' },
|
||||
{ no: 2, code: 'BA-A-001-02', name: '内圈', material: 'GCr15', qty: 2, unit: '件', supplier: '洛阳轴承' },
|
||||
{ no: 3, code: 'BA-A-001-03', name: '滚动体', material: 'Si3N4', qty: 12, unit: '件', supplier: '日本精工' },
|
||||
{ no: 4, code: 'BA-A-001-04', name: '保持架', material: 'PA66', qty: 1, unit: '件', supplier: '本地供应商' },
|
||||
])
|
||||
|
||||
const selectedProduct = ref<any>(null)
|
||||
const showBomModal = ref(false)
|
||||
|
||||
const viewBom = (product: any) => {
|
||||
selectedProduct.value = product
|
||||
showBomModal.value = true
|
||||
}
|
||||
|
||||
const modal = reactive({ visible: false, title: '', record: null as any })
|
||||
|
||||
const openModal = (title: string, record?: any) => {
|
||||
modal.title = title
|
||||
modal.record = record || {}
|
||||
modal.visible = true
|
||||
}
|
||||
|
||||
const statusFilter = ref<string[]>([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">产品设计</h2>
|
||||
<a-button type="primary" @click="openModal('新增产品')">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建产品
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[20, 20]">
|
||||
<a-col :xs="24" :xl="16">
|
||||
<!-- 产品列表 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">产品设计列表</span>
|
||||
<a-select
|
||||
v-model:value="statusFilter"
|
||||
mode="multiple"
|
||||
placeholder="筛选状态"
|
||||
style="width: 200px"
|
||||
allowClear
|
||||
>
|
||||
<a-select-option value="设计中">设计中</a-select-option>
|
||||
<a-select-option value="评审中">评审中</a-select-option>
|
||||
<a-select-option value="已发布">已发布</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<a-table
|
||||
:dataSource="products"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
size="small"
|
||||
rowKey="id"
|
||||
>
|
||||
<a-table-column title="产品编号" dataIndex="id" width="100" />
|
||||
<a-table-column title="产品名称" dataIndex="name">
|
||||
<template #default="{ record }">
|
||||
<a @click="viewBom(record)" class="link">{{ record.name }}</a>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="产品编码" dataIndex="code" width="120" />
|
||||
<a-table-column title="类别" dataIndex="category" width="100" />
|
||||
<a-table-column title="版本" dataIndex="version" width="80" />
|
||||
<a-table-column title="状态" dataIndex="status" width="100" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="statusMap[text]">{{ text }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="设计进度" dataIndex="progress" width="140" align="center">
|
||||
<template #default="{ record }">
|
||||
<a-progress :percent="record.progress" size="small" :showInfo="false" />
|
||||
<span class="text-xs text-gray-400 ml-2">{{ record.progress }}%</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="负责人" dataIndex="designer" width="80" align="center" />
|
||||
<a-table-column title="更新时间" dataIndex="updateTime" width="110" />
|
||||
<a-table-column title="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ record }">
|
||||
<a-space>
|
||||
<a @click="openModal('编辑产品', record)">编辑</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm title="确认删除?" ok-text="确认" cancel-text="取消">
|
||||
<a class="danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :xl="8">
|
||||
<!-- 设计任务 -->
|
||||
<div class="card mb-6">
|
||||
<div class="card-header">
|
||||
<span class="card-title">设计任务</span>
|
||||
<a-button type="link" size="small">全部</a-button>
|
||||
</div>
|
||||
<div class="task-list">
|
||||
<div v-for="task in designTasks" :key="task.id" class="task-item">
|
||||
<div class="task-header">
|
||||
<span class="task-name">{{ task.task }}</span>
|
||||
<a-tag :color="priorityMap[task.priority]" size="small">
|
||||
{{ task.priority === 'high' ? '高' : task.priority === 'medium' ? '中' : '低' }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span><UserOutlined /> {{ task.assignee }}</span>
|
||||
<span><CalendarOutlined /> {{ task.deadline }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设计统计 -->
|
||||
<div class="card">
|
||||
<div class="card-title">设计统计</div>
|
||||
<div class="stat-row">
|
||||
<div class="stat-item">
|
||||
<span class="stat-num">12</span>
|
||||
<span class="stat-lbl">设计中</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-num">5</span>
|
||||
<span class="stat-lbl">评审中</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-num">28</span>
|
||||
<span class="stat-lbl">已发布</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- BOM 弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="showBomModal"
|
||||
:title="`BOM清单 - ${selectedProduct?.name || ''}`"
|
||||
width="700px"
|
||||
:footer="null"
|
||||
>
|
||||
<a-table
|
||||
:dataSource="bomItems"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
rowKey="no"
|
||||
>
|
||||
<a-table-column title="序号" dataIndex="no" width="60" align="center" />
|
||||
<a-table-column title="物料编码" dataIndex="code" width="130" />
|
||||
<a-table-column title="物料名称" dataIndex="name" />
|
||||
<a-table-column title="材质" dataIndex="material" width="100" />
|
||||
<a-table-column title="用量" dataIndex="qty" width="60" align="center" />
|
||||
<a-table-column title="单位" dataIndex="unit" width="60" align="center" />
|
||||
<a-table-column title="供应商" dataIndex="supplier" />
|
||||
</a-table>
|
||||
</a-modal>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modal.visible"
|
||||
:title="modal.title"
|
||||
width="500px"
|
||||
@ok="modal.visible = false"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="产品名称">
|
||||
<a-input v-model:value="modal.record.name" placeholder="请输入产品名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="产品编码">
|
||||
<a-input v-model:value="modal.record.code" placeholder="请输入产品编码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="产品类别">
|
||||
<a-select v-model:value="modal.record.category" placeholder="请选择类别">
|
||||
<a-select-option value="轴承类">轴承类</a-select-option>
|
||||
<a-select-option value="液压类">液压类</a-select-option>
|
||||
<a-select-option value="传动类">传动类</a-select-option>
|
||||
<a-select-option value="密封类">密封类</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="负责人">
|
||||
<a-select v-model:value="modal.record.designer" placeholder="请选择负责人">
|
||||
<a-select-option value="张工">张工</a-select-option>
|
||||
<a-select-option value="李工">李工</a-select-option>
|
||||
<a-select-option value="王工">王工</a-select-option>
|
||||
<a-select-option value="赵工">赵工</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
padding: 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.task-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.task-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-num {
|
||||
display: block;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #4f46e5;
|
||||
}
|
||||
|
||||
.stat-lbl {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #4f46e5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
169
app/pages/admin/product/marketing.vue
Normal file
169
app/pages/admin/product/marketing.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'product-marketing'
|
||||
|
||||
// 销售数据
|
||||
const salesData = ref([
|
||||
{ month: '1月', orders: 245, revenue: 128.5, target: 120 },
|
||||
{ month: '2月', orders: 312, revenue: 156.8, target: 130 },
|
||||
{ month: '3月', orders: 278, revenue: 142.3, target: 135 },
|
||||
])
|
||||
|
||||
const stats = ref([
|
||||
{ label: '本月订单', value: 156, unit: '单', change: '+12%', icon: '📋', color: '#6366f1' },
|
||||
{ label: '本月营收', value: 86.5, unit: '万', change: '+8%', icon: '💰', color: '#10b981' },
|
||||
{ label: '新增客户', value: 23, unit: '家', change: '+15%', icon: '🏢', color: '#f59e0b' },
|
||||
{ label: '平均单价', value: 5546, unit: '元', change: '-2%', icon: '💎', color: '#3b82f6' },
|
||||
])
|
||||
|
||||
// 客户列表
|
||||
const customers = ref([
|
||||
{ id: 'C001', name: '比亚迪股份有限公司', industry: '汽车制造', contact: '李经理', phone: '0755-89888888', orderCount: 45, amount: 280.5, status: 'VIP' },
|
||||
{ id: 'C002', name: '宁德时代新能源', industry: '新能源', contact: '王经理', phone: '0591-87654321', orderCount: 32, amount: 198.2, status: 'VIP' },
|
||||
{ id: 'C003', name: '华为技术有限公司', industry: '电子通信', contact: '张经理', phone: '0755-28780808', orderCount: 28, amount: 156.8, status: '重点' },
|
||||
{ id: 'C004', name: '富士康科技集团', industry: '电子制造', contact: '刘经理', phone: '0755-28129999', orderCount: 21, amount: 125.3, status: '普通' },
|
||||
{ id: 'C005', name: '美的集团', industry: '家电制造', contact: '陈经理', phone: '0757-26608888', orderCount: 18, amount: 98.6, status: '重点' },
|
||||
])
|
||||
|
||||
// 跟进记录
|
||||
const followRecords = ref([
|
||||
{ id: 1, customer: '比亚迪股份有限公司', content: '拜访客户,沟通轴承采购需求,预计月订单量增加30%', contact: '李经理', nextDate: '2026-04-15', status: '跟进中' },
|
||||
{ id: 2, customer: '宁德时代新能源', content: '技术方案对接完成,等待客户内部评审', contact: '王经理', nextDate: '2026-04-12', status: '待联系' },
|
||||
{ id: 3, customer: '华为技术有限公司', content: '完成样品交付,客户反馈良好', contact: '张经理', nextDate: '-', status: '已完成' },
|
||||
])
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
'VIP': 'purple',
|
||||
'重点': 'blue',
|
||||
'普通': 'default',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">营销管理</h2>
|
||||
<a-space>
|
||||
<a-button @click="() => {}">导出数据</a-button>
|
||||
<a-button type="primary" @click="() => {}">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建客户
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="12" :sm="6" v-for="stat in stats" :key="stat.label">
|
||||
<div class="stat-card" :style="{ '--accent': stat.color }">
|
||||
<div class="stat-icon">{{ stat.icon }}</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">{{ stat.value }}<span class="stat-unit">{{ stat.unit }}</span></div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
<div :class="['stat-change', stat.change.startsWith('+') ? 'up' : 'down']">{{ stat.change }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="[20, 20]">
|
||||
<a-col :xs="24" :xl="16">
|
||||
<!-- 客户列表 -->
|
||||
<div class="card">
|
||||
<div class="card-title">客户列表</div>
|
||||
<a-table :dataSource="customers" :pagination="{ pageSize: 8 }" size="small" rowKey="id">
|
||||
<a-table-column title="客户编码" dataIndex="id" width="90" />
|
||||
<a-table-column title="客户名称" dataIndex="name" />
|
||||
<a-table-column title="行业" dataIndex="industry" width="100" />
|
||||
<a-table-column title="联系人" dataIndex="contact" width="80" />
|
||||
<a-table-column title="订单数" dataIndex="orderCount" width="80" align="center" />
|
||||
<a-table-column title="累计金额(万)" dataIndex="amount" width="110" align="right" />
|
||||
<a-table-column title="等级" dataIndex="status" width="80" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="statusColor[text]">{{ text }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" width="100" align="center">
|
||||
<template #default>
|
||||
<a-space>
|
||||
<a>详情</a>
|
||||
<a-divider type="vertical" />
|
||||
<a>跟进</a>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :xl="8">
|
||||
<!-- 跟进记录 -->
|
||||
<div class="card">
|
||||
<div class="card-title">跟进记录</div>
|
||||
<div class="follow-list">
|
||||
<div v-for="record in followRecords" :key="record.id" class="follow-item">
|
||||
<div class="follow-header">
|
||||
<span class="follow-customer">{{ record.customer }}</span>
|
||||
<a-tag size="small" :color="record.status === '已完成' ? 'success' : record.status === '跟进中' ? 'processing' : 'warning'">
|
||||
{{ record.status }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="follow-content">{{ record.content }}</div>
|
||||
<div class="follow-meta">
|
||||
<span><UserOutlined /> {{ record.contact }}</span>
|
||||
<span v-if="record.nextDate !== '-'"><CalendarOutlined /> 下次: {{ record.nextDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||
border: 1px solid #f0f0f0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.stat-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 4px; height: 100%;
|
||||
background: var(--accent);
|
||||
}
|
||||
.stat-icon { font-size: 28px; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; color: #1f2937; }
|
||||
.stat-unit { font-size: 12px; color: #9ca3af; margin-left: 2px; }
|
||||
.stat-label { font-size: 12px; color: #9ca3af; margin: 2px 0; }
|
||||
.stat-change { font-size: 12px; }
|
||||
.stat-change.up { color: #10b981; }
|
||||
.stat-change.down { color: #ef4444; }
|
||||
|
||||
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
|
||||
.card-title { font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 16px; }
|
||||
|
||||
.follow-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.follow-item { padding: 12px; background: #fafafa; border-radius: 8px; }
|
||||
.follow-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
||||
.follow-customer { font-size: 13px; font-weight: 600; color: #374151; }
|
||||
.follow-content { font-size: 12px; color: #6b7280; margin-bottom: 8px; line-height: 1.5; }
|
||||
.follow-meta { display: flex; gap: 16px; font-size: 11px; color: #9ca3af; }
|
||||
.follow-meta span { display: flex; align-items: center; gap: 4px; }
|
||||
|
||||
.mb-6 { margin-bottom: 20px; }
|
||||
</style>
|
||||
127
app/pages/admin/product/service.vue
Normal file
127
app/pages/admin/product/service.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'product-service'
|
||||
|
||||
const activeTabKey = ref('tickets')
|
||||
|
||||
const tabs = [
|
||||
{ key: 'tickets', label: '服务工单' },
|
||||
{ key: 'feedback', label: '客户反馈' },
|
||||
{ key: 'warranty', label: '质保管理' },
|
||||
]
|
||||
|
||||
// 服务工单
|
||||
const tickets = ref([
|
||||
{ id: 'ST-2026040901', customer: '比亚迪股份有限公司', product: '精密轴承组件 A型', type: '技术支持', status: '处理中', assignee: '张工', createTime: '2026-04-09 10:30', urgent: 'high' },
|
||||
{ id: 'ST-2026040802', customer: '宁德时代新能源', product: '液压缸体 B型', type: '故障报修', status: '待派单', assignee: '-', createTime: '2026-04-08 15:20', urgent: 'medium' },
|
||||
{ id: 'ST-2026040801', customer: '华为技术有限公司', product: '传动齿轮组 C型', type: '咨询', status: '已完成', assignee: '李工', createTime: '2026-04-08 09:15', urgent: 'low' },
|
||||
{ id: 'ST-2026040703', customer: '美的集团', product: '密封圈组件 D型', type: '技术支持', status: '已完成', assignee: '王工', createTime: '2026-04-07 14:00', urgent: 'medium' },
|
||||
])
|
||||
|
||||
const statusColor: Record<string, string> = { '处理中': 'processing', '待派单': 'warning', '已完成': 'success' }
|
||||
const urgentColor: Record<string, string> = { 'high': 'error', 'medium': 'warning', 'low': 'default' }
|
||||
const urgentLabel: Record<string, string> = { 'high': '紧急', 'medium': '普通', 'low': '低' }
|
||||
|
||||
// 客户反馈
|
||||
const feedback = ref([
|
||||
{ id: 'FB-001', customer: '比亚迪股份有限公司', content: '产品性能稳定,交付及时,服务态度好', rating: 5, createTime: '2026-04-08' },
|
||||
{ id: 'FB-002', customer: '宁德时代新能源', content: '希望增加技术培训频次', rating: 4, createTime: '2026-04-07' },
|
||||
{ id: 'FB-003', customer: '华为技术有限公司', content: '产品质量可靠,但包装可以改进', rating: 4, createTime: '2026-04-06' },
|
||||
{ id: 'FB-004', customer: '富士康科技集团', content: '技术响应速度需要提升', rating: 3, createTime: '2026-04-05' },
|
||||
])
|
||||
|
||||
// 质保列表
|
||||
const warranty = ref([
|
||||
{ id: 'WA-001', product: '精密轴承组件 A型', batch: 'B-20260301-01', quantity: 500, warrantyPeriod: '2年', expireDate: '2028-03-01', status: '有效' },
|
||||
{ id: 'WA-002', product: '液压缸体 B型', batch: 'B-20260215-02', quantity: 200, warrantyPeriod: '1年', expireDate: '2027-02-15', status: '有效' },
|
||||
{ id: 'WA-003', product: '传动齿轮组 C型', batch: 'B-20251201-03', quantity: 1000, warrantyPeriod: '2年', expireDate: '2027-12-01', status: '即将到期' },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">售后服务</h2>
|
||||
<a-button type="primary">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建工单
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-tabs v-model:activeKey="activeTabKey">
|
||||
<a-tab-pane key="tickets" tab="服务工单">
|
||||
<div class="card">
|
||||
<a-table :dataSource="tickets" :pagination="{ pageSize: 10 }" size="small" rowKey="id">
|
||||
<a-table-column title="工单编号" dataIndex="id" width="150" />
|
||||
<a-table-column title="客户" dataIndex="customer" />
|
||||
<a-table-column title="产品" dataIndex="product" />
|
||||
<a-table-column title="类型" dataIndex="type" width="100" align="center" />
|
||||
<a-table-column title="紧急度" dataIndex="urgent" width="90" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="urgentColor[text]">{{ urgentLabel[text] }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="状态" dataIndex="status" width="100" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-badge :status="statusColor[text] === 'processing' ? 'processing' : statusColor[text] === 'success' ? 'success' : 'warning'" :text="text" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="处理人" dataIndex="assignee" width="80" align="center" />
|
||||
<a-table-column title="创建时间" dataIndex="createTime" width="150" />
|
||||
<a-table-column title="操作" width="100" align="center">
|
||||
<template #default>
|
||||
<a>详情</a>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="feedback" tab="客户反馈">
|
||||
<div class="card">
|
||||
<a-table :dataSource="feedback" :pagination="{ pageSize: 10 }" size="small" rowKey="id">
|
||||
<a-table-column title="反馈编号" dataIndex="id" width="100" />
|
||||
<a-table-column title="客户" dataIndex="customer" />
|
||||
<a-table-column title="反馈内容" dataIndex="content" />
|
||||
<a-table-column title="评分" dataIndex="rating" width="120" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-rate :value="text" disabled :tooltips="['很差', '较差', '一般', '满意', '非常满意']" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="时间" dataIndex="createTime" width="120" />
|
||||
</a-table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="warranty" tab="质保管理">
|
||||
<div class="card">
|
||||
<a-table :dataSource="warranty" :pagination="false" size="small" rowKey="id">
|
||||
<a-table-column title="产品名称" dataIndex="product" />
|
||||
<a-table-column title="批次号" dataIndex="batch" width="160" />
|
||||
<a-table-column title="数量" dataIndex="quantity" width="80" align="center" />
|
||||
<a-table-column title="质保期" dataIndex="warrantyPeriod" width="90" align="center" />
|
||||
<a-table-column title="到期日期" dataIndex="expireDate" width="120" />
|
||||
<a-table-column title="状态" dataIndex="status" width="100" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="text === '有效' ? 'success' : 'warning'">{{ text }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" width="100" align="center">
|
||||
<template #default>
|
||||
<a>详情</a>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
|
||||
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
|
||||
</style>
|
||||
113
app/pages/admin/production/control.vue
Normal file
113
app/pages/admin/production/control.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'production-control'
|
||||
|
||||
const realTimeData = ref([
|
||||
{ id: 'WO2026040901', product: '精密轴承组件 A型', line: '产线1', planQty: 500, doneQty: 325, passQty: 318, passRate: 97.8, status: '生产中' },
|
||||
{ id: 'WO2026040703', product: '密封圈组件 D型', line: '产线3', planQty: 3000, doneQty: 2100, passQty: 2058, passRate: 98.0, status: '生产中' },
|
||||
])
|
||||
|
||||
const alerts = ref([
|
||||
{ id: 1, type: 'warning', msg: '产线1:主轴温度偏高,当前78°C', time: '10:30' },
|
||||
{ id: 2, type: 'info', msg: 'WO2026040901 完成500件,当前进度65%', time: '10:15' },
|
||||
{ id: 3, type: 'error', msg: '产线2:刀具寿命预警,请及时更换', time: '09:45' },
|
||||
])
|
||||
|
||||
const activeTab2 = ref('realtime')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">生产管控</h2>
|
||||
<a-space>
|
||||
<a-button @click="() => {}">导出报表</a-button>
|
||||
<a-button type="primary">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建工单
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-tabs v-model:activeKey="activeTab2">
|
||||
<a-tab-pane key="realtime" tab="实时监控">
|
||||
<a-row :gutter="[20, 20]">
|
||||
<a-col :xs="24" :xl="16">
|
||||
<div class="card">
|
||||
<div class="card-title">实时生产进度</div>
|
||||
<a-table :dataSource="realTimeData" :pagination="false" size="small" rowKey="id">
|
||||
<a-table-column title="工单编号" dataIndex="id" width="150" />
|
||||
<a-table-column title="产品" dataIndex="product" />
|
||||
<a-table-column title="产线" dataIndex="line" width="80" align="center" />
|
||||
<a-table-column title="计划数量" dataIndex="planQty" width="100" align="center" />
|
||||
<a-table-column title="完成数量" dataIndex="doneQty" width="100" align="center">
|
||||
<template #default="{ record }">
|
||||
<span class="num-highlight">{{ record.doneQty }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="良品数量" dataIndex="passQty" width="100" align="center" />
|
||||
<a-table-column title="良品率" dataIndex="passRate" width="90" align="center">
|
||||
<template #default="{ text }">
|
||||
<span :class="text >= 97 ? 'rate-good' : 'rate-warn'">{{ text }}%</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="进度" dataIndex="doneQty" width="150" align="center">
|
||||
<template #default="{ record }">
|
||||
<a-progress :percent="Math.round(record.doneQty / record.planQty * 100)" size="small" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="状态" dataIndex="status" width="100" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-badge status="processing" :text="text" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :xl="8">
|
||||
<div class="card">
|
||||
<div class="card-title">实时告警</div>
|
||||
<div class="alert-list">
|
||||
<div v-for="alert in alerts" :key="alert.id" class="alert-item" :class="alert.type">
|
||||
<span class="alert-icon">{{ alert.type === 'error' ? '🔴' : alert.type === 'warning' ? '🟡' : '🔵' }}</span>
|
||||
<div class="alert-body">
|
||||
<div class="alert-msg">{{ alert.msg }}</div>
|
||||
<div class="alert-time">{{ alert.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="history" tab="历史记录">
|
||||
<div class="card">
|
||||
<a-empty description="历史记录列表" />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
|
||||
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
|
||||
.card-title { font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 16px; }
|
||||
|
||||
.num-highlight { font-weight: 700; color: #4f46e5; }
|
||||
.rate-good { color: #10b981; font-weight: 600; }
|
||||
.rate-warn { color: #f59e0b; font-weight: 600; }
|
||||
|
||||
.alert-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.alert-item { display: flex; align-items: flex-start; gap: 10px; padding: 12px; border-radius: 8px; }
|
||||
.alert-item.error { background: #fef2f2; }
|
||||
.alert-item.warning { background: #fffbeb; }
|
||||
.alert-item.info { background: #eff6ff; }
|
||||
.alert-icon { font-size: 14px; }
|
||||
.alert-msg { font-size: 13px; color: #374151; }
|
||||
.alert-time { font-size: 11px; color: #9ca3af; margin-top: 4px; }
|
||||
</style>
|
||||
111
app/pages/admin/production/energy.vue
Normal file
111
app/pages/admin/production/energy.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'production-energy'
|
||||
|
||||
const stats = ref([
|
||||
{ label: '今日用电', value: 2856, unit: 'kWh', cost: '¥2,142', icon: '⚡', color: '#f59e0b' },
|
||||
{ label: '今日用水', value: 128, unit: 'm³', cost: '¥384', icon: '💧', color: '#3b82f6' },
|
||||
{ label: '今日用气', value: 560, unit: 'm³', cost: '¥840', icon: '🌬️', color: '#6366f1' },
|
||||
{ label: '碳排放', value: 1.8, unit: '吨', icon: '🌱', color: '#10b981' },
|
||||
])
|
||||
|
||||
const energyUsage = ref([
|
||||
{ name: 'CNC-01', electric: 450, water: 8, gas: 120, cost: 337.5 },
|
||||
{ name: 'CNC-02', electric: 420, water: 7, gas: 115, cost: 315.0 },
|
||||
{ name: 'CNC-03', electric: 0, water: 5, gas: 0, cost: 0 },
|
||||
{ name: 'MILL-01', electric: 0, water: 3, gas: 0, cost: 0 },
|
||||
{ name: 'MILL-02', electric: 380, water: 6, gas: 100, cost: 285.0 },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">能耗管理</h2>
|
||||
<a-space>
|
||||
<a-select value="2026-04" style="width: 120px">
|
||||
<a-select-option value="2026-04">2026年4月</a-select-option>
|
||||
<a-select-option value="2026-03">2026年3月</a-select-option>
|
||||
</a-select>
|
||||
<a-button @click="() => {}">导出报表</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="12" :sm="6" v-for="stat in stats" :key="stat.label">
|
||||
<div class="stat-card" :style="{ '--c': stat.color }">
|
||||
<div class="stat-icon">{{ stat.icon }}</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">{{ stat.value }}<span class="stat-unit">{{ stat.unit }}</span></div>
|
||||
<div class="stat-sub">{{ stat.cost }}</div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">设备能耗明细</div>
|
||||
<a-table :dataSource="energyUsage" :pagination="false" size="small" rowKey="name">
|
||||
<a-table-column title="设备" dataIndex="name" width="120" />
|
||||
<a-table-column title="用电(kWh)" dataIndex="electric" width="120" align="center">
|
||||
<template #default="{ text }">
|
||||
<span :class="text > 0 ? 'val' : 'dim'">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="用水(m³)" dataIndex="water" width="120" align="center">
|
||||
<template #default="{ text }">
|
||||
<span :class="text > 0 ? 'val' : 'dim'">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="用气(m³)" dataIndex="gas" width="120" align="center">
|
||||
<template #default="{ text }">
|
||||
<span :class="text > 0 ? 'val' : 'dim'">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="能耗成本(元)" dataIndex="cost" width="130" align="right">
|
||||
<template #default="{ text }">
|
||||
<span :class="text > 0 ? 'val' : 'dim'">¥{{ text.toFixed(1) }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="占比" dataIndex="cost" align="center">
|
||||
<template #default="{ record, text }">
|
||||
<a-progress
|
||||
:percent="Math.round(text / energyUsage.reduce((a, b) => a + b.cost, 0) * 100)"
|
||||
size="small"
|
||||
:showInfo="false"
|
||||
/>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
<div class="total-row">
|
||||
<span>合计</span>
|
||||
<span>2856 kWh</span>
|
||||
<span>128 m³</span>
|
||||
<span>560 m³</span>
|
||||
<span>¥3,366.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
|
||||
.stat-card { background: white; border-radius: 12px; padding: 16px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
|
||||
.stat-icon { font-size: 28px; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; color: #1f2937; }
|
||||
.stat-unit { font-size: 12px; color: #9ca3af; margin-left: 2px; }
|
||||
.stat-sub { font-size: 13px; color: #6b7280; }
|
||||
.stat-label { font-size: 12px; color: #9ca3af; margin-top: 2px; }
|
||||
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
|
||||
.card-title { font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 16px; }
|
||||
.val { font-weight: 600; color: #374151; }
|
||||
.dim { color: #d1d5db; }
|
||||
.total-row { display: flex; justify-content: space-between; padding: 12px 8px; border-top: 1px solid #f0f0f0; font-weight: 600; font-size: 13px; color: #374151; }
|
||||
.total-row span:nth-child(2), .total-row span:nth-child(3), .total-row span:nth-child(4) { width: 120px; text-align: center; }
|
||||
.total-row span:nth-child(5) { width: 130px; text-align: right; }
|
||||
.mb-6 { margin-bottom: 20px; }
|
||||
</style>
|
||||
617
app/pages/admin/production/equipment.vue
Normal file
617
app/pages/admin/production/equipment.vue
Normal file
@@ -0,0 +1,617 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
// 设备统计数据
|
||||
const equipmentStats = ref([
|
||||
{ label: '设备总数', value: 128, icon: 'fa-cogs', color: 'blue', gradient: 'from-blue-500 to-purple-500', change: '+12%', up: true },
|
||||
{ label: '运行中', value: 96, icon: 'fa-play-circle', color: 'green', gradient: 'from-green-500 to-teal-500', change: '+8%', up: true },
|
||||
{ label: '待机中', value: 24, icon: 'fa-pause-circle', color: 'orange', gradient: 'from-orange-500 to-yellow-500', change: '-5%', up: false },
|
||||
{ label: '故障告警', value: 8, icon: 'fa-exclamation-triangle', color: 'red', gradient: 'from-red-500 to-pink-500', change: '+2', up: false },
|
||||
])
|
||||
|
||||
// 设备列表
|
||||
const equipmentList = ref([
|
||||
{ id: 'EQ-001', name: 'CNC加工中心 01', model: 'CNC-850', location: '1号车间 A区', status: 'running', utilization: 92, temp: 45, vibration: 0.8, lastMaintenance: '2026-04-01', nextMaintenance: '2026-05-01' },
|
||||
{ id: 'EQ-002', name: 'CNC加工中心 02', model: 'CNC-850', location: '1号车间 A区', status: 'running', utilization: 88, temp: 43, vibration: 0.7, lastMaintenance: '2026-04-02', nextMaintenance: '2026-05-02' },
|
||||
{ id: 'EQ-003', name: 'CNC加工中心 03', model: 'CNC-650', location: '1号车间 B区', status: 'warning', utilization: 0, temp: 78, vibration: 2.5, lastMaintenance: '2026-03-15', nextMaintenance: '2026-04-15' },
|
||||
{ id: 'EQ-004', name: '数控铣床 01', model: 'XK-500', location: '2号车间', status: 'idle', utilization: 0, temp: 28, vibration: 0.3, lastMaintenance: '2026-03-28', nextMaintenance: '2026-04-28' },
|
||||
{ id: 'EQ-005', name: '数控铣床 02', model: 'XK-500', location: '2号车间', status: 'running', utilization: 75, temp: 38, vibration: 0.6, lastMaintenance: '2026-04-03', nextMaintenance: '2026-05-03' },
|
||||
{ id: 'EQ-006', name: '冲压机 01', model: 'CP-200T', location: '3号车间', status: 'running', utilization: 85, temp: 42, vibration: 1.2, lastMaintenance: '2026-03-20', nextMaintenance: '2026-04-20' },
|
||||
{ id: 'EQ-007', name: '激光切割机 01', model: 'JC-3000', location: '3号车间', status: 'idle', utilization: 0, temp: 25, vibration: 0.2, lastMaintenance: '2026-04-05', nextMaintenance: '2026-05-05' },
|
||||
{ id: 'EQ-008', name: '空压机 01', model: 'KY-50', location: '动力站房', status: 'running', utilization: 68, temp: 55, vibration: 1.5, lastMaintenance: '2026-03-10', nextMaintenance: '2026-05-10' },
|
||||
])
|
||||
|
||||
// 筛选状态
|
||||
const statusFilter = ref('all')
|
||||
const searchKeyword = ref('')
|
||||
|
||||
// 筛选后的列表
|
||||
const filteredList = computed(() => {
|
||||
return equipmentList.value.filter((item) => {
|
||||
const matchStatus = statusFilter.value === 'all' || item.status === statusFilter.value
|
||||
const matchSearch = !searchKeyword.value ||
|
||||
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
item.id.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
return matchStatus && matchSearch
|
||||
})
|
||||
})
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string; bg: string }> = {
|
||||
running: { label: '运行中', color: 'text-green-600', bg: 'bg-green-100' },
|
||||
idle: { label: '待机', color: 'text-orange-600', bg: 'bg-orange-100' },
|
||||
warning: { label: '告警', color: 'text-red-600', bg: 'bg-red-100' },
|
||||
offline: { label: '离线', color: 'text-gray-600', bg: 'bg-gray-100' },
|
||||
}
|
||||
|
||||
// 设备详情
|
||||
const selectedEquipment = ref<typeof equipmentList.value[0] | null>(null)
|
||||
const detailVisible = ref(false)
|
||||
|
||||
function showDetail(item: typeof equipmentList.value[0]) {
|
||||
selectedEquipment.value = item
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 维保记录
|
||||
const maintenanceRecords = ref([
|
||||
{ date: '2026-04-01', type: '例行保养', technician: '李师傅', cost: 500, status: '完成' },
|
||||
{ date: '2026-03-01', type: '例行保养', technician: '李师傅', cost: 480, status: '完成' },
|
||||
{ date: '2026-02-01', type: '更换配件', technician: '王师傅', cost: 1200, status: '完成' },
|
||||
])
|
||||
|
||||
// 设备效率趋势(模拟数据)
|
||||
const efficiencyTrend = ref([85, 88, 82, 90, 92, 88, 91, 89, 93, 92])
|
||||
const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日', '周一', '周二', '今天']
|
||||
|
||||
// 添加设备表单
|
||||
const addFormVisible = ref(false)
|
||||
const addForm = reactive({
|
||||
name: '',
|
||||
model: '',
|
||||
location: '',
|
||||
supplier: '',
|
||||
purchaseDate: '',
|
||||
warrantyEnd: '',
|
||||
})
|
||||
|
||||
function handleAdd() {
|
||||
// 模拟添加
|
||||
addFormVisible.value = false
|
||||
message.success('设备添加成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="equipment-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">设备管理</h2>
|
||||
<p class="text-gray-500 mt-1">实时监控设备状态,优化生产效率</p>
|
||||
</div>
|
||||
<a-button type="primary" @click="addFormVisible = true">
|
||||
<template #icon><i class="fas fa-plus mr-1"></i></template>
|
||||
添加设备
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-4 gap-6 mb-6">
|
||||
<div
|
||||
v-for="stat in equipmentStats"
|
||||
:key="stat.label"
|
||||
class="glass rounded-2xl p-6 card-hover cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
|
||||
:class="`bg-gradient-to-br ${stat.gradient}`"
|
||||
>
|
||||
<i :class="`fas ${stat.icon}`"></i>
|
||||
</div>
|
||||
<span
|
||||
class="text-sm font-medium"
|
||||
:class="stat.up ? 'text-green-500' : 'text-red-500'"
|
||||
>
|
||||
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
|
||||
{{ stat.change }}
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-gray-800 mb-1">{{ stat.value }}</h3>
|
||||
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="glass rounded-2xl p-4 mb-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-gray-600 font-medium">状态筛选:</span>
|
||||
<a-radio-group v-model:value="statusFilter" button-style="solid">
|
||||
<a-radio-button value="all">全部</a-radio-button>
|
||||
<a-radio-button value="running">运行中</a-radio-button>
|
||||
<a-radio-button value="idle">待机</a-radio-button>
|
||||
<a-radio-button value="warning">告警</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索设备名称或编号..."
|
||||
style="width: 280px"
|
||||
allow-clear
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<div class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-list text-purple-500 mr-2"></i>
|
||||
设备列表
|
||||
</h3>
|
||||
<span class="text-sm text-gray-500">共 {{ filteredList.length }} 台设备</span>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:dataSource="filteredList"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
rowKey="id"
|
||||
:scroll="{ x: 1200 }"
|
||||
>
|
||||
<a-table-column title="设备编号" dataIndex="id" width="100" />
|
||||
<a-table-column title="设备名称" dataIndex="name" width="180">
|
||||
<template #default="{ record }">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-8 h-8 rounded-lg flex items-center justify-center text-white text-xs"
|
||||
:class="statusMap[record.status]?.bg.replace('100', '500') || 'bg-gray-500'"
|
||||
>
|
||||
<i class="fas fa-cog"></i>
|
||||
</div>
|
||||
<span class="font-medium">{{ record.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="型号" dataIndex="model" width="100" />
|
||||
<a-table-column title="位置" dataIndex="location" width="120" />
|
||||
<a-table-column title="状态" dataIndex="status" width="100" align="center">
|
||||
<template #default="{ text }">
|
||||
<span
|
||||
:class="statusMap[text]?.color"
|
||||
class="px-2 py-1 rounded-lg text-sm font-medium"
|
||||
:class="statusMap[text]?.bg"
|
||||
>
|
||||
{{ statusMap[text]?.label }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="利用率" dataIndex="utilization" width="140">
|
||||
<template #default="{ record }">
|
||||
<div class="flex items-center gap-2">
|
||||
<a-progress
|
||||
:percent="record.utilization"
|
||||
:status="record.utilization > 80 ? 'success' : 'active'"
|
||||
:showInfo="false"
|
||||
size="small"
|
||||
:stroke-color="record.utilization > 80 ? '#10b981' : '#6366f1'"
|
||||
style="width: 80px"
|
||||
/>
|
||||
<span class="text-sm text-gray-600">{{ record.utilization }}%</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="温度(°C)" dataIndex="temp" width="100" align="center">
|
||||
<template #default="{ text, record }">
|
||||
<span :class="text > 60 ? 'text-red-500 font-medium' : 'text-gray-600'">
|
||||
{{ text }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="振动(mm/s)" dataIndex="vibration" width="110" align="center">
|
||||
<template #default="{ text }">
|
||||
<span :class="text > 2 ? 'text-red-500 font-medium' : 'text-gray-600'">
|
||||
{{ text }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="下次保养" dataIndex="nextMaintenance" width="120" />
|
||||
<a-table-column title="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ record }">
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<a-button type="link" size="small" @click="showDetail(record)">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a-button>
|
||||
<a-button type="link" size="small">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 设备详情抽屉 -->
|
||||
<a-drawer
|
||||
v-model:open="detailVisible"
|
||||
:title="selectedEquipment?.name"
|
||||
width="500"
|
||||
placement="right"
|
||||
>
|
||||
<template v-if="selectedEquipment">
|
||||
<!-- 基本信息 -->
|
||||
<div class="mb-6">
|
||||
<h4 class="font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-info-circle text-blue-500"></i>
|
||||
基本信息
|
||||
</h4>
|
||||
<a-descriptions :column="2" bordered size="small">
|
||||
<a-descriptions-item label="设备编号">{{ selectedEquipment.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="设备型号">{{ selectedEquipment.model }}</a-descriptions-item>
|
||||
<a-descriptions-item label="安装位置">{{ selectedEquipment.location }}</a-descriptions-item>
|
||||
<a-descriptions-item label="当前状态">
|
||||
<span
|
||||
:class="statusMap[selectedEquipment.status]?.color"
|
||||
class="px-2 py-0.5 rounded text-sm font-medium"
|
||||
:class="statusMap[selectedEquipment.status]?.bg"
|
||||
>
|
||||
{{ statusMap[selectedEquipment.status]?.label }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 实时监控 -->
|
||||
<div class="mb-6">
|
||||
<h4 class="font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-chart-line text-green-500"></i>
|
||||
实时监控
|
||||
</h4>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="bg-gray-50 rounded-xl p-4 text-center">
|
||||
<div class="text-2xl font-bold text-blue-600">{{ selectedEquipment.utilization }}%</div>
|
||||
<div class="text-xs text-gray-500 mt-1">设备利用率</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-4 text-center">
|
||||
<div
|
||||
class="text-2xl font-bold"
|
||||
:class="selectedEquipment.temp > 60 ? 'text-red-600' : 'text-orange-600'"
|
||||
>
|
||||
{{ selectedEquipment.temp }}°C
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">当前温度</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-4 text-center">
|
||||
<div
|
||||
class="text-2xl font-bold"
|
||||
:class="selectedEquipment.vibration > 2 ? 'text-red-600' : 'text-purple-600'"
|
||||
>
|
||||
{{ selectedEquipment.vibration }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">振动值</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 效率趋势图 -->
|
||||
<div class="mb-6">
|
||||
<h4 class="font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-chart-area text-purple-500"></i>
|
||||
效率趋势
|
||||
</h4>
|
||||
<div class="h-32 flex items-end gap-2">
|
||||
<div
|
||||
v-for="(value, idx) in efficiencyTrend"
|
||||
:key="idx"
|
||||
class="flex-1 flex flex-col items-center"
|
||||
>
|
||||
<div
|
||||
class="w-full bg-gradient-to-t from-purple-500 to-purple-300 rounded-t-lg transition-all hover:opacity-80"
|
||||
:style="{ height: value + '%' }"
|
||||
></div>
|
||||
<span class="text-xs text-gray-400 mt-1">{{ days[idx] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 维保记录 -->
|
||||
<div>
|
||||
<h4 class="font-bold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<i class="fas fa-wrench text-orange-500"></i>
|
||||
维保记录
|
||||
</h4>
|
||||
<a-timeline>
|
||||
<a-timeline-item
|
||||
v-for="record in maintenanceRecords"
|
||||
:key="record.date"
|
||||
:color="record.status === '完成' ? 'green' : 'gray'"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">{{ record.type }}</span>
|
||||
<span class="text-gray-500 text-sm">{{ record.date }}</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
技师:{{ record.technician }} | 费用:¥{{ record.cost }}
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-6 flex gap-3">
|
||||
<a-button type="primary" block>发起保养</a-button>
|
||||
<a-button block>导出报表</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-drawer>
|
||||
|
||||
<!-- 添加设备弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="addFormVisible"
|
||||
title="添加新设备"
|
||||
@ok="handleAdd"
|
||||
ok-text="确认添加"
|
||||
cancel-text="取消"
|
||||
>
|
||||
<a-form :model="addForm" layout="vertical">
|
||||
<a-form-item label="设备名称" required>
|
||||
<a-input v-model:value="addForm.name" placeholder="请输入设备名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="设备型号" required>
|
||||
<a-input v-model:value="addForm.model" placeholder="请输入设备型号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="安装位置" required>
|
||||
<a-input v-model:value="addForm.location" placeholder="请输入安装位置" />
|
||||
</a-form-item>
|
||||
<a-form-item label="供应商">
|
||||
<a-input v-model:value="addForm.supplier" placeholder="请输入供应商" />
|
||||
</a-form-item>
|
||||
<a-form-item label="采购日期">
|
||||
<a-date-picker v-model:value="addForm.purchaseDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="保修截止日期">
|
||||
<a-date-picker v-model:value="addForm.warrantyEnd" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.equipment-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rounded-2xl {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.gap-6 {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.text-gray-800 {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.text-orange-600 {
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.text-red-600 {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.text-purple-600 {
|
||||
color: #9333ea;
|
||||
}
|
||||
|
||||
.bg-gray-50 {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.bg-green-100 {
|
||||
background-color: #dcfce7;
|
||||
}
|
||||
|
||||
.bg-orange-100 {
|
||||
background-color: #ffedd5;
|
||||
}
|
||||
|
||||
.bg-red-100 {
|
||||
background-color: #fee2e2;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.from-blue-500 {
|
||||
--tw-gradient-from: #3b82f6;
|
||||
}
|
||||
|
||||
.to-purple-500 {
|
||||
--tw-gradient-to: #a855f7;
|
||||
}
|
||||
|
||||
.from-green-500 {
|
||||
--tw-gradient-from: #22c55e;
|
||||
}
|
||||
|
||||
.to-teal-500 {
|
||||
--tw-gradient-to: #14b8a6;
|
||||
}
|
||||
|
||||
.from-orange-500 {
|
||||
--tw-gradient-from: #f97316;
|
||||
}
|
||||
|
||||
.to-yellow-500 {
|
||||
--tw-gradient-to: #eab308;
|
||||
}
|
||||
|
||||
.from-red-500 {
|
||||
--tw-gradient-from: #ef4444;
|
||||
}
|
||||
|
||||
.to-pink-500 {
|
||||
--tw-gradient-to: #ec4899;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
98
app/pages/admin/production/quality.vue
Normal file
98
app/pages/admin/production/quality.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'production-quality'
|
||||
|
||||
const qualityRecords = ref([
|
||||
{ id: 'QC-2026040901', wo: 'WO2026040901', product: '精密轴承组件 A型', batch: 'B-20260409-01', inspector: '质检员A', checkTime: '2026-04-09 10:30', sampleSize: 50, passSize: 49, passRate: 98.0, result: '合格', issues: '1件尺寸超差' },
|
||||
{ id: 'QC-2026040801', wo: 'WO2026040703', product: '密封圈组件 D型', batch: 'B-20260408-03', inspector: '质检员B', checkTime: '2026-04-08 14:20', sampleSize: 100, passSize: 100, passRate: 100, result: '合格', issues: '-' },
|
||||
{ id: 'QC-2026040702', wo: 'WO2026040702', product: '弹簧组件 E型', batch: 'B-20260407-02', inspector: '质检员A', checkTime: '2026-04-07 09:00', sampleSize: 80, passSize: 72, passRate: 90.0, result: '不合格', issues: '8件弹性不足' },
|
||||
])
|
||||
|
||||
const resultColor: Record<string, string> = { '合格': 'success', '不合格': 'error' }
|
||||
const activeTab3 = ref('records')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">质量管理</h2>
|
||||
<a-button type="primary">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建质检
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="12" :sm="6">
|
||||
<div class="stat-card" style="--c:#10b981">
|
||||
<div class="stat-num">98.5%</div>
|
||||
<div class="stat-lbl">今日良品率</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="6">
|
||||
<div class="stat-card" style="--c:#3b82f6">
|
||||
<div class="stat-num">156</div>
|
||||
<div class="stat-lbl">今日检验数</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="6">
|
||||
<div class="stat-card" style="--c:#f59e0b">
|
||||
<div class="stat-num">3</div>
|
||||
<div class="stat-lbl">待处理异常</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="12" :sm="6">
|
||||
<div class="stat-card" style="--c:#6366f1">
|
||||
<div class="stat-num">12</div>
|
||||
<div class="stat-lbl">质检标准数</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-tabs v-model:activeKey="activeTab3">
|
||||
<a-tab-pane key="records" tab="检验记录">
|
||||
<div class="card">
|
||||
<a-table :dataSource="qualityRecords" :pagination="{ pageSize: 10 }" size="small" rowKey="id">
|
||||
<a-table-column title="质检编号" dataIndex="id" width="150" />
|
||||
<a-table-column title="工单号" dataIndex="wo" width="150" />
|
||||
<a-table-column title="产品" dataIndex="product" />
|
||||
<a-table-column title="批次" dataIndex="batch" width="160" />
|
||||
<a-table-column title="检验员" dataIndex="inspector" width="90" align="center" />
|
||||
<a-table-column title="检验时间" dataIndex="checkTime" width="160" />
|
||||
<a-table-column title="抽样数" dataIndex="sampleSize" width="80" align="center" />
|
||||
<a-table-column title="良品数" dataIndex="passSize" width="80" align="center" />
|
||||
<a-table-column title="良品率" dataIndex="passRate" width="90" align="center">
|
||||
<template #default="{ text, record }">
|
||||
<span :class="record.result === '合格' ? 'ok' : 'warn'">{{ text }}%</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="结果" dataIndex="result" width="90" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="resultColor[text]">{{ text }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="问题描述" dataIndex="issues" />
|
||||
</a-table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="standards" tab="质检标准">
|
||||
<div class="card"><a-empty description="质检标准管理" /></div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
|
||||
.stat-card { background: white; border-radius: 12px; padding: 20px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; position: relative; overflow: hidden; }
|
||||
.stat-card::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 3px; background: var(--c); }
|
||||
.stat-num { font-size: 28px; font-weight: 700; color: #1f2937; }
|
||||
.stat-lbl { font-size: 13px; color: #9ca3af; margin-top: 4px; }
|
||||
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
|
||||
.ok { color: #10b981; font-weight: 600; }
|
||||
.warn { color: #ef4444; font-weight: 600; }
|
||||
.mb-6 { margin-bottom: 20px; }
|
||||
</style>
|
||||
95
app/pages/admin/production/safety.vue
Normal file
95
app/pages/admin/production/safety.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'production-safety'
|
||||
|
||||
const stats = ref([
|
||||
{ label: '安全生产天数', value: 156, unit: '天', icon: '🛡️', color: '#10b981' },
|
||||
{ label: '本月巡检次数', value: 28, unit: '次', icon: '🔍', color: '#3b82f6' },
|
||||
{ label: '安全隐患', value: 3, unit: '条', icon: '⚠️', color: '#f59e0b' },
|
||||
{ label: '应急演练', value: 2, unit: '次', icon: '🚨', color: '#6366f1' },
|
||||
])
|
||||
|
||||
const safetyLogs = ref([
|
||||
{ id: 'SL-001', type: '巡检', area: '生产车间A区', inspector: '安全员A', result: '正常', time: '2026-04-09 08:30', remark: '-' },
|
||||
{ id: 'SL-002', type: '巡检', area: '电工房', inspector: '安全员B', result: '隐患', time: '2026-04-09 09:15', remark: '发现1处接线松动' },
|
||||
{ id: 'SL-003', type: '设备检查', area: 'CNC-03区域', inspector: '安全员A', result: '告警', time: '2026-04-09 10:00', remark: '设备温度异常,已停机' },
|
||||
{ id: 'SL-004', type: '巡检', area: '仓储区', inspector: '安全员C', result: '正常', time: '2026-04-08 14:00', remark: '-' },
|
||||
])
|
||||
|
||||
const resultColor: Record<string, string> = { '正常': 'success', '隐患': 'warning', '告警': 'error' }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">安全生产</h2>
|
||||
<a-button type="primary">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建巡检
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[16, 16]" class="mb-6">
|
||||
<a-col :xs="12" :sm="6" v-for="stat in stats" :key="stat.label">
|
||||
<div class="stat-card" :style="{ '--c': stat.color }">
|
||||
<div class="stat-icon">{{ stat.icon }}</div>
|
||||
<div class="stat-body">
|
||||
<div class="stat-value">{{ stat.value }}<span class="stat-unit">{{ stat.unit }}</span></div>
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="[20, 20]">
|
||||
<a-col :xs="24" :xl="16">
|
||||
<div class="card">
|
||||
<div class="card-title">安全巡检记录</div>
|
||||
<a-table :dataSource="safetyLogs" :pagination="{ pageSize: 10 }" size="small" rowKey="id">
|
||||
<a-table-column title="记录编号" dataIndex="id" width="100" />
|
||||
<a-table-column title="类型" dataIndex="type" width="100" align="center" />
|
||||
<a-table-column title="区域" dataIndex="area" />
|
||||
<a-table-column title="检查人" dataIndex="inspector" width="100" align="center" />
|
||||
<a-table-column title="结果" dataIndex="result" width="90" align="center">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="resultColor[text]">{{ text }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="备注" dataIndex="remark" />
|
||||
<a-table-column title="时间" dataIndex="time" width="160" />
|
||||
</a-table>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :xl="8">
|
||||
<div class="card">
|
||||
<div class="card-title">安全知识库</div>
|
||||
<div class="doc-list">
|
||||
<div class="doc-item">📄 安全生产管理制度 v3.2</div>
|
||||
<div class="doc-item">📄 应急救援预案</div>
|
||||
<div class="doc-item">📄 特种设备操作规程</div>
|
||||
<div class="doc-item">📄 危险源辨识清单</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
|
||||
.stat-card { background: white; border-radius: 12px; padding: 16px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
|
||||
.stat-card::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background: var(--c); }
|
||||
.stat-icon { font-size: 28px; }
|
||||
.stat-value { font-size: 22px; font-weight: 700; color: #1f2937; }
|
||||
.stat-unit { font-size: 12px; color: #9ca3af; margin-left: 2px; }
|
||||
.stat-label { font-size: 12px; color: #9ca3af; }
|
||||
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; }
|
||||
.card-title { font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 16px; }
|
||||
.doc-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.doc-item { padding: 10px; background: #fafafa; border-radius: 6px; font-size: 13px; color: #374151; cursor: pointer; }
|
||||
.doc-item:hover { background: #f0f0f0; }
|
||||
.mb-6 { margin-bottom: 20px; }
|
||||
</style>
|
||||
155
app/pages/admin/production/schedule.vue
Normal file
155
app/pages/admin/production/schedule.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
const { activeTab } = useNav()
|
||||
activeTab.value = 'production-schedule'
|
||||
|
||||
const selectedDate = ref('2026-04-09')
|
||||
|
||||
const scheduleData = ref([
|
||||
{ time: '08:00', tasks: [
|
||||
{ wo: 'WO2026040901', product: '精密轴承组件 A型', qty: 500, line: '产线1', status: 'running' },
|
||||
]},
|
||||
{ time: '10:00', tasks: [
|
||||
{ wo: 'WO2026040802', product: '液压缸体 B型', qty: 200, line: '产线2', status: 'pending' },
|
||||
]},
|
||||
{ time: '14:00', tasks: [
|
||||
{ wo: 'WO2026040703', product: '密封圈组件 D型', qty: 3000, line: '产线3', status: 'pending' },
|
||||
]},
|
||||
])
|
||||
|
||||
const productionLines = ref([
|
||||
{ id: 'L1', name: '产线1', status: 'running', utilization: 92, output: 245, target: 260 },
|
||||
{ id: 'L2', name: '产线2', status: 'idle', utilization: 0, output: 0, target: 200 },
|
||||
{ id: 'L3', name: '产线3', status: 'running', utilization: 78, output: 180, target: 220 },
|
||||
{ id: 'L4', name: '产线4', status: 'maintenance', utilization: 0, output: 0, target: 180 },
|
||||
])
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
running: '#10b981',
|
||||
pending: '#f59e0b',
|
||||
idle: '#9ca3af',
|
||||
maintenance: '#ef4444',
|
||||
}
|
||||
|
||||
const lineStatusMap: Record<string, string> = {
|
||||
running: { status: 'running', text: '运行中' },
|
||||
idle: { status: 'default', text: '空闲' },
|
||||
maintenance: { status: 'exception', text: '维护中' },
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">计划排程</h2>
|
||||
<a-space>
|
||||
<a-date-picker v-model:value="selectedDate" />
|
||||
<a-button type="primary">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新建排程
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[20, 20]">
|
||||
<!-- 排程甘特图 -->
|
||||
<a-col :xs="24" :xl="16">
|
||||
<div class="card">
|
||||
<div class="card-title">生产排程</div>
|
||||
<div class="gantt">
|
||||
<div class="gantt-header">
|
||||
<div class="gantt-time">时间段</div>
|
||||
<div class="gantt-tasks">排程任务</div>
|
||||
</div>
|
||||
<div v-for="slot in scheduleData" :key="slot.time" class="gantt-row">
|
||||
<div class="gantt-time">{{ slot.time }}</div>
|
||||
<div class="gantt-tasks">
|
||||
<div v-for="task in slot.tasks" :key="task.wo" class="task-card" :class="task.status">
|
||||
<div class="task-wo">{{ task.wo }}</div>
|
||||
<div class="task-info">{{ task.product }} · {{ task.qty }}件</div>
|
||||
<div class="task-line">{{ task.line }}</div>
|
||||
</div>
|
||||
<div v-if="slot.tasks.length === 0" class="task-empty">暂无排程</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 产线状态 -->
|
||||
<a-col :xs="24" :xl="8">
|
||||
<div class="card">
|
||||
<div class="card-title">产线状态</div>
|
||||
<div class="line-list">
|
||||
<div v-for="line in productionLines" :key="line.id" class="line-item">
|
||||
<div class="line-header">
|
||||
<span class="line-name">{{ line.name }}</span>
|
||||
<a-badge :status="lineStatusMap[line.status].status as any" :text="lineStatusMap[line.status].text" />
|
||||
</div>
|
||||
<div class="line-stats">
|
||||
<div class="line-stat">
|
||||
<span class="line-num">{{ line.utilization }}%</span>
|
||||
<span class="line-lbl">利用率</span>
|
||||
</div>
|
||||
<div class="line-stat">
|
||||
<span class="line-num">{{ line.output }}</span>
|
||||
<span class="line-lbl">实际产量</span>
|
||||
</div>
|
||||
<div class="line-stat">
|
||||
<span class="line-num">{{ line.target }}</span>
|
||||
<span class="line-lbl">目标产量</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="line.utilization"
|
||||
:showInfo="false"
|
||||
size="small"
|
||||
:status="line.status === 'running' ? 'active' : 'exception'"
|
||||
:strokeColor="statusColor[line.status]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-container { padding: 24px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.page-title { font-size: 20px; font-weight: 600; color: #1f2937; margin: 0; }
|
||||
|
||||
.card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.06); border: 1px solid #f0f0f0; margin-bottom: 20px; }
|
||||
.card-title { font-size: 16px; font-weight: 600; color: #1f2937; margin-bottom: 16px; }
|
||||
|
||||
.gantt { border: 1px solid #f0f0f0; border-radius: 8px; overflow: hidden; }
|
||||
.gantt-header { display: flex; background: #fafafa; border-bottom: 1px solid #f0f0f0; font-weight: 600; font-size: 13px; }
|
||||
.gantt-time { width: 80px; padding: 10px 12px; border-right: 1px solid #f0f0f0; }
|
||||
.gantt-tasks { flex: 1; padding: 10px 12px; }
|
||||
.gantt-row { display: flex; border-bottom: 1px solid #f0f0f0; }
|
||||
.gantt-row:last-child { border-bottom: none; }
|
||||
.gantt-row .gantt-time { padding: 16px 12px; font-size: 13px; color: #6b7280; }
|
||||
.gantt-row .gantt-tasks { display: flex; flex-wrap: wrap; gap: 8px; padding: 12px; min-height: 60px; align-items: center; }
|
||||
|
||||
.task-card {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.task-card.running { background: #ecfdf5; border-left: 3px solid #10b981; }
|
||||
.task-card.pending { background: #fffbeb; border-left: 3px solid #f59e0b; }
|
||||
.task-wo { font-weight: 600; color: #374151; }
|
||||
.task-info { color: #6b7280; margin: 2px 0; }
|
||||
.task-line { color: #9ca3af; font-size: 11px; }
|
||||
.task-empty { color: #d1d5db; font-size: 13px; }
|
||||
|
||||
.line-list { display: flex; flex-direction: column; gap: 16px; }
|
||||
.line-item { padding: 14px; background: #fafafa; border-radius: 8px; }
|
||||
.line-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||||
.line-name { font-weight: 600; color: #374151; font-size: 14px; }
|
||||
.line-stats { display: flex; justify-content: space-between; margin-bottom: 10px; }
|
||||
.line-stat { text-align: center; }
|
||||
.line-num { display: block; font-size: 16px; font-weight: 700; color: #1f2937; }
|
||||
.line-lbl { font-size: 11px; color: #9ca3af; }
|
||||
</style>
|
||||
465
app/pages/admin/supply/purchase.vue
Normal file
465
app/pages/admin/supply/purchase.vue
Normal file
@@ -0,0 +1,465 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
// 采购统计
|
||||
const purchaseStats = ref([
|
||||
{ label: '采购单总数', value: 156, icon: 'fa-file-alt', gradient: 'from-blue-500 to-purple-500', change: '+23%', up: true },
|
||||
{ label: '待审批', value: 12, icon: 'fa-clock', gradient: 'from-orange-500 to-yellow-500', change: '+3', up: false },
|
||||
{ label: '已完成', value: 138, icon: 'fa-check-circle', gradient: 'from-green-500 to-teal-500', change: '+18%', up: true },
|
||||
{ label: '采购总额', value: '89.5', unit: '万', icon: 'fa-wallet', gradient: 'from-pink-500 to-rose-500', change: '+15%', up: true },
|
||||
])
|
||||
|
||||
// 采购单列表
|
||||
const purchaseOrders = ref([
|
||||
{ id: 'PO-2026040901', supplier: '深圳市精密机械有限公司', material: '轴承组件 A型', quantity: 500, unit: '套', amount: 25000, status: 'pending', applicant: '张经理', date: '2026-04-09' },
|
||||
{ id: 'PO-2026040802', supplier: '上海五金工具厂', material: '数控刀具套装', quantity: 20, unit: '套', amount: 36000, status: 'approved', applicant: '李主管', date: '2026-04-08' },
|
||||
{ id: 'PO-2026040801', supplier: '东莞市金属材料公司', material: '铝合金板材', quantity: 200, unit: '张', amount: 48000, status: 'processing', applicant: '王经理', date: '2026-04-08' },
|
||||
{ id: 'PO-2026040703', supplier: '苏州液压设备厂', material: '液压缸体 B型', quantity: 50, unit: '件', amount: 75000, status: 'completed', applicant: '赵主管', date: '2026-04-07' },
|
||||
{ id: 'PO-2026040702', supplier: '广州润滑油脂公司', material: '工业润滑油', quantity: 100, unit: '桶', amount: 15000, status: 'completed', applicant: '张经理', date: '2026-04-07' },
|
||||
{ id: 'PO-2026040601', supplier: '深圳市精密机械有限公司', material: '密封圈组件', quantity: 1000, unit: '个', amount: 8000, status: 'completed', applicant: '李主管', date: '2026-04-06' },
|
||||
])
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string; bg: string }> = {
|
||||
pending: { label: '待审批', color: 'text-orange-600', bg: 'bg-orange-100' },
|
||||
approved: { label: '已审批', color: 'text-blue-600', bg: 'bg-blue-100' },
|
||||
processing: { label: '执行中', color: 'text-purple-600', bg: 'bg-purple-100' },
|
||||
completed: { label: '已完成', color: 'text-green-600', bg: 'bg-green-100' },
|
||||
rejected: { label: '已驳回', color: 'text-red-600', bg: 'bg-red-100' },
|
||||
}
|
||||
|
||||
const statusFilter = ref('all')
|
||||
const searchKeyword = ref('')
|
||||
|
||||
const filteredList = computed(() => {
|
||||
return purchaseOrders.value.filter((item) => {
|
||||
const matchStatus = statusFilter.value === 'all' || item.status === statusFilter.value
|
||||
const matchSearch = !searchKeyword.value ||
|
||||
item.id.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
item.supplier.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
item.material.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
return matchStatus && matchSearch
|
||||
})
|
||||
})
|
||||
|
||||
// 供应商列表
|
||||
const suppliers = ref([
|
||||
{ name: '深圳市精密机械有限公司', contact: '陈经理', phone: '138****1234', items: 45, total: '120万', rating: 4.8 },
|
||||
{ name: '上海五金工具厂', contact: '李总', phone: '139****5678', items: 32, total: '85万', rating: 4.6 },
|
||||
{ name: '东莞市金属材料公司', contact: '王经理', phone: '137****9012', items: 28, total: '200万', rating: 4.9 },
|
||||
{ name: '苏州液压设备厂', contact: '张工', phone: '136****3456', items: 15, total: '95万', rating: 4.7 },
|
||||
])
|
||||
|
||||
const addFormVisible = ref(false)
|
||||
const addForm = reactive({
|
||||
supplier: '',
|
||||
material: '',
|
||||
quantity: '',
|
||||
unit: '',
|
||||
estimatedAmount: '',
|
||||
deliveryDate: '',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
function handleAdd() {
|
||||
addFormVisible.value = false
|
||||
message.success('采购单创建成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="purchase-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">采购管理</h2>
|
||||
<p class="text-gray-500 mt-1">管理采购订单,跟踪供应商履约情况</p>
|
||||
</div>
|
||||
<a-button type="primary" @click="addFormVisible = true">
|
||||
<template #icon><i class="fas fa-plus mr-1"></i></template>
|
||||
新建采购单
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-4 gap-6 mb-6">
|
||||
<div
|
||||
v-for="stat in purchaseStats"
|
||||
:key="stat.label"
|
||||
class="glass rounded-2xl p-6 card-hover cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
|
||||
:class="`bg-gradient-to-br ${stat.gradient}`"
|
||||
>
|
||||
<i :class="`fas ${stat.icon}`"></i>
|
||||
</div>
|
||||
<span
|
||||
class="text-sm font-medium"
|
||||
:class="stat.up ? 'text-green-500' : 'text-red-500'"
|
||||
>
|
||||
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
|
||||
{{ stat.change }}
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-gray-800 mb-1">
|
||||
{{ stat.value }}<span v-if="stat.unit" class="text-base text-gray-500 ml-1">{{ stat.unit }}</span>
|
||||
</h3>
|
||||
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="glass rounded-2xl p-4 mb-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-gray-600 font-medium">状态筛选:</span>
|
||||
<a-radio-group v-model:value="statusFilter" button-style="solid">
|
||||
<a-radio-button value="all">全部</a-radio-button>
|
||||
<a-radio-button value="pending">待审批</a-radio-button>
|
||||
<a-radio-button value="approved">已审批</a-radio-button>
|
||||
<a-radio-button value="processing">执行中</a-radio-button>
|
||||
<a-radio-button value="completed">已完成</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索采购单号、供应商、物料..."
|
||||
style="width: 300px"
|
||||
allow-clear
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 采购单列表 -->
|
||||
<div class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-file-alt text-blue-500 mr-2"></i>
|
||||
采购单列表
|
||||
</h3>
|
||||
<span class="text-sm text-gray-500">共 {{ filteredList.length }} 条记录</span>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:dataSource="filteredList"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
rowKey="id"
|
||||
:scroll="{ x: 1200 }"
|
||||
>
|
||||
<a-table-column title="采购单号" dataIndex="id" width="140" />
|
||||
<a-table-column title="供应商" dataIndex="supplier" width="200">
|
||||
<template #default="{ text }">
|
||||
<span class="font-medium">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="采购物料" dataIndex="material" width="150" />
|
||||
<a-table-column title="数量" dataIndex="quantity" width="100" align="center">
|
||||
<template #default="{ record }">
|
||||
{{ record.quantity }} {{ record.unit }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="金额(元)" dataIndex="amount" width="120" align="right">
|
||||
<template #default="{ text }">
|
||||
<span class="font-medium text-orange-600">¥{{ text.toLocaleString() }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="状态" dataIndex="status" width="100" align="center">
|
||||
<template #default="{ text }">
|
||||
<span
|
||||
:class="statusMap[text]?.color"
|
||||
class="px-2 py-1 rounded-lg text-sm font-medium"
|
||||
:class="statusMap[text]?.bg"
|
||||
>
|
||||
{{ statusMap[text]?.label }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="申请人" dataIndex="applicant" width="100" align="center" />
|
||||
<a-table-column title="申请日期" dataIndex="date" width="120" />
|
||||
<a-table-column title="操作" width="160" align="center" fixed="right">
|
||||
<template #default>
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<a-button type="link" size="small" title="查看详情">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a-button>
|
||||
<a-button type="link" size="small" title="审批" v-if="statusFilter === 'pending'">
|
||||
<i class="fas fa-check"></i>
|
||||
</a-button>
|
||||
<a-button type="link" size="small" title="编辑">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 供应商管理 -->
|
||||
<div class="glass rounded-2xl p-6 mt-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-building text-green-500 mr-2"></i>
|
||||
供应商列表
|
||||
</h3>
|
||||
<a-button type="link">
|
||||
<i class="fas fa-plus mr-1"></i>添加供应商
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[16, 16]">
|
||||
<a-col :xs="24" :sm="12" :lg="6" v-for="supplier in suppliers" :key="supplier.name">
|
||||
<div class="supplier-card glass rounded-xl p-4 card-hover">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white">
|
||||
<i class="fas fa-building"></i>
|
||||
</div>
|
||||
<a-rate :default-value="supplier.rating" disabled size="small" />
|
||||
</div>
|
||||
<h4 class="font-bold text-gray-800 mb-1">{{ supplier.name }}</h4>
|
||||
<p class="text-sm text-gray-500 mb-2">{{ supplier.contact }} | {{ supplier.phone }}</p>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500">合作物料:<span class="text-blue-600 font-medium">{{ supplier.items }}</span> 种</span>
|
||||
<span class="text-gray-500">累计:<span class="text-green-600 font-medium">{{ supplier.total }}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 新建采购单弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="addFormVisible"
|
||||
title="新建采购单"
|
||||
@ok="handleAdd"
|
||||
ok-text="确认创建"
|
||||
cancel-text="取消"
|
||||
width="600px"
|
||||
>
|
||||
<a-form :model="addForm" layout="vertical">
|
||||
<a-form-item label="供应商" required>
|
||||
<a-select v-model:value="addForm.supplier" placeholder="请选择供应商">
|
||||
<a-select-option v-for="s in suppliers" :key="s.name" :value="s.name">{{ s.name }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="采购物料" required>
|
||||
<a-input v-model:value="addForm.material" placeholder="请输入物料名称" />
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="数量" required>
|
||||
<a-input-number v-model:value="addForm.quantity" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="单位">
|
||||
<a-input v-model:value="addForm.unit" placeholder="如:套、件、个" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="预估金额(元)">
|
||||
<a-input-number v-model:value="addForm.estimatedAmount" style="width: 100%" :min="0" />
|
||||
</a-form-item>
|
||||
<a-form-item label="期望交货日期">
|
||||
<a-date-picker v-model:value="addForm.deliveryDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注">
|
||||
<a-textarea v-model:value="addForm.remark" :rows="3" placeholder="请输入备注信息" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.purchase-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.mt-1 {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text-base {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rounded-2xl {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.gap-6 {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text-gray-800 {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-orange-600 {
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.bg-gradient-to-br {
|
||||
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
|
||||
}
|
||||
</style>
|
||||
462
app/pages/admin/supply/warehouse.vue
Normal file
462
app/pages/admin/supply/warehouse.vue
Normal file
@@ -0,0 +1,462 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'admin' })
|
||||
|
||||
// 仓储统计
|
||||
const warehouseStats = ref([
|
||||
{ label: '物料种类', value: 5230, icon: 'fa-boxes', gradient: 'from-blue-500 to-cyan-500', change: '+126', up: true },
|
||||
{ label: '库存总量', value: '8.5', unit: '万', icon: 'fa-warehouse', gradient: 'from-green-500 to-teal-500', change: '+1.2万', up: true },
|
||||
{ label: '待入库', value: 45, icon: 'fa-arrow-down', gradient: 'from-orange-500 to-yellow-500', change: '+12', up: false },
|
||||
{ label: '待出库', value: 28, icon: 'fa-truck', gradient: 'from-purple-500 to-pink-500', change: '-8', up: true },
|
||||
])
|
||||
|
||||
// 库存列表
|
||||
const inventoryList = ref([
|
||||
{ code: 'MAT-001', name: '轴承组件 A型', category: '标准件', warehouse: 'A区', location: 'A-01-03', stock: 1500, safeStock: 500, unit: '套', status: 'normal' },
|
||||
{ code: 'MAT-002', name: '铝合金板材', category: '原材料', warehouse: 'B区', location: 'B-02-05', stock: 320, safeStock: 200, unit: '张', status: 'normal' },
|
||||
{ code: 'MAT-003', name: '液压缸体 B型', category: '半成品', warehouse: 'C区', location: 'C-01-02', stock: 80, safeStock: 100, unit: '件', status: 'low' },
|
||||
{ code: 'MAT-004', name: '数控刀具套装', category: '工装', warehouse: 'D区', location: 'D-03-01', stock: 45, safeStock: 20, unit: '套', status: 'normal' },
|
||||
{ code: 'MAT-005', name: '密封圈组件', category: '标准件', warehouse: 'A区', location: 'A-02-01', stock: 2800, safeStock: 1000, unit: '个', status: 'normal' },
|
||||
{ code: 'MAT-006', name: '传动齿轮组 C型', category: '零部件', warehouse: 'C区', location: 'C-02-03', stock: 150, safeStock: 200, unit: '组', status: 'low' },
|
||||
])
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string; bg: string }> = {
|
||||
normal: { label: '正常', color: 'text-green-600', bg: 'bg-green-100' },
|
||||
low: { label: '偏低', color: 'text-orange-600', bg: 'bg-orange-100' },
|
||||
out: { label: '缺货', color: 'text-red-600', bg: 'bg-red-100' },
|
||||
over: { label: '超储', color: 'text-blue-600', bg: 'bg-blue-100' },
|
||||
}
|
||||
|
||||
// 入库记录
|
||||
const inboundRecords = ref([
|
||||
{ id: 'IN-2026040901', material: '轴承组件 A型', quantity: 500, unit: '套', type: '采购入库', operator: '仓管员-张三', date: '2026-04-09 09:30', status: 'completed' },
|
||||
{ id: 'IN-2026040802', material: '铝合金板材', quantity: 200, unit: '张', type: '采购入库', operator: '仓管员-李四', date: '2026-04-08 14:20', status: 'completed' },
|
||||
{ id: 'IN-2026040801', material: '数控刀具套装', quantity: 20, unit: '套', type: '采购入库', operator: '仓管员-张三', date: '2026-04-08 10:15', status: 'completed' },
|
||||
{ id: 'IN-2026040703', material: '工业润滑油', quantity: 50, unit: '桶', type: '采购入库', operator: '仓管员-王五', date: '2026-04-07 16:45', status: 'completed' },
|
||||
])
|
||||
|
||||
// 出库记录
|
||||
const outboundRecords = ref([
|
||||
{ id: 'OUT-2026040901', material: '轴承组件 A型', quantity: 100, unit: '套', type: '生产领料', recipient: '1号车间', operator: '仓管员-张三', date: '2026-04-09 08:30', status: 'completed' },
|
||||
{ id: 'OUT-2026040902', material: '密封圈组件', quantity: 200, unit: '个', type: '生产领料', recipient: '2号车间', operator: '仓管员-李四', date: '2026-04-09 10:20', status: 'completed' },
|
||||
{ id: 'OUT-2026040801', material: '铝合金板材', quantity: 50, unit: '张', type: '生产领料', recipient: '3号车间', operator: '仓管员-王五', date: '2026-04-08 15:30', status: 'completed' },
|
||||
])
|
||||
|
||||
const activeTab = ref('inventory')
|
||||
const searchKeyword = ref('')
|
||||
|
||||
const filteredInventory = computed(() => {
|
||||
return inventoryList.value.filter((item) => {
|
||||
return !searchKeyword.value ||
|
||||
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
item.code.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
item.category.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
})
|
||||
})
|
||||
|
||||
// 入库表单
|
||||
const inboundVisible = ref(false)
|
||||
const inboundForm = reactive({
|
||||
material: '',
|
||||
quantity: '',
|
||||
unit: '',
|
||||
type: 'purchase',
|
||||
supplier: '',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
function handleInbound() {
|
||||
inboundVisible.value = false
|
||||
message.success('入库登记成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="warehouse-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">仓储物流</h2>
|
||||
<p class="text-gray-500 mt-1">管理库存物料,跟踪出入库记录</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<a-button @click="inboundVisible = true">
|
||||
<template #icon><i class="fas fa-arrow-down mr-1"></i></template>
|
||||
入库登记
|
||||
</a-button>
|
||||
<a-button type="primary">
|
||||
<template #icon><i class="fas fa-arrow-up mr-1"></i></template>
|
||||
出库登记
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-4 gap-6 mb-6">
|
||||
<div
|
||||
v-for="stat in warehouseStats"
|
||||
:key="stat.label"
|
||||
class="glass rounded-2xl p-6 card-hover cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
|
||||
:class="`bg-gradient-to-br ${stat.gradient}`"
|
||||
>
|
||||
<i :class="`fas ${stat.icon}`"></i>
|
||||
</div>
|
||||
<span
|
||||
class="text-sm font-medium"
|
||||
:class="stat.up ? 'text-green-500' : 'text-red-500'"
|
||||
>
|
||||
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
|
||||
{{ stat.change }}
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-gray-800 mb-1">
|
||||
{{ stat.value }}<span v-if="stat.unit" class="text-base text-gray-500 ml-1">{{ stat.unit }}</span>
|
||||
</h3>
|
||||
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab切换 -->
|
||||
<div class="glass rounded-2xl p-4 mb-6">
|
||||
<a-radio-group v-model:value="activeTab" button-style="solid">
|
||||
<a-radio-button value="inventory">
|
||||
<i class="fas fa-boxes mr-1"></i>库存查询
|
||||
</a-radio-button>
|
||||
<a-radio-button value="inbound">
|
||||
<i class="fas fa-arrow-down mr-1"></i>入库记录
|
||||
</a-radio-button>
|
||||
<a-radio-button value="outbound">
|
||||
<i class="fas fa-arrow-up mr-1"></i>出库记录
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索物料名称、编号..."
|
||||
style="width: 280px; float: right"
|
||||
allow-clear
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 库存列表 -->
|
||||
<div v-show="activeTab === 'inventory'" class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-boxes text-blue-500 mr-2"></i>
|
||||
库存列表
|
||||
</h3>
|
||||
<span class="text-sm text-gray-500">共 {{ filteredInventory.length }} 种物料</span>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:dataSource="filteredInventory"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
rowKey="code"
|
||||
:scroll="{ x: 1100 }"
|
||||
>
|
||||
<a-table-column title="物料编码" dataIndex="code" width="110" />
|
||||
<a-table-column title="物料名称" dataIndex="name" width="160">
|
||||
<template #default="{ text }">
|
||||
<span class="font-medium">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="分类" dataIndex="category" width="100" />
|
||||
<a-table-column title="仓库" dataIndex="warehouse" width="80" align="center" />
|
||||
<a-table-column title="库位" dataIndex="location" width="100" align="center" />
|
||||
<a-table-column title="库存量" dataIndex="stock" width="120" align="right">
|
||||
<template #default="{ record }">
|
||||
<span class="font-medium">{{ record.stock.toLocaleString() }} {{ record.unit }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="安全库存" dataIndex="safeStock" width="100" align="right">
|
||||
<template #default="{ record }">
|
||||
<span class="text-gray-500">{{ record.safeStock }} {{ record.unit }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="状态" dataIndex="status" width="100" align="center">
|
||||
<template #default="{ text }">
|
||||
<span
|
||||
:class="statusMap[text]?.color"
|
||||
class="px-2 py-1 rounded-lg text-sm font-medium"
|
||||
:class="statusMap[text]?.bg"
|
||||
>
|
||||
{{ statusMap[text]?.label }}
|
||||
</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作" width="120" align="center" fixed="right">
|
||||
<template #default>
|
||||
<div class="flex items-center gap-2 justify-center">
|
||||
<a-button type="link" size="small" title="详情">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a-button>
|
||||
<a-button type="link" size="small" title="调整">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 入库记录 -->
|
||||
<div v-show="activeTab === 'inbound'" class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-arrow-down text-green-500 mr-2"></i>
|
||||
入库记录
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:dataSource="inboundRecords"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
rowKey="id"
|
||||
>
|
||||
<a-table-column title="入库单号" dataIndex="id" width="140" />
|
||||
<a-table-column title="物料名称" dataIndex="material" width="160">
|
||||
<template #default="{ text }">
|
||||
<span class="font-medium">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="数量" width="120" align="center">
|
||||
<template #default="{ record }">
|
||||
<span class="text-green-600 font-medium">+{{ record.quantity }} {{ record.unit }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="入库类型" dataIndex="type" width="120">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="text === '采购入库' ? 'blue' : 'purple'">{{ text }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="操作员" dataIndex="operator" width="120" />
|
||||
<a-table-column title="入库时间" dataIndex="date" width="160" />
|
||||
<a-table-column title="状态" width="100" align="center">
|
||||
<template #default>
|
||||
<a-tag color="success">已完成</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 出库记录 -->
|
||||
<div v-show="activeTab === 'outbound'" class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg text-gray-800">
|
||||
<i class="fas fa-arrow-up text-orange-500 mr-2"></i>
|
||||
出库记录
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:dataSource="outboundRecords"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
rowKey="id"
|
||||
>
|
||||
<a-table-column title="出库单号" dataIndex="id" width="140" />
|
||||
<a-table-column title="物料名称" dataIndex="material" width="160">
|
||||
<template #default="{ text }">
|
||||
<span class="font-medium">{{ text }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="数量" width="120" align="center">
|
||||
<template #default="{ record }">
|
||||
<span class="text-orange-600 font-medium">-{{ record.quantity }} {{ record.unit }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="出库类型" dataIndex="type" width="120">
|
||||
<template #default="{ text }">
|
||||
<a-tag :color="text === '生产领料' ? 'orange' : 'cyan'">{{ text }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="领用部门" dataIndex="recipient" width="120" />
|
||||
<a-table-column title="操作员" dataIndex="operator" width="120" />
|
||||
<a-table-column title="出库时间" dataIndex="date" width="160" />
|
||||
<a-table-column title="状态" width="100" align="center">
|
||||
<template #default>
|
||||
<a-tag color="success">已完成</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 入库登记弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="inboundVisible"
|
||||
title="入库登记"
|
||||
@ok="handleInbound"
|
||||
ok-text="确认入库"
|
||||
cancel-text="取消"
|
||||
>
|
||||
<a-form :model="inboundForm" layout="vertical">
|
||||
<a-form-item label="物料" required>
|
||||
<a-select v-model:value="inboundForm.material" placeholder="请选择物料">
|
||||
<a-select-option v-for="item in inventoryList" :key="item.code" :value="item.name">
|
||||
{{ item.name }} ({{ item.code }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="数量" required>
|
||||
<a-input-number v-model:value="inboundForm.quantity" style="width: 100%" :min="1" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="单位">
|
||||
<a-input v-model:value="inboundForm.unit" placeholder="如:套、件、个" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="入库类型">
|
||||
<a-radio-group v-model:value="inboundForm.type">
|
||||
<a-radio value="purchase">采购入库</a-radio>
|
||||
<a-radio value="return">退货入库</a-radio>
|
||||
<a-radio value="transfer">调拨入库</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="供应商" v-if="inboundForm.type === 'purchase'">
|
||||
<a-input v-model:value="inboundForm.supplier" placeholder="请输入供应商名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注">
|
||||
<a-textarea v-model:value="inboundForm.remark" :rows="2" placeholder="请输入备注信息" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.warehouse-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-base {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rounded-2xl {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.gap-6 {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text-gray-800 {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.text-orange-600 {
|
||||
color: #ea580c;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user