20261008-ide1.0发布 #1

Merged
zzh merged 24 commits from test into master 2026-10-08 09:19:21 +08:00
9 changed files with 204 additions and 6 deletions
Showing only changes of commit 9f9107544f - Show all commits

View File

@ -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
### 表单

View File

@ -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

View File

@ -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 }

View File

@ -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;

View File

@ -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);

View File

@ -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',

View File

@ -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': '请输入组织名称',

View File

@ -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 {

View File

@ -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"