feat: 权限精细化 + 成本人力调整 + 仪表盘模块权限
- 用户管理页面权限拆分: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:
parent
dc42306c24
commit
a43bed1d64
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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' },
|
||||
]
|
||||
|
||||
|
||||
@ -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' } },
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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({})
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user