refactor(developer-config): 移除开发者配置页面相关代码和文档

- 删除应用配置页面及相关组件,重构路由为 /developer/config/[id].vue
- 移除开发者文档页面及其导航与样式实现
- 清理开发者侧功能完善工作日志文件
- 删除全局.gitignore配置文件,清理无用忽略规则
- 优化应用配置页面的参数读取和路由结构,解决刷新404问题
- 解决数据库配置唯一键冲突,调整保存逻辑避免重复插入
- 移除对后端配置加密字段的 secret 标记,修正加密异常问题
This commit is contained in:
2026-04-09 07:35:34 +08:00
parent 3209d92cc5
commit f9e1286ab1
130 changed files with 18656 additions and 22143 deletions

247
app/pages/admin/account.vue Normal file
View 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">支持 JPGPNG 格式文件小于 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>

View File

@@ -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>

View 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>

View 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>

View 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
View 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>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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
View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 </span>
<span>560 </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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>