seaislee1209 74106ac21b
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 1m40s
Build and Deploy Web / build-and-deploy (push) Successful in 2m1s
feat: 密码管理 — 用户自改密码 + 管理员重置密码
- 侧边栏新增「修改密码」入口,任何用户可改自己的密码(需验证原密码)
- 用户管理编辑弹窗新增「重置密码」区域,管理员可直接重设任意用户密码
- 后端新增 POST /api/users/change-password 接口
- UserUpdate schema 增加 password 可选字段

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 15:51:30 +08:00

175 lines
8.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>
&nbsp;&nbsp; 日成本 <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>