feat: 用户编辑重置密码、顶栏下载打印控件,并禁用 outline 按钮
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m22s
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m22s
补齐系统用户重置密码入口与网盘下载提示;规范禁止 type=outline;顺带修复揽收单下游预报后刷新列表。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
da620fbe13
commit
9f9107544f
@ -20,6 +20,7 @@ alwaysApply: true
|
||||
|
||||
### 组件
|
||||
- 按钮 32px 高,border-radius 4px,间距 8px
|
||||
- **禁止** `a-button type="outline"`(Arco 默认描边与主题不搭);主操作用 `primary` / `app-btn-primary-soft`,查询用 `app-btn-search`,次要用默认按钮或抽屉内软按钮
|
||||
- 优先 FormPro、TablePro、ModalPro、CustomList、HandsontablePro
|
||||
|
||||
### 表单
|
||||
|
||||
@ -197,12 +197,17 @@ font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
|
||||
### 5.1 按钮
|
||||
|
||||
- 高度:`32px`,`border-radius: 4px`
|
||||
- 高度:`32px`,`border-radius: 4px`(抽屉/卡片内软操作可用 `12px`,与亚克力面板一致)
|
||||
- 图标按钮:`32px × 32px` 正方形
|
||||
- **相邻按钮间距**:统一 `8px`
|
||||
- 使用 `a-space :size="8"` 或 `gap: 8px` / `gap: var(--app-button-gap)`
|
||||
- 适用于工具栏、表格操作列、弹窗底部按钮组
|
||||
- 主操作使用 `type="primary"`
|
||||
- **推荐用法**
|
||||
- 主操作:`type="primary"`,或工具栏新增用 `app-btn-primary-soft`
|
||||
- 查询:`app-btn-search`
|
||||
- 次要/取消:默认 `a-button`(无 type)或抽屉内与面板同风格的软按钮
|
||||
- 表格行内操作:优先图标按钮 / `type="text"`(见操作列规范)
|
||||
- **禁止**:Arco `type="outline"` 默认描边按钮(灰硬边框、白底,与主题亚克力/软主色体系完全不搭)。新页面与改版时一律不要使用;次要操作改用上文推荐样式。
|
||||
|
||||
### 5.2 输入类(Input / Select / Picker)
|
||||
|
||||
@ -541,7 +546,7 @@ const { t } = useI18n();
|
||||
- [ ] 白卡片与 **`.list-wrap`** 内边距勿重复叠加(推荐白卡片 `padding: 0` + 分区 padding)
|
||||
- [ ] **`.list-wrap`**:`padding: 16px`、`flex: 1; min-height: 0`;需测高时 `ref` 挂在本层
|
||||
- [ ] 工具栏使用 `table-toolbar`、`toolbar-left`、`toolbar-right`,工具栏 **`padding: 16px 16px 0`**
|
||||
- [ ] 按钮、输入框符合全局样式(高度、圆角)
|
||||
- [ ] 按钮、输入框符合全局样式(高度、圆角);未使用 `type="outline"` 描边按钮
|
||||
- [ ] 使用主题色变量,不硬编码 `#1d7281`
|
||||
- [ ] 弹窗 / 抽屉使用 ModalPro / DrawerPro + useModal
|
||||
- [ ] 表单使用 FormPro 或统一 label/span
|
||||
|
||||
@ -7,6 +7,10 @@ function internalUserListUrl() {
|
||||
return resolveAdminApiPath('/rbac/user/internal-list')
|
||||
}
|
||||
|
||||
function resetPasswordUrl() {
|
||||
return resolveAdminApiPath('/rbac/user/reset-password')
|
||||
}
|
||||
|
||||
/** 内部用户列表(排除纯客户账号),响应结构与 UserDtoPagedResultDto 兼容 */
|
||||
export async function getInternalUserList(params: {
|
||||
keyword?: string
|
||||
@ -18,4 +22,9 @@ export async function getInternalUserList(params: {
|
||||
return res.data
|
||||
}
|
||||
|
||||
/** 管理员重置用户密码(无需旧密码) */
|
||||
export async function resetUserPassword(id: string, newPassword: string): Promise<void> {
|
||||
await axiosInstance.post(resetPasswordUrl(), { newPassword }, { params: { id } })
|
||||
}
|
||||
|
||||
export type { UserDto }
|
||||
|
||||
@ -51,6 +51,16 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isEdit && canResetPassword" class="ue-preview__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="ue-preview__action"
|
||||
:disabled="resetPwdLoading"
|
||||
@click="openResetPasswordModal"
|
||||
>
|
||||
{{ t('system.user.actions.resetPassword') }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<a-form
|
||||
@ -248,19 +258,54 @@
|
||||
</a-button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="resetPwdVisible"
|
||||
:title="t('system.user.modal.resetPasswordTitle')"
|
||||
:ok-loading="resetPwdLoading"
|
||||
unmount-on-close
|
||||
@ok="handleResetPasswordSubmit"
|
||||
@cancel="closeResetPasswordModal"
|
||||
>
|
||||
<p class="ue-reset-tip">
|
||||
{{
|
||||
t('system.user.modal.resetPasswordContentSingle', {
|
||||
account: formState.account || t('system.user.drawer.accountFallback'),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item :label="t('system.user.form.newPassword')" required>
|
||||
<a-input-password
|
||||
v-model="resetPwdForm.newPassword"
|
||||
:placeholder="t('system.user.form.newPasswordPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('system.user.form.confirmPassword')" required>
|
||||
<a-input-password
|
||||
v-model="resetPwdForm.confirmPassword"
|
||||
:placeholder="t('system.user.form.confirmPasswordPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { IconClose, IconPlus } from '@arco-design/web-vue/es/icon'
|
||||
import EmployeeProfileFields from '@/components/EmployeeProfileFields/index.vue'
|
||||
import { resetUserPassword } from '@/api/rbac/internal-user'
|
||||
import type { EmployeeFormState } from '@/types/employee-profile'
|
||||
import type { UserFormExtState } from '@/types/rbac-user-profile'
|
||||
import { resolveAvatarUrl } from '@/utils/avatar'
|
||||
import { buildAdvisorInviteRegisterUrl } from '@/utils/inviteLink'
|
||||
import { hasPermission } from '@/utils/rbac-permission'
|
||||
import { $baseMessage } from '@/utils'
|
||||
import { readFileAsDataUrl } from '@/utils/image-data-url'
|
||||
|
||||
@ -279,6 +324,7 @@ const props = withDefaults(
|
||||
visible: boolean
|
||||
confirmLoading?: boolean
|
||||
isEdit?: boolean
|
||||
userId?: string
|
||||
formState: UserEditFormState
|
||||
employeeFormState: EmployeeFormState
|
||||
organizationTreeData?: any[]
|
||||
@ -289,6 +335,7 @@ const props = withDefaults(
|
||||
{
|
||||
confirmLoading: false,
|
||||
isEdit: false,
|
||||
userId: '',
|
||||
organizationTreeData: () => [],
|
||||
organizationLoading: false,
|
||||
roleOptions: () => [],
|
||||
@ -304,6 +351,16 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const formRef = ref()
|
||||
const resetPwdVisible = ref(false)
|
||||
const resetPwdLoading = ref(false)
|
||||
const resetPwdForm = reactive({
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
|
||||
const canResetPassword = computed(
|
||||
() => hasPermission('Rbac.Users.ResetPassword') || hasPermission('Rbac.Users.Update'),
|
||||
)
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
const raw = props.formState.avatar?.trim()
|
||||
@ -358,6 +415,55 @@ async function copyAdvisorInviteLink() {
|
||||
}
|
||||
}
|
||||
|
||||
function openResetPasswordModal() {
|
||||
if (!props.userId) {
|
||||
Message.warning(t('system.user.message.resetPasswordFailed'))
|
||||
return
|
||||
}
|
||||
resetPwdForm.newPassword = ''
|
||||
resetPwdForm.confirmPassword = ''
|
||||
resetPwdVisible.value = true
|
||||
}
|
||||
|
||||
function closeResetPasswordModal() {
|
||||
resetPwdVisible.value = false
|
||||
resetPwdForm.newPassword = ''
|
||||
resetPwdForm.confirmPassword = ''
|
||||
}
|
||||
|
||||
async function handleResetPasswordSubmit() {
|
||||
const newPassword = resetPwdForm.newPassword.trim()
|
||||
const confirmPassword = resetPwdForm.confirmPassword.trim()
|
||||
if (!newPassword) {
|
||||
Message.warning(t('system.user.rules.password.required'))
|
||||
return Promise.reject()
|
||||
}
|
||||
if (newPassword.length < 6) {
|
||||
Message.warning(t('system.user.rules.password.minLength'))
|
||||
return Promise.reject()
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
Message.warning(t('system.user.rules.password.confirmMismatch'))
|
||||
return Promise.reject()
|
||||
}
|
||||
if (!props.userId) {
|
||||
Message.warning(t('system.user.message.resetPasswordFailed'))
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
resetPwdLoading.value = true
|
||||
try {
|
||||
await resetUserPassword(props.userId, newPassword)
|
||||
Message.success(t('system.user.message.resetPasswordSuccess'))
|
||||
closeResetPasswordModal()
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || t('system.user.message.resetPasswordFailed'))
|
||||
return Promise.reject(e)
|
||||
} finally {
|
||||
resetPwdLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onVisibleChange(val: boolean) {
|
||||
emit('update:visible', val)
|
||||
}
|
||||
@ -504,6 +610,50 @@ defineExpose({
|
||||
color: var(--ue-ink);
|
||||
}
|
||||
|
||||
.ue-preview__meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ue-preview__actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ue-preview__action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--ue-edge);
|
||||
border-radius: 12px;
|
||||
background: var(--ue-panel-bg);
|
||||
color: var(--app-primary-color);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 22%, var(--ue-edge));
|
||||
background: var(--app-acrylic-hover-bg);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.ue-reset-tip {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--ue-muted);
|
||||
}
|
||||
|
||||
.ue-preview__avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
|
||||
@ -206,6 +206,23 @@
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="top-nav-right-item top-nav-right-item--icon">
|
||||
<a-tooltip position="bottom" :mouse-enter-delay="0" :mouse-leave-delay="0">
|
||||
<template #content>
|
||||
<span>{{ t('nav.printControl.download') }}</span>
|
||||
</template>
|
||||
<a
|
||||
class="print-connection-trigger print-control-download"
|
||||
:href="PRINT_CONTROL_DOWNLOAD_URL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:aria-label="t('nav.printControl.download')"
|
||||
>
|
||||
<icon-download class="print-connection-icon" />
|
||||
</a>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="top-nav-right-item top-nav-right-item--warehouse">
|
||||
<ContactService variant="navbar" />
|
||||
</div>
|
||||
@ -246,7 +263,7 @@ import RightTopUser from './RightTopUser.vue';
|
||||
import RightTopMessage from './RightTopMessage.vue';
|
||||
import SelectLang from './SelectLang.vue';
|
||||
import ContactService from '@/components/ContactService/index.vue';
|
||||
import { IconPrinter } from '@arco-design/web-vue/es/icon';
|
||||
import { IconPrinter, IconDownload } from '@arco-design/web-vue/es/icon';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { openWindow, regexUrl } from '@/utils';
|
||||
import { MenuService, AuthService } from '@/api/rbac';
|
||||
@ -277,6 +294,9 @@ const topMenuCon = ref<HTMLElement | null>(null);
|
||||
const topNavEnable = computed(() => true); // 启用顶部导航
|
||||
const tabNavEnable = computed(() => appStore.tabBar);
|
||||
const printConnectionState = useHiprintClientState();
|
||||
/** 打印控件安装包(百度网盘,提取码 idex) */
|
||||
const PRINT_CONTROL_DOWNLOAD_URL =
|
||||
'https://pan.baidu.com/s/1FrpI1prvtRKG1f9jXYSL1A?pwd=idex';
|
||||
|
||||
const menuItemEls = new Map<string, HTMLElement>();
|
||||
const indicatorReady = ref(false);
|
||||
|
||||
@ -1502,6 +1502,7 @@ export default {
|
||||
'nav.balance.recharge': 'Recharge',
|
||||
'nav.balance.rechargeRecord': 'Recharge records',
|
||||
'nav.balance.accountFlow': 'Account transactions',
|
||||
'nav.printControl.download': 'Download print control / one-click parcel plugin',
|
||||
|
||||
// Order - My Orders
|
||||
'order.myOrders.tabs.all': 'All',
|
||||
@ -2959,6 +2960,11 @@ export default {
|
||||
'system.user.modal.resetPasswordTitle': 'Reset password confirmation',
|
||||
'system.user.modal.resetPasswordContentSingle': 'Are you sure to reset password for user "{account}"?',
|
||||
'system.user.modal.resetPasswordContentBatch': 'Are you sure to reset password for {count} selected users?',
|
||||
'system.user.form.newPassword': 'New password',
|
||||
'system.user.form.newPasswordPlaceholder': 'Enter new password (min 6 chars)',
|
||||
'system.user.form.confirmPassword': 'Confirm password',
|
||||
'system.user.form.confirmPasswordPlaceholder': 'Enter new password again',
|
||||
'system.user.rules.password.confirmMismatch': 'The two passwords do not match',
|
||||
|
||||
// System - Role Management
|
||||
'system.role.search.placeholder.orgName': 'Enter organization name',
|
||||
|
||||
@ -1286,6 +1286,7 @@ export default {
|
||||
'nav.balance.recharge': '去充值',
|
||||
'nav.balance.rechargeRecord': '充值记录',
|
||||
'nav.balance.accountFlow': '资金流水',
|
||||
'nav.printControl.download': '下载打印控件/一键添加包裹插件',
|
||||
|
||||
// 订单 - 批量任务
|
||||
'order.batchJobs.filter.jobId': '任务 ID',
|
||||
@ -2851,6 +2852,11 @@ export default {
|
||||
'system.user.modal.resetPasswordTitle': '重置密码确认',
|
||||
'system.user.modal.resetPasswordContentSingle': '确定要重置用户"{account}"的密码吗?',
|
||||
'system.user.modal.resetPasswordContentBatch': '确定要重置选中的 {count} 个用户的密码吗?',
|
||||
'system.user.form.newPassword': '新密码',
|
||||
'system.user.form.newPasswordPlaceholder': '请输入新密码(至少6位)',
|
||||
'system.user.form.confirmPassword': '确认新密码',
|
||||
'system.user.form.confirmPasswordPlaceholder': '请再次输入新密码',
|
||||
'system.user.rules.password.confirmMismatch': '两次输入的密码不一致',
|
||||
|
||||
// 系统 - 角色管理
|
||||
'system.role.search.placeholder.orgName': '请输入组织名称',
|
||||
|
||||
@ -638,7 +638,7 @@ async function handleForwardDownstreamCreate() {
|
||||
}
|
||||
downstreamForwardResultText.value = resultBlocks.join('\n\n')
|
||||
downstreamForwardModalVisible.value = true
|
||||
await loadData()
|
||||
await loadList()
|
||||
} catch (e: any) {
|
||||
$baseMessage(e?.message || t('order.pickupOrder.message.actionFailed'), 'error')
|
||||
} finally {
|
||||
|
||||
@ -67,6 +67,7 @@
|
||||
v-model:visible="modalVisible"
|
||||
:confirm-loading="modalLoading"
|
||||
:is-edit="isEdit"
|
||||
:user-id="currentId"
|
||||
:form-state="formState"
|
||||
:employee-form-state="employeeFormState"
|
||||
:organization-tree-data="organizationTreeData"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user