feat: 权限精细化 + 成本人力调整 + 仪表盘模块权限
Some checks failed
Build and Deploy Backend / build-and-deploy (push) Successful in 2m1s
Build and Deploy Web / build-and-deploy (push) Has been cancelled

- 用户管理页面权限拆分:user:view可查看列表,user:manage控制新增/编辑,user:view_cost控制薪资列显示
- 成本管理新增"人力调整"Tab,支持查看和录入CostOverride记录
- 仪表盘新增4个子权限(成本/损耗/盈亏/风险预警),管理员可按角色灵活配置可见模块
- 修复组长进入成本管理页面弹出"权限不足"提示的问题
- 修复主管无法访问用户管理页面的路由权限问题
- 提交页面延期原因字段仅在里程碑超期时显示

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-02-25 14:13:51 +08:00
parent dc42306c24
commit a43bed1d64
9 changed files with 270 additions and 36 deletions

View File

@ -13,7 +13,11 @@ import enum
ALL_PERMISSIONS = [
# 仪表盘
("dashboard:view", "查看仪表盘", "仪表盘"),
("dashboard:view", "查看仪表盘(基础)", "仪表盘"),
("dashboard:view_cost", "查看成本模块", "仪表盘"),
("dashboard:view_waste", "查看损耗模块", "仪表盘"),
("dashboard:view_profit", "查看盈亏模块", "仪表盘"),
("dashboard:view_risk", "查看风险预警", "仪表盘"),
# 项目管理
("project:view", "查看项目", "项目管理"),
("project:create", "创建项目", "项目管理"),
@ -38,6 +42,7 @@ ALL_PERMISSIONS = [
("cost_labor:create", "录入人力调整", "成本管理"),
# 用户与角色
("user:view", "查看用户列表", "用户与角色"),
("user:view_cost", "查看成员薪资成本", "用户与角色"),
("user:manage", "管理用户", "用户与角色"),
("role:manage", "管理角色", "用户与角色"),
# 结算与效率
@ -66,7 +71,7 @@ BUILTIN_ROLES = {
"主管": {
"description": "管理项目和成本,不可管理用户和角色",
"permissions": [
"dashboard:view",
"dashboard:view", "dashboard:view_cost", "dashboard:view_waste", "dashboard:view_profit", "dashboard:view_risk",
"project:view", "project:create", "project:edit", "project:complete",
"submission:view", "submission:create",
"cost_ai:view", "cost_ai:create", "cost_ai:delete",

View File

@ -10,27 +10,32 @@ from auth import get_current_user, hash_password, require_permission
router = APIRouter(prefix="/api/users", tags=["用户管理"])
def user_to_out(u: User) -> UserOut:
def user_to_out(u: User, hide_cost: bool = False) -> UserOut:
return UserOut(
id=u.id, username=u.username, name=u.name,
phase_group=u.phase_group.value if hasattr(u.phase_group, 'value') else u.phase_group,
role_id=u.role_id, role_name=u.role_name, permissions=u.permissions,
monthly_salary=u.monthly_salary,
bonus=u.bonus or 0,
social_insurance=u.social_insurance or 0,
monthly_total_cost=u.monthly_total_cost,
daily_cost=u.daily_cost,
monthly_salary=0 if hide_cost else u.monthly_salary,
bonus=0 if hide_cost else (u.bonus or 0),
social_insurance=0 if hide_cost else (u.social_insurance or 0),
monthly_total_cost=0 if hide_cost else u.monthly_total_cost,
daily_cost=0 if hide_cost else u.daily_cost,
is_active=u.is_active, created_at=u.created_at,
)
def _can_view_cost(user: User) -> bool:
return "user:view_cost" in (user.permissions or [])
@router.get("", response_model=List[UserOut])
def list_users(
db: Session = Depends(get_db),
current_user: User = Depends(require_permission("user:view"))
):
hide = not _can_view_cost(current_user)
users = db.query(User).order_by(User.created_at.desc()).all()
return [user_to_out(u) for u in users]
return [user_to_out(u, hide_cost=hide) for u in users]
@router.post("", response_model=UserOut)
@ -95,4 +100,5 @@ def get_user(
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
return user_to_out(user)
hide = not _can_view_cost(current_user)
return user_to_out(user, hide_cost=hide)

View File

@ -73,7 +73,7 @@ const menuItems = [
{ path: '/projects', label: '项目管理', icon: 'FolderOpened', perm: 'project:view' },
{ path: '/submissions', label: '内容提交', icon: 'EditPen', perm: 'submission:view' },
{ path: '/costs', label: '成本管理', icon: 'Money', perm: ['cost_ai:view', 'cost_outsource:view', 'cost_overhead:view', 'cost_labor:view'] },
{ path: '/users', label: '用户管理', icon: 'User', perm: 'user:manage' },
{ path: '/users', label: '用户管理', icon: 'User', perm: 'user:view' },
{ path: '/roles', label: '角色管理', icon: 'Lock', perm: 'role:manage' },
]

View File

@ -12,7 +12,7 @@ const routes = [
{ path: 'projects/:id', name: 'ProjectDetail', component: () => import('../views/ProjectDetail.vue'), meta: { perm: 'project:view' } },
{ path: 'submissions', name: 'Submissions', component: () => import('../views/Submissions.vue'), meta: { perm: 'submission:view' } },
{ path: 'costs', name: 'Costs', component: () => import('../views/Costs.vue'), meta: { perm: ['cost_ai:view', 'cost_outsource:view', 'cost_overhead:view', 'cost_labor:view'] } },
{ path: 'users', name: 'Users', component: () => import('../views/Users.vue'), meta: { perm: 'user:manage' } },
{ path: 'users', name: 'Users', component: () => import('../views/Users.vue'), meta: { perm: 'user:view' } },
{ path: 'users/:id/detail', name: 'MemberDetail', component: () => import('../views/MemberDetail.vue'), meta: { perm: 'user:view' } },
{ path: 'roles', name: 'Roles', component: () => import('../views/Roles.vue'), meta: { perm: 'role:manage' } },
{ path: 'settlement/:id', name: 'Settlement', component: () => import('../views/Settlement.vue'), meta: { perm: 'settlement:view' } },

View File

@ -62,6 +62,29 @@
</el-table-column>
</el-table>
</el-tab-pane>
<!-- 人力调整 -->
<el-tab-pane v-if="authStore.hasPermission('cost_labor:view')" label="人力调整" name="labor">
<div class="tab-header">
<el-button v-if="authStore.hasPermission('cost_labor:create')" type="primary" size="small" @click="showLaborForm = true"><el-icon><Plus /></el-icon> 新增调整</el-button>
</div>
<el-table :data="laborOverrides" v-loading="loadingLabor" stripe size="small">
<el-table-column label="成员" width="100">
<template #default="{row}">{{ userMap[row.user_id] || row.user_id }}</template>
</el-table-column>
<el-table-column label="项目" width="160">
<template #default="{row}">{{ projectMap[row.project_id] || row.project_id }}</template>
</el-table-column>
<el-table-column prop="date" label="调整日期" width="110" />
<el-table-column label="调整金额" width="120" align="right">
<template #default="{row}">¥{{ row.override_amount.toLocaleString() }}</template>
</el-table-column>
<el-table-column prop="reason" label="调整原因" show-overflow-tooltip />
<el-table-column label="操作人" width="80">
<template #default="{row}">{{ userMap[row.adjusted_by] || row.adjusted_by }}</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<!-- AI 工具新增弹窗 -->
@ -155,47 +178,84 @@
<el-button type="primary" @click="createOH">保存</el-button>
</template>
</el-dialog>
<!-- 人力调整新增弹窗 -->
<el-dialog v-model="showLaborForm" title="新增人力成本调整" width="480px" destroy-on-close>
<el-form :model="laborForm" label-width="100px">
<el-form-item label="调整成员">
<el-select v-model="laborForm.user_id" filterable placeholder="选择成员" style="width:100%">
<el-option v-for="u in users" :key="u.id" :label="u.name" :value="u.id" />
</el-select>
</el-form-item>
<el-form-item label="所属项目">
<el-select v-model="laborForm.project_id" filterable placeholder="选择项目" style="width:100%">
<el-option v-for="p in projects" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
</el-form-item>
<el-form-item label="调整日期">
<el-date-picker v-model="laborForm.date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
<el-form-item label="调整金额">
<el-input-number v-model="laborForm.override_amount" :controls="false" placeholder="覆盖该成员当天在该项目上的人力成本(元)" style="width:100%" />
</el-form-item>
<el-form-item label="调整原因">
<el-input v-model="laborForm.reason" type="textarea" :rows="2" placeholder="(选填)如:当天仅投入半天、临时借调等" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showLaborForm = false">取消</el-button>
<el-button type="primary" @click="createLabor">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, reactive, onMounted } from 'vue'
import { costApi, projectApi } from '../api'
import { costApi, projectApi, userApi } from '../api'
import { useAuthStore } from '../stores/auth'
import { ElMessage, ElMessageBox } from 'element-plus'
const authStore = useAuthStore()
// tab
const tabOrder = ['ai', 'outsource', 'overhead']
const tabPermMap = { ai: 'cost_ai:view', outsource: 'cost_outsource:view', overhead: 'cost_overhead:view' }
const tabOrder = ['ai', 'outsource', 'overhead', 'labor']
const tabPermMap = { ai: 'cost_ai:view', outsource: 'cost_outsource:view', overhead: 'cost_overhead:view', labor: 'cost_labor:view' }
const defaultTab = tabOrder.find(t => authStore.hasPermission(tabPermMap[t])) || 'ai'
const activeTab = ref(defaultTab)
const loadingAI = ref(false)
const loadingOut = ref(false)
const loadingOH = ref(false)
const loadingLabor = ref(false)
const aiCosts = ref([])
const outCosts = ref([])
const overheadCosts = ref([])
const laborOverrides = ref([])
const projects = ref([])
const users = ref([])
const showAIForm = ref(false)
const showOutForm = ref(false)
const showOHForm = ref(false)
const showLaborForm = ref(false)
const projectMap = computed(() => {
const m = {}; projects.value.forEach(p => m[p.id] = p.name); return m
})
const userMap = computed(() => {
const m = {}; users.value.forEach(u => m[u.id] = u.name); return m
})
const today = new Date().toISOString().split('T')[0]
const currentMonth = today.slice(0, 7)
const aiForm = reactive({ tool_name: '', subscription_period: '月', amount: 0, allocation_type: '内容组整体', project_id: null, record_date: today })
const outForm = reactive({ project_id: null, outsource_type: '动画', episode_start: 1, episode_end: 1, amount: 0, record_date: today })
const ohForm = reactive({ cost_type: '办公室租金', amount: 0, record_month: currentMonth, note: '' })
const laborForm = reactive({ user_id: null, project_id: null, date: today, override_amount: 0, reason: '' })
async function loadAI() { loadingAI.value = true; try { aiCosts.value = await costApi.listAITools() } finally { loadingAI.value = false } }
async function loadOut() { loadingOut.value = true; try { outCosts.value = await costApi.listOutsource({}) } finally { loadingOut.value = false } }
async function loadOH() { loadingOH.value = true; try { overheadCosts.value = await costApi.listOverhead() } finally { loadingOH.value = false } }
async function loadLabor() { loadingLabor.value = true; try { laborOverrides.value = await costApi.listOverrides({}) } finally { loadingLabor.value = false } }
async function createAI() {
await costApi.createAITool(aiForm); ElMessage.success('已添加'); showAIForm.value = false; loadAI()
@ -206,6 +266,11 @@ async function createOut() {
async function createOH() {
await costApi.createOverhead(ohForm); ElMessage.success('已添加'); showOHForm.value = false; loadOH()
}
async function createLabor() {
if (!laborForm.user_id) { ElMessage.warning('请选择调整成员'); return }
if (!laborForm.project_id) { ElMessage.warning('请选择所属项目'); return }
await costApi.createOverride(laborForm); ElMessage.success('已添加'); showLaborForm.value = false; loadLabor()
}
async function deleteAI(id) {
await ElMessageBox.confirm('确认删除?'); await costApi.deleteAITool(id); ElMessage.success('已删除'); loadAI()
}
@ -221,7 +286,11 @@ onMounted(async () => {
if (authStore.hasPermission('cost_ai:view')) loadAI()
if (authStore.hasPermission('cost_outsource:view')) loadOut()
if (authStore.hasPermission('cost_overhead:view')) loadOH()
if (authStore.hasPermission('cost_labor:view')) loadLabor()
try { projects.value = await projectApi.list({}) } catch {}
if (authStore.hasPermission('cost_labor:view') || authStore.hasPermission('cost_labor:create')) {
try { users.value = await userApi.list() } catch {}
}
})
</script>

View File

@ -9,7 +9,7 @@
<div class="stat-label">进行中项目</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card" v-if="authStore.hasPermission('dashboard:view_cost')">
<div class="stat-icon orange"><el-icon :size="20"><Money /></el-icon></div>
<div class="stat-body">
<div class="stat-value">¥{{ formatNum(data.monthly_labor_cost) }}</div>
@ -30,7 +30,7 @@
<div class="stat-label">人均日产出</div>
</div>
</div>
<div class="stat-card" v-if="data.profitability">
<div class="stat-card" v-if="data.profitability && authStore.hasPermission('dashboard:view_profit')">
<div class="stat-icon" :class="data.profitability.total_profit >= 0 ? 'green' : 'red'">
<el-icon :size="20"><Coin /></el-icon>
</div>
@ -41,7 +41,7 @@
<div class="stat-label">已结算利润</div>
</div>
</div>
<div class="stat-card" v-if="data.profitability">
<div class="stat-card" v-if="data.profitability && authStore.hasPermission('dashboard:view_profit')">
<div class="stat-icon" :class="data.profitability.profit_rate >= 0 ? 'green' : 'red'">
<el-icon :size="20"><DataAnalysis /></el-icon>
</div>
@ -52,7 +52,7 @@
<div class="stat-label">利润率</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card" v-if="authStore.hasPermission('dashboard:view_waste')">
<div class="stat-icon" :class="(data.total_waste_rate || 0) > 20 ? 'red' : 'orange'">
<el-icon :size="20"><WarnTriangleFilled /></el-icon>
</div>
@ -67,7 +67,7 @@
</div>
<!-- 风险预警 -->
<div class="card risk-card" v-if="data.risk_alerts?.length">
<div class="card risk-card" v-if="data.risk_alerts?.length && authStore.hasPermission('dashboard:view_risk')">
<div class="card-header">
<span class="card-title">
<el-icon :size="16" style="color:#FF9500;margin-right:6px;vertical-align:-2px"><WarnTriangleFilled /></el-icon>
@ -95,11 +95,11 @@
<!-- 图表行产出趋势 + 成本构成 -->
<div class="chart-row">
<div class="card chart-card wide">
<div class="card chart-card" :class="authStore.hasPermission('dashboard:view_cost') ? 'wide' : 'full-width'">
<div class="card-header"><span class="card-title"> 30 天产出趋势</span></div>
<div class="card-body"><div ref="trendChartRef" class="chart-container"></div></div>
</div>
<div class="card chart-card narrow">
<div v-if="authStore.hasPermission('dashboard:view_cost')" class="card chart-card narrow">
<div class="card-header"><span class="card-title">成本构成</span></div>
<div class="card-body"><div ref="costChartRef" class="chart-container"></div></div>
</div>
@ -132,7 +132,7 @@
</div>
<div class="progress-meta">
<span>{{ formatSecs(p.submitted_seconds) }} / {{ formatSecs(p.target_seconds) }}</span>
<span v-if="p.waste_rate > 0" :style="{color: p.waste_rate > 30 ? '#FF3B30' : '#8F959E'}">
<span v-if="p.waste_rate > 0 && authStore.hasPermission('dashboard:view_waste')" :style="{color: p.waste_rate > 30 ? '#FF3B30' : '#8F959E'}">
损耗 {{ p.waste_rate }}%
<template v-if="p.waste_hours > 0"> · 工时{{ p.waste_hours }}h</template>
</span>
@ -144,18 +144,18 @@
<!-- 图表行项目产出对比 + 损耗排行 -->
<div class="chart-row">
<div class="card chart-card half">
<div class="card chart-card" :class="authStore.hasPermission('dashboard:view_waste') ? 'half' : 'full-width'">
<div class="card-header"><span class="card-title">项目产出对比</span></div>
<div class="card-body"><div ref="comparisonChartRef" class="chart-container"></div></div>
</div>
<div class="card chart-card half">
<div v-if="authStore.hasPermission('dashboard:view_waste')" class="card chart-card half">
<div class="card-header"><span class="card-title">损耗排行</span></div>
<div class="card-body"><div ref="wasteChartRef" class="chart-container"></div></div>
</div>
</div>
<!-- 盈利分析图表 -->
<div class="chart-row" v-if="data.profitability?.profit_by_project?.length">
<div class="chart-row" v-if="data.profitability?.profit_by_project?.length && authStore.hasPermission('dashboard:view_profit')">
<div class="card chart-card full-width">
<div class="card-header">
<span class="card-title">项目盈亏分析</span>
@ -168,7 +168,7 @@
</div>
<!-- 已结算项目 -->
<div class="card" v-if="data.settled_projects?.length">
<div class="card" v-if="data.settled_projects?.length && authStore.hasPermission('dashboard:view_profit')">
<div class="card-header"><span class="card-title">已结算项目</span></div>
<div class="card-body">
<el-table :data="data.settled_projects" size="small">

View File

@ -12,11 +12,11 @@
<!-- 基本信息 -->
<div class="card info-card">
<div class="info-grid">
<div class="info-item">
<div class="info-item" v-if="authStore.hasPermission('user:view_cost')">
<span class="info-label">日成本</span>
<span class="info-value">¥{{ member.daily_cost || 0 }}</span>
</div>
<div class="info-item">
<div class="info-item" v-if="authStore.hasPermission('user:view_cost')">
<span class="info-label">月总成本</span>
<span class="info-value">¥{{ (member.monthly_total_cost || 0).toLocaleString() }}</span>
</div>
@ -117,7 +117,9 @@
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { userApi, submissionApi } from '../api'
import { useAuthStore } from '../stores/auth'
const authStore = useAuthStore()
const route = useRoute()
const loading = ref(false)
const member = ref({})

View File

@ -35,6 +35,11 @@
<template #default="{row}">{{ row.hours_spent ? row.hours_spent + 'h' : '—' }}</template>
</el-table-column>
<el-table-column prop="description" label="描述" show-overflow-tooltip />
<el-table-column v-if="authStore.hasPermission('submission:create')" label="操作" width="70" fixed="right">
<template #default="{row}">
<el-button text type="primary" size="small" @click="openEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增提交对话框 -->
@ -130,6 +135,93 @@
<el-button type="primary" :loading="creating" @click="handleCreate">提交</el-button>
</template>
</el-dialog>
<!-- 编辑提交对话框 -->
<el-dialog v-model="showEdit" title="编辑提交记录" width="580px" destroy-on-close>
<el-form :model="editForm" label-width="110px" label-position="left">
<el-form-item label="所属项目">
<el-input :model-value="editForm._project_name" disabled />
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="项目阶段" required>
<el-select v-model="editForm.project_phase" style="width:100%">
<el-option label="前期" value="前期" />
<el-option label="制作" value="制作" />
<el-option label="后期" value="后期" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工作类型" required>
<el-select v-model="editForm.work_type" style="width:100%">
<el-option label="制作" value="制作" />
<el-option label="测试" value="测试" />
<el-option label="方案" value="方案" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="内容类型" required>
<el-select v-model="editForm.content_type" style="width:100%">
<el-option-group label="前期">
<el-option label="策划案" value="策划案" />
<el-option label="剧本" value="剧本" />
<el-option label="分镜" value="分镜" />
<el-option label="人设图" value="人设图" />
<el-option label="场景图" value="场景图" />
</el-option-group>
<el-option-group label="制作">
<el-option label="动画制作" value="动画制作" />
</el-option-group>
<el-option-group label="后期">
<el-option label="配音" value="配音" />
<el-option label="音效" value="音效" />
<el-option label="修补镜头" value="修补镜头" />
<el-option label="剪辑" value="剪辑" />
</el-option-group>
<el-option label="其他" value="其他" />
</el-select>
</el-form-item>
<el-form-item label="产出时长">
<div class="inline-field">
<el-input-number v-model="editForm.duration_minutes" :min="0" :step="1" style="width:120px" />
<span class="field-unit"></span>
<el-input-number v-model="editForm.duration_seconds" :min="0" :max="59" :step="5" style="width:120px" />
<span class="field-unit"></span>
</div>
</el-form-item>
<el-form-item label="投入时长">
<div class="inline-field">
<el-input-number v-model="editForm.hours_spent" :min="0" :step="0.5" :precision="1" style="width:140px" />
<span class="field-unit">小时</span>
</div>
</el-form-item>
<el-form-item label="提交对象" required>
<el-select v-model="editForm.submit_to" style="width:100%">
<el-option label="组长 — 日常提交给组长" value="组长" />
<el-option label="制片 — 直接提交给制片人" value="制片" />
<el-option label="内部 — 内部评审/测试" value="内部" />
<el-option label="外部 — 提交给甲方/客户" value="外部" />
</el-select>
</el-form-item>
<el-form-item label="制作描述">
<el-input v-model="editForm.description" type="textarea" :rows="2" />
</el-form-item>
<el-form-item label="提交日期" required>
<el-date-picker v-model="editForm.submit_date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
<el-form-item required>
<template #label><span style="color:#FF3B30">修改原因</span></template>
<el-input v-model="editForm.change_reason" type="textarea" :rows="2"
placeholder="请说明修改原因(必填)" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showEdit = false">取消</el-button>
<el-button type="primary" :loading="editing" @click="handleUpdate">保存修改</el-button>
</template>
</el-dialog>
</div>
</template>
@ -137,6 +229,9 @@
import { ref, computed, onMounted, reactive, watch } from 'vue'
import { submissionApi, projectApi } from '../api'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '../stores/auth'
const authStore = useAuthStore()
const CONTENT_PHASE_MAP = {
'策划案': '前期', '剧本': '前期', '分镜': '前期', '人设图': '前期', '场景图': '前期',
@ -223,6 +318,59 @@ async function handleCreate() {
} finally { creating.value = false }
}
//
const showEdit = ref(false)
const editing = ref(false)
const editForm = reactive({
_id: null,
_project_name: '',
project_phase: '',
work_type: '',
content_type: '',
duration_minutes: 0,
duration_seconds: 0,
hours_spent: null,
submit_to: '',
description: '',
submit_date: '',
change_reason: '',
})
function openEdit(row) {
editForm._id = row.id
editForm._project_name = row.project_name
editForm.project_phase = row.project_phase
editForm.work_type = row.work_type
editForm.content_type = row.content_type
editForm.duration_minutes = row.duration_minutes || 0
editForm.duration_seconds = row.duration_seconds || 0
editForm.hours_spent = row.hours_spent
editForm.submit_to = row.submit_to
editForm.description = row.description || ''
editForm.submit_date = row.submit_date
editForm.change_reason = ''
showEdit.value = true
}
async function handleUpdate() {
if (!editForm.change_reason?.trim()) {
ElMessage.warning('请填写修改原因')
return
}
editing.value = true
try {
const { _id, _project_name, ...payload } = editForm
await submissionApi.update(_id, payload)
ElMessage.success('修改成功')
showEdit.value = false
load()
} catch {
// axios
} finally {
editing.value = false
}
}
onMounted(async () => {
load()
try { projects.value = await projectApi.list({}) } catch {}

View File

@ -2,7 +2,7 @@
<div>
<div class="page-header">
<h2>用户管理</h2>
<el-button type="primary" @click="showCreate = true"><el-icon><Plus /></el-icon> 新增用户</el-button>
<el-button v-if="authStore.hasPermission('user:manage')" type="primary" @click="showCreate = true"><el-icon><Plus /></el-icon> 新增用户</el-button>
</div>
<el-table :data="sortedUsers" v-loading="loading" stripe>
@ -18,27 +18,27 @@
<el-tag size="small">{{ row.role_name }}</el-tag>
</template>
</el-table-column>
<el-table-column label="月薪" width="100" align="right">
<el-table-column v-if="canViewCost" label="月薪" width="100" align="right">
<template #default="{row}">¥{{ row.monthly_salary.toLocaleString() }}</template>
</el-table-column>
<el-table-column label="奖金" width="90" align="right">
<el-table-column v-if="canViewCost" label="奖金" width="90" align="right">
<template #default="{row}">{{ row.bonus > 0 ? '¥' + row.bonus.toLocaleString() : '—' }}</template>
</el-table-column>
<el-table-column label="五险一金" width="100" align="right">
<el-table-column v-if="canViewCost" label="五险一金" width="100" align="right">
<template #default="{row}">{{ row.social_insurance > 0 ? '¥' + row.social_insurance.toLocaleString() : '—' }}</template>
</el-table-column>
<el-table-column label="月总成本" width="110" align="right">
<el-table-column v-if="canViewCost" label="月总成本" width="110" align="right">
<template #default="{row}">
<span style="font-weight:600;color:var(--text-primary)">¥{{ row.monthly_total_cost.toLocaleString() }}</span>
</template>
</el-table-column>
<el-table-column label="日成本" width="90" align="right">
<el-table-column v-if="canViewCost" label="日成本" width="90" align="right">
<template #default="{row}">¥{{ row.daily_cost.toLocaleString() }}</template>
</el-table-column>
<el-table-column label="状态" width="70">
<template #default="{row}"><el-tag :type="row.is_active ? 'success' : 'danger'" size="small">{{ row.is_active ? '启用' : '停用' }}</el-tag></template>
</el-table-column>
<el-table-column label="操作" width="80">
<el-table-column v-if="authStore.hasPermission('user:manage')" label="操作" width="80">
<template #default="{row}">
<el-button text size="small" @click="editUser(row)">编辑</el-button>
</template>
@ -92,8 +92,12 @@
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { userApi, roleApi } from '../api'
import { useAuthStore } from '../stores/auth'
import { ElMessage } from 'element-plus'
const authStore = useAuthStore()
const canViewCost = computed(() => authStore.hasPermission('user:view_cost'))
const loading = ref(false)
const showCreate = ref(false)
const editingId = ref(null)