- 侧边栏新增「修改密码」入口,任何用户可改自己的密码(需验证原密码) - 用户管理编辑弹窗新增「重置密码」区域,管理员可直接重设任意用户密码 - 后端新增 POST /api/users/change-password 接口 - UserUpdate schema 增加 password 可选字段 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
175 lines
8.2 KiB
Vue
175 lines
8.2 KiB
Vue
<template>
|
||
<div>
|
||
<div class="page-header">
|
||
<h2>用户管理</h2>
|
||
<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>
|
||
<el-table-column label="姓名" width="100">
|
||
<template #default="{row}">
|
||
<router-link :to="`/users/${row.id}/detail`" class="user-link">{{ row.name }}</router-link>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="username" label="用户名" width="120" />
|
||
<el-table-column prop="phase_group" label="阶段" width="70" />
|
||
<el-table-column label="角色" width="80">
|
||
<template #default="{row}">
|
||
<el-tag size="small">{{ row.role_name }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<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 v-if="canViewCost" label="奖金" width="90" align="right">
|
||
<template #default="{row}">{{ row.bonus > 0 ? '¥' + row.bonus.toLocaleString() : '—' }}</template>
|
||
</el-table-column>
|
||
<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 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 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 v-if="authStore.hasPermission('user:manage')" label="操作" width="80">
|
||
<template #default="{row}">
|
||
<el-button text size="small" @click="editUser(row)">编辑</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<!-- 新增/编辑对话框 -->
|
||
<el-dialog v-model="showCreate" :title="editingId ? '编辑用户' : '新增用户'" width="520px" destroy-on-close>
|
||
<el-form :model="form" label-width="90px">
|
||
<el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>
|
||
<el-form-item v-if="!editingId" label="用户名"><el-input v-model="form.username" /></el-form-item>
|
||
<el-form-item v-if="!editingId" label="密码"><el-input v-model="form.password" type="password" /></el-form-item>
|
||
<el-form-item label="所属阶段">
|
||
<el-select v-model="form.phase_group" placeholder="该成员主要参与的制作阶段" style="width:100%">
|
||
<el-option label="前期 — 剧本/策划/设定" value="前期" />
|
||
<el-option label="制作 — 动画/图片制作" value="制作" />
|
||
<el-option label="后期 — 剪辑/合成/调色" value="后期" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="角色">
|
||
<el-select v-model="form.role_id" placeholder="系统权限角色" style="width:100%">
|
||
<el-option v-for="r in roles" :key="r.id" :label="r.name" :value="r.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-divider content-position="left">成本信息</el-divider>
|
||
<el-form-item label="月薪">
|
||
<el-input-number v-model="form.monthly_salary" :min="0" :step="1000" :controls="false" placeholder="基本月薪(元)" style="width:100%" />
|
||
</el-form-item>
|
||
<el-form-item label="奖金">
|
||
<el-input-number v-model="form.bonus" :min="0" :step="500" :controls="false" placeholder="月度奖金(元),无则填0" style="width:100%" />
|
||
</el-form-item>
|
||
<el-form-item label="五险一金">
|
||
<el-input-number v-model="form.social_insurance" :min="0" :step="500" :controls="false" placeholder="公司承担的五险一金(元/月)" style="width:100%" />
|
||
</el-form-item>
|
||
<div class="cost-summary" v-if="form.monthly_salary || form.bonus || form.social_insurance">
|
||
月总成本 = ¥{{ (form.monthly_salary || 0).toLocaleString() }} + ¥{{ (form.bonus || 0).toLocaleString() }} + ¥{{ (form.social_insurance || 0).toLocaleString() }} = <strong>¥{{ ((form.monthly_salary || 0) + (form.bonus || 0) + (form.social_insurance || 0)).toLocaleString() }}</strong>
|
||
→ 日成本 <strong>¥{{ (((form.monthly_salary || 0) + (form.bonus || 0) + (form.social_insurance || 0)) / 22).toFixed(0) }}</strong>
|
||
</div>
|
||
<el-form-item v-if="editingId" label="状态" style="margin-top:16px">
|
||
<el-switch v-model="form.is_active" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="停用" />
|
||
</el-form-item>
|
||
<el-divider v-if="editingId" content-position="left">重置密码</el-divider>
|
||
<el-form-item v-if="editingId" label="新密码">
|
||
<el-input v-model="form.password" type="password" show-password placeholder="留空则不修改密码" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="showCreate = false">取消</el-button>
|
||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<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)
|
||
const users = ref([])
|
||
const roles = ref([])
|
||
|
||
const ROLE_ORDER = ['超级管理员', '主管', '组长', '组员']
|
||
const sortedUsers = computed(() => {
|
||
return [...users.value].sort((a, b) => {
|
||
const ia = ROLE_ORDER.indexOf(a.role_name)
|
||
const ib = ROLE_ORDER.indexOf(b.role_name)
|
||
const ai = ia >= 0 ? ia : 999
|
||
const bi = ib >= 0 ? ib : 999
|
||
return ai - bi
|
||
})
|
||
})
|
||
const form = reactive({
|
||
username: '', password: '', name: '', phase_group: '制作', role_id: null,
|
||
monthly_salary: 0, bonus: 0, social_insurance: 0, is_active: 1,
|
||
})
|
||
|
||
async function load() { loading.value = true; try { users.value = await userApi.list() } finally { loading.value = false } }
|
||
|
||
function editUser(u) {
|
||
editingId.value = u.id
|
||
Object.assign(form, {
|
||
name: u.name, phase_group: u.phase_group, role_id: u.role_id,
|
||
monthly_salary: u.monthly_salary, bonus: u.bonus || 0,
|
||
social_insurance: u.social_insurance || 0, is_active: u.is_active,
|
||
password: '',
|
||
})
|
||
showCreate.value = true
|
||
}
|
||
|
||
async function handleSave() {
|
||
if (editingId.value) {
|
||
const data = {
|
||
name: form.name, phase_group: form.phase_group, role_id: form.role_id,
|
||
monthly_salary: form.monthly_salary, bonus: form.bonus,
|
||
social_insurance: form.social_insurance, is_active: form.is_active,
|
||
}
|
||
if (form.password) data.password = form.password
|
||
await userApi.update(editingId.value, data)
|
||
} else {
|
||
await userApi.create(form)
|
||
}
|
||
ElMessage.success('已保存')
|
||
showCreate.value = false
|
||
editingId.value = null
|
||
load()
|
||
}
|
||
|
||
onMounted(async () => {
|
||
load()
|
||
try { roles.value = await roleApi.list() } catch {}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||
.page-header h2 { font-size: 18px; font-weight: 600; }
|
||
.cost-summary {
|
||
background: #F7F8FA; border-radius: 6px; padding: 10px 16px;
|
||
font-size: 13px; color: var(--text-secondary); margin: -4px 0 8px;
|
||
line-height: 1.6;
|
||
}
|
||
.cost-summary strong { color: var(--text-primary); }
|
||
.user-link { color: #3370FF; text-decoration: none; font-weight: 500; }
|
||
.user-link:hover { text-decoration: underline; }
|
||
</style>
|