style: 代码格式优化
This commit is contained in:
@@ -47,9 +47,11 @@ export default [
|
||||
PageResult: "readonly", // 分页结果数据类型
|
||||
PageQuery: "readonly", // 分页查询数据类型
|
||||
OptionType: "readonly", // 选项类型
|
||||
definePage: "readonly", // uni-pages 页面宏
|
||||
getCurrentPages: "readonly", // uni-app 全局 API
|
||||
wx: "readonly", // 微信小程序 API
|
||||
useToast: "readonly", // wot-design-uni
|
||||
useDialog: "readonly", // wot-design-uni
|
||||
useMessage: "readonly", // wot-design-uni
|
||||
useNotify: "readonly", // wot-design-uni
|
||||
},
|
||||
|
||||
@@ -41,8 +41,11 @@
|
||||
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
||||
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"check": "pnpm type-check && pnpm lint:eslint:check && pnpm lint:stylelint:check",
|
||||
"lint:eslint:check": "eslint \"src/**/*.{vue,ts}\"",
|
||||
"lint:eslint": "eslint \"src/**/*.{vue,ts}\" --fix",
|
||||
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,css,scss,vue,html,md}\"",
|
||||
"lint:stylelint:check": "stylelint \"**/*.{css,scss,vue,html}\"",
|
||||
"lint:stylelint": "stylelint \"**/*.{css,scss,vue,html}\" --fix",
|
||||
"lint:lint-staged": "lint-staged",
|
||||
"prepare": "husky",
|
||||
|
||||
@@ -125,4 +125,4 @@ export interface DictDataItem {
|
||||
sort?: number;
|
||||
status?: number;
|
||||
remark?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,4 +37,4 @@ export const themeColorOptions: ThemeColorOption[] = [
|
||||
|
||||
export interface ProviderThemeVars extends ConfigProviderThemeVars {
|
||||
colorTheme: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,9 @@ function createSseConnection(options: UseSseOptions = {}) {
|
||||
bindEvent(eventName);
|
||||
}
|
||||
}
|
||||
eventHandlers.get(eventName)!.add(handler);
|
||||
const handlers = eventHandlers.get(eventName);
|
||||
if (!handlers) return () => {};
|
||||
handlers.add(handler);
|
||||
log(`已订阅事件: ${eventName}`);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -17,7 +17,11 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-config-provider :theme-vars="themeVars" :custom-class="theme === 'dark' ? 'dark' : ''" :theme="theme === 'dark' ? 'dark' : ''">
|
||||
<wd-config-provider
|
||||
:theme-vars="themeVars"
|
||||
:custom-class="theme === 'dark' ? 'dark' : ''"
|
||||
:theme="theme === 'dark' ? 'dark' : ''"
|
||||
>
|
||||
<slot />
|
||||
<wd-notify />
|
||||
<wd-toast />
|
||||
|
||||
@@ -92,8 +92,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
import { computed, ref } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { onReady, onShow } from "@dcloudio/uni-app";
|
||||
@@ -105,11 +103,11 @@ import { hasPermission } from "@/utils/permission";
|
||||
import LogAPI, { type VisitOverview, type VisitTrend } from "@/api/log";
|
||||
import NoticeAPI, { type NoticeItem } from "@/api/notice";
|
||||
|
||||
definePage({
|
||||
name: "home",
|
||||
style: { navigationStyle: "custom" },
|
||||
layout: "tabbar",
|
||||
});
|
||||
definePage({
|
||||
name: "home",
|
||||
style: { navigationStyle: "custom" },
|
||||
layout: "tabbar",
|
||||
});
|
||||
|
||||
interface NavItem {
|
||||
icon: string;
|
||||
@@ -124,9 +122,7 @@ const { handleNavClick } = useNavigation();
|
||||
const current = ref(0);
|
||||
const recentDaysRange = ref(7);
|
||||
|
||||
const swiperList = ref([
|
||||
"https://www.youlai.tech/storage/youlai/bg02.png"
|
||||
]);
|
||||
const swiperList = ref(["https://www.youlai.tech/storage/youlai/bg02.png"]);
|
||||
|
||||
/** 访问概览数据 */
|
||||
const visitOverviewData = ref<VisitOverview>({
|
||||
@@ -179,7 +175,14 @@ const chartOpts = ref({
|
||||
xAxis: { fontSize: 10, rotateLabel: true, rotateAngle: 30 },
|
||||
yAxis: { disabled: true },
|
||||
extra: {
|
||||
area: { type: "curve", opacity: 0.2, addLine: true, width: 2, gradient: true, activeType: "hollow" },
|
||||
area: {
|
||||
type: "curve",
|
||||
opacity: 0.2,
|
||||
addLine: true,
|
||||
width: 2,
|
||||
gradient: true,
|
||||
activeType: "hollow",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -199,22 +202,28 @@ async function loadNoticeData() {
|
||||
|
||||
/** 加载访问概览统计 */
|
||||
function loadVisitOverviewData() {
|
||||
LogAPI.getVisitOverview().then((data) => (visitOverviewData.value = data)).catch(() => {});
|
||||
LogAPI.getVisitOverview()
|
||||
.then((data) => (visitOverviewData.value = data))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
/** 加载访问趋势图表 */
|
||||
async function loadVisitTrendData() {
|
||||
const endDate = dayjs().format("YYYY-MM-DD");
|
||||
const startDate = dayjs().subtract(recentDaysRange.value - 1, "day").format("YYYY-MM-DD");
|
||||
const startDate = dayjs()
|
||||
.subtract(recentDaysRange.value - 1, "day")
|
||||
.format("YYYY-MM-DD");
|
||||
try {
|
||||
const data: VisitTrend = await LogAPI.getVisitTrend({ startDate, endDate });
|
||||
chartData.value = JSON.parse(JSON.stringify({
|
||||
categories: (data.dates || []).map((d) => dayjs(d).format("MM-DD")),
|
||||
series: [
|
||||
{ name: "访客数", data: data.uvList || [] },
|
||||
{ name: "浏览量", data: data.pvList || [] },
|
||||
],
|
||||
}));
|
||||
chartData.value = JSON.parse(
|
||||
JSON.stringify({
|
||||
categories: (data.dates || []).map((d) => dayjs(d).format("MM-DD")),
|
||||
series: [
|
||||
{ name: "访客数", data: data.uvList || [] },
|
||||
{ name: "浏览量", data: data.pvList || [] },
|
||||
],
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
chartData.value = { categories: [], series: [] };
|
||||
}
|
||||
@@ -280,18 +289,18 @@ onShow(() => {
|
||||
|
||||
.chart-wrapper {
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg);
|
||||
border: 1rpx solid var(--color-border);
|
||||
border-radius: 16rpx;
|
||||
box-shadow: var(--shadow-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-wrapper .chart-header {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx 20rpx;
|
||||
}
|
||||
|
||||
|
||||
@@ -258,7 +258,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { onLoad, onShow } from "@dcloudio/uni-app";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import type { FormSchema } from "@wot-ui/ui/components/wd-form/types";
|
||||
@@ -267,10 +266,10 @@ import { useUserStore } from "@/store/modules/user";
|
||||
import { useCountdown } from "@/composables/useCountdown";
|
||||
import AuthAPI from "@/api/auth";
|
||||
|
||||
definePage({
|
||||
name: "login",
|
||||
style: { navigationStyle: "custom", navigationBarTitleText: "" },
|
||||
});
|
||||
definePage({
|
||||
name: "login",
|
||||
style: { navigationStyle: "custom", navigationBarTitleText: "" },
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const dialog = useDialog("policy-box");
|
||||
@@ -443,8 +442,8 @@ async function doFormLogin() {
|
||||
|
||||
const handleLogin = async () => {
|
||||
// 先校验表单必填项(内联提示,不再弹窗)
|
||||
const { valid } = await loginFormRef.value?.validate();
|
||||
if (!valid) return;
|
||||
const validateResult = await loginFormRef.value?.validate();
|
||||
if (!validateResult?.valid) return;
|
||||
// 再校验隐私协议
|
||||
if (!isAgreePolicy.value) {
|
||||
openPolicyDialog("FORM");
|
||||
@@ -655,8 +654,8 @@ onShow(() => uni.setNavigationBarTitle({ title: "" }));
|
||||
.login__navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: var(--z-navbar);
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
@@ -706,8 +705,8 @@ onShow(() => uni.setNavigationBarTitle({ title: "" }));
|
||||
.login__navbar-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-text);
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.login__body {
|
||||
@@ -734,8 +733,8 @@ onShow(() => uni.setNavigationBarTitle({ title: "" }));
|
||||
.login__brand-name {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-text);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.login__card {
|
||||
@@ -922,8 +921,8 @@ onShow(() => uni.setNavigationBarTitle({ title: "" }));
|
||||
|
||||
.login__oauth-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 64rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login__wx-icon {
|
||||
@@ -963,8 +962,8 @@ onShow(() => uni.setNavigationBarTitle({ title: "" }));
|
||||
margin-bottom: 48rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login__demo-hint {
|
||||
|
||||
@@ -2,10 +2,27 @@
|
||||
<view class="page py-16rpx">
|
||||
<wd-card>
|
||||
<wd-cell-group border>
|
||||
<wd-cell icon="secured" title="账户密码" value="修改" is-link @click="handleOpenDialog(DialogType.PASSWORD)" />
|
||||
<wd-cell icon="mobile" title="绑定手机" :value="userProfile?.mobile || '未绑定手机号'" is-link @click="handleOpenDialog(DialogType.MOBILE)" />
|
||||
<wd-cell icon="mail" title="绑定邮箱" :value="userProfile?.email ? userProfile.email : '未绑定邮箱'" is-link
|
||||
@click="handleOpenDialog(DialogType.EMAIL)" />
|
||||
<wd-cell
|
||||
icon="secured"
|
||||
title="账户密码"
|
||||
value="修改"
|
||||
is-link
|
||||
@click="handleOpenDialog(DialogType.PASSWORD)"
|
||||
/>
|
||||
<wd-cell
|
||||
icon="mobile"
|
||||
title="绑定手机"
|
||||
:value="userProfile?.mobile || '未绑定手机号'"
|
||||
is-link
|
||||
@click="handleOpenDialog(DialogType.MOBILE)"
|
||||
/>
|
||||
<wd-cell
|
||||
icon="mail"
|
||||
title="绑定邮箱"
|
||||
:value="userProfile?.email ? userProfile.email : '未绑定邮箱'"
|
||||
is-link
|
||||
@click="handleOpenDialog(DialogType.EMAIL)"
|
||||
/>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<wd-cell icon="wechat" title="微信" value="解绑" is-link @click="handleUnbindWechat" />
|
||||
<!-- #endif -->
|
||||
@@ -13,28 +30,84 @@
|
||||
</wd-card>
|
||||
|
||||
<!--用户信息编辑弹出框-->
|
||||
<wd-popup v-model="dialog.visible" position="bottom" custom-style="border-top-left-radius: 24rpx; border-top-right-radius: 24rpx;">
|
||||
<wd-form v-if="dialog.type === DialogType.PASSWORD" ref="passwordChangeFormRef" :model="passwordChangeForm" custom-class="edit-form">
|
||||
<wd-popup
|
||||
v-model="dialog.visible"
|
||||
position="bottom"
|
||||
custom-style="border-top-left-radius: 24rpx; border-top-right-radius: 24rpx;"
|
||||
>
|
||||
<wd-form
|
||||
v-if="dialog.type === DialogType.PASSWORD"
|
||||
ref="passwordChangeFormRef"
|
||||
:model="passwordChangeForm"
|
||||
custom-class="edit-form"
|
||||
>
|
||||
<wd-cell-group border>
|
||||
<wd-input v-model="passwordChangeForm.oldPassword" label="原密码" label-width="160rpx" show-password clearable placeholder="请输入原密码"
|
||||
prop="oldPassword" :rules="rules.oldPassword" />
|
||||
<wd-input v-model="passwordChangeForm.newPassword" label="新密码" label-width="160rpx" show-password clearable placeholder="请输入新密码"
|
||||
prop="newPassword" :rules="rules.newPassword" />
|
||||
<wd-input v-model="passwordChangeForm.confirmPassword" label="确认密码" label-width="160rpx" show-password clearable placeholder="请确认新密码"
|
||||
prop="confirmPassword" :rules="rules.confirmPassword" />
|
||||
<wd-input
|
||||
v-model="passwordChangeForm.oldPassword"
|
||||
label="原密码"
|
||||
label-width="160rpx"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请输入原密码"
|
||||
prop="oldPassword"
|
||||
:rules="rules.oldPassword"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="passwordChangeForm.newPassword"
|
||||
label="新密码"
|
||||
label-width="160rpx"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请输入新密码"
|
||||
prop="newPassword"
|
||||
:rules="rules.newPassword"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="passwordChangeForm.confirmPassword"
|
||||
label="确认密码"
|
||||
label-width="160rpx"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请确认新密码"
|
||||
prop="confirmPassword"
|
||||
:rules="rules.confirmPassword"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
<view class="p-24rpx">
|
||||
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
<wd-form v-if="dialog.type === DialogType.MOBILE" ref="mobileBindingFormRef" :model="mobileBindingForm" custom-class="edit-form">
|
||||
<wd-form
|
||||
v-if="dialog.type === DialogType.MOBILE"
|
||||
ref="mobileBindingFormRef"
|
||||
:model="mobileBindingForm"
|
||||
custom-class="edit-form"
|
||||
>
|
||||
<wd-cell-group border>
|
||||
<wd-input v-model="mobileBindingForm.mobile" label="手机号码" label-width="160rpx" clearable placeholder="请输入手机号码" prop="mobile"
|
||||
:rules="rules.mobile" />
|
||||
<wd-input v-model="mobileBindingForm.code" label="验证码" label-width="160rpx" clearable placeholder="请输入验证码" prop="code"
|
||||
:rules="rules.code">
|
||||
<wd-input
|
||||
v-model="mobileBindingForm.mobile"
|
||||
label="手机号码"
|
||||
label-width="160rpx"
|
||||
clearable
|
||||
placeholder="请输入手机号码"
|
||||
prop="mobile"
|
||||
:rules="rules.mobile"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="mobileBindingForm.code"
|
||||
label="验证码"
|
||||
label-width="160rpx"
|
||||
clearable
|
||||
placeholder="请输入验证码"
|
||||
prop="code"
|
||||
:rules="rules.code"
|
||||
>
|
||||
<template #suffix>
|
||||
<wd-button plain :disabled="mobileCountdown > 0" @click="handleSendVerificationCode('MOBILE')">
|
||||
<wd-button
|
||||
plain
|
||||
:disabled="mobileCountdown > 0"
|
||||
@click="handleSendVerificationCode('MOBILE')"
|
||||
>
|
||||
{{ mobileCountdown > 0 ? `${mobileCountdown}s后重新发送` : "发送验证码" }}
|
||||
</wd-button>
|
||||
</template>
|
||||
@@ -44,14 +117,37 @@
|
||||
<wd-button type="primary" size="large" block @click="handleSubmit">提交</wd-button>
|
||||
</view>
|
||||
</wd-form>
|
||||
<wd-form v-if="dialog.type === DialogType.EMAIL" ref="emailBindingFormRef" :model="emailBindingForm" custom-class="edit-form">
|
||||
<wd-form
|
||||
v-if="dialog.type === DialogType.EMAIL"
|
||||
ref="emailBindingFormRef"
|
||||
:model="emailBindingForm"
|
||||
custom-class="edit-form"
|
||||
>
|
||||
<wd-cell-group border>
|
||||
<wd-input v-model="emailBindingForm.email" label="邮箱" label-width="160rpx" clearable placeholder="请输入邮箱" prop="email"
|
||||
:rules="rules.email" />
|
||||
<wd-input v-model="emailBindingForm.code" label="验证码" label-width="160rpx" clearable placeholder="请输入验证码" prop="code"
|
||||
:rules="rules.code">
|
||||
<wd-input
|
||||
v-model="emailBindingForm.email"
|
||||
label="邮箱"
|
||||
label-width="160rpx"
|
||||
clearable
|
||||
placeholder="请输入邮箱"
|
||||
prop="email"
|
||||
:rules="rules.email"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="emailBindingForm.code"
|
||||
label="验证码"
|
||||
label-width="160rpx"
|
||||
clearable
|
||||
placeholder="请输入验证码"
|
||||
prop="code"
|
||||
:rules="rules.code"
|
||||
>
|
||||
<template #suffix>
|
||||
<wd-button plain :disabled="emailCountdown > 0" @click="handleSendVerificationCode('EMAIL')">
|
||||
<wd-button
|
||||
plain
|
||||
:disabled="emailCountdown > 0"
|
||||
@click="handleSendVerificationCode('EMAIL')"
|
||||
>
|
||||
{{ emailCountdown > 0 ? `${emailCountdown}s后重新发送` : "发送验证码" }}
|
||||
</wd-button>
|
||||
</template>
|
||||
@@ -65,172 +161,172 @@
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import { useCountdown } from "@/composables/useCountdown";
|
||||
import UserAPI, {
|
||||
PasswordChangeForm,
|
||||
MobileBindingForm,
|
||||
EmailBindingForm,
|
||||
UserProfile,
|
||||
} from "@/api/user";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import { useCountdown } from "@/composables/useCountdown";
|
||||
import UserAPI, {
|
||||
PasswordChangeForm,
|
||||
MobileBindingForm,
|
||||
EmailBindingForm,
|
||||
UserProfile,
|
||||
} from "@/api/user";
|
||||
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
|
||||
const validatorConfirmPassword = (value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject("请确认密码");
|
||||
const validatorConfirmPassword = (value: string) => {
|
||||
if (!value) {
|
||||
return Promise.reject("请确认密码");
|
||||
} else {
|
||||
if (value !== passwordChangeForm.newPassword) {
|
||||
return Promise.reject("两次输入的密码不一致");
|
||||
} else {
|
||||
if (value !== passwordChangeForm.newPassword) {
|
||||
return Promise.reject("两次输入的密码不一致");
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
};
|
||||
// 本页面中所有的校验规则
|
||||
const rules = reactive({
|
||||
oldPassword: [{ required: true, message: "请填写原密码" }],
|
||||
newPassword: [{ required: true, message: "请填写新密码" }],
|
||||
confirmPassword: [{ required: true, message: "请确认密码", validator: validatorConfirmPassword }],
|
||||
mobile: [{ required: true, pattern: /^1[3-9]\d{9}$/, message: "请填写正确的手机号码" }],
|
||||
code: [{ required: true, message: "请填写验证码" }],
|
||||
email: [
|
||||
{
|
||||
required: true,
|
||||
pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
|
||||
message: "请填写正确的邮箱地址",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
enum DialogType {
|
||||
PASSWORD = "password",
|
||||
MOBILE = "mobile",
|
||||
EMAIL = "email",
|
||||
}
|
||||
|
||||
const dialog = reactive({
|
||||
visible: false,
|
||||
type: "" as DialogType, // 修改账号资料,修改密码、绑定手机、绑定邮箱
|
||||
});
|
||||
|
||||
const userProfile = ref<UserProfile>(); //用户信息
|
||||
const passwordChangeForm = reactive<PasswordChangeForm>({});
|
||||
const mobileBindingForm = reactive<MobileBindingForm>({});
|
||||
const emailBindingForm = reactive<EmailBindingForm>({});
|
||||
const passwordChangeFormRef = ref();
|
||||
const mobileBindingFormRef = ref();
|
||||
const emailBindingFormRef = ref();
|
||||
|
||||
const { countdown: mobileCountdown, start: startMobileCountdown } = useCountdown(60);
|
||||
const { countdown: emailCountdown, start: startEmailCountdown } = useCountdown(60);
|
||||
|
||||
const handleUnbindWechat = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "提示",
|
||||
msg: "确定要解绑微信吗?解绑后将无法使用微信小程序登录",
|
||||
headerImage: "warning"
|
||||
});
|
||||
await UserAPI.unbindSocial("WECHAT_MINI");
|
||||
toast.success("解绑成功");
|
||||
loadUserProfile();
|
||||
} catch {
|
||||
// 用户取消或解绑失败
|
||||
}
|
||||
};
|
||||
|
||||
/** 加载用户信息 */
|
||||
const loadUserProfile = async () => {
|
||||
userProfile.value = await UserAPI.getProfile();
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开弹窗
|
||||
* @param type 弹窗类型 ACCOUNT: 账号资料 PASSWORD: 修改密码 MOBILE: 绑定手机 EMAIL: 绑定邮箱
|
||||
*/
|
||||
const handleOpenDialog = (type: DialogType) => {
|
||||
dialog.type = type;
|
||||
dialog.visible = true;
|
||||
switch (type) {
|
||||
case DialogType.PASSWORD:
|
||||
passwordChangeForm.oldPassword = "";
|
||||
passwordChangeForm.newPassword = "";
|
||||
passwordChangeForm.confirmPassword = "";
|
||||
break;
|
||||
case DialogType.MOBILE:
|
||||
mobileBindingForm.mobile = "";
|
||||
mobileBindingForm.code = "";
|
||||
break;
|
||||
case DialogType.EMAIL:
|
||||
emailBindingForm.email = "";
|
||||
emailBindingForm.code = "";
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*
|
||||
* @param contactType 联系方式类型 MOBILE: 手机号码 EMAIL: 邮箱
|
||||
*/
|
||||
const handleSendVerificationCode = async (contactType: string) => {
|
||||
try {
|
||||
if (contactType === "MOBILE") {
|
||||
const { valid } = await mobileBindingFormRef.value.validate("mobile");
|
||||
if (valid) {
|
||||
await UserAPI.sendVerificationCode(mobileBindingForm.mobile!, "MOBILE");
|
||||
uni.showToast({ title: "验证码已发送", icon: "none" });
|
||||
startMobileCountdown();
|
||||
}
|
||||
} else if (contactType === "EMAIL") {
|
||||
const { valid } = await emailBindingFormRef.value.validate("email");
|
||||
if (valid) {
|
||||
await UserAPI.sendVerificationCode(emailBindingForm.email!, "EMAIL");
|
||||
uni.showToast({ title: "验证码已发送", icon: "none" });
|
||||
startEmailCountdown();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("发送验证码失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
if (dialog.type === DialogType.PASSWORD) {
|
||||
const { valid } = await passwordChangeFormRef.value.validate();
|
||||
if (valid) {
|
||||
await UserAPI.changePassword(passwordChangeForm);
|
||||
uni.showToast({ title: "密码修改成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
}
|
||||
} else if (dialog.type === DialogType.MOBILE) {
|
||||
const { valid } = await mobileBindingFormRef.value.validate();
|
||||
if (valid) {
|
||||
await UserAPI.bindMobile(mobileBindingForm);
|
||||
uni.showToast({ title: "手机号绑定成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
loadUserProfile();
|
||||
}
|
||||
} else if (dialog.type === DialogType.EMAIL) {
|
||||
const { valid } = await emailBindingFormRef.value.validate();
|
||||
if (valid) {
|
||||
await UserAPI.bindEmail(emailBindingForm);
|
||||
uni.showToast({ title: "邮箱绑定成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
loadUserProfile();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("提交表单失败:", error);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
};
|
||||
// 本页面中所有的校验规则
|
||||
const rules = reactive({
|
||||
oldPassword: [{ required: true, message: "请填写原密码" }],
|
||||
newPassword: [{ required: true, message: "请填写新密码" }],
|
||||
confirmPassword: [{ required: true, message: "请确认密码", validator: validatorConfirmPassword }],
|
||||
mobile: [{ required: true, pattern: /^1[3-9]\d{9}$/, message: "请填写正确的手机号码" }],
|
||||
code: [{ required: true, message: "请填写验证码" }],
|
||||
email: [
|
||||
{
|
||||
required: true,
|
||||
pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
|
||||
message: "请填写正确的邮箱地址",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
enum DialogType {
|
||||
PASSWORD = "password",
|
||||
MOBILE = "mobile",
|
||||
EMAIL = "email",
|
||||
}
|
||||
|
||||
const dialog = reactive({
|
||||
visible: false,
|
||||
type: "" as DialogType, // 修改账号资料,修改密码、绑定手机、绑定邮箱
|
||||
});
|
||||
|
||||
const userProfile = ref<UserProfile>(); //用户信息
|
||||
const passwordChangeForm = reactive<PasswordChangeForm>({});
|
||||
const mobileBindingForm = reactive<MobileBindingForm>({});
|
||||
const emailBindingForm = reactive<EmailBindingForm>({});
|
||||
const passwordChangeFormRef = ref();
|
||||
const mobileBindingFormRef = ref();
|
||||
const emailBindingFormRef = ref();
|
||||
|
||||
const { countdown: mobileCountdown, start: startMobileCountdown } = useCountdown(60);
|
||||
const { countdown: emailCountdown, start: startEmailCountdown } = useCountdown(60);
|
||||
|
||||
const handleUnbindWechat = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "提示",
|
||||
msg: "确定要解绑微信吗?解绑后将无法使用微信小程序登录",
|
||||
headerImage: "warning",
|
||||
});
|
||||
await UserAPI.unbindSocial("WECHAT_MINI");
|
||||
toast.success("解绑成功");
|
||||
loadUserProfile();
|
||||
});
|
||||
} catch {
|
||||
// 用户取消或解绑失败
|
||||
}
|
||||
};
|
||||
|
||||
/** 加载用户信息 */
|
||||
const loadUserProfile = async () => {
|
||||
userProfile.value = await UserAPI.getProfile();
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开弹窗
|
||||
* @param type 弹窗类型 ACCOUNT: 账号资料 PASSWORD: 修改密码 MOBILE: 绑定手机 EMAIL: 绑定邮箱
|
||||
*/
|
||||
const handleOpenDialog = (type: DialogType) => {
|
||||
dialog.type = type;
|
||||
dialog.visible = true;
|
||||
switch (type) {
|
||||
case DialogType.PASSWORD:
|
||||
passwordChangeForm.oldPassword = "";
|
||||
passwordChangeForm.newPassword = "";
|
||||
passwordChangeForm.confirmPassword = "";
|
||||
break;
|
||||
case DialogType.MOBILE:
|
||||
mobileBindingForm.mobile = "";
|
||||
mobileBindingForm.code = "";
|
||||
break;
|
||||
case DialogType.EMAIL:
|
||||
emailBindingForm.email = "";
|
||||
emailBindingForm.code = "";
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
*
|
||||
* @param contactType 联系方式类型 MOBILE: 手机号码 EMAIL: 邮箱
|
||||
*/
|
||||
const handleSendVerificationCode = async (contactType: string) => {
|
||||
try {
|
||||
if (contactType === "MOBILE") {
|
||||
const { valid } = await mobileBindingFormRef.value.validate("mobile");
|
||||
if (valid) {
|
||||
await UserAPI.sendVerificationCode(mobileBindingForm.mobile!, "MOBILE");
|
||||
uni.showToast({ title: "验证码已发送", icon: "none" });
|
||||
startMobileCountdown();
|
||||
}
|
||||
} else if (contactType === "EMAIL") {
|
||||
const { valid } = await emailBindingFormRef.value.validate("email");
|
||||
if (valid) {
|
||||
await UserAPI.sendVerificationCode(emailBindingForm.email!, "EMAIL");
|
||||
uni.showToast({ title: "验证码已发送", icon: "none" });
|
||||
startEmailCountdown();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("发送验证码失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
if (dialog.type === DialogType.PASSWORD) {
|
||||
const { valid } = await passwordChangeFormRef.value.validate();
|
||||
if (valid) {
|
||||
await UserAPI.changePassword(passwordChangeForm);
|
||||
uni.showToast({ title: "密码修改成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
}
|
||||
} else if (dialog.type === DialogType.MOBILE) {
|
||||
const { valid } = await mobileBindingFormRef.value.validate();
|
||||
if (valid) {
|
||||
await UserAPI.bindMobile(mobileBindingForm);
|
||||
uni.showToast({ title: "手机号绑定成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
loadUserProfile();
|
||||
}
|
||||
} else if (dialog.type === DialogType.EMAIL) {
|
||||
const { valid } = await emailBindingFormRef.value.validate();
|
||||
if (valid) {
|
||||
await UserAPI.bindEmail(emailBindingForm);
|
||||
uni.showToast({ title: "邮箱绑定成功", icon: "none" });
|
||||
dialog.visible = false;
|
||||
loadUserProfile();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("提交表单失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUserProfile();
|
||||
});
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,9 +21,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
|
||||
definePage({
|
||||
name: "official",
|
||||
@@ -38,8 +36,8 @@ onLoad(() => {
|
||||
<style lang="scss" scoped>
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
align-items: center;
|
||||
padding: 28rpx;
|
||||
background: var(--color-bg);
|
||||
border-radius: 20rpx;
|
||||
@@ -70,8 +68,8 @@ onLoad(() => {
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-top: 20rpx;
|
||||
padding: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text);
|
||||
@@ -80,8 +78,8 @@ onLoad(() => {
|
||||
}
|
||||
|
||||
.qrcode-card {
|
||||
margin-top: 24rpx;
|
||||
padding: 28rpx;
|
||||
margin-top: 24rpx;
|
||||
background: var(--color-bg);
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
@@ -94,8 +92,8 @@ onLoad(() => {
|
||||
|
||||
.qrcode-hint {
|
||||
margin-top: 20rpx;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,364 +14,411 @@
|
||||
<view class="profile-form__label">头像</view>
|
||||
<view class="avatar-picker">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button class="avatar-picker__trigger" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="avatar-picker__trigger" @click="chooseAvatar">
|
||||
<!-- #endif -->
|
||||
<image v-if="profileForm.avatar" :src="profileForm.avatar" class="avatar-picker__img" mode="aspectFill" />
|
||||
<view v-else class="avatar-picker__placeholder">
|
||||
<wd-icon name="camera" size="40" color="var(--color-text-placeholder)" />
|
||||
<text class="avatar-picker__hint">选择头像</text>
|
||||
</view>
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button
|
||||
class="avatar-picker__trigger"
|
||||
open-type="chooseAvatar"
|
||||
@chooseavatar="onChooseAvatar"
|
||||
>
|
||||
<image
|
||||
v-if="profileForm.avatar"
|
||||
:src="profileForm.avatar"
|
||||
class="avatar-picker__img"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="avatar-picker__placeholder">
|
||||
<wd-icon name="camera" size="40" color="var(--color-text-placeholder)" />
|
||||
<text class="avatar-picker__hint">选择头像</text>
|
||||
</view>
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<view class="avatar-picker__trigger" @click="chooseAvatar">
|
||||
<image
|
||||
v-if="profileForm.avatar"
|
||||
:src="profileForm.avatar"
|
||||
class="avatar-picker__img"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="avatar-picker__placeholder">
|
||||
<wd-icon name="camera" size="40" color="var(--color-text-placeholder)" />
|
||||
<text class="avatar-picker__hint">选择头像</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 昵称 -->
|
||||
<view class="profile-form__section">
|
||||
<view class="profile-form__label">
|
||||
昵称
|
||||
<text class="profile-form__required">*</text>
|
||||
</view>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<input
|
||||
v-model="profileForm.nickname"
|
||||
type="nickname"
|
||||
class="profile-form__nickname-input"
|
||||
placeholder="请输入昵称"
|
||||
:maxlength="20"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<wd-input
|
||||
v-model="profileForm.nickname"
|
||||
placeholder="请输入昵称"
|
||||
prop="nickname"
|
||||
:rules="rules.nickname"
|
||||
custom-class="profile-form__nickname-input"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
|
||||
<!-- 性别 -->
|
||||
<view class="profile-form__section">
|
||||
<view class="profile-form__label">性别</view>
|
||||
<wd-radio-group v-model="profileForm.gender" type="button" class="gender-group">
|
||||
<wd-radio :value="1" class="gender-radio">男</wd-radio>
|
||||
<wd-radio :value="2" class="gender-radio">女</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
|
||||
<!-- 手机号 -->
|
||||
<view class="profile-form__section">
|
||||
<view class="profile-form__label">
|
||||
手机号
|
||||
<text class="profile-form__required">*</text>
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="profileForm.mobile"
|
||||
placeholder="请输入手机号"
|
||||
prop="mobile"
|
||||
custom-class="profile-form__nickname-input"
|
||||
/>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 昵称 -->
|
||||
<view class="profile-form__section">
|
||||
<view class="profile-form__label">
|
||||
昵称
|
||||
<text class="profile-form__required">*</text>
|
||||
<!-- 底部 -->
|
||||
<view class="profile-footer">
|
||||
<wd-button
|
||||
class="profile-footer__submit"
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:disabled="!canComplete || isLoading"
|
||||
:loading="isLoading"
|
||||
@click="handleComplete"
|
||||
>
|
||||
完成
|
||||
</wd-button>
|
||||
<view class="profile-footer__skip" @click="handleSkip">
|
||||
<text>暂时跳过</text>
|
||||
</view>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<input v-model="profileForm.nickname" type="nickname" class="profile-form__nickname-input" placeholder="请输入昵称" :maxlength="20" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<wd-input v-model="profileForm.nickname" placeholder="请输入昵称" prop="nickname" :rules="rules.nickname"
|
||||
custom-class="profile-form__nickname-input" />
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
|
||||
<!-- 性别 -->
|
||||
<view class="profile-form__section">
|
||||
<view class="profile-form__label">性别</view>
|
||||
<wd-radio-group v-model="profileForm.gender" type="button" class="gender-group">
|
||||
<wd-radio :value="1" class="gender-radio">男</wd-radio>
|
||||
<wd-radio :value="2" class="gender-radio">女</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<!-- 头像裁剪 -->
|
||||
<wd-img-cropper
|
||||
v-model="cropperVisible"
|
||||
:img-src="originalImageSrc"
|
||||
@confirm="handleAvatarConfirm"
|
||||
/>
|
||||
|
||||
<!-- 手机号 -->
|
||||
<view class="profile-form__section">
|
||||
<view class="profile-form__label">
|
||||
手机号
|
||||
<text class="profile-form__required">*</text>
|
||||
</view>
|
||||
<wd-input v-model="profileForm.mobile" placeholder="请输入手机号" prop="mobile" custom-class="profile-form__nickname-input" />
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部 -->
|
||||
<view class="profile-footer">
|
||||
<wd-button class="profile-footer__submit" type="primary" size="large" block :disabled="!canComplete || isLoading" :loading="isLoading"
|
||||
@click="handleComplete">
|
||||
完成
|
||||
</wd-button>
|
||||
<view class="profile-footer__skip" @click="handleSkip">
|
||||
<text>暂时跳过</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 头像裁剪 -->
|
||||
<wd-img-cropper v-model="cropperVisible" :img-src="originalImageSrc" @confirm="handleAvatarConfirm" />
|
||||
|
||||
<wd-toast />
|
||||
<wd-toast />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useUserStore } from "@/store";
|
||||
import UserAPI, { type UserProfileForm } from "@/api/user";
|
||||
import FileAPI, { type FileInfo } from "@/api/file";
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useDialog, useToast } from "@wot-ui/ui";
|
||||
import { useUserStore } from "@/store";
|
||||
import UserAPI, { type UserProfileForm } from "@/api/user";
|
||||
import FileAPI, { type FileInfo } from "@/api/file";
|
||||
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const userStore = useUserStore();
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const redirect = ref("/pages/index/index");
|
||||
const redirect = ref("/pages/index/index");
|
||||
|
||||
const profileForm = reactive<UserProfileForm & { mobile?: string }>({
|
||||
nickname: "",
|
||||
avatar: "",
|
||||
gender: 1,
|
||||
mobile: "",
|
||||
const profileForm = reactive<UserProfileForm & { mobile?: string }>({
|
||||
nickname: "",
|
||||
avatar: "",
|
||||
gender: 1,
|
||||
mobile: "",
|
||||
});
|
||||
|
||||
const rules = {
|
||||
nickname: [
|
||||
{ required: true, message: "请输入昵称" },
|
||||
{ required: true, min: 2, max: 20, message: "昵称长度为2-20个字符" },
|
||||
],
|
||||
};
|
||||
|
||||
const isLoading = ref(false);
|
||||
const cropperVisible = ref(false);
|
||||
const originalImageSrc = ref("");
|
||||
const profileFormRef = ref();
|
||||
|
||||
const canComplete = computed(() => {
|
||||
return profileForm.nickname && profileForm.mobile;
|
||||
});
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options?.redirect) {
|
||||
redirect.value = decodeURIComponent(options.redirect);
|
||||
}
|
||||
const userInfo = userStore.userInfo;
|
||||
if (userInfo) {
|
||||
profileForm.nickname = userInfo.nickname || "";
|
||||
profileForm.avatar = userInfo.avatar || "";
|
||||
profileForm.gender = (userInfo as any).gender || 1;
|
||||
}
|
||||
});
|
||||
|
||||
const onChooseAvatar = async (e: any) => {
|
||||
try {
|
||||
const { avatarUrl } = e.detail;
|
||||
toast.loading("上传中...");
|
||||
const fileInfo: FileInfo = await FileAPI.upload(avatarUrl);
|
||||
profileForm.avatar = fileInfo.url;
|
||||
toast.success("头像上传成功");
|
||||
} catch {
|
||||
toast.error("头像上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
const chooseAvatar = () => {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ["image"],
|
||||
sourceType: ["album", "camera"],
|
||||
success: (res) => {
|
||||
originalImageSrc.value = res.tempFiles[0].tempFilePath;
|
||||
cropperVisible.value = true;
|
||||
},
|
||||
fail: () => {
|
||||
toast.error("选择图片失败");
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
|
||||
const rules = {
|
||||
nickname: [
|
||||
{ required: true, message: "请输入昵称" },
|
||||
{ required: true, min: 2, max: 20, message: "昵称长度为2-20个字符" },
|
||||
],
|
||||
};
|
||||
|
||||
const isLoading = ref(false);
|
||||
const cropperVisible = ref(false);
|
||||
const originalImageSrc = ref("");
|
||||
const profileFormRef = ref();
|
||||
|
||||
const canComplete = computed(() => {
|
||||
return profileForm.nickname && profileForm.mobile;
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ["album", "camera"],
|
||||
success: (res) => {
|
||||
originalImageSrc.value = res.tempFilePaths[0];
|
||||
cropperVisible.value = true;
|
||||
},
|
||||
fail: () => {
|
||||
toast.error("选择图片失败");
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
};
|
||||
|
||||
onLoad((options: any) => {
|
||||
if (options?.redirect) {
|
||||
redirect.value = decodeURIComponent(options.redirect);
|
||||
}
|
||||
const userInfo = userStore.userInfo;
|
||||
if (userInfo) {
|
||||
profileForm.nickname = userInfo.nickname || "";
|
||||
profileForm.avatar = userInfo.avatar || "";
|
||||
profileForm.gender = (userInfo as any).gender || 1;
|
||||
}
|
||||
});
|
||||
const handleAvatarConfirm = async (event: any) => {
|
||||
try {
|
||||
const { tempFilePath } = event;
|
||||
toast.loading("上传中...");
|
||||
const fileInfo: FileInfo = await FileAPI.upload(tempFilePath);
|
||||
profileForm.avatar = fileInfo.url;
|
||||
toast.success("头像上传成功");
|
||||
} catch {
|
||||
toast.error("头像上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
const onChooseAvatar = async (e: any) => {
|
||||
try {
|
||||
const { avatarUrl } = e.detail;
|
||||
toast.loading("上传中...");
|
||||
const fileInfo: FileInfo = await FileAPI.upload(avatarUrl);
|
||||
profileForm.avatar = fileInfo.url;
|
||||
toast.success("头像上传成功");
|
||||
} catch {
|
||||
toast.error("头像上传失败");
|
||||
}
|
||||
};
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
const { valid } = await profileFormRef.value.validate();
|
||||
if (!valid) return;
|
||||
|
||||
const chooseAvatar = () => {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ["image"],
|
||||
sourceType: ["album", "camera"],
|
||||
success: (res) => {
|
||||
originalImageSrc.value = res.tempFiles[0].tempFilePath;
|
||||
cropperVisible.value = true;
|
||||
},
|
||||
fail: () => {
|
||||
toast.error("选择图片失败");
|
||||
},
|
||||
if (!profileForm.mobile) {
|
||||
toast.error("请输入手机号");
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
await UserAPI.updateProfile({
|
||||
nickname: profileForm.nickname,
|
||||
avatar: profileForm.avatar,
|
||||
gender: profileForm.gender,
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ["album", "camera"],
|
||||
success: (res) => {
|
||||
originalImageSrc.value = res.tempFilePaths[0];
|
||||
cropperVisible.value = true;
|
||||
},
|
||||
fail: () => {
|
||||
toast.error("选择图片失败");
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
};
|
||||
|
||||
const handleAvatarConfirm = async (event: any) => {
|
||||
try {
|
||||
const { tempFilePath } = event;
|
||||
toast.loading("上传中...");
|
||||
const fileInfo: FileInfo = await FileAPI.upload(tempFilePath);
|
||||
profileForm.avatar = fileInfo.url;
|
||||
toast.success("头像上传成功");
|
||||
} catch {
|
||||
toast.error("头像上传失败");
|
||||
if (profileForm.mobile) {
|
||||
await UserAPI.bindMobile({ mobile: profileForm.mobile });
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = async () => {
|
||||
try {
|
||||
const { valid } = await profileFormRef.value.validate();
|
||||
if (!valid) return;
|
||||
await userStore.getInfo();
|
||||
toast.success("信息完善成功");
|
||||
|
||||
if (!profileForm.mobile) {
|
||||
toast.error("请输入手机号");
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
await UserAPI.updateProfile({
|
||||
nickname: profileForm.nickname,
|
||||
avatar: profileForm.avatar,
|
||||
gender: profileForm.gender,
|
||||
});
|
||||
|
||||
if (profileForm.mobile) {
|
||||
await UserAPI.bindMobile({ mobile: profileForm.mobile });
|
||||
}
|
||||
|
||||
await userStore.getInfo();
|
||||
toast.success("信息完善成功");
|
||||
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: redirect.value });
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "完善信息失败");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkip = async () => {
|
||||
try {
|
||||
await confirm({ title: "提示", msg: "跳过信息完善可能会影响部分功能使用,确定要跳过吗?", headerImage: "warning" });
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: redirect.value });
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
};
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "完善信息失败");
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkip = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "提示",
|
||||
msg: "跳过信息完善可能会影响部分功能使用,确定要跳过吗?",
|
||||
headerImage: "warning",
|
||||
});
|
||||
uni.reLaunch({ url: redirect.value });
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.complete-profile__hero {
|
||||
margin-bottom: 60rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.complete-profile__hero {
|
||||
margin-bottom: 60rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.complete-profile__hero-title {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-inverse);
|
||||
}
|
||||
.complete-profile__hero-title {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-inverse);
|
||||
}
|
||||
|
||||
.complete-profile__hero-subtitle {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
.complete-profile__hero-subtitle {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.profile-form {
|
||||
padding: 40rpx;
|
||||
.profile-form {
|
||||
padding: 40rpx;
|
||||
margin-bottom: 40rpx;
|
||||
background: var(--color-bg);
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
&__section {
|
||||
margin-bottom: 40rpx;
|
||||
background: var(--color-bg);
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
&__section {
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
margin-bottom: 20rpx;
|
||||
&__label {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&__required {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
&__nickname-input {
|
||||
:deep(.wd-input__inner) {
|
||||
padding: 24rpx 20rpx;
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 1rpx solid var(--color-border-light);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-picker {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&__trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 4rpx solid var(--color-border-light);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 2rpx dashed var(--color-border);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
.gender-group {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.gender-radio {
|
||||
flex: 1;
|
||||
|
||||
:deep(.wd-radio) {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-footer {
|
||||
&__submit {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
:deep(.wd-button) {
|
||||
height: 88rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&__required {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
&__nickname-input {
|
||||
:deep(.wd-input__inner) {
|
||||
padding: 24rpx 20rpx;
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 1rpx solid var(--color-border-light);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-picker {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
&__skip {
|
||||
text-align: center;
|
||||
|
||||
&__trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 4rpx solid var(--color-border-light);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 2rpx dashed var(--color-border);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
.gender-group {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
|
||||
.gender-radio {
|
||||
flex: 1;
|
||||
|
||||
:deep(.wd-radio) {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-footer {
|
||||
&__submit {
|
||||
margin-bottom: 30rpx;
|
||||
|
||||
:deep(.wd-button) {
|
||||
height: 88rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&__skip {
|
||||
text-align: center;
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-decoration: underline;
|
||||
}
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
|
||||
const activeNames = ref(["0"]); // 默认展开第一项
|
||||
|
||||
const agreementContent = [
|
||||
@@ -57,10 +55,10 @@ const agreementContent = [
|
||||
},
|
||||
];
|
||||
|
||||
definePage({
|
||||
name: "theme",
|
||||
style: { navigationBarTitleText: "用户协议" },
|
||||
});
|
||||
definePage({
|
||||
name: "theme",
|
||||
style: { navigationBarTitleText: "用户协议" },
|
||||
});
|
||||
|
||||
// 同意协议
|
||||
const handleAgree = () => {
|
||||
@@ -86,7 +84,7 @@ const handleAgree = () => {
|
||||
}
|
||||
|
||||
.agreement__action {
|
||||
margin-top: 48rpx;
|
||||
padding: 0 32rpx;
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,7 +11,13 @@
|
||||
<view class="settings-group-wrap">
|
||||
<wd-cell-group custom-style="margin-top:24rpx">
|
||||
<wd-cell title="网络测试" icon="wifi" is-link @click="navigateToNetworkTest" />
|
||||
<wd-cell title="清空缓存" icon="delete" :value="cacheSize" clickable @click="handleClearCache" />
|
||||
<wd-cell
|
||||
title="清空缓存"
|
||||
icon="delete"
|
||||
:value="cacheSize"
|
||||
clickable
|
||||
@click="handleClearCache"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
@@ -21,202 +27,203 @@
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<wd-loading v-if="isClearing" v-model="isClearing" text="正在清理..." mask custom-class="loading-center" />
|
||||
<wd-loading
|
||||
v-if="isClearing"
|
||||
v-model="isClearing"
|
||||
text="正在清理..."
|
||||
mask
|
||||
custom-class="loading-center"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from "@/store";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useToast } from "@wot-ui/ui";
|
||||
|
||||
import { useUserStore } from "@/store";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useToast } from "@wot-ui/ui";
|
||||
definePage({
|
||||
name: "settings",
|
||||
style: { navigationBarTitleText: "设置" },
|
||||
});
|
||||
|
||||
definePage({
|
||||
name: "settings",
|
||||
style: { navigationBarTitleText: "设置" },
|
||||
const userStore = useUserStore();
|
||||
const toast = useToast();
|
||||
const isLogin = computed(() => !!userStore.userInfo);
|
||||
|
||||
// 主题设置
|
||||
const navigateToTheme = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/theme/index",
|
||||
});
|
||||
};
|
||||
|
||||
const userStore = useUserStore();
|
||||
const toast = useToast();
|
||||
const isLogin = computed(() => !!userStore.userInfo);
|
||||
// 用户协议
|
||||
const navigateToUserAgreement = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/agreement/index",
|
||||
});
|
||||
};
|
||||
|
||||
// 主题设置
|
||||
const navigateToTheme = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/theme/index",
|
||||
// 关于我们
|
||||
const navigateToAbout = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/about/index",
|
||||
});
|
||||
};
|
||||
|
||||
// 网络测试
|
||||
const navigateToNetworkTest = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/settings/network/index" });
|
||||
};
|
||||
|
||||
// 是否正在清理
|
||||
const isClearing = ref(false);
|
||||
// 缓存大小
|
||||
const cacheSize = ref<string>("计算中...");
|
||||
// 获取缓存大小
|
||||
const getCacheSize = async () => {
|
||||
try {
|
||||
// #ifdef MP-WEIXIN
|
||||
const res = await uni.getStorageInfo();
|
||||
cacheSize.value = formatSize(res.currentSize);
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
cacheSize.value = formatSize(
|
||||
Object.keys(localStorage).reduce((size, key) => size + localStorage[key].length, 0)
|
||||
);
|
||||
// #endif
|
||||
if (!cacheSize.value) {
|
||||
cacheSize.value = "0B";
|
||||
}
|
||||
} catch {
|
||||
cacheSize.value = "获取失败";
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化存储大小
|
||||
const formatSize = (size: number) => {
|
||||
if (size < 1024) {
|
||||
return size + "B";
|
||||
} else if (size < 1024 * 1024) {
|
||||
return (size / 1024).toFixed(2) + "KB";
|
||||
} else {
|
||||
return (size / 1024 / 1024).toFixed(2) + "MB";
|
||||
}
|
||||
};
|
||||
|
||||
// 处理清除缓存
|
||||
const handleClearCache = async () => {
|
||||
if (cacheSize.value === "获取失败") {
|
||||
uni.showToast({
|
||||
title: "获取缓存信息失败,请稍后重试",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
};
|
||||
|
||||
// 用户协议
|
||||
const navigateToUserAgreement = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/agreement/index",
|
||||
return;
|
||||
}
|
||||
if (cacheSize.value === "0B") {
|
||||
uni.showToast({
|
||||
title: "暂无缓存需要清理",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (isClearing.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 关于我们
|
||||
const navigateToAbout = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/about/index",
|
||||
try {
|
||||
isClearing.value = true;
|
||||
// 模拟清理过程
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
// 清除缓存
|
||||
await uni.clearStorage();
|
||||
// 更新缓存大小显示
|
||||
await getCacheSize();
|
||||
// 提示清理成功
|
||||
uni.showToast({
|
||||
title: "清理成功",
|
||||
icon: "success",
|
||||
});
|
||||
};
|
||||
} catch {
|
||||
uni.showToast({
|
||||
title: "清理失败",
|
||||
icon: "error",
|
||||
});
|
||||
} finally {
|
||||
isClearing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 网络测试
|
||||
const navigateToNetworkTest = () => {
|
||||
uni.navigateTo({ url: "/pages/mine/settings/network/index" });
|
||||
};
|
||||
|
||||
// 是否正在清理
|
||||
const isClearing = ref(false);
|
||||
// 缓存大小
|
||||
const cacheSize = ref<string>("计算中...");
|
||||
// 获取缓存大小
|
||||
const getCacheSize = async () => {
|
||||
try {
|
||||
// #ifdef MP-WEIXIN
|
||||
const res = await uni.getStorageInfo();
|
||||
cacheSize.value = formatSize(res.currentSize);
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
cacheSize.value = formatSize(
|
||||
Object.keys(localStorage).reduce((size, key) => size + localStorage[key].length, 0)
|
||||
);
|
||||
// #endif
|
||||
if (!cacheSize.value) {
|
||||
cacheSize.value = "0B";
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要退出登录吗?",
|
||||
confirmColor: "#ff4757",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
userStore.logout();
|
||||
toast.success("已退出登录");
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: "/pages/mine/index" });
|
||||
}, 800);
|
||||
}
|
||||
} catch {
|
||||
cacheSize.value = "获取失败";
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化存储大小
|
||||
const formatSize = (size: number) => {
|
||||
if (size < 1024) {
|
||||
return size + "B";
|
||||
} else if (size < 1024 * 1024) {
|
||||
return (size / 1024).toFixed(2) + "KB";
|
||||
} else {
|
||||
return (size / 1024 / 1024).toFixed(2) + "MB";
|
||||
}
|
||||
};
|
||||
|
||||
// 处理清除缓存
|
||||
const handleClearCache = async () => {
|
||||
if (cacheSize.value === "获取失败") {
|
||||
uni.showToast({
|
||||
title: "获取缓存信息失败,请稍后重试",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (cacheSize.value === "0B") {
|
||||
uni.showToast({
|
||||
title: "暂无缓存需要清理",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isClearing.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isClearing.value = true;
|
||||
// 模拟清理过程
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
// 清除缓存
|
||||
await uni.clearStorage();
|
||||
// 更新缓存大小显示
|
||||
await getCacheSize();
|
||||
// 提示清理成功
|
||||
uni.showToast({
|
||||
title: "清理成功",
|
||||
icon: "success",
|
||||
});
|
||||
} catch {
|
||||
uni.showToast({
|
||||
title: "清理失败",
|
||||
icon: "error",
|
||||
});
|
||||
} finally {
|
||||
isClearing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 退出登录
|
||||
const handleLogout = () => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要退出登录吗?",
|
||||
confirmColor: "#ff4757",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
userStore.logout();
|
||||
toast.success("已退出登录");
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: "/pages/mine/index" });
|
||||
}, 800);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 检查登录状态
|
||||
onLoad(() => {
|
||||
getCacheSize();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 检查登录状态
|
||||
onLoad(() => {
|
||||
getCacheSize();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logout-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
.logout-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.settings-page {
|
||||
padding: 24rpx;
|
||||
}
|
||||
.settings-page {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.settings-group-wrap {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.settings-group-wrap {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.logout-section {
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
:deep(.wd-cell:active) {
|
||||
background-color: var(--color-bg-tertiary) !important;
|
||||
}
|
||||
|
||||
:deep(.wd-cell:active) {
|
||||
background-color: var(--color-bg-tertiary) !important;
|
||||
}
|
||||
:deep(.logout-btn) {
|
||||
height: 88rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
:deep(.logout-btn) {
|
||||
height: 88rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
.loading-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.loading-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
.loading-center :deep(.wd-loading__spinner) {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loading-center :deep(.wd-loading__spinner) {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loading-center :deep(.wd-loading__text) {
|
||||
margin-top: 20rpx;
|
||||
color: var(--color-text-inverse);
|
||||
text-align: center;
|
||||
}
|
||||
.loading-center :deep(.wd-loading__text) {
|
||||
margin-top: 20rpx;
|
||||
color: var(--color-text-inverse);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
|
||||
definePage({
|
||||
@@ -40,7 +39,6 @@ definePage({
|
||||
style: { navigationBarTitleText: "网络测试" },
|
||||
});
|
||||
|
||||
|
||||
const result = ref<number | null>(null);
|
||||
const isTesting = ref(false);
|
||||
|
||||
@@ -93,10 +91,10 @@ onLoad(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.result-card {
|
||||
background: var(--color-bg);
|
||||
border-radius: 16rpx;
|
||||
border: 1rpx solid var(--color-border);
|
||||
overflow: hidden;
|
||||
background: var(--color-bg);
|
||||
border: 1rpx solid var(--color-border);
|
||||
border-radius: 16rpx;
|
||||
|
||||
&__header {
|
||||
padding: 24rpx;
|
||||
@@ -107,8 +105,8 @@ onLoad(() => {
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
|
||||
& + & {
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
const activeNames = ref(["0"]); // 默认展开第一项
|
||||
|
||||
const privacyContent = [
|
||||
@@ -71,11 +70,10 @@ const privacyContent = [
|
||||
},
|
||||
];
|
||||
|
||||
definePage({
|
||||
name: "privacy",
|
||||
style: { navigationBarTitleText: "隐私政策" },
|
||||
});
|
||||
|
||||
definePage({
|
||||
name: "privacy",
|
||||
style: { navigationBarTitleText: "隐私政策" },
|
||||
});
|
||||
|
||||
// 同意隐私政策
|
||||
const handleAgree = () => {
|
||||
@@ -101,7 +99,7 @@ const handleAgree = () => {
|
||||
}
|
||||
|
||||
.privacy__action {
|
||||
margin-top: 48rpx;
|
||||
padding: 0 32rpx;
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,10 +13,20 @@
|
||||
<wd-cell title="预设主题色">
|
||||
<template #default>
|
||||
<view class="flex flex-wrap gap-12rpx">
|
||||
<view v-for="(color, index) in themeColorOptions" :key="index" class="color-option"
|
||||
:class="{ 'color-option--active': currentThemeColor === color.primary }" :style="{ backgroundColor: color.primary }"
|
||||
@click="handleSelectColor(color)">
|
||||
<wd-icon v-if="currentThemeColor === color.primary" name="check" size="14" color="var(--color-text-inverse)" />
|
||||
<view
|
||||
v-for="(color, index) in themeColorOptions"
|
||||
:key="index"
|
||||
class="color-option"
|
||||
:class="{ 'color-option--active': currentThemeColor === color.primary }"
|
||||
:style="{ backgroundColor: color.primary }"
|
||||
@click="handleSelectColor(color)"
|
||||
>
|
||||
<wd-icon
|
||||
v-if="currentThemeColor === color.primary"
|
||||
name="check"
|
||||
size="14"
|
||||
color="var(--color-text-inverse)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -67,8 +77,11 @@
|
||||
<view class="popup-content">
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">自定义主题色</text>
|
||||
<view class="w-64rpx h-64rpx flex-center rounded-full" hover-class="bg-[var(--color-text-placeholder)]/16"
|
||||
@click="showCustomColorInput = false">
|
||||
<view
|
||||
class="w-64rpx h-64rpx flex-center rounded-full"
|
||||
hover-class="bg-[var(--color-text-placeholder)]/16"
|
||||
@click="showCustomColorInput = false"
|
||||
>
|
||||
<wd-icon name="close" size="18" class="color-text-secondary" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -77,7 +90,12 @@
|
||||
<text class="text-28rpx color-text-secondary">请输入十六进制颜色值</text>
|
||||
<view class="flex items-center gap-16rpx mt-16rpx">
|
||||
<view class="color-dot-lg" :style="{ backgroundColor: customColor || '#FF4757' }" />
|
||||
<wd-input v-model="customColor" placeholder="例如: #FF4757" :maxlength="7" custom-class="theme-input" />
|
||||
<wd-input
|
||||
v-model="customColor"
|
||||
placeholder="例如: #FF4757"
|
||||
:maxlength="7"
|
||||
custom-class="theme-input"
|
||||
/>
|
||||
</view>
|
||||
<text class="text-24rpx color-text-placeholder mt-12rpx">支持格式:#RGB 或 #RRGGBB</text>
|
||||
</view>
|
||||
@@ -92,184 +110,182 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
|
||||
definePage({
|
||||
name: "theme",
|
||||
style: { navigationBarTitleText: "主题设置" },
|
||||
definePage({
|
||||
name: "theme",
|
||||
style: { navigationBarTitleText: "主题设置" },
|
||||
});
|
||||
|
||||
// 使用主题组合函数
|
||||
const { isDark, themeColorOptions, toggleTheme, setThemeColor } = useTheme();
|
||||
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
|
||||
// 创建响应式的计算属性
|
||||
const isDarkMode = computed(() => isDark.value);
|
||||
|
||||
// 自定义颜色输入
|
||||
const customColor = ref("");
|
||||
const showCustomColorInput = ref(false);
|
||||
|
||||
// 当前选中的主题色
|
||||
const currentThemeColor = computed(() => {
|
||||
return themeColorOptions[0].primary;
|
||||
});
|
||||
|
||||
// 选择预设颜色
|
||||
const handleSelectColor = (color: (typeof themeColorOptions)[0]) => {
|
||||
setThemeColor(color);
|
||||
customColor.value = color.primary;
|
||||
|
||||
// 提示
|
||||
uni.showToast({
|
||||
title: "主题色已更新",
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
};
|
||||
|
||||
// 使用主题组合函数
|
||||
const { isDark, themeColorOptions, toggleTheme, setThemeColor } = useTheme();
|
||||
// 显示自定义颜色输入
|
||||
const showCustomInput = () => {
|
||||
showCustomColorInput.value = true;
|
||||
customColor.value = currentThemeColor.value;
|
||||
};
|
||||
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
|
||||
// 创建响应式的计算属性
|
||||
const isDarkMode = computed(() => isDark.value);
|
||||
|
||||
// 自定义颜色输入
|
||||
const customColor = ref("");
|
||||
const showCustomColorInput = ref(false);
|
||||
|
||||
// 当前选中的主题色
|
||||
const currentThemeColor = computed(() => {
|
||||
return themeColorOptions[0].primary;
|
||||
});
|
||||
|
||||
// 选择预设颜色
|
||||
const handleSelectColor = (color: (typeof themeColorOptions)[0]) => {
|
||||
setThemeColor(color);
|
||||
customColor.value = color.primary;
|
||||
|
||||
// 提示
|
||||
// 应用自定义颜色
|
||||
const applyCustomColor = () => {
|
||||
// 验证颜色格式
|
||||
const colorRegex = /^#([0-9A-F]{6}|[0-9A-F]{3})$/i;
|
||||
if (!colorRegex.test(customColor.value)) {
|
||||
uni.showToast({
|
||||
title: "主题色已更新",
|
||||
title: "请输入有效的颜色值",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换3位颜色值为6位
|
||||
let color = customColor.value;
|
||||
if (color.length === 4) {
|
||||
color = "#" + color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
|
||||
}
|
||||
|
||||
// 创建自定义主题色选项
|
||||
const customColorOption = {
|
||||
name: "自定义",
|
||||
value: "custom",
|
||||
primary: color,
|
||||
};
|
||||
|
||||
setThemeColor(customColorOption);
|
||||
showCustomColorInput.value = false;
|
||||
|
||||
// 提示
|
||||
uni.showToast({
|
||||
title: "自定义主题色已应用",
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
};
|
||||
|
||||
// 重置为默认主题
|
||||
const handleResetTheme = async () => {
|
||||
try {
|
||||
await confirm({ title: "确认重置", msg: "确定要重置为默认主题吗?", headerImage: "warning" });
|
||||
setThemeColor(themeColorOptions[0]);
|
||||
customColor.value = themeColorOptions[0].primary;
|
||||
toast.success("已重置为默认主题");
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
};
|
||||
|
||||
// 切换暗黑模式
|
||||
const handleToggleDarkMode = () => {
|
||||
toggleTheme();
|
||||
nextTick(() => {
|
||||
uni.showToast({
|
||||
title: `已切换到${isDarkMode.value ? "暗黑" : "浅色"}模式`,
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
};
|
||||
|
||||
// 显示自定义颜色输入
|
||||
const showCustomInput = () => {
|
||||
showCustomColorInput.value = true;
|
||||
customColor.value = currentThemeColor.value;
|
||||
};
|
||||
|
||||
// 应用自定义颜色
|
||||
const applyCustomColor = () => {
|
||||
// 验证颜色格式
|
||||
const colorRegex = /^#([0-9A-F]{6}|[0-9A-F]{3})$/i;
|
||||
if (!colorRegex.test(customColor.value)) {
|
||||
uni.showToast({
|
||||
title: "请输入有效的颜色值",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换3位颜色值为6位
|
||||
let color = customColor.value;
|
||||
if (color.length === 4) {
|
||||
color = "#" + color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
|
||||
}
|
||||
|
||||
// 创建自定义主题色选项
|
||||
const customColorOption = {
|
||||
name: "自定义",
|
||||
value: "custom",
|
||||
primary: color,
|
||||
};
|
||||
|
||||
setThemeColor(customColorOption);
|
||||
showCustomColorInput.value = false;
|
||||
|
||||
// 提示
|
||||
uni.showToast({
|
||||
title: "自定义主题色已应用",
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
};
|
||||
|
||||
// 重置为默认主题
|
||||
const handleResetTheme = async () => {
|
||||
try {
|
||||
await confirm({ title: "确认重置", msg: "确定要重置为默认主题吗?", headerImage: "warning" });
|
||||
setThemeColor(themeColorOptions[0]);
|
||||
customColor.value = themeColorOptions[0].primary;
|
||||
toast.success("已重置为默认主题");
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
};
|
||||
|
||||
// 切换暗黑模式
|
||||
const handleToggleDarkMode = () => {
|
||||
toggleTheme();
|
||||
nextTick(() => {
|
||||
uni.showToast({
|
||||
title: `已切换到${isDarkMode.value ? "暗黑" : "浅色"}模式`,
|
||||
icon: "success",
|
||||
duration: 1500,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
customColor.value = currentThemeColor.value;
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
customColor.value = currentThemeColor.value;
|
||||
});
|
||||
onLoad(() => {
|
||||
customColor.value = currentThemeColor.value;
|
||||
});
|
||||
|
||||
// 页面显示时确保主题色同步
|
||||
onShow(() => {
|
||||
customColor.value = currentThemeColor.value;
|
||||
});
|
||||
onMounted(() => {
|
||||
customColor.value = currentThemeColor.value;
|
||||
});
|
||||
|
||||
// 页面显示时确保主题色同步
|
||||
onShow(() => {
|
||||
customColor.value = currentThemeColor.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.color-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
.color-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
|
||||
&--active {
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
&--active {
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.color-dot {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.color-dot-lg {
|
||||
flex-shrink: 0;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.color-dot-lg {
|
||||
flex-shrink: 0;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
.popup-content {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 32rpx;
|
||||
border-bottom: 1rpx solid var(--color-border);
|
||||
}
|
||||
.popup-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 32rpx;
|
||||
border-bottom: 1rpx solid var(--color-border);
|
||||
}
|
||||
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.popup-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
border-top: 1rpx solid var(--color-border);
|
||||
}
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
border-top: 1rpx solid var(--color-border);
|
||||
}
|
||||
|
||||
:deep(.theme-input) {
|
||||
background-color: var(--color-bg-secondary) !important;
|
||||
color: var(--color-text) !important;
|
||||
}
|
||||
:deep(.theme-input) {
|
||||
color: var(--color-text) !important;
|
||||
background-color: var(--color-bg-secondary) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { ref } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
|
||||
definePage({
|
||||
name: "webview",
|
||||
@@ -30,5 +28,3 @@ onLoad((query: any) => {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
<template>
|
||||
<view class="page page--padding">
|
||||
<view>
|
||||
<wd-search v-model="queryParams.keywords" placeholder="搜索配置名称/键名" hide-cancel @search="handleSearch" />
|
||||
<wd-search
|
||||
v-model="queryParams.keywords"
|
||||
placeholder="搜索配置名称/键名"
|
||||
hide-cancel
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="mt-16rpx">
|
||||
<wd-card v-for="item in pageData" :key="item.id" custom-class="item-card" @click="openConfigDialog(item.id)">
|
||||
<wd-card
|
||||
v-for="item in pageData"
|
||||
:key="item.id"
|
||||
custom-class="item-card"
|
||||
@click="openConfigDialog(item.id)"
|
||||
>
|
||||
<view class="flex-between">
|
||||
<text class="font-bold text-32rpx">{{ item.configName }}</text>
|
||||
<view class="w-88rpx h-88rpx flex-center rounded-full" hover-class="bg-[var(--color-text-placeholder)]/16"
|
||||
@click.stop="showConfigActions(item)">
|
||||
<view
|
||||
class="w-88rpx h-88rpx flex-center rounded-full"
|
||||
hover-class="bg-[var(--color-text-placeholder)]/16"
|
||||
@click.stop="showConfigActions(item)"
|
||||
>
|
||||
<wd-icon name="more" size="18" class="color-text-secondary" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -28,7 +41,12 @@
|
||||
</view>
|
||||
|
||||
<!-- 弹窗表单 -->
|
||||
<wd-popup v-model="dialog.visible" position="bottom" custom-class="popup-bottom" @close="closeConfigDialog">
|
||||
<wd-popup
|
||||
v-model="dialog.visible"
|
||||
position="bottom"
|
||||
custom-class="popup-bottom"
|
||||
@close="closeConfigDialog"
|
||||
>
|
||||
<view class="p-4">
|
||||
<view class="popup-title">
|
||||
{{ formData.id ? "编辑配置" : "新增配置" }}
|
||||
@@ -44,7 +62,12 @@
|
||||
<wd-input v-model="formData.configValue" placeholder="请输入配置键值" />
|
||||
</wd-form-item>
|
||||
<wd-form-item prop="remark" title="描述">
|
||||
<wd-textarea v-model="formData.remark" placeholder="请输入配置描述" :maxlength="100" show-word-limit />
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
placeholder="请输入配置描述"
|
||||
:maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="popup-actions">
|
||||
@@ -57,178 +80,184 @@
|
||||
</wd-popup>
|
||||
|
||||
<!-- 浮动新增按钮 -->
|
||||
<wd-fab v-if="hasPermission('sys:config:create') && !dialog.visible" :expandable="false" :gap="{ bottom: 32 }"
|
||||
@click="openConfigDialog()" />
|
||||
<wd-fab
|
||||
v-if="hasPermission('sys:config:create') && !dialog.visible"
|
||||
:expandable="false"
|
||||
:gap="{ bottom: 32 }"
|
||||
@click="openConfigDialog()"
|
||||
/>
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
<wd-action-sheet v-model="actionSheetVisible" :actions="actionSheetActions" cancel-text="取消" @select="handleActionSelect" />
|
||||
<wd-action-sheet
|
||||
v-model="actionSheetVisible"
|
||||
:actions="actionSheetActions"
|
||||
cancel-text="取消"
|
||||
@select="handleActionSelect"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import ConfigAPI, { type ConfigPageQuery, ConfigItem, ConfigForm } from "@/api/config";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import ConfigAPI, { type ConfigPageQuery, ConfigItem, ConfigForm } from "@/api/config";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
definePage({
|
||||
name: "config",
|
||||
style: { navigationBarTitleText: "系统配置" },
|
||||
});
|
||||
|
||||
definePage({
|
||||
name: "config",
|
||||
style: { navigationBarTitleText: "系统配置" },
|
||||
});
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
const queryParams = reactive<ConfigPageQuery>({ pageNum: 1, pageSize: 10, keywords: "" });
|
||||
const total = ref(0);
|
||||
const pageData = ref<ConfigItem[]>([]);
|
||||
const dialog = reactive({ visible: false });
|
||||
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const currentActionItem = ref<any>(null);
|
||||
|
||||
const queryParams = reactive<ConfigPageQuery>({ pageNum: 1, pageSize: 10, keywords: "" });
|
||||
const total = ref(0);
|
||||
const pageData = ref<ConfigItem[]>([]);
|
||||
const dialog = reactive({ visible: false });
|
||||
const initialFormData: ConfigForm = {
|
||||
id: undefined,
|
||||
configName: undefined,
|
||||
configKey: undefined,
|
||||
configValue: undefined,
|
||||
remark: undefined,
|
||||
};
|
||||
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const currentActionItem = ref<any>(null);
|
||||
const formData = reactive<ConfigForm>({ ...initialFormData });
|
||||
|
||||
const initialFormData: ConfigForm = {
|
||||
id: undefined,
|
||||
configName: undefined,
|
||||
configKey: undefined,
|
||||
configValue: undefined,
|
||||
remark: undefined,
|
||||
};
|
||||
const rules = toFormSchema({
|
||||
configName: [{ required: true, message: "请输入配置名称" }],
|
||||
configKey: [{ required: true, message: "请输入配置键名" }],
|
||||
configValue: [{ required: true, message: "请输入配置键值" }],
|
||||
});
|
||||
|
||||
const formData = reactive<ConfigForm>({ ...initialFormData });
|
||||
// 搜索触发
|
||||
const handleSearch = () => loadConfigList();
|
||||
|
||||
const rules = toFormSchema({
|
||||
configName: [{ required: true, message: "请输入配置名称" }],
|
||||
configKey: [{ required: true, message: "请输入配置键名" }],
|
||||
configValue: [{ required: true, message: "请输入配置键值" }],
|
||||
});
|
||||
// 加载列表
|
||||
function loadConfigList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchConfigList();
|
||||
}
|
||||
|
||||
// 搜索触发
|
||||
const handleSearch = () => loadConfigList();
|
||||
// 分页加载列表
|
||||
function fetchConfigList() {
|
||||
loadMoreState.value = "loading";
|
||||
ConfigAPI.getPage(queryParams)
|
||||
.then((data: any) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
});
|
||||
}
|
||||
|
||||
// 加载列表
|
||||
function loadConfigList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchConfigList();
|
||||
// 打开弹窗(新增/编辑)
|
||||
async function openConfigDialog(id?: number) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
dialog.visible = true;
|
||||
if (id) {
|
||||
formData.id = id;
|
||||
const data = await ConfigAPI.getFormData(id);
|
||||
Object.assign(formData, data, { id });
|
||||
}
|
||||
}
|
||||
|
||||
// 分页加载列表
|
||||
function fetchConfigList() {
|
||||
loadMoreState.value = "loading";
|
||||
ConfigAPI.getPage(queryParams)
|
||||
.then((data: any) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
// 提交表单
|
||||
function submitConfigForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
isSubmitting.value = true;
|
||||
const action = formData.id ? ConfigAPI.update(formData.id, formData) : ConfigAPI.add(formData);
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeConfigDialog();
|
||||
loadConfigList();
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
isSubmitting.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 打开弹窗(新增/编辑)
|
||||
async function openConfigDialog(id?: number) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
dialog.visible = true;
|
||||
if (id) {
|
||||
formData.id = id;
|
||||
const data = await ConfigAPI.getFormData(id);
|
||||
Object.assign(formData, data, { id });
|
||||
}
|
||||
}
|
||||
// 关闭弹窗
|
||||
function closeConfigDialog() {
|
||||
dialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function submitConfigForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
isSubmitting.value = true;
|
||||
const action = formData.id ? ConfigAPI.update(formData.id, formData) : ConfigAPI.add(formData);
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeConfigDialog();
|
||||
loadConfigList();
|
||||
})
|
||||
.finally(() => {
|
||||
isSubmitting.value = false;
|
||||
});
|
||||
// 操作菜单分发
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
const value = item.name;
|
||||
const configItem = currentActionItem.value;
|
||||
if (value === "编辑") {
|
||||
openConfigDialog(configItem.id);
|
||||
} else if (value === "删除") {
|
||||
confirm({
|
||||
title: "确认删除",
|
||||
msg: `确定要删除配置「${configItem.configName}」吗?`,
|
||||
headerImage: "warning",
|
||||
}).then(async () => {
|
||||
await ConfigAPI.deleteById(configItem.id!);
|
||||
toast.success("删除成功");
|
||||
loadConfigList();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeConfigDialog() {
|
||||
dialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
// 更多操作
|
||||
function showConfigActions(item: ConfigItem) {
|
||||
const actions: { name: string; color?: string }[] = [];
|
||||
|
||||
if (hasPermission("sys:config:update")) {
|
||||
actions.push({ name: "编辑" });
|
||||
}
|
||||
|
||||
// 操作菜单分发
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
const value = item.name;
|
||||
const configItem = currentActionItem.value;
|
||||
if (value === "编辑") {
|
||||
openConfigDialog(configItem.id);
|
||||
} else if (value === "删除") {
|
||||
confirm({
|
||||
title: "确认删除",
|
||||
msg: `确定要删除配置「${configItem.configName}」吗?`,
|
||||
headerImage: "warning",
|
||||
}).then(async () => {
|
||||
await ConfigAPI.deleteById(configItem.id!);
|
||||
toast.success("删除成功");
|
||||
loadConfigList();
|
||||
});
|
||||
}
|
||||
if (hasPermission("sys:config:delete")) {
|
||||
actions.push({ name: "删除", color: "var(--color-danger)" });
|
||||
}
|
||||
|
||||
// 更多操作
|
||||
function showConfigActions(item: ConfigItem) {
|
||||
const actions: { name: string; color?: string }[] = [];
|
||||
|
||||
if (hasPermission("sys:config:update")) {
|
||||
actions.push({ name: "编辑" });
|
||||
}
|
||||
|
||||
if (hasPermission("sys:config:delete")) {
|
||||
actions.push({ name: "删除", color: "var(--color-danger)" });
|
||||
}
|
||||
|
||||
if (actions.length === 0) {
|
||||
toast.warning("暂无操作权限");
|
||||
return;
|
||||
}
|
||||
|
||||
currentActionItem.value = item;
|
||||
actionSheetActions.value = actions;
|
||||
actionSheetVisible.value = true;
|
||||
if (actions.length === 0) {
|
||||
toast.warning("暂无操作权限");
|
||||
return;
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
fetchConfigList();
|
||||
} else {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
});
|
||||
currentActionItem.value = item;
|
||||
actionSheetActions.value = actions;
|
||||
actionSheetVisible.value = true;
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
loadConfigList();
|
||||
});
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
fetchConfigList();
|
||||
} else {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
});
|
||||
|
||||
onLoad(() => {
|
||||
loadConfigList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -45,10 +45,23 @@
|
||||
</view>
|
||||
<wd-form ref="formRef" :model="formData" :schema="rules">
|
||||
<!-- 上级部门选择:触发交 wd-form-item,选择器只负责弹出 -->
|
||||
<wd-form-item title="上级部门" prop="parentId" required is-link
|
||||
:value="parentLabel" placeholder="请选择上级部门" @click="showParentPicker = true" />
|
||||
<wd-cascader v-model="parentSelected" v-model:visible="showParentPicker" :options="parentOptions"
|
||||
:lazy-load="handleParentLazyLoad" :display-format="displayParentFormat" @confirm="handleParentConfirm" />
|
||||
<wd-form-item
|
||||
title="上级部门"
|
||||
prop="parentId"
|
||||
required
|
||||
is-link
|
||||
:value="parentLabel"
|
||||
placeholder="请选择上级部门"
|
||||
@click="showParentPicker = true"
|
||||
/>
|
||||
<wd-cascader
|
||||
v-model="parentSelected"
|
||||
v-model:visible="showParentPicker"
|
||||
:options="parentOptions"
|
||||
:lazy-load="handleParentLazyLoad"
|
||||
:display-format="displayParentFormat"
|
||||
@confirm="handleParentConfirm"
|
||||
/>
|
||||
<wd-form-item prop="name" title="部门名称" required>
|
||||
<wd-input v-model="formData.name" placeholder="请输入部门名称" />
|
||||
</wd-form-item>
|
||||
@@ -70,7 +83,12 @@
|
||||
</wd-popup>
|
||||
|
||||
<!-- 浮动新增按钮 -->
|
||||
<wd-fab v-if="hasPermission('sys:dept:create') && !dialog.visible" :expandable="false" :gap="{ bottom: 32 }" @click="openDeptDialog()" />
|
||||
<wd-fab
|
||||
v-if="hasPermission('sys:dept:create') && !dialog.visible"
|
||||
:expandable="false"
|
||||
:gap="{ bottom: 32 }"
|
||||
@click="openDeptDialog()"
|
||||
/>
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
<wd-action-sheet
|
||||
@@ -83,7 +101,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
@@ -91,14 +108,13 @@ import DeptAPI, { type DeptQuery, DeptItem, DeptForm } from "@/api/dept";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
import CustomTree from "@/components/custom-tree/index.vue";
|
||||
|
||||
definePage({
|
||||
name: "dept",
|
||||
style: { navigationBarTitleText: "部门管理" },
|
||||
});
|
||||
|
||||
definePage({
|
||||
name: "dept",
|
||||
style: { navigationBarTitleText: "部门管理" },
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const { confirm } = useDialog();
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
@@ -328,14 +344,12 @@ onLoad(() => {
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dept-node {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dept-node__id {
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<template>
|
||||
<view class="page page--padding">
|
||||
<view>
|
||||
<wd-search v-model="queryParams.keywords" placeholder="搜索字典名称/编码" hide-cancel @search="handleSearch" />
|
||||
<wd-search
|
||||
v-model="queryParams.keywords"
|
||||
placeholder="搜索字典名称/编码"
|
||||
hide-cancel
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="mt-16rpx">
|
||||
<wd-card v-for="item in pageData" :key="item.id" custom-class="item-card" @click="openDictItemPage(item)">
|
||||
<wd-card
|
||||
v-for="item in pageData"
|
||||
:key="item.id"
|
||||
custom-class="item-card"
|
||||
@click="openDictItemPage(item)"
|
||||
>
|
||||
<view class="flex-start">
|
||||
<view class="flex-1">
|
||||
<view class="flex-start">
|
||||
@@ -20,8 +30,11 @@
|
||||
|
||||
<view class="flex-between mt-16rpx">
|
||||
<text class="text-24rpx color-text-placeholder">{{ item.remark || "暂无备注" }}</text>
|
||||
<view class="w-88rpx h-88rpx flex-center rounded-full" hover-class="bg-[var(--color-text-placeholder)]/16"
|
||||
@click.stop="showDictActions(item)">
|
||||
<view
|
||||
class="w-88rpx h-88rpx flex-center rounded-full"
|
||||
hover-class="bg-[var(--color-text-placeholder)]/16"
|
||||
@click.stop="showDictActions(item)"
|
||||
>
|
||||
<wd-icon name="more" size="18" class="color-text-secondary" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -31,7 +44,12 @@
|
||||
<wd-empty v-else-if="total === 0" icon="search" tip="暂无数据" />
|
||||
</view>
|
||||
|
||||
<wd-popup v-model="dialog.visible" position="bottom" custom-class="popup-bottom" @close="closeDictDialog">
|
||||
<wd-popup
|
||||
v-model="dialog.visible"
|
||||
position="bottom"
|
||||
custom-class="popup-bottom"
|
||||
@close="closeDictDialog"
|
||||
>
|
||||
<view class="p-4">
|
||||
<view class="popup-title">
|
||||
{{ formData.id ? "编辑字典" : "新增字典" }}
|
||||
@@ -47,7 +65,12 @@
|
||||
<wd-switch v-model="formData.status" :active-value="1" :inactive-value="0" />
|
||||
</wd-form-item>
|
||||
<wd-form-item prop="remark" title="备注">
|
||||
<wd-textarea v-model="formData.remark" placeholder="请输入备注" :maxlength="100" show-word-limit />
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
placeholder="请输入备注"
|
||||
:maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="popup-actions">
|
||||
@@ -57,185 +80,192 @@
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<wd-action-sheet v-model="actionSheetVisible" :actions="actionSheetActions" cancel-text="取消" @select="handleActionSelect" />
|
||||
<wd-action-sheet
|
||||
v-model="actionSheetVisible"
|
||||
:actions="actionSheetActions"
|
||||
cancel-text="取消"
|
||||
@select="handleActionSelect"
|
||||
/>
|
||||
|
||||
<wd-fab v-if="hasPermission('sys:dict:create') && !dialog.visible" :expandable="false" :gap="{ bottom: 32 }" @click="openDictDialog()" />
|
||||
<wd-fab
|
||||
v-if="hasPermission('sys:dict:create') && !dialog.visible"
|
||||
:expandable="false"
|
||||
:gap="{ bottom: 32 }"
|
||||
@click="openDictDialog()"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { useRouter } from "uni-mini-router";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import DictAPI, { type DictTypeForm, type DictTypePageQuery, type DictTypeItem } from "@/api/dict";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
|
||||
definePage({
|
||||
name: "dict",
|
||||
style: { navigationBarTitleText: "字典管理" },
|
||||
});
|
||||
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { useRouter } from "uni-mini-router";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import DictAPI, { type DictTypeForm, type DictTypePageQuery, type DictTypeItem } from "@/api/dict";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
definePage({
|
||||
name: "dict",
|
||||
style: { navigationBarTitleText: "字典管理" },
|
||||
const queryParams = reactive<DictTypePageQuery>({ pageNum: 1, pageSize: 10, keywords: "" });
|
||||
const total = ref(0);
|
||||
const pageData = ref<DictTypeItem[]>([]);
|
||||
const dialog = reactive({ visible: false });
|
||||
|
||||
const initialFormData: DictTypeForm = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
dictCode: undefined,
|
||||
status: 1,
|
||||
remark: undefined,
|
||||
};
|
||||
|
||||
const formData = reactive<DictTypeForm>({ ...initialFormData });
|
||||
|
||||
const rules = toFormSchema({
|
||||
name: [{ required: true, message: "请输入字典名称" }],
|
||||
dictCode: [{ required: true, message: "请输入字典编码" }],
|
||||
});
|
||||
|
||||
const handleSearch = () => loadDictTypeList();
|
||||
|
||||
function loadDictTypeList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchDictTypeList();
|
||||
}
|
||||
|
||||
function fetchDictTypeList() {
|
||||
loadMoreState.value = "loading";
|
||||
DictAPI.getPage(queryParams)
|
||||
.then((data: any) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
});
|
||||
}
|
||||
|
||||
function openDictItemPage(item: DictTypeItem) {
|
||||
if (!item.dictCode) return;
|
||||
router.push({
|
||||
path: "/pages/work/dict/item/index",
|
||||
query: { dictCode: item.dictCode, title: `【${item.name}】字典数据` },
|
||||
});
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
const queryParams = reactive<DictTypePageQuery>({ pageNum: 1, pageSize: 10, keywords: "" });
|
||||
const total = ref(0);
|
||||
const pageData = ref<DictTypeItem[]>([]);
|
||||
const dialog = reactive({ visible: false });
|
||||
|
||||
const initialFormData: DictTypeForm = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
dictCode: undefined,
|
||||
status: 1,
|
||||
remark: undefined,
|
||||
};
|
||||
|
||||
const formData = reactive<DictTypeForm>({ ...initialFormData });
|
||||
|
||||
const rules = toFormSchema({
|
||||
name: [{ required: true, message: "请输入字典名称" }],
|
||||
dictCode: [{ required: true, message: "请输入字典编码" }],
|
||||
});
|
||||
|
||||
const handleSearch = () => loadDictTypeList();
|
||||
|
||||
function loadDictTypeList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchDictTypeList();
|
||||
async function openDictDialog(id?: string) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
dialog.visible = true;
|
||||
if (id) {
|
||||
formData.id = id;
|
||||
const data = await DictAPI.getFormData(id);
|
||||
Object.assign(formData, data, { id });
|
||||
}
|
||||
}
|
||||
|
||||
function fetchDictTypeList() {
|
||||
loadMoreState.value = "loading";
|
||||
DictAPI.getPage(queryParams)
|
||||
.then((data: any) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
function submitDictForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
isSubmitting.value = true;
|
||||
const id = formData.id;
|
||||
const action = id ? DictAPI.update(id, formData) : DictAPI.create(formData);
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeDictDialog();
|
||||
loadDictTypeList();
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
isSubmitting.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeDictDialog() {
|
||||
dialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const pendingAction = ref<Record<string, () => void>>({});
|
||||
|
||||
function showDictActions(item: DictTypeItem) {
|
||||
const actions: { name: string; color?: string }[] = [];
|
||||
const actionMap: Record<string, () => void> = {};
|
||||
|
||||
actions.push({ name: "字典数据" });
|
||||
actionMap["字典数据"] = () => openDictItemPage(item);
|
||||
|
||||
if (hasPermission("sys:dict:update")) {
|
||||
actions.push({ name: "编辑" });
|
||||
actionMap["编辑"] = () => openDictDialog(item.id);
|
||||
}
|
||||
|
||||
function openDictItemPage(item: DictTypeItem) {
|
||||
if (!item.dictCode) return;
|
||||
router.push({
|
||||
path: "/pages/work/dict/item/index",
|
||||
query: { dictCode: item.dictCode, title: `【${item.name}】字典数据` },
|
||||
});
|
||||
}
|
||||
|
||||
async function openDictDialog(id?: string) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
dialog.visible = true;
|
||||
if (id) {
|
||||
formData.id = id;
|
||||
const data = await DictAPI.getFormData(id);
|
||||
Object.assign(formData, data, { id });
|
||||
}
|
||||
}
|
||||
|
||||
function submitDictForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
isSubmitting.value = true;
|
||||
const id = formData.id;
|
||||
const action = id ? DictAPI.update(id, formData) : DictAPI.create(formData);
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeDictDialog();
|
||||
loadDictTypeList();
|
||||
})
|
||||
.finally(() => {
|
||||
isSubmitting.value = false;
|
||||
if (hasPermission("sys:dict:delete")) {
|
||||
actions.push({ name: "删除", color: "var(--color-danger)" });
|
||||
actionMap["删除"] = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "确认删除",
|
||||
msg: `确定要删除字典「${item.name}」吗?`,
|
||||
headerImage: "warning",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeDictDialog() {
|
||||
dialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const pendingAction = ref<Record<string, () => void>>({});
|
||||
|
||||
function showDictActions(item: DictTypeItem) {
|
||||
const actions: { name: string; color?: string }[] = [];
|
||||
const actionMap: Record<string, () => void> = {};
|
||||
|
||||
actions.push({ name: "字典数据" });
|
||||
actionMap["字典数据"] = () => openDictItemPage(item);
|
||||
|
||||
if (hasPermission("sys:dict:update")) {
|
||||
actions.push({ name: "编辑" });
|
||||
actionMap["编辑"] = () => openDictDialog(item.id);
|
||||
}
|
||||
|
||||
if (hasPermission("sys:dict:delete")) {
|
||||
actions.push({ name: "删除", color: "var(--color-danger)" });
|
||||
actionMap["删除"] = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "确认删除",
|
||||
msg: `确定要删除字典「${item.name}」吗?`,
|
||||
headerImage: "warning",
|
||||
});
|
||||
if (item.id) {
|
||||
await DictAPI.deleteByIds(String(item.id));
|
||||
toast.success("删除成功");
|
||||
loadDictTypeList();
|
||||
}
|
||||
} catch {
|
||||
// 用户取消操作
|
||||
if (item.id) {
|
||||
await DictAPI.deleteByIds(String(item.id));
|
||||
toast.success("删除成功");
|
||||
loadDictTypeList();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (actions.length === 0) {
|
||||
toast.warning("暂无操作权限");
|
||||
return;
|
||||
}
|
||||
|
||||
actionSheetActions.value = actions;
|
||||
pendingAction.value = actionMap;
|
||||
actionSheetVisible.value = true;
|
||||
} catch {
|
||||
// 用户取消操作
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
pendingAction.value[item.name]?.();
|
||||
if (actions.length === 0) {
|
||||
toast.warning("暂无操作权限");
|
||||
return;
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
fetchDictTypeList();
|
||||
} else {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
});
|
||||
actionSheetActions.value = actions;
|
||||
pendingAction.value = actionMap;
|
||||
actionSheetVisible.value = true;
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
loadDictTypeList();
|
||||
});
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
pendingAction.value[item.name]?.();
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
fetchDictTypeList();
|
||||
} else {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
});
|
||||
|
||||
onLoad(() => {
|
||||
loadDictTypeList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
<template>
|
||||
<view class="page page--padding">
|
||||
<view>
|
||||
<wd-search v-model="queryParams.keywords" placeholder="搜索字典标签/字典值" hide-cancel @search="handleSearch" />
|
||||
<wd-search
|
||||
v-model="queryParams.keywords"
|
||||
placeholder="搜索字典标签/字典值"
|
||||
hide-cancel
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="mt-16rpx">
|
||||
<wd-card v-for="item in pageData" :key="item.id" custom-class="item-card" @click="openItemDialog(item.id)">
|
||||
<wd-card
|
||||
v-for="item in pageData"
|
||||
:key="item.id"
|
||||
custom-class="item-card"
|
||||
@click="openItemDialog(item.id)"
|
||||
>
|
||||
<view class="flex-start">
|
||||
<view class="flex-1">
|
||||
<view class="flex-start">
|
||||
@@ -22,8 +32,11 @@
|
||||
|
||||
<view class="flex-between mt-16rpx">
|
||||
<text class="text-24rpx color-text-placeholder">{{ item.remark || "暂无备注" }}</text>
|
||||
<view class="w-88rpx h-88rpx flex-center rounded-full" hover-class="bg-[var(--color-text-placeholder)]/16"
|
||||
@click.stop="showItemActions(item)">
|
||||
<view
|
||||
class="w-88rpx h-88rpx flex-center rounded-full"
|
||||
hover-class="bg-[var(--color-text-placeholder)]/16"
|
||||
@click.stop="showItemActions(item)"
|
||||
>
|
||||
<wd-icon name="more" size="18" class="color-text-secondary" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -33,7 +46,12 @@
|
||||
<wd-empty v-else-if="total === 0" icon="search" tip="暂无数据" />
|
||||
</view>
|
||||
|
||||
<wd-popup v-model="dialog.visible" position="bottom" custom-class="popup-bottom" @close="closeItemDialog">
|
||||
<wd-popup
|
||||
v-model="dialog.visible"
|
||||
position="bottom"
|
||||
custom-class="popup-bottom"
|
||||
@close="closeItemDialog"
|
||||
>
|
||||
<view class="p-4">
|
||||
<view class="popup-title">
|
||||
{{ formData.id ? "编辑字典数据" : "新增字典数据" }}
|
||||
@@ -52,7 +70,12 @@
|
||||
<wd-input-number v-model="formData.sort" :min="0" />
|
||||
</wd-form-item>
|
||||
<wd-form-item prop="remark" title="备注">
|
||||
<wd-textarea v-model="formData.remark" placeholder="请输入备注" :maxlength="100" show-word-limit />
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
placeholder="请输入备注"
|
||||
:maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="popup-actions">
|
||||
@@ -62,201 +85,207 @@
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<wd-action-sheet v-model="actionSheetVisible" :actions="actionSheetActions" cancel-text="取消" @select="handleActionSelect" />
|
||||
<wd-action-sheet
|
||||
v-model="actionSheetVisible"
|
||||
:actions="actionSheetActions"
|
||||
cancel-text="取消"
|
||||
@select="handleActionSelect"
|
||||
/>
|
||||
|
||||
<wd-fab v-if="hasPermission('sys:dict-item:create') && !dialog.visible" :expandable="false" :gap="{ bottom: 32 }"
|
||||
@click="openItemDialog()" />
|
||||
<wd-fab
|
||||
v-if="hasPermission('sys:dict-item:create') && !dialog.visible"
|
||||
:expandable="false"
|
||||
:gap="{ bottom: 32 }"
|
||||
@click="openItemDialog()"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import DictAPI, { type DictItemForm, type DictItemPageQuery, type DictDataItem } from "@/api/dict";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
|
||||
definePage({
|
||||
name: "dict-item",
|
||||
style: { navigationBarTitleText: "字典数据" },
|
||||
});
|
||||
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import DictAPI, { type DictItemForm, type DictItemPageQuery, type DictDataItem } from "@/api/dict";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
|
||||
definePage({
|
||||
name: "dict-item",
|
||||
style: { navigationBarTitleText: "字典数据" },
|
||||
});
|
||||
const dictCode = ref<string>("");
|
||||
const pageTitle = ref<string>("字典数据");
|
||||
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
const dictCode = ref<string>("");
|
||||
const pageTitle = ref<string>("字典数据");
|
||||
const queryParams = reactive<DictItemPageQuery>({ pageNum: 1, pageSize: 10, keywords: "" });
|
||||
const total = ref(0);
|
||||
const pageData = ref<DictDataItem[]>([]);
|
||||
const dialog = reactive({ visible: false });
|
||||
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
const initialFormData: DictItemForm = {
|
||||
id: undefined,
|
||||
dictCode: undefined,
|
||||
label: undefined,
|
||||
value: undefined,
|
||||
sort: 1,
|
||||
status: 1,
|
||||
remark: undefined,
|
||||
};
|
||||
|
||||
const queryParams = reactive<DictItemPageQuery>({ pageNum: 1, pageSize: 10, keywords: "" });
|
||||
const total = ref(0);
|
||||
const pageData = ref<DictDataItem[]>([]);
|
||||
const dialog = reactive({ visible: false });
|
||||
const formData = reactive<DictItemForm>({ ...initialFormData });
|
||||
|
||||
const initialFormData: DictItemForm = {
|
||||
id: undefined,
|
||||
dictCode: undefined,
|
||||
label: undefined,
|
||||
value: undefined,
|
||||
sort: 1,
|
||||
status: 1,
|
||||
remark: undefined,
|
||||
};
|
||||
const rules = toFormSchema({
|
||||
label: [{ required: true, message: "请输入字典项标签" }],
|
||||
value: [{ required: true, message: "请输入字典项值" }],
|
||||
});
|
||||
|
||||
const formData = reactive<DictItemForm>({ ...initialFormData });
|
||||
const handleSearch = () => loadItemList();
|
||||
|
||||
const rules = toFormSchema({
|
||||
label: [{ required: true, message: "请输入字典项标签" }],
|
||||
value: [{ required: true, message: "请输入字典项值" }],
|
||||
});
|
||||
function loadItemList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchItemList();
|
||||
}
|
||||
|
||||
const handleSearch = () => loadItemList();
|
||||
|
||||
function loadItemList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchItemList();
|
||||
function fetchItemList() {
|
||||
if (!dictCode.value) {
|
||||
pageData.value = [];
|
||||
total.value = 0;
|
||||
loadMoreState.value = "finished";
|
||||
return;
|
||||
}
|
||||
|
||||
function fetchItemList() {
|
||||
if (!dictCode.value) {
|
||||
loadMoreState.value = "loading";
|
||||
DictAPI.getItemPage(dictCode.value, queryParams)
|
||||
.then((data: any) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
total.value = 0;
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
});
|
||||
}
|
||||
|
||||
async function openItemDialog(id?: string) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
dialog.visible = true;
|
||||
formData.dictCode = dictCode.value;
|
||||
|
||||
if (id) {
|
||||
formData.id = id;
|
||||
const data = await DictAPI.getItemFormData(dictCode.value, id);
|
||||
Object.assign(formData, data, { id });
|
||||
}
|
||||
}
|
||||
|
||||
function submitItemForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
if (!dictCode.value) {
|
||||
toast.error("缺少字典编码");
|
||||
return;
|
||||
}
|
||||
|
||||
loadMoreState.value = "loading";
|
||||
DictAPI.getItemPage(dictCode.value, queryParams)
|
||||
.then((data: any) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
isSubmitting.value = true;
|
||||
const id = formData.id;
|
||||
const action = id
|
||||
? DictAPI.updateItem(dictCode.value, id, formData)
|
||||
: DictAPI.createItem(dictCode.value, formData);
|
||||
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeItemDialog();
|
||||
loadItemList();
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
isSubmitting.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeItemDialog() {
|
||||
dialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const pendingAction = ref<Record<string, () => void>>({});
|
||||
|
||||
function showItemActions(item: DictDataItem) {
|
||||
const actions: { name: string; color?: string }[] = [];
|
||||
const actionMap: Record<string, () => void> = {};
|
||||
|
||||
if (hasPermission("sys:dict-item:update")) {
|
||||
actions.push({ name: "编辑" });
|
||||
actionMap["编辑"] = () => openItemDialog(item.id);
|
||||
}
|
||||
|
||||
async function openItemDialog(id?: string) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
dialog.visible = true;
|
||||
formData.dictCode = dictCode.value;
|
||||
|
||||
if (id) {
|
||||
formData.id = id;
|
||||
const data = await DictAPI.getItemFormData(dictCode.value, id);
|
||||
Object.assign(formData, data, { id });
|
||||
}
|
||||
}
|
||||
|
||||
function submitItemForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
if (!dictCode.value) {
|
||||
toast.error("缺少字典编码");
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
const id = formData.id;
|
||||
const action = id
|
||||
? DictAPI.updateItem(dictCode.value, id, formData)
|
||||
: DictAPI.createItem(dictCode.value, formData);
|
||||
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeItemDialog();
|
||||
loadItemList();
|
||||
})
|
||||
.finally(() => {
|
||||
isSubmitting.value = false;
|
||||
if (hasPermission("sys:dict-item:delete")) {
|
||||
actions.push({ name: "删除", color: "var(--color-danger)" });
|
||||
actionMap["删除"] = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "确认删除",
|
||||
msg: `确定要删除字典数据「${item.label}」吗?`,
|
||||
headerImage: "warning",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeItemDialog() {
|
||||
dialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const pendingAction = ref<Record<string, () => void>>({});
|
||||
|
||||
function showItemActions(item: DictDataItem) {
|
||||
const actions: { name: string; color?: string }[] = [];
|
||||
const actionMap: Record<string, () => void> = {};
|
||||
|
||||
if (hasPermission("sys:dict-item:update")) {
|
||||
actions.push({ name: "编辑" });
|
||||
actionMap["编辑"] = () => openItemDialog(item.id);
|
||||
}
|
||||
|
||||
if (hasPermission("sys:dict-item:delete")) {
|
||||
actions.push({ name: "删除", color: "var(--color-danger)" });
|
||||
actionMap["删除"] = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "确认删除",
|
||||
msg: `确定要删除字典数据「${item.label}」吗?`,
|
||||
headerImage: "warning",
|
||||
});
|
||||
if (item.id) {
|
||||
await DictAPI.deleteItems(dictCode.value, String(item.id));
|
||||
toast.success("删除成功");
|
||||
loadItemList();
|
||||
}
|
||||
} catch {
|
||||
// 用户取消操作
|
||||
if (item.id) {
|
||||
await DictAPI.deleteItems(dictCode.value, String(item.id));
|
||||
toast.success("删除成功");
|
||||
loadItemList();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (actions.length === 0) {
|
||||
toast.warning("暂无操作权限");
|
||||
return;
|
||||
}
|
||||
|
||||
actionSheetActions.value = actions;
|
||||
pendingAction.value = actionMap;
|
||||
actionSheetVisible.value = true;
|
||||
} catch {
|
||||
// 用户取消操作
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
pendingAction.value[item.name]?.();
|
||||
if (actions.length === 0) {
|
||||
toast.warning("暂无操作权限");
|
||||
return;
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
fetchItemList();
|
||||
} else {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
});
|
||||
actionSheetActions.value = actions;
|
||||
pendingAction.value = actionMap;
|
||||
actionSheetVisible.value = true;
|
||||
}
|
||||
|
||||
onLoad((query: any) => {
|
||||
dictCode.value = String((query as any)?.dictCode || "");
|
||||
pageTitle.value = String((query as any)?.title || "字典数据");
|
||||
if (pageTitle.value) {
|
||||
uni.setNavigationBarTitle({ title: pageTitle.value });
|
||||
}
|
||||
loadItemList();
|
||||
});
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
pendingAction.value[item.name]?.();
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
fetchItemList();
|
||||
} else {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
});
|
||||
|
||||
onLoad((query: any) => {
|
||||
dictCode.value = String((query as any)?.dictCode || "");
|
||||
pageTitle.value = String((query as any)?.title || "字典数据");
|
||||
if (pageTitle.value) {
|
||||
uni.setNavigationBarTitle({ title: pageTitle.value });
|
||||
}
|
||||
loadItemList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
<template v-for="(item, index) in visibleGridList" :key="index">
|
||||
<wd-card :title="item.title">
|
||||
<wd-grid clickable :column="4">
|
||||
<wd-grid-item v-for="(child, childIndex) in item.children" :key="childIndex" use-slot @click="handleNavClick(child)">
|
||||
<wd-grid-item
|
||||
v-for="(child, childIndex) in item.children"
|
||||
:key="childIndex"
|
||||
use-slot
|
||||
@click="handleNavClick(child)"
|
||||
>
|
||||
<view class="work-grid__icon p-2">
|
||||
<image class="w-72rpx h-72rpx rounded-8rpx" :src="child.icon" />
|
||||
</view>
|
||||
@@ -17,39 +22,37 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import { useNavbar } from "@/composables/useNavbar";
|
||||
import { useNavigation } from "@/composables/useNavigation";
|
||||
import { menuConfig } from "@/config/menu";
|
||||
import { hasPermission as checkPermission } from "@/utils/permission";
|
||||
|
||||
import { computed } from "vue";
|
||||
import { useNavbar } from "@/composables/useNavbar";
|
||||
import { useNavigation } from "@/composables/useNavigation";
|
||||
import { menuConfig } from "@/config/menu";
|
||||
import { hasPermission as checkPermission } from "@/utils/permission";
|
||||
definePage({
|
||||
name: "work",
|
||||
style: {
|
||||
navigationStyle: "custom",
|
||||
navigationBarTitleText: "工作台",
|
||||
},
|
||||
layout: "tabbar",
|
||||
});
|
||||
|
||||
definePage({
|
||||
name: "work",
|
||||
style: {
|
||||
navigationStyle: "custom",
|
||||
navigationBarTitleText: "工作台",
|
||||
},
|
||||
layout: "tabbar",
|
||||
});
|
||||
const navbar = useNavbar();
|
||||
const { handleNavClick } = useNavigation();
|
||||
|
||||
const navbar = useNavbar();
|
||||
const { handleNavClick } = useNavigation();
|
||||
// 检查是否有权限
|
||||
const hasPermission = (perm: string) => {
|
||||
if (!perm) return true;
|
||||
return checkPermission(perm);
|
||||
};
|
||||
|
||||
// 检查是否有权限
|
||||
const hasPermission = (perm: string) => {
|
||||
if (!perm) return true;
|
||||
return checkPermission(perm);
|
||||
};
|
||||
|
||||
// 根据权限过滤后的菜单列表
|
||||
const visibleGridList = computed(() => {
|
||||
return menuConfig
|
||||
.map((group) => ({
|
||||
...group,
|
||||
children: group.children.filter((item) => hasPermission(item.perm)),
|
||||
}))
|
||||
.filter((group) => group.children.length > 0);
|
||||
});
|
||||
// 根据权限过滤后的菜单列表
|
||||
const visibleGridList = computed(() => {
|
||||
return menuConfig
|
||||
.map((group) => ({
|
||||
...group,
|
||||
children: group.children.filter((item) => hasPermission(item.perm)),
|
||||
}))
|
||||
.filter((group) => group.children.length > 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
<template>
|
||||
<view class="page page--padding">
|
||||
<view>
|
||||
<wd-search v-model="queryParams.keywords" placeholder="搜索日志内容" hide-cancel @search="handleSearch" />
|
||||
<wd-search
|
||||
v-model="queryParams.keywords"
|
||||
placeholder="搜索日志内容"
|
||||
hide-cancel
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<view class="mt-16rpx">
|
||||
<wd-card v-for="item in pageData" :key="item.id" custom-class="item-card" @click="openLogDetail(item)">
|
||||
<wd-card
|
||||
v-for="item in pageData"
|
||||
:key="item.id"
|
||||
custom-class="item-card"
|
||||
@click="openLogDetail(item)"
|
||||
>
|
||||
<!-- 主信息行 -->
|
||||
<view class="flex-start">
|
||||
<view class="flex-1">
|
||||
<view class="flex-start mt-12rpx">
|
||||
<text class="font-bold text-32rpx">{{ item.operatorName }}</text>
|
||||
<wd-tag plain size="small" class="ml-16rpx" :type="item.status === 1 ? 'success' : 'danger'">
|
||||
<wd-tag
|
||||
plain
|
||||
size="small"
|
||||
class="ml-16rpx"
|
||||
:type="item.status === 1 ? 'success' : 'danger'"
|
||||
>
|
||||
{{ item.status === 1 ? "成功" : "失败" }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
@@ -33,8 +48,11 @@
|
||||
<!-- 元信息行 -->
|
||||
<view class="flex-between mt-16rpx">
|
||||
<text class="text-24rpx color-text-placeholder">{{ item.createTime }}</text>
|
||||
<view class="w-64rpx h-64rpx flex-center rounded-full" hover-class="bg-[var(--color-text-placeholder)]/16"
|
||||
@click.stop="openLogDetail(item)">
|
||||
<view
|
||||
class="w-64rpx h-64rpx flex-center rounded-full"
|
||||
hover-class="bg-[var(--color-text-placeholder)]/16"
|
||||
@click.stop="openLogDetail(item)"
|
||||
>
|
||||
<wd-icon name="view" size="16" class="color-text-secondary" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -45,7 +63,12 @@
|
||||
</view>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<wd-popup v-model="detailDialog.visible" position="bottom" custom-class="popup-bottom" @close="closeLogDetail">
|
||||
<wd-popup
|
||||
v-model="detailDialog.visible"
|
||||
position="bottom"
|
||||
custom-class="popup-bottom"
|
||||
@close="closeLogDetail"
|
||||
>
|
||||
<view class="p-4">
|
||||
<view class="popup-title">日志详情</view>
|
||||
<wd-cell-group border>
|
||||
@@ -78,77 +101,74 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import LogAPI, { type LogPageQuery, LogItem } from "@/api/log";
|
||||
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import LogAPI, { type LogPageQuery, LogItem } from "@/api/log";
|
||||
definePage({
|
||||
name: "log",
|
||||
style: { navigationBarTitleText: "系统日志" },
|
||||
});
|
||||
|
||||
definePage({
|
||||
name: "log",
|
||||
style: { navigationBarTitleText: "系统日志" },
|
||||
});
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const queryParams = reactive<LogPageQuery>({ pageNum: 1, pageSize: 10 });
|
||||
const total = ref(0);
|
||||
const pageData = ref<LogItem[]>([]);
|
||||
|
||||
const queryParams = reactive<LogPageQuery>({ pageNum: 1, pageSize: 10 });
|
||||
const total = ref(0);
|
||||
const pageData = ref<LogItem[]>([]);
|
||||
const logDetail = ref<LogItem>({});
|
||||
const detailDialog = reactive({ visible: false });
|
||||
|
||||
const logDetail = ref<LogItem>({});
|
||||
const detailDialog = reactive({ visible: false });
|
||||
// 搜索触发
|
||||
const handleSearch = () => loadLogList();
|
||||
|
||||
// 搜索触发
|
||||
const handleSearch = () => loadLogList();
|
||||
// 加载列表
|
||||
function loadLogList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchLogList();
|
||||
}
|
||||
|
||||
// 加载列表
|
||||
function loadLogList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchLogList();
|
||||
}
|
||||
|
||||
// 分页加载列表
|
||||
function fetchLogList() {
|
||||
loadMoreState.value = "loading";
|
||||
LogAPI.getPage(queryParams)
|
||||
.then((data) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
});
|
||||
}
|
||||
|
||||
// 打开详情弹窗
|
||||
function openLogDetail(item: LogItem) {
|
||||
logDetail.value = item;
|
||||
detailDialog.visible = true;
|
||||
}
|
||||
|
||||
// 关闭详情弹窗
|
||||
function closeLogDetail() {
|
||||
detailDialog.visible = false;
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
fetchLogList();
|
||||
} else {
|
||||
// 分页加载列表
|
||||
function fetchLogList() {
|
||||
loadMoreState.value = "loading";
|
||||
LogAPI.getPage(queryParams)
|
||||
.then((data) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
loadLogList();
|
||||
});
|
||||
// 打开详情弹窗
|
||||
function openLogDetail(item: LogItem) {
|
||||
logDetail.value = item;
|
||||
detailDialog.visible = true;
|
||||
}
|
||||
|
||||
// 关闭详情弹窗
|
||||
function closeLogDetail() {
|
||||
detailDialog.visible = false;
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
fetchLogList();
|
||||
} else {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
});
|
||||
|
||||
onLoad(() => {
|
||||
loadLogList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
<template>
|
||||
<view class="page page--padding">
|
||||
<view>
|
||||
<wd-search v-model="queryParams.keywords" placeholder="搜索菜单名称" hide-cancel @search="handleSearch" />
|
||||
<wd-search
|
||||
v-model="queryParams.keywords"
|
||||
placeholder="搜索菜单名称"
|
||||
hide-cancel
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 菜单树形列表 -->
|
||||
<view class="mt-16rpx">
|
||||
<custom-tree :data="treeData" :default-expand-all="true" :show-action="true" @action="handleNodeAction">
|
||||
<custom-tree
|
||||
:data="treeData"
|
||||
:default-expand-all="true"
|
||||
:show-action="true"
|
||||
@action="handleNodeAction"
|
||||
>
|
||||
<!-- 自定义节点内容:ID + 名称 + 状态 -->
|
||||
<template #content="{ node }">
|
||||
<view class="menu-node">
|
||||
@@ -16,7 +26,11 @@
|
||||
<wd-tag class="flex-shrink-0" :type="getMenuTypeTag(node.type)" size="small">
|
||||
{{ getMenuTypeText(node.type) }}
|
||||
</wd-tag>
|
||||
<wd-tag class="flex-shrink-0" :type="node.visible === 1 ? 'success' : 'primary'" size="small">
|
||||
<wd-tag
|
||||
class="flex-shrink-0"
|
||||
:type="node.visible === 1 ? 'success' : 'primary'"
|
||||
size="small"
|
||||
>
|
||||
{{ node.visible === 1 ? "显示" : "隐藏" }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
@@ -27,7 +41,12 @@
|
||||
</view>
|
||||
|
||||
<!-- 弹窗表单 -->
|
||||
<wd-popup v-model="dialog.visible" position="bottom" custom-class="popup-bottom-scroll" @close="closeMenuDialog">
|
||||
<wd-popup
|
||||
v-model="dialog.visible"
|
||||
position="bottom"
|
||||
custom-class="popup-bottom-scroll"
|
||||
@close="closeMenuDialog"
|
||||
>
|
||||
<view class="p-4">
|
||||
<view class="popup-title">
|
||||
{{ formData.id ? "编辑菜单" : "新增菜单" }}
|
||||
@@ -35,10 +54,23 @@
|
||||
<scroll-view scroll-y class="max-h-60vh">
|
||||
<wd-form ref="formRef" :model="formData" :schema="rules">
|
||||
<!-- 上级菜单选择:触发交 wd-form-item,选择器只负责弹出 -->
|
||||
<wd-form-item title="上级菜单" prop="parentId" required is-link
|
||||
:value="parentLabel" placeholder="请选择上级菜单" @click="showParentPicker = true" />
|
||||
<wd-cascader v-model="parentSelected" v-model:visible="showParentPicker" :options="parentOptions"
|
||||
:lazy-load="handleParentLazyLoad" :display-format="displayParentFormat" @confirm="handleParentConfirm" />
|
||||
<wd-form-item
|
||||
title="上级菜单"
|
||||
prop="parentId"
|
||||
required
|
||||
is-link
|
||||
:value="parentLabel"
|
||||
placeholder="请选择上级菜单"
|
||||
@click="showParentPicker = true"
|
||||
/>
|
||||
<wd-cascader
|
||||
v-model="parentSelected"
|
||||
v-model:visible="showParentPicker"
|
||||
:options="parentOptions"
|
||||
:lazy-load="handleParentLazyLoad"
|
||||
:display-format="displayParentFormat"
|
||||
@confirm="handleParentConfirm"
|
||||
/>
|
||||
<wd-form-item prop="name" title="菜单名称" required>
|
||||
<wd-input v-model="formData.name" placeholder="请输入菜单名称" />
|
||||
</wd-form-item>
|
||||
@@ -77,293 +109,300 @@
|
||||
</wd-popup>
|
||||
|
||||
<!-- 浮动新增按钮 -->
|
||||
<wd-fab v-if="hasPermission('sys:menu:create') && !dialog.visible" :expandable="false" :gap="{ bottom: 32 }" @click="openMenuDialog()" />
|
||||
<wd-fab
|
||||
v-if="hasPermission('sys:menu:create') && !dialog.visible"
|
||||
:expandable="false"
|
||||
:gap="{ bottom: 32 }"
|
||||
@click="openMenuDialog()"
|
||||
/>
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
<wd-action-sheet v-model="actionSheetVisible" :actions="actionSheetActions" cancel-text="取消" @select="handleActionSelect" />
|
||||
<wd-action-sheet
|
||||
v-model="actionSheetVisible"
|
||||
:actions="actionSheetActions"
|
||||
cancel-text="取消"
|
||||
@select="handleActionSelect"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import MenuAPI, { type MenuQuery, MenuItem, MenuForm } from "@/api/menu";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
import CustomTree from "@/components/custom-tree/index.vue";
|
||||
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import MenuAPI, { type MenuQuery, MenuItem, MenuForm } from "@/api/menu";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
import CustomTree from "@/components/custom-tree/index.vue";
|
||||
definePage({
|
||||
name: "menu",
|
||||
style: { navigationBarTitleText: "菜单管理" },
|
||||
});
|
||||
|
||||
definePage({
|
||||
name: "menu",
|
||||
style: { navigationBarTitleText: "菜单管理" },
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
const queryParams = reactive<MenuQuery>({ keywords: "" });
|
||||
const menuList = ref<MenuItem[]>([]);
|
||||
const dialog = reactive({ visible: false });
|
||||
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const currentActionItem = ref<any>(null);
|
||||
|
||||
const initialFormData: MenuForm = {
|
||||
id: undefined,
|
||||
parentId: "0",
|
||||
name: undefined,
|
||||
type: "M",
|
||||
routePath: undefined,
|
||||
component: undefined,
|
||||
perm: undefined,
|
||||
icon: undefined,
|
||||
sort: 1,
|
||||
visible: 1,
|
||||
};
|
||||
|
||||
const formData = reactive<MenuForm>({ ...initialFormData });
|
||||
|
||||
// 转换为树组件数据格式
|
||||
const treeData = computed(() => menuList.value.map((menu: MenuItem) => transformMenuToTree(menu)));
|
||||
|
||||
function transformMenuToTree(menu: MenuItem): any {
|
||||
return {
|
||||
value: menu.id,
|
||||
label: menu.name,
|
||||
id: menu.id,
|
||||
name: menu.name,
|
||||
type: menu.type,
|
||||
icon: menu.icon,
|
||||
visible: menu.visible,
|
||||
perm: menu.perm,
|
||||
children: menu.children?.map((child: MenuItem) => transformMenuToTree(child)) || [],
|
||||
};
|
||||
}
|
||||
|
||||
// 菜单类型标签
|
||||
function normalizeMenuType(type?: string | number) {
|
||||
if (type === 1 || type === "1") return "C";
|
||||
if (type === 2 || type === "2") return "M";
|
||||
if (type === 3 || type === "3") return "B";
|
||||
return type;
|
||||
}
|
||||
|
||||
function getMenuTypeTag(type?: string | number): "primary" | "success" | "warning" | "danger" {
|
||||
const map: Record<string, "primary" | "success" | "warning" | "danger"> = {
|
||||
C: "warning",
|
||||
M: "success",
|
||||
B: "danger",
|
||||
};
|
||||
const key = String(normalizeMenuType(type) || "M");
|
||||
return map[key] || "primary";
|
||||
}
|
||||
|
||||
function getMenuTypeText(type?: string | number) {
|
||||
const map: Record<string, string> = { C: "目录", M: "菜单", B: "按钮" };
|
||||
const key = String(normalizeMenuType(type) || "M");
|
||||
return map[key] || "菜单";
|
||||
}
|
||||
|
||||
// 上级菜单选择器
|
||||
const showParentPicker = ref(false);
|
||||
const parentSelected = ref<(string | number)[]>([]);
|
||||
const parentOptions = ref<OptionType[]>([]);
|
||||
const parentLabel = ref("");
|
||||
|
||||
const displayParentFormat = (selectedItems: Record<string, any>[]) => {
|
||||
if (!selectedItems || selectedItems.length === 0) return "";
|
||||
return selectedItems
|
||||
.map((item) => item?.label)
|
||||
.filter((label) => !!label)
|
||||
.join("/");
|
||||
};
|
||||
|
||||
function findMenuPath(
|
||||
data: Record<string, any>[],
|
||||
targetId: string,
|
||||
path: (string | number)[] = []
|
||||
): (string | number)[] | null {
|
||||
for (const item of data) {
|
||||
const currentPath = [...path, item.value];
|
||||
if (String(item.value) === targetId) {
|
||||
return currentPath;
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
const found = findMenuPath(item.children, targetId, currentPath);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// v2 Cascader 懒加载
|
||||
const handleParentLazyLoad = ({ selectedItem, resolve, finish }: any) => {
|
||||
if (String(selectedItem?.value) === "0") {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
const children = selectedItem.children;
|
||||
if (children && children.length > 0) {
|
||||
resolve(children);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
const handleParentConfirm = ({ value, selectedItems }: any) => {
|
||||
parentSelected.value = value;
|
||||
formData.parentId = String(value[value.length - 1]) || "0";
|
||||
parentLabel.value = selectedItems.map((item: any) => item.label).join("/");
|
||||
};
|
||||
|
||||
const rules = toFormSchema({
|
||||
name: [{ required: true, message: "请输入菜单名称" }],
|
||||
type: [{ required: true, message: "请选择菜单类型" }],
|
||||
parentId: [{ required: true, message: "请选择上级菜单" }],
|
||||
});
|
||||
|
||||
const handleSearch = () => loadMenuList();
|
||||
|
||||
function loadMenuList() {
|
||||
MenuAPI.getList(queryParams).then((data: any) => {
|
||||
menuList.value = data;
|
||||
});
|
||||
}
|
||||
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
const queryParams = reactive<MenuQuery>({ keywords: "" });
|
||||
const menuList = ref<MenuItem[]>([]);
|
||||
const dialog = reactive({ visible: false });
|
||||
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const currentActionItem = ref<any>(null);
|
||||
|
||||
const initialFormData: MenuForm = {
|
||||
id: undefined,
|
||||
parentId: "0",
|
||||
name: undefined,
|
||||
type: "M",
|
||||
routePath: undefined,
|
||||
component: undefined,
|
||||
perm: undefined,
|
||||
icon: undefined,
|
||||
sort: 1,
|
||||
visible: 1,
|
||||
};
|
||||
|
||||
const formData = reactive<MenuForm>({ ...initialFormData });
|
||||
|
||||
// 转换为树组件数据格式
|
||||
const treeData = computed(() => menuList.value.map((menu: MenuItem) => transformMenuToTree(menu)));
|
||||
|
||||
function transformMenuToTree(menu: MenuItem): any {
|
||||
return {
|
||||
value: menu.id,
|
||||
label: menu.name,
|
||||
id: menu.id,
|
||||
name: menu.name,
|
||||
type: menu.type,
|
||||
icon: menu.icon,
|
||||
visible: menu.visible,
|
||||
perm: menu.perm,
|
||||
children: menu.children?.map((child: MenuItem) => transformMenuToTree(child)) || [],
|
||||
};
|
||||
}
|
||||
|
||||
// 菜单类型标签
|
||||
function normalizeMenuType(type?: string | number) {
|
||||
if (type === 1 || type === "1") return "C";
|
||||
if (type === 2 || type === "2") return "M";
|
||||
if (type === 3 || type === "3") return "B";
|
||||
return type;
|
||||
}
|
||||
|
||||
function getMenuTypeTag(type?: string | number): "primary" | "success" | "warning" | "danger" {
|
||||
const map: Record<string, "primary" | "success" | "warning" | "danger"> = {
|
||||
C: "warning",
|
||||
M: "success",
|
||||
B: "danger",
|
||||
};
|
||||
const key = String(normalizeMenuType(type) || "M");
|
||||
return map[key] || "primary";
|
||||
}
|
||||
|
||||
function getMenuTypeText(type?: string | number) {
|
||||
const map: Record<string, string> = { C: "目录", M: "菜单", B: "按钮" };
|
||||
const key = String(normalizeMenuType(type) || "M");
|
||||
return map[key] || "菜单";
|
||||
}
|
||||
|
||||
// 上级菜单选择器
|
||||
const showParentPicker = ref(false);
|
||||
const parentSelected = ref<(string | number)[]>([]);
|
||||
const parentOptions = ref<OptionType[]>([]);
|
||||
const parentLabel = ref("");
|
||||
|
||||
const displayParentFormat = (selectedItems: Record<string, any>[]) => {
|
||||
if (!selectedItems || selectedItems.length === 0) return "";
|
||||
return selectedItems
|
||||
.map((item) => item?.label)
|
||||
.filter((label) => !!label)
|
||||
.join("/");
|
||||
};
|
||||
|
||||
function findMenuPath(
|
||||
data: Record<string, any>[],
|
||||
targetId: string,
|
||||
path: (string | number)[] = []
|
||||
): (string | number)[] | null {
|
||||
for (const item of data) {
|
||||
const currentPath = [...path, item.value];
|
||||
if (String(item.value) === targetId) {
|
||||
return currentPath;
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
const found = findMenuPath(item.children, targetId, currentPath);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// v2 Cascader 懒加载
|
||||
const handleParentLazyLoad = ({ selectedItem, resolve, finish }: any) => {
|
||||
if (String(selectedItem?.value) === "0") {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
const children = selectedItem.children;
|
||||
if (children && children.length > 0) {
|
||||
resolve(children);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
const handleParentConfirm = ({ value, selectedItems }: any) => {
|
||||
parentSelected.value = value;
|
||||
formData.parentId = String(value[value.length - 1]) || "0";
|
||||
parentLabel.value = selectedItems.map((item: any) => item.label).join("/");
|
||||
};
|
||||
|
||||
const rules = toFormSchema({
|
||||
name: [{ required: true, message: "请输入菜单名称" }],
|
||||
type: [{ required: true, message: "请选择菜单类型" }],
|
||||
parentId: [{ required: true, message: "请选择上级菜单" }],
|
||||
});
|
||||
|
||||
const handleSearch = () => loadMenuList();
|
||||
|
||||
function loadMenuList() {
|
||||
MenuAPI.getList(queryParams).then((data: any) => {
|
||||
menuList.value = data;
|
||||
// 操作菜单分发
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
const value = item.name;
|
||||
const menu = currentActionItem.value;
|
||||
if (value === "新增子菜单") {
|
||||
handleAddChild(menu);
|
||||
} else if (value === "编辑") {
|
||||
openMenuDialog(menu);
|
||||
} else if (value === "删除") {
|
||||
confirm({
|
||||
title: "确认删除",
|
||||
msg: `确定要删除菜单「${menu.name}」吗?`,
|
||||
headerImage: "warning",
|
||||
}).then(async () => {
|
||||
await MenuAPI.deleteById(menu.id!);
|
||||
toast.success("删除成功");
|
||||
loadMenuList();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 操作菜单分发
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
const value = item.name;
|
||||
const menu = currentActionItem.value;
|
||||
if (value === "新增子菜单") {
|
||||
handleAddChild(menu);
|
||||
} else if (value === "编辑") {
|
||||
openMenuDialog(menu);
|
||||
} else if (value === "删除") {
|
||||
confirm({
|
||||
title: "确认删除",
|
||||
msg: `确定要删除菜单「${menu.name}」吗?`,
|
||||
headerImage: "warning",
|
||||
}).then(async () => {
|
||||
await MenuAPI.deleteById(menu.id!);
|
||||
toast.success("删除成功");
|
||||
// 处理节点操作按钮点击
|
||||
function handleNodeAction(node: any) {
|
||||
const menu: MenuItem = {
|
||||
id: node.id,
|
||||
name: node.label,
|
||||
type: node.type,
|
||||
visible: node.visible,
|
||||
children: node.children,
|
||||
} as MenuItem;
|
||||
|
||||
const actions: { name: string; color?: string }[] = [];
|
||||
|
||||
if (hasPermission("sys:menu:create") && node.type !== 3) {
|
||||
actions.push({ name: "新增子菜单" });
|
||||
}
|
||||
|
||||
if (hasPermission("sys:menu:update")) {
|
||||
actions.push({ name: "编辑" });
|
||||
}
|
||||
|
||||
if (hasPermission("sys:menu:delete")) {
|
||||
actions.push({ name: "删除", color: "var(--color-danger)" });
|
||||
}
|
||||
|
||||
if (actions.length === 0) {
|
||||
toast.warning("暂无操作权限");
|
||||
return;
|
||||
}
|
||||
|
||||
currentActionItem.value = menu;
|
||||
actionSheetActions.value = actions;
|
||||
actionSheetVisible.value = true;
|
||||
}
|
||||
|
||||
// 打开弹窗(新增/编辑)
|
||||
async function openMenuDialog(menu?: MenuItem) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
parentSelected.value = [];
|
||||
parentLabel.value = "";
|
||||
dialog.visible = true;
|
||||
|
||||
const data = await MenuAPI.getOptions(true);
|
||||
parentOptions.value = [{ value: "0", label: "顶级菜单" }, ...data];
|
||||
|
||||
if (menu) {
|
||||
formData.id = menu.id;
|
||||
const form = await MenuAPI.getFormData(menu.id!);
|
||||
Object.assign(formData, form, { id: menu.id });
|
||||
|
||||
if (form.parentId && String(form.parentId) !== "0") {
|
||||
const path = findMenuPath(data, String(form.parentId));
|
||||
if (path) {
|
||||
parentSelected.value = path;
|
||||
} else {
|
||||
parentSelected.value = [String(form.parentId)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增子菜单
|
||||
function handleAddChild(menu: MenuItem) {
|
||||
openMenuDialog({ id: undefined, parentId: menu.id } as MenuItem);
|
||||
formData.parentId = menu.id!;
|
||||
parentSelected.value = [menu.id!];
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function submitMenuForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
isSubmitting.value = true;
|
||||
const action = formData.id ? MenuAPI.update(formData.id, formData) : MenuAPI.add(formData);
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeMenuDialog();
|
||||
loadMenuList();
|
||||
})
|
||||
.finally(() => {
|
||||
isSubmitting.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理节点操作按钮点击
|
||||
function handleNodeAction(node: any) {
|
||||
const menu: MenuItem = {
|
||||
id: node.id,
|
||||
name: node.label,
|
||||
type: node.type,
|
||||
visible: node.visible,
|
||||
children: node.children,
|
||||
} as MenuItem;
|
||||
|
||||
const actions: { name: string; color?: string }[] = [];
|
||||
|
||||
if (hasPermission("sys:menu:create") && node.type !== 3) {
|
||||
actions.push({ name: "新增子菜单" });
|
||||
}
|
||||
|
||||
if (hasPermission("sys:menu:update")) {
|
||||
actions.push({ name: "编辑" });
|
||||
}
|
||||
|
||||
if (hasPermission("sys:menu:delete")) {
|
||||
actions.push({ name: "删除", color: "var(--color-danger)" });
|
||||
}
|
||||
|
||||
if (actions.length === 0) {
|
||||
toast.warning("暂无操作权限");
|
||||
return;
|
||||
}
|
||||
|
||||
currentActionItem.value = menu;
|
||||
actionSheetActions.value = actions;
|
||||
actionSheetVisible.value = true;
|
||||
}
|
||||
|
||||
// 打开弹窗(新增/编辑)
|
||||
async function openMenuDialog(menu?: MenuItem) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
parentSelected.value = [];
|
||||
parentLabel.value = "";
|
||||
dialog.visible = true;
|
||||
|
||||
const data = await MenuAPI.getOptions(true);
|
||||
parentOptions.value = [{ value: "0", label: "顶级菜单" }, ...data];
|
||||
|
||||
if (menu) {
|
||||
formData.id = menu.id;
|
||||
const form = await MenuAPI.getFormData(menu.id!);
|
||||
Object.assign(formData, form, { id: menu.id });
|
||||
|
||||
if (form.parentId && String(form.parentId) !== "0") {
|
||||
const path = findMenuPath(data, String(form.parentId));
|
||||
if (path) {
|
||||
parentSelected.value = path;
|
||||
} else {
|
||||
parentSelected.value = [String(form.parentId)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增子菜单
|
||||
function handleAddChild(menu: MenuItem) {
|
||||
openMenuDialog({ id: undefined, parentId: menu.id } as MenuItem);
|
||||
formData.parentId = menu.id!;
|
||||
parentSelected.value = [menu.id!];
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function submitMenuForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
isSubmitting.value = true;
|
||||
const action = formData.id ? MenuAPI.update(formData.id, formData) : MenuAPI.add(formData);
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeMenuDialog();
|
||||
loadMenuList();
|
||||
})
|
||||
.finally(() => {
|
||||
isSubmitting.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeMenuDialog() {
|
||||
dialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
loadMenuList();
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeMenuDialog() {
|
||||
dialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
loadMenuList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu-node {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: nowrap;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
}
|
||||
.menu-node {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: nowrap;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
definePage({
|
||||
name: "monitor",
|
||||
@@ -21,5 +21,3 @@ const monitorUrl = ref("http://localhost:9090/wallboard");
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
<template>
|
||||
<view class="page page--padding">
|
||||
<view>
|
||||
<wd-search v-model="queryParams.title" placeholder="搜索通知标题" hide-cancel @search="handleSearch" />
|
||||
<wd-search
|
||||
v-model="queryParams.title"
|
||||
placeholder="搜索通知标题"
|
||||
hide-cancel
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 通知列表 -->
|
||||
<view class="mt-16rpx">
|
||||
<wd-card v-for="item in pageData" :key="item.id" custom-class="item-card" @click="openNoticeDetail(item)">
|
||||
<wd-card
|
||||
v-for="item in pageData"
|
||||
:key="item.id"
|
||||
custom-class="item-card"
|
||||
@click="openNoticeDetail(item)"
|
||||
>
|
||||
<!-- 主信息行 -->
|
||||
<view class="flex-start">
|
||||
<view class="notice-card__main">
|
||||
@@ -41,7 +51,11 @@
|
||||
<!-- 元信息行 -->
|
||||
<view class="notice-card__footer">
|
||||
<text class="notice-card__time">{{ formatTime(item) }}</text>
|
||||
<view class="notice-card__action" hover-class="notice-card__action--hover" @click.stop="showNoticeActions(item)">
|
||||
<view
|
||||
class="notice-card__action"
|
||||
hover-class="notice-card__action--hover"
|
||||
@click.stop="showNoticeActions(item)"
|
||||
>
|
||||
<wd-icon name="more" size="16" class="color-text-secondary" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -52,7 +66,12 @@
|
||||
</view>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<wd-popup v-model="detailDialog.visible" position="bottom" custom-class="popup-bottom" @close="closeNoticeDetail">
|
||||
<wd-popup
|
||||
v-model="detailDialog.visible"
|
||||
position="bottom"
|
||||
custom-class="popup-bottom"
|
||||
@close="closeNoticeDetail"
|
||||
>
|
||||
<view class="p-4">
|
||||
<view class="popup-title">通知详情</view>
|
||||
<wd-cell-group border>
|
||||
@@ -77,7 +96,12 @@
|
||||
</wd-popup>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<wd-popup v-model="formDialog.visible" position="bottom" custom-class="popup-bottom" @close="closeNoticeForm">
|
||||
<wd-popup
|
||||
v-model="formDialog.visible"
|
||||
position="bottom"
|
||||
custom-class="popup-bottom"
|
||||
@close="closeNoticeForm"
|
||||
>
|
||||
<view class="p-4">
|
||||
<view class="popup-title">
|
||||
{{ formData.id ? "编辑通知" : "新增通知" }}
|
||||
@@ -100,7 +124,12 @@
|
||||
</wd-radio-group>
|
||||
</wd-form-item>
|
||||
<wd-form-item prop="content" title="内容">
|
||||
<wd-textarea v-model="formData.content" placeholder="请输入通知内容" :maxlength="500" show-word-limit />
|
||||
<wd-textarea
|
||||
v-model="formData.content"
|
||||
placeholder="请输入通知内容"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="popup-actions">
|
||||
@@ -112,326 +141,331 @@
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<wd-action-sheet v-model="actionSheetVisible" :actions="actionSheetActions" cancel-text="取消" @select="handleActionSelect" />
|
||||
<wd-action-sheet
|
||||
v-model="actionSheetVisible"
|
||||
:actions="actionSheetActions"
|
||||
cancel-text="取消"
|
||||
@select="handleActionSelect"
|
||||
/>
|
||||
|
||||
<wd-fab v-if="hasPermission('sys:notice:create') && !formDialog.visible && !detailDialog.visible" :expandable="false" :gap="{ bottom: 32 }"
|
||||
@click="openNoticeForm()" />
|
||||
<wd-fab
|
||||
v-if="hasPermission('sys:notice:create') && !formDialog.visible && !detailDialog.visible"
|
||||
:expandable="false"
|
||||
:gap="{ bottom: 32 }"
|
||||
@click="openNoticeForm()"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import NoticeAPI, {
|
||||
type NoticePageQuery,
|
||||
NoticeItem,
|
||||
NoticeDetail,
|
||||
NoticeForm,
|
||||
} from "@/api/notice";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import NoticeAPI, {
|
||||
type NoticePageQuery,
|
||||
NoticeItem,
|
||||
NoticeDetail,
|
||||
NoticeForm,
|
||||
} from "@/api/notice";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
definePage({
|
||||
name: "notice",
|
||||
style: { navigationBarTitleText: "通知公告" },
|
||||
});
|
||||
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
definePage({
|
||||
name: "notice",
|
||||
style: { navigationBarTitleText: "通知公告" },
|
||||
});
|
||||
const queryParams = reactive<NoticePageQuery>({ pageNum: 1, pageSize: 10 });
|
||||
const total = ref(0);
|
||||
const pageData = ref<NoticeItem[]>([]);
|
||||
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
const noticeDetail = ref<NoticeDetail>({});
|
||||
const detailDialog = reactive({ visible: false });
|
||||
|
||||
const queryParams = reactive<NoticePageQuery>({ pageNum: 1, pageSize: 10 });
|
||||
const total = ref(0);
|
||||
const pageData = ref<NoticeItem[]>([]);
|
||||
const formDialog = reactive({ visible: false });
|
||||
const initialFormData: NoticeForm = {
|
||||
id: undefined,
|
||||
title: undefined,
|
||||
content: undefined,
|
||||
type: undefined,
|
||||
level: "M",
|
||||
targetType: 1,
|
||||
targetUserIds: undefined,
|
||||
};
|
||||
const formData = reactive<NoticeForm>({ ...initialFormData });
|
||||
|
||||
const noticeDetail = ref<NoticeDetail>({});
|
||||
const detailDialog = reactive({ visible: false });
|
||||
const formRules = toFormSchema({
|
||||
title: [{ required: true, message: "请输入通知标题" }],
|
||||
});
|
||||
|
||||
const formDialog = reactive({ visible: false });
|
||||
const initialFormData: NoticeForm = {
|
||||
id: undefined,
|
||||
title: undefined,
|
||||
content: undefined,
|
||||
type: undefined,
|
||||
level: "M",
|
||||
targetType: 1,
|
||||
targetUserIds: undefined,
|
||||
// 获取状态样式
|
||||
const getStatusType = (
|
||||
status?: number
|
||||
): "default" | "primary" | "danger" | "warning" | "success" => {
|
||||
const map: Record<number, "default" | "primary" | "danger" | "warning" | "success"> = {
|
||||
0: "primary",
|
||||
1: "success",
|
||||
[-1]: "warning",
|
||||
};
|
||||
const formData = reactive<NoticeForm>({ ...initialFormData });
|
||||
return status !== undefined ? map[status] || "default" : "default";
|
||||
};
|
||||
|
||||
const formRules = toFormSchema({
|
||||
title: [{ required: true, message: "请输入通知标题" }],
|
||||
});
|
||||
// 获取状态文本
|
||||
const getStatusText = (status?: number): string => {
|
||||
const map: Record<number, string> = { 0: "未发布", 1: "已发布", [-1]: "已撤回" };
|
||||
return status !== undefined ? map[status] || "未知" : "-";
|
||||
};
|
||||
|
||||
// 获取状态样式
|
||||
const getStatusType = (
|
||||
status?: number
|
||||
): "default" | "primary" | "danger" | "warning" | "success" => {
|
||||
const map: Record<number, "default" | "primary" | "danger" | "warning" | "success"> = {
|
||||
0: "primary",
|
||||
1: "success",
|
||||
[-1]: "warning",
|
||||
};
|
||||
return status !== undefined ? map[status] || "default" : "default";
|
||||
};
|
||||
// 获取级别文本
|
||||
const getLevelText = (level?: string | number): string => {
|
||||
const map: Record<string, string> = { L: "低", M: "中", H: "高" };
|
||||
return level ? map[String(level)] || String(level) : "-";
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status?: number): string => {
|
||||
const map: Record<number, string> = { 0: "未发布", 1: "已发布", [-1]: "已撤回" };
|
||||
return status !== undefined ? map[status] || "未知" : "-";
|
||||
};
|
||||
|
||||
// 获取级别文本
|
||||
const getLevelText = (level?: string | number): string => {
|
||||
const map: Record<string, string> = { L: "低", M: "中", H: "高" };
|
||||
return level ? map[String(level)] || String(level) : "-";
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (item: NoticeItem): string => {
|
||||
if (item.publishStatus === 1 && item.publishTime) {
|
||||
return String(item.publishTime);
|
||||
}
|
||||
if (item.publishStatus === -1 && item.revokeTime) {
|
||||
return String(item.revokeTime);
|
||||
}
|
||||
return "-";
|
||||
};
|
||||
|
||||
// 搜索触发
|
||||
const handleSearch = () => loadNoticeList();
|
||||
|
||||
// 加载列表
|
||||
function loadNoticeList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchNoticeList();
|
||||
// 格式化时间
|
||||
const formatTime = (item: NoticeItem): string => {
|
||||
if (item.publishStatus === 1 && item.publishTime) {
|
||||
return String(item.publishTime);
|
||||
}
|
||||
if (item.publishStatus === -1 && item.revokeTime) {
|
||||
return String(item.revokeTime);
|
||||
}
|
||||
return "-";
|
||||
};
|
||||
|
||||
// 分页加载列表
|
||||
function fetchNoticeList() {
|
||||
loadMoreState.value = "loading";
|
||||
NoticeAPI.getPage(queryParams)
|
||||
.then((data) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
// 搜索触发
|
||||
const handleSearch = () => loadNoticeList();
|
||||
|
||||
// 加载列表
|
||||
function loadNoticeList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchNoticeList();
|
||||
}
|
||||
|
||||
// 分页加载列表
|
||||
function fetchNoticeList() {
|
||||
loadMoreState.value = "loading";
|
||||
NoticeAPI.getPage(queryParams)
|
||||
.then((data) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
});
|
||||
}
|
||||
|
||||
// 打开详情弹窗
|
||||
async function openNoticeDetail(item: NoticeItem) {
|
||||
const detail = await NoticeAPI.getDetail(item.id);
|
||||
noticeDetail.value = detail;
|
||||
detailDialog.visible = true;
|
||||
}
|
||||
|
||||
// 关闭详情弹窗
|
||||
function closeNoticeDetail() {
|
||||
detailDialog.visible = false;
|
||||
}
|
||||
|
||||
// 打开表单弹窗
|
||||
async function openNoticeForm(id?: number) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
formDialog.visible = true;
|
||||
if (id) {
|
||||
formData.id = id;
|
||||
const data = await NoticeAPI.getFormData(id);
|
||||
Object.assign(formData, data, { id });
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭表单弹窗
|
||||
function closeNoticeForm() {
|
||||
formDialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function submitNoticeForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
isSubmitting.value = true;
|
||||
const id = formData.id;
|
||||
const action = id ? NoticeAPI.update(id, formData) : NoticeAPI.add(formData);
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeNoticeForm();
|
||||
loadNoticeList();
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
isSubmitting.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 打开详情弹窗
|
||||
async function openNoticeDetail(item: NoticeItem) {
|
||||
const detail = await NoticeAPI.getDetail(item.id);
|
||||
noticeDetail.value = detail;
|
||||
detailDialog.visible = true;
|
||||
}
|
||||
// 更多操作
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const pendingAction = ref<Record<string, () => void>>({});
|
||||
|
||||
// 关闭详情弹窗
|
||||
function closeNoticeDetail() {
|
||||
detailDialog.visible = false;
|
||||
}
|
||||
function showNoticeActions(item: NoticeItem) {
|
||||
const actions: { name: string; color?: string }[] = [{ name: "查看" }];
|
||||
const actionMap: Record<string, () => void> = {
|
||||
查看: () => openNoticeDetail(item),
|
||||
};
|
||||
|
||||
// 打开表单弹窗
|
||||
async function openNoticeForm(id?: number) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
formDialog.visible = true;
|
||||
if (id) {
|
||||
formData.id = id;
|
||||
const data = await NoticeAPI.getFormData(id);
|
||||
Object.assign(formData, data, { id });
|
||||
if (item.publishStatus !== 1) {
|
||||
if (hasPermission("sys:notice:update")) {
|
||||
actions.push({ name: "编辑" });
|
||||
actionMap["编辑"] = () => openNoticeForm(Number(item.id));
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭表单弹窗
|
||||
function closeNoticeForm() {
|
||||
formDialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function submitNoticeForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
isSubmitting.value = true;
|
||||
const id = formData.id;
|
||||
const action = id ? NoticeAPI.update(id, formData) : NoticeAPI.add(formData);
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeNoticeForm();
|
||||
if (hasPermission("sys:notice:delete")) {
|
||||
actions.push({ name: "删除", color: "var(--color-danger)" });
|
||||
actionMap["删除"] = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "确认删除",
|
||||
msg: `确定要删除通知「${item.title}」吗?`,
|
||||
headerImage: "warning",
|
||||
});
|
||||
await NoticeAPI.deleteByIds(item.id);
|
||||
toast.success("删除成功");
|
||||
loadNoticeList();
|
||||
})
|
||||
.finally(() => {
|
||||
isSubmitting.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 更多操作
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const pendingAction = ref<Record<string, () => void>>({});
|
||||
|
||||
function showNoticeActions(item: NoticeItem) {
|
||||
const actions: { name: string; color?: string }[] = [{ name: "查看" }];
|
||||
const actionMap: Record<string, () => void> = {
|
||||
查看: () => openNoticeDetail(item),
|
||||
};
|
||||
|
||||
if (item.publishStatus !== 1) {
|
||||
if (hasPermission("sys:notice:update")) {
|
||||
actions.push({ name: "编辑" });
|
||||
actionMap["编辑"] = () => openNoticeForm(Number(item.id));
|
||||
}
|
||||
if (hasPermission("sys:notice:delete")) {
|
||||
actions.push({ name: "删除", color: "var(--color-danger)" });
|
||||
actionMap["删除"] = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "确认删除",
|
||||
msg: `确定要删除通知「${item.title}」吗?`,
|
||||
headerImage: "warning",
|
||||
});
|
||||
await NoticeAPI.deleteByIds(item.id);
|
||||
toast.success("删除成功");
|
||||
loadNoticeList();
|
||||
} catch {
|
||||
// 用户取消操作
|
||||
}
|
||||
};
|
||||
}
|
||||
if (hasPermission("sys:notice:publish")) {
|
||||
actions.push({ name: "发布" });
|
||||
actionMap["发布"] = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "确认发布",
|
||||
msg: `确定要发布通知「${item.title}」吗?`,
|
||||
headerImage: "warning",
|
||||
});
|
||||
await NoticeAPI.publish(Number(item.id));
|
||||
toast.success("发布成功");
|
||||
loadNoticeList();
|
||||
} catch {
|
||||
// 用户取消操作
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (hasPermission("sys:notice:revoke")) {
|
||||
actions.push({ name: "撤回", color: "var(--color-warning)" });
|
||||
actionMap["撤回"] = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "确认撤回",
|
||||
msg: `确定要撤回通知「${item.title}」吗?`,
|
||||
headerImage: "warning",
|
||||
});
|
||||
await NoticeAPI.revoke(Number(item.id));
|
||||
toast.success("撤回成功");
|
||||
loadNoticeList();
|
||||
} catch {
|
||||
// 用户取消操作
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// 用户取消操作
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
actionSheetActions.value = actions;
|
||||
pendingAction.value = actionMap;
|
||||
actionSheetVisible.value = true;
|
||||
}
|
||||
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
pendingAction.value[item.name]?.();
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
fetchNoticeList();
|
||||
} else {
|
||||
loadMoreState.value = "finished";
|
||||
if (hasPermission("sys:notice:publish")) {
|
||||
actions.push({ name: "发布" });
|
||||
actionMap["发布"] = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "确认发布",
|
||||
msg: `确定要发布通知「${item.title}」吗?`,
|
||||
headerImage: "warning",
|
||||
});
|
||||
await NoticeAPI.publish(Number(item.id));
|
||||
toast.success("发布成功");
|
||||
loadNoticeList();
|
||||
} catch {
|
||||
// 用户取消操作
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (hasPermission("sys:notice:revoke")) {
|
||||
actions.push({ name: "撤回", color: "var(--color-warning)" });
|
||||
actionMap["撤回"] = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "确认撤回",
|
||||
msg: `确定要撤回通知「${item.title}」吗?`,
|
||||
headerImage: "warning",
|
||||
});
|
||||
await NoticeAPI.revoke(Number(item.id));
|
||||
toast.success("撤回成功");
|
||||
loadNoticeList();
|
||||
} catch {
|
||||
// 用户取消操作
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
loadNoticeList();
|
||||
});
|
||||
actionSheetActions.value = actions;
|
||||
pendingAction.value = actionMap;
|
||||
actionSheetVisible.value = true;
|
||||
}
|
||||
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
pendingAction.value[item.name]?.();
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
fetchNoticeList();
|
||||
} else {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
});
|
||||
|
||||
onLoad(() => {
|
||||
loadNoticeList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notice-card__main {
|
||||
flex: 1;
|
||||
}
|
||||
.notice-card__main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.notice-card__title {
|
||||
font-weight: 700;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.notice-card__title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.notice-card__publisher {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.notice-card__publisher {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.notice-card__meta {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
.notice-card__meta {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.notice-card__detail {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
.notice-card__detail {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notice-card__detail-text {
|
||||
margin-left: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.notice-card__detail-text {
|
||||
margin-left: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.notice-card__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
.notice-card__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.notice-card__time {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
.notice-card__time {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
|
||||
.notice-card__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.notice-card__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.notice-card__action--hover {
|
||||
background: rgba(var(--color-text-placeholder-rgb, 148, 163, 184), 0.16);
|
||||
}
|
||||
.notice-card__action--hover {
|
||||
background: rgba(var(--color-text-placeholder-rgb, 148, 163, 184), 0.16);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,21 +17,47 @@
|
||||
|
||||
<!-- 可滚动区域:权限树 -->
|
||||
<scroll-view class="tree-container" scroll-y>
|
||||
<custom-tree :data="menuTree" :checkable="true" :checked-keys="checkedKeys" :default-expand-all="true" value-key="id" label-key="name"
|
||||
children-key="children" @check="handleCheck">
|
||||
<custom-tree
|
||||
:data="menuTree"
|
||||
:checkable="true"
|
||||
:checked-keys="checkedKeys"
|
||||
:default-expand-all="true"
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
children-key="children"
|
||||
@check="handleCheck"
|
||||
>
|
||||
<template #content="{ node, checked }">
|
||||
<view class="node-content">
|
||||
<view class="node-content__info">
|
||||
<wd-icon v-if="node.type === 'C'" name="folder" size="18"
|
||||
:color="checked ? 'var(--color-primary)' : 'var(--color-text-secondary)'" />
|
||||
<wd-icon v-else-if="node.type === 'M'" name="menu" size="18"
|
||||
:color="checked ? 'var(--color-primary)' : 'var(--color-text-secondary)'" />
|
||||
<wd-icon v-else name="click" size="16" :color="checked ? 'var(--color-primary)' : 'var(--color-text-secondary)'" />
|
||||
<wd-icon
|
||||
v-if="node.type === 'C'"
|
||||
name="folder"
|
||||
size="18"
|
||||
:color="checked ? 'var(--color-primary)' : 'var(--color-text-secondary)'"
|
||||
/>
|
||||
<wd-icon
|
||||
v-else-if="node.type === 'M'"
|
||||
name="menu"
|
||||
size="18"
|
||||
:color="checked ? 'var(--color-primary)' : 'var(--color-text-secondary)'"
|
||||
/>
|
||||
<wd-icon
|
||||
v-else
|
||||
name="click"
|
||||
size="16"
|
||||
:color="checked ? 'var(--color-primary)' : 'var(--color-text-secondary)'"
|
||||
/>
|
||||
<text class="node-content__label" :class="{ 'is-checked': checked }">
|
||||
{{ node.name }}
|
||||
</text>
|
||||
</view>
|
||||
<wd-tag v-if="node.type" size="small" :type="node.type === 'C' ? 'primary' : node.type === 'M' ? 'success' : 'warning'" plain>
|
||||
<wd-tag
|
||||
v-if="node.type"
|
||||
size="small"
|
||||
:type="node.type === 'C' ? 'primary' : node.type === 'M' ? 'success' : 'warning'"
|
||||
plain
|
||||
>
|
||||
{{ node.type === "C" ? "目录" : node.type === "M" ? "菜单" : "按钮" }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
@@ -50,216 +76,214 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import MenuAPI, { type MenuItem } from "@/api/menu";
|
||||
import RoleAPI from "@/api/role";
|
||||
import CustomTree from "@/components/custom-tree/index.vue";
|
||||
import type { TreeOption } from "@/components/custom-tree/index.vue";
|
||||
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import MenuAPI, { type MenuItem } from "@/api/menu";
|
||||
import RoleAPI from "@/api/role";
|
||||
import CustomTree from "@/components/custom-tree/index.vue";
|
||||
import type { TreeOption } from "@/components/custom-tree/index.vue";
|
||||
definePage({
|
||||
name: "role-assign-perm",
|
||||
style: { navigationBarTitleText: "分配权限" },
|
||||
});
|
||||
|
||||
definePage({
|
||||
name: "role-assign-perm",
|
||||
style: { navigationBarTitleText: "分配权限" },
|
||||
const toast = useToast();
|
||||
const isSubmitting = ref(false);
|
||||
const roleId = ref<number>(0);
|
||||
const roleName = ref("");
|
||||
const menuList = ref<MenuItem[]>([]);
|
||||
const checkedKeys = ref<string[]>([]);
|
||||
|
||||
// 转换菜单为树结构
|
||||
const menuTree = computed<TreeOption[]>(() => {
|
||||
if (!menuList.value.length) return [];
|
||||
return transformMenuToTree(menuList.value);
|
||||
});
|
||||
|
||||
// 转换菜单为树组件格式
|
||||
function transformMenuToTree(menus: MenuItem[]): TreeOption[] {
|
||||
return menus.map((menu) => {
|
||||
const id = String(menu.id);
|
||||
const name = menu.name ?? "";
|
||||
return {
|
||||
value: id,
|
||||
label: name,
|
||||
id,
|
||||
name,
|
||||
type: menu.type,
|
||||
children: menu.children ? transformMenuToTree(menu.children) : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const toast = useToast();
|
||||
const isSubmitting = ref(false);
|
||||
const roleId = ref<number>(0);
|
||||
const roleName = ref("");
|
||||
const menuList = ref<MenuItem[]>([]);
|
||||
const checkedKeys = ref<string[]>([]);
|
||||
// 处理选中变化
|
||||
function handleCheck(keys: string[]) {
|
||||
checkedKeys.value = keys;
|
||||
}
|
||||
|
||||
// 转换菜单为树结构
|
||||
const menuTree = computed<TreeOption[]>(() => {
|
||||
if (!menuList.value.length) return [];
|
||||
return transformMenuToTree(menuList.value);
|
||||
});
|
||||
// 全选
|
||||
function handleSelectAll() {
|
||||
const allIds = getAllMenuIds(menuList.value);
|
||||
checkedKeys.value = allIds;
|
||||
}
|
||||
|
||||
// 转换菜单为树组件格式
|
||||
function transformMenuToTree(menus: MenuItem[]): TreeOption[] {
|
||||
return menus.map((menu) => {
|
||||
const id = String(menu.id);
|
||||
const name = menu.name ?? "";
|
||||
return {
|
||||
value: id,
|
||||
label: name,
|
||||
id,
|
||||
name,
|
||||
type: menu.type,
|
||||
children: menu.children ? transformMenuToTree(menu.children) : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
// 取消全选
|
||||
function handleClearAll() {
|
||||
checkedKeys.value = [];
|
||||
}
|
||||
|
||||
// 处理选中变化
|
||||
function handleCheck(keys: string[]) {
|
||||
checkedKeys.value = keys;
|
||||
}
|
||||
|
||||
// 全选
|
||||
function handleSelectAll() {
|
||||
const allIds = getAllMenuIds(menuList.value);
|
||||
checkedKeys.value = allIds;
|
||||
}
|
||||
|
||||
// 取消全选
|
||||
function handleClearAll() {
|
||||
checkedKeys.value = [];
|
||||
}
|
||||
|
||||
// 获取所有菜单ID
|
||||
function getAllMenuIds(menus: MenuItem[]): string[] {
|
||||
const ids: string[] = [];
|
||||
function traverse(list: MenuItem[]) {
|
||||
for (const menu of list) {
|
||||
if (menu.id) ids.push(String(menu.id));
|
||||
if (menu.children) traverse(menu.children);
|
||||
}
|
||||
// 获取所有菜单ID
|
||||
function getAllMenuIds(menus: MenuItem[]): string[] {
|
||||
const ids: string[] = [];
|
||||
function traverse(list: MenuItem[]) {
|
||||
for (const menu of list) {
|
||||
if (menu.id) ids.push(String(menu.id));
|
||||
if (menu.children) traverse(menu.children);
|
||||
}
|
||||
traverse(menus);
|
||||
return ids;
|
||||
}
|
||||
traverse(menus);
|
||||
return ids;
|
||||
}
|
||||
|
||||
// 取消
|
||||
function handleCancel() {
|
||||
// 取消
|
||||
function handleCancel() {
|
||||
uni.navigateBack();
|
||||
}
|
||||
|
||||
// 提交
|
||||
async function handleSubmit() {
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
await RoleAPI.updateRoleMenus(roleId.value, checkedKeys.value.map(Number));
|
||||
toast.success("保存成功");
|
||||
uni.navigateBack();
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 提交
|
||||
async function handleSubmit() {
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
await RoleAPI.updateRoleMenus(roleId.value, checkedKeys.value.map(Number));
|
||||
toast.success("保存成功");
|
||||
uni.navigateBack();
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
try {
|
||||
// 并行加载菜单列表和角色已有权限
|
||||
const [menus, roleMenuIds, roleForm] = await Promise.all([
|
||||
MenuAPI.getList(),
|
||||
RoleAPI.getRoleMenuIds(roleId.value),
|
||||
RoleAPI.getFormData(roleId.value),
|
||||
]);
|
||||
menuList.value = menus;
|
||||
checkedKeys.value = roleMenuIds.map(String);
|
||||
roleName.value = roleForm?.name ?? "";
|
||||
} catch {
|
||||
toast.error("加载失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
async function loadData() {
|
||||
try {
|
||||
// 并行加载菜单列表和角色已有权限
|
||||
const [menus, roleMenuIds, roleForm] = await Promise.all([
|
||||
MenuAPI.getList(),
|
||||
RoleAPI.getRoleMenuIds(roleId.value),
|
||||
RoleAPI.getFormData(roleId.value),
|
||||
]);
|
||||
menuList.value = menus;
|
||||
checkedKeys.value = roleMenuIds.map(String);
|
||||
roleName.value = roleForm?.name ?? "";
|
||||
} catch {
|
||||
toast.error("加载失败");
|
||||
}
|
||||
onLoad((query) => {
|
||||
if (query?.id) {
|
||||
roleId.value = Number(query.id);
|
||||
loadData();
|
||||
}
|
||||
|
||||
onLoad((query) => {
|
||||
if (query?.id) {
|
||||
roleId.value = Number(query.id);
|
||||
loadData();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: var(--color-bg);
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12rpx 24rpx;
|
||||
background: var(--color-bg);
|
||||
border-bottom: 2rpx solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.role-bar {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
|
||||
&__label {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12rpx 24rpx;
|
||||
background: var(--color-bg);
|
||||
border-bottom: 2rpx solid var(--color-border-light);
|
||||
&__value {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.role-bar {
|
||||
.action-bar {
|
||||
&__left {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
|
||||
&__label {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
&__value {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
&__left {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
}
|
||||
.tree-container {
|
||||
flex: 1;
|
||||
height: 0; // 重要:让 flex + scroll-view 生效
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.tree-container {
|
||||
flex: 1;
|
||||
height: 0; // 重要:让 flex + scroll-view 生效
|
||||
background: var(--color-bg);
|
||||
}
|
||||
.node-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
min-width: 0;
|
||||
|
||||
.node-content {
|
||||
&__info {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
&__label {
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
color: var(--color-text);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&__label {
|
||||
overflow: hidden;
|
||||
font-size: 28rpx;
|
||||
color: var(--color-text);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&.is-checked {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
&.is-checked {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-content :deep(.wd-tag) {
|
||||
margin-left: auto;
|
||||
}
|
||||
.node-content :deep(.wd-tag) {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx;
|
||||
background: var(--color-bg);
|
||||
box-shadow: 0 -2rpx 16rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.bottom-bar {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx;
|
||||
background: var(--color-bg);
|
||||
box-shadow: 0 -2rpx 16rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.bottom-bar :deep(.wd-button) {
|
||||
flex: 1;
|
||||
}
|
||||
.bottom-bar :deep(.wd-button) {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
<template>
|
||||
<view class="page page--padding">
|
||||
<view>
|
||||
<wd-search v-model="queryParams.keywords" placeholder="搜索角色名称/编码" hide-cancel @search="handleSearch" />
|
||||
<wd-search
|
||||
v-model="queryParams.keywords"
|
||||
placeholder="搜索角色名称/编码"
|
||||
hide-cancel
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 角色列表 -->
|
||||
<view class="mt-16rpx">
|
||||
<wd-card v-for="item in pageData" :key="item.id" custom-class="item-card" @click="openRoleDialog(item.id)">
|
||||
<wd-card
|
||||
v-for="item in pageData"
|
||||
:key="item.id"
|
||||
custom-class="item-card"
|
||||
@click="openRoleDialog(item.id)"
|
||||
>
|
||||
<!-- 主信息行 -->
|
||||
<view class="flex-start">
|
||||
<view class="role-card__main">
|
||||
@@ -35,7 +45,11 @@
|
||||
<!-- 元信息行 -->
|
||||
<view class="role-card__footer">
|
||||
<text class="role-card__time">{{ item.createTime }}</text>
|
||||
<view class="role-card__action" hover-class="role-card__action--hover" @click.stop="showRoleActions(item)">
|
||||
<view
|
||||
class="role-card__action"
|
||||
hover-class="role-card__action--hover"
|
||||
@click.stop="showRoleActions(item)"
|
||||
>
|
||||
<wd-icon name="more" size="18" class="color-text-secondary" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -46,7 +60,12 @@
|
||||
</view>
|
||||
|
||||
<!-- 弹窗表单 -->
|
||||
<wd-popup v-model="dialog.visible" position="bottom" custom-class="popup-bottom" @close="closeRoleDialog">
|
||||
<wd-popup
|
||||
v-model="dialog.visible"
|
||||
position="bottom"
|
||||
custom-class="popup-bottom"
|
||||
@close="closeRoleDialog"
|
||||
>
|
||||
<view class="p-4">
|
||||
<view class="popup-title">
|
||||
{{ formData.id ? "编辑角色" : "新增角色" }}
|
||||
@@ -59,10 +78,21 @@
|
||||
<wd-input v-model="formData.code" placeholder="请输入角色编码" />
|
||||
</wd-form-item>
|
||||
<!-- 数据权限选择:触发交 wd-form-item,选择器只负责弹出 -->
|
||||
<wd-form-item title="数据权限" prop="dataScope" required is-link
|
||||
:value="dataScopeLabel" placeholder="请选择数据权限" @click="showDataScopePicker = true" />
|
||||
<wd-select-picker v-model="formData.dataScope" v-model:visible="showDataScopePicker"
|
||||
:columns="dataScopeOptions" @confirm="handleDataScopeConfirm" />
|
||||
<wd-form-item
|
||||
title="数据权限"
|
||||
prop="dataScope"
|
||||
required
|
||||
is-link
|
||||
:value="dataScopeLabel"
|
||||
placeholder="请选择数据权限"
|
||||
@click="showDataScopePicker = true"
|
||||
/>
|
||||
<wd-select-picker
|
||||
v-model="formData.dataScope"
|
||||
v-model:visible="showDataScopePicker"
|
||||
:columns="dataScopeOptions"
|
||||
@confirm="handleDataScopeConfirm"
|
||||
/>
|
||||
<wd-form-item prop="status" title="状态">
|
||||
<wd-switch v-model="formData.status" :active-value="1" :inactive-value="0" />
|
||||
</wd-form-item>
|
||||
@@ -78,271 +108,277 @@
|
||||
</wd-popup>
|
||||
|
||||
<!-- 浮动新增按钮 -->
|
||||
<wd-fab v-if="hasPermission('sys:role:create') && !dialog.visible" :expandable="false" :gap="{ bottom: 32 }" @click="openRoleDialog()" />
|
||||
<wd-fab
|
||||
v-if="hasPermission('sys:role:create') && !dialog.visible"
|
||||
:expandable="false"
|
||||
:gap="{ bottom: 32 }"
|
||||
@click="openRoleDialog()"
|
||||
/>
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
<wd-action-sheet v-model="actionSheetVisible" :actions="actionSheetActions" cancel-text="取消" @select="handleActionSelect" />
|
||||
<wd-action-sheet
|
||||
v-model="actionSheetVisible"
|
||||
:actions="actionSheetActions"
|
||||
cancel-text="取消"
|
||||
@select="handleActionSelect"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import RoleAPI, { type RolePageQuery, RoleItem, RoleForm } from "@/api/role";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useToast, useDialog } from "@wot-ui/ui";
|
||||
import RoleAPI, { type RolePageQuery, RoleItem, RoleForm } from "@/api/role";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
definePage({
|
||||
name: "role",
|
||||
style: { navigationBarTitleText: "角色管理" },
|
||||
});
|
||||
|
||||
definePage({
|
||||
name: "role",
|
||||
style: { navigationBarTitleText: "角色管理" },
|
||||
});
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
const queryParams = reactive<RolePageQuery>({ pageNum: 1, pageSize: 10, keywords: "" });
|
||||
const total = ref(0);
|
||||
const pageData = ref<RoleItem[]>([]);
|
||||
const dialog = reactive({ visible: false });
|
||||
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const currentActionItem = ref<any>(null);
|
||||
|
||||
const queryParams = reactive<RolePageQuery>({ pageNum: 1, pageSize: 10, keywords: "" });
|
||||
const total = ref(0);
|
||||
const pageData = ref<RoleItem[]>([]);
|
||||
const dialog = reactive({ visible: false });
|
||||
const initialFormData: RoleForm = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
dataScope: 1,
|
||||
status: 1,
|
||||
sort: 1,
|
||||
};
|
||||
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const currentActionItem = ref<any>(null);
|
||||
const formData = reactive<RoleForm>({ ...initialFormData });
|
||||
|
||||
const initialFormData: RoleForm = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
dataScope: 1,
|
||||
status: 1,
|
||||
sort: 1,
|
||||
};
|
||||
// 数据权限选择器状态
|
||||
const showDataScopePicker = ref(false);
|
||||
const dataScopeLabel = computed(() => {
|
||||
const opt = dataScopeOptions.value.find((o) => o.value === formData.dataScope);
|
||||
return opt?.label || "";
|
||||
});
|
||||
|
||||
const formData = reactive<RoleForm>({ ...initialFormData });
|
||||
// 数据权限确认
|
||||
const handleDataScopeConfirm = () => {
|
||||
// label 由 computed 自动计算
|
||||
};
|
||||
|
||||
// 数据权限选择器状态
|
||||
const showDataScopePicker = ref(false);
|
||||
const dataScopeLabel = computed(() => {
|
||||
const opt = dataScopeOptions.value.find((o) => o.value === formData.dataScope);
|
||||
return opt?.label || "";
|
||||
});
|
||||
const dataScopeOptions = ref<Record<string, any>[]>([
|
||||
{ label: "全部数据", value: 1 },
|
||||
{ label: "部门及子部门数据", value: 2 },
|
||||
{ label: "本部门数据", value: 3 },
|
||||
{ label: "本人数据", value: 4 },
|
||||
{ label: "自定义部门数据", value: 5 },
|
||||
]);
|
||||
|
||||
// 数据权限确认
|
||||
const handleDataScopeConfirm = () => {
|
||||
// label 由 computed 自动计算
|
||||
};
|
||||
const rules = toFormSchema({
|
||||
name: [{ required: true, message: "请输入角色名称" }],
|
||||
code: [{ required: true, message: "请输入角色编码" }],
|
||||
dataScope: [{ required: true, message: "请选择数据权限" }],
|
||||
});
|
||||
|
||||
const dataScopeOptions = ref<Record<string, any>[]>([
|
||||
{ label: "全部数据", value: 1 },
|
||||
{ label: "部门及子部门数据", value: 2 },
|
||||
{ label: "本部门数据", value: 3 },
|
||||
{ label: "本人数据", value: 4 },
|
||||
{ label: "自定义部门数据", value: 5 },
|
||||
]);
|
||||
// 搜索触发
|
||||
const handleSearch = () => loadRoleList();
|
||||
|
||||
const rules = toFormSchema({
|
||||
name: [{ required: true, message: "请输入角色名称" }],
|
||||
code: [{ required: true, message: "请输入角色编码" }],
|
||||
dataScope: [{ required: true, message: "请选择数据权限" }],
|
||||
});
|
||||
// 加载列表
|
||||
function loadRoleList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchRoleList();
|
||||
}
|
||||
|
||||
// 搜索触发
|
||||
const handleSearch = () => loadRoleList();
|
||||
// 分页加载列表
|
||||
function fetchRoleList() {
|
||||
loadMoreState.value = "loading";
|
||||
RoleAPI.getPage(queryParams)
|
||||
.then((data) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
});
|
||||
}
|
||||
|
||||
// 加载列表
|
||||
function loadRoleList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchRoleList();
|
||||
// 打开弹窗(新增/编辑)
|
||||
async function openRoleDialog(id?: number) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
dialog.visible = true;
|
||||
if (id) {
|
||||
formData.id = id;
|
||||
const data = await RoleAPI.getFormData(id);
|
||||
Object.assign(formData, data, { id });
|
||||
}
|
||||
}
|
||||
|
||||
// 分页加载列表
|
||||
function fetchRoleList() {
|
||||
loadMoreState.value = "loading";
|
||||
RoleAPI.getPage(queryParams)
|
||||
.then((data) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
// 提交表单
|
||||
function submitRoleForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
isSubmitting.value = true;
|
||||
const action = formData.id ? RoleAPI.update(formData.id, formData) : RoleAPI.add(formData);
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeRoleDialog();
|
||||
loadRoleList();
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
isSubmitting.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 打开弹窗(新增/编辑)
|
||||
async function openRoleDialog(id?: number) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
dialog.visible = true;
|
||||
if (id) {
|
||||
formData.id = id;
|
||||
const data = await RoleAPI.getFormData(id);
|
||||
Object.assign(formData, data, { id });
|
||||
}
|
||||
}
|
||||
// 关闭弹窗
|
||||
function closeRoleDialog() {
|
||||
dialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function submitRoleForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
isSubmitting.value = true;
|
||||
const action = formData.id ? RoleAPI.update(formData.id, formData) : RoleAPI.add(formData);
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeRoleDialog();
|
||||
loadRoleList();
|
||||
})
|
||||
.finally(() => {
|
||||
isSubmitting.value = false;
|
||||
});
|
||||
// 操作菜单分发
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
const value = item.name;
|
||||
const role = currentActionItem.value;
|
||||
if (value === "编辑") {
|
||||
openRoleDialog(role.id);
|
||||
} else if (value === "分配权限") {
|
||||
handleAssignPerm(role.id);
|
||||
} else if (value === "删除") {
|
||||
confirm({
|
||||
title: "确认删除",
|
||||
msg: `确定要删除角色「${role.name}」吗?`,
|
||||
headerImage: "warning",
|
||||
}).then(async () => {
|
||||
await RoleAPI.deleteByIds(String(item.id));
|
||||
toast.success("删除成功");
|
||||
loadRoleList();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeRoleDialog() {
|
||||
dialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
// 更多操作
|
||||
function showRoleActions(item: RoleItem) {
|
||||
const actions: { name: string; color?: string }[] = [];
|
||||
|
||||
if (hasPermission("sys:role:update")) {
|
||||
actions.push({ name: "编辑" });
|
||||
}
|
||||
|
||||
// 操作菜单分发
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
const value = item.name;
|
||||
const role = currentActionItem.value;
|
||||
if (value === "编辑") {
|
||||
openRoleDialog(role.id);
|
||||
} else if (value === "分配权限") {
|
||||
handleAssignPerm(role.id);
|
||||
} else if (value === "删除") {
|
||||
confirm({
|
||||
title: "确认删除",
|
||||
msg: `确定要删除角色「${role.name}」吗?`,
|
||||
headerImage: "warning",
|
||||
}).then(async () => {
|
||||
await RoleAPI.deleteByIds(String(item.id));
|
||||
toast.success("删除成功");
|
||||
loadRoleList();
|
||||
});
|
||||
}
|
||||
if (hasPermission("sys:role:assign")) {
|
||||
actions.push({ name: "分配权限" });
|
||||
}
|
||||
|
||||
// 更多操作
|
||||
function showRoleActions(item: RoleItem) {
|
||||
const actions: { name: string; color?: string }[] = [];
|
||||
|
||||
if (hasPermission("sys:role:update")) {
|
||||
actions.push({ name: "编辑" });
|
||||
}
|
||||
|
||||
if (hasPermission("sys:role:assign")) {
|
||||
actions.push({ name: "分配权限" });
|
||||
}
|
||||
|
||||
if (hasPermission("sys:role:delete")) {
|
||||
actions.push({ name: "删除", color: "var(--color-danger)" });
|
||||
}
|
||||
|
||||
if (actions.length === 0) {
|
||||
toast.warning("暂无操作权限");
|
||||
return;
|
||||
}
|
||||
|
||||
currentActionItem.value = item;
|
||||
actionSheetActions.value = actions;
|
||||
actionSheetVisible.value = true;
|
||||
if (hasPermission("sys:role:delete")) {
|
||||
actions.push({ name: "删除", color: "var(--color-danger)" });
|
||||
}
|
||||
|
||||
// 分配权限
|
||||
function handleAssignPerm(id: number) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/work/role/assign-perm?id=" + id,
|
||||
});
|
||||
if (actions.length === 0) {
|
||||
toast.warning("暂无操作权限");
|
||||
return;
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
fetchRoleList();
|
||||
} else {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
currentActionItem.value = item;
|
||||
actionSheetActions.value = actions;
|
||||
actionSheetVisible.value = true;
|
||||
}
|
||||
|
||||
// 分配权限
|
||||
function handleAssignPerm(id: number) {
|
||||
uni.navigateTo({
|
||||
url: "/pages/work/role/assign-perm?id=" + id,
|
||||
});
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
loadRoleList();
|
||||
});
|
||||
onReachBottom(() => {
|
||||
if (queryParams.pageNum * queryParams.pageSize < total.value) {
|
||||
fetchRoleList();
|
||||
} else {
|
||||
loadMoreState.value = "finished";
|
||||
}
|
||||
});
|
||||
|
||||
onLoad(() => {
|
||||
loadRoleList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
export default { options: { styleIsolation: "shared" } };
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.role-card__main {
|
||||
flex: 1;
|
||||
}
|
||||
.role-card__main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.role-card__name {
|
||||
font-weight: 700;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
.role-card__name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.role-card__code {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.role-card__code {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.role-card__meta {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
.role-card__meta {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.role-card__detail {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
.role-card__detail {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.role-card__detail-text {
|
||||
margin-left: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.role-card__detail-text {
|
||||
margin-left: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.role-card__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
.role-card__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.role-card__time {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
.role-card__time {
|
||||
font-size: 24rpx;
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
|
||||
.role-card__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.role-card__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.role-card__action--hover {
|
||||
background: rgba(var(--color-text-placeholder-rgb, 148, 163, 184), 0.16);
|
||||
}
|
||||
.role-card__action--hover {
|
||||
background: rgba(var(--color-text-placeholder-rgb, 148, 163, 184), 0.16);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<template>
|
||||
<view class="page page--padding">
|
||||
<view>
|
||||
<wd-search v-model="queryParams.keywords" placeholder="搜索用户名/手机号" hide-cancel @search="handleSearch" />
|
||||
<wd-search
|
||||
v-model="queryParams.keywords"
|
||||
placeholder="搜索用户名/手机号"
|
||||
hide-cancel
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 排序筛选:搜索框→筛选 12rpx -->
|
||||
<view class="filter-bar" @click="closeOutside">
|
||||
<view class="flex-1">
|
||||
<wd-drop-menu>
|
||||
<wd-drop-menu-item v-model="sortValue" title="排序" :options="sortOptions" @change="handleSortChange" />
|
||||
<wd-drop-menu-item
|
||||
v-model="sortValue"
|
||||
title="排序"
|
||||
:options="sortOptions"
|
||||
@change="handleSortChange"
|
||||
/>
|
||||
</wd-drop-menu>
|
||||
</view>
|
||||
<wd-divider vertical />
|
||||
@@ -16,7 +26,11 @@
|
||||
<wd-drop-menu>
|
||||
<wd-drop-menu-item title="筛选" @open="handleFilterOpen">
|
||||
<view class="p-4">
|
||||
<wd-input v-model="queryParams.keywords" label="关键字" placeholder="用户名/昵称/手机号" />
|
||||
<wd-input
|
||||
v-model="queryParams.keywords"
|
||||
label="关键字"
|
||||
placeholder="用户名/昵称/手机号"
|
||||
/>
|
||||
<cu-date-query v-model="queryParams.createTime" label="创建时间" />
|
||||
<view class="popup-actions">
|
||||
<wd-button type="info" variant="plain" @click="resetUserFilter">重置</wd-button>
|
||||
@@ -30,15 +44,30 @@
|
||||
|
||||
<!-- 用户列表:筛选→列表 16rpx -->
|
||||
<view class="mt-16rpx">
|
||||
<wd-card v-for="item in pageData" :key="item.id" custom-class="item-card" @click="openUserDialog(item.id)">
|
||||
<wd-card
|
||||
v-for="item in pageData"
|
||||
:key="item.id"
|
||||
custom-class="item-card"
|
||||
@click="openUserDialog(item.id)"
|
||||
>
|
||||
<!-- 主信息行 -->
|
||||
<view class="flex-start">
|
||||
<image class="user-card__avatar" :src="item.avatar" mode="aspectFill" lazy-load />
|
||||
<view class="user-card__main">
|
||||
<view class="flex-start mt-12rpx">
|
||||
<text class="user-card__name">{{ item.nickname }}</text>
|
||||
<wd-icon v-if="item.gender === 1" name="gender-male" color="var(--color-primary)" class="ml-8rpx" />
|
||||
<wd-icon v-else-if="item.gender === 2" name="gender-female" color="var(--color-danger)" class="ml-8rpx" />
|
||||
<wd-icon
|
||||
v-if="item.gender === 1"
|
||||
name="gender-male"
|
||||
color="var(--color-primary)"
|
||||
class="ml-8rpx"
|
||||
/>
|
||||
<wd-icon
|
||||
v-else-if="item.gender === 2"
|
||||
name="gender-female"
|
||||
color="var(--color-danger)"
|
||||
class="ml-8rpx"
|
||||
/>
|
||||
</view>
|
||||
<text class="user-card__role">{{ item.roleNames }} · {{ item.deptName }}</text>
|
||||
</view>
|
||||
@@ -62,7 +91,11 @@
|
||||
<!-- 元信息行 -->
|
||||
<view class="user-card__footer">
|
||||
<text class="user-card__time">{{ item.createTime }}</text>
|
||||
<view class="user-card__action" hover-class="user-card__action--hover" @click.stop="showUserActions(item)">
|
||||
<view
|
||||
class="user-card__action"
|
||||
hover-class="user-card__action--hover"
|
||||
@click.stop="showUserActions(item)"
|
||||
>
|
||||
<wd-icon name="more" size="18" class="color-text-secondary" />
|
||||
</view>
|
||||
</view>
|
||||
@@ -73,29 +106,62 @@
|
||||
</view>
|
||||
|
||||
<!-- 弹窗表单 -->
|
||||
<wd-popup v-model="dialog.visible" position="bottom" custom-class="popup-bottom" @close="closeUserDialog">
|
||||
<wd-popup
|
||||
v-model="dialog.visible"
|
||||
position="bottom"
|
||||
custom-class="popup-bottom"
|
||||
@close="closeUserDialog"
|
||||
>
|
||||
<view class="p-4">
|
||||
<view class="popup-title">
|
||||
{{ formData.id ? "编辑用户" : "新增用户" }}
|
||||
</view>
|
||||
<wd-form ref="formRef" :model="formData" :schema="rules">
|
||||
<wd-form-item prop="username" title="用户名" required>
|
||||
<wd-input v-model="formData.username" placeholder="请输入用户名" :readonly="!!formData.id" />
|
||||
<wd-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入用户名"
|
||||
:readonly="!!formData.id"
|
||||
/>
|
||||
</wd-form-item>
|
||||
<wd-form-item prop="nickname" title="昵称" required>
|
||||
<wd-input v-model="formData.nickname" placeholder="请输入昵称" />
|
||||
</wd-form-item>
|
||||
<!-- 部门选择:触发交 wd-form-item,选择器只负责弹出 -->
|
||||
<wd-form-item title="部门" prop="deptId" required is-link
|
||||
:value="deptLabel" placeholder="请选择部门" @click="showDeptPicker = true" />
|
||||
<wd-cascader v-model="deptSelected" v-model:visible="showDeptPicker" :options="deptOptions"
|
||||
:lazy-load="handleDeptLazyLoad" :display-format="displayDeptFormat" @confirm="handleDeptConfirm" />
|
||||
<wd-form-item
|
||||
title="部门"
|
||||
prop="deptId"
|
||||
required
|
||||
is-link
|
||||
:value="deptLabel"
|
||||
placeholder="请选择部门"
|
||||
@click="showDeptPicker = true"
|
||||
/>
|
||||
<wd-cascader
|
||||
v-model="deptSelected"
|
||||
v-model:visible="showDeptPicker"
|
||||
:options="deptOptions"
|
||||
:lazy-load="handleDeptLazyLoad"
|
||||
:display-format="displayDeptFormat"
|
||||
@confirm="handleDeptConfirm"
|
||||
/>
|
||||
|
||||
<!-- 角色选择:触发交 wd-form-item,选择器只负责弹出 -->
|
||||
<wd-form-item title="角色" prop="roleIds" required is-link
|
||||
:value="roleLabel" placeholder="请选择角色" @click="showRolePicker = true" />
|
||||
<wd-select-picker v-model="formData.roleIds" v-model:visible="showRolePicker" :columns="roleOptions"
|
||||
@confirm="handleRoleConfirm" />
|
||||
<wd-form-item
|
||||
title="角色"
|
||||
prop="roleIds"
|
||||
required
|
||||
is-link
|
||||
:value="roleLabel"
|
||||
placeholder="请选择角色"
|
||||
@click="showRolePicker = true"
|
||||
/>
|
||||
<wd-select-picker
|
||||
v-model="formData.roleIds"
|
||||
v-model:visible="showRolePicker"
|
||||
:columns="roleOptions"
|
||||
@confirm="handleRoleConfirm"
|
||||
/>
|
||||
<wd-form-item prop="mobile" title="手机号">
|
||||
<wd-input v-model="formData.mobile" placeholder="请输入手机号" />
|
||||
</wd-form-item>
|
||||
@@ -117,7 +183,12 @@
|
||||
<wd-fab :expandable="false" :gap="{ bottom: 100 }" @click="openUserDialog()" />
|
||||
|
||||
<!-- 操作菜单 -->
|
||||
<wd-action-sheet v-model="actionSheetVisible" :actions="actionSheetActions" cancel-text="取消" @select="handleActionSelect" />
|
||||
<wd-action-sheet
|
||||
v-model="actionSheetVisible"
|
||||
:actions="actionSheetActions"
|
||||
cancel-text="取消"
|
||||
@select="handleActionSelect"
|
||||
/>
|
||||
|
||||
<!-- 重置密码弹窗 -->
|
||||
<wd-popup v-model="resetPwdDialog.visible" position="bottom" custom-class="popup-bottom">
|
||||
@@ -129,8 +200,14 @@
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="popup-actions">
|
||||
<wd-button type="info" variant="plain" @click="resetPwdDialog.visible = false">取消</wd-button>
|
||||
<wd-button type="primary" :loading="resetPwdDialog.isSubmitting" @click="handleResetPassword">
|
||||
<wd-button type="info" variant="plain" @click="resetPwdDialog.visible = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button
|
||||
type="primary"
|
||||
:loading="resetPwdDialog.isSubmitting"
|
||||
@click="handleResetPassword"
|
||||
>
|
||||
确认
|
||||
</wd-button>
|
||||
</view>
|
||||
@@ -140,407 +217,403 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useQueue, useToast, useDialog } from "@wot-ui/ui";
|
||||
import UserAPI, { type UserPageQuery, UserItem, UserForm } from "@/api/user";
|
||||
import RoleAPI from "@/api/role";
|
||||
import DeptAPI from "@/api/dept";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
|
||||
import { onLoad, onReachBottom } from "@dcloudio/uni-app";
|
||||
import { LoadMoreState } from "@wot-ui/ui/components/wd-loadmore/types";
|
||||
import { toFormSchema } from "@/utils/form";
|
||||
import { useQueue, useToast, useDialog } from "@wot-ui/ui";
|
||||
import UserAPI, { type UserPageQuery, UserItem, UserForm } from "@/api/user";
|
||||
import RoleAPI from "@/api/role";
|
||||
import DeptAPI from "@/api/dept";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
definePage({
|
||||
name: "user",
|
||||
style: { navigationBarTitleText: "用户管理" },
|
||||
});
|
||||
|
||||
definePage({
|
||||
name: "user",
|
||||
style: { navigationBarTitleText: "用户管理" },
|
||||
});
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const { closeOutside } = useQueue();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
const sortValue = ref(0);
|
||||
const sortOptions = ref([
|
||||
{ label: "默认排序", value: 0 },
|
||||
{ label: "最近创建", value: 1 },
|
||||
{ label: "最近更新", value: 2 },
|
||||
]);
|
||||
|
||||
const toast = useToast();
|
||||
const { confirm } = useDialog();
|
||||
const { closeOutside } = useQueue();
|
||||
const loadMoreState = ref<LoadMoreState>("loading");
|
||||
const formRef = ref();
|
||||
const isSubmitting = ref(false);
|
||||
const queryParams = reactive<UserPageQuery>({ pageNum: 1, pageSize: 10, keywords: "" });
|
||||
const total = ref(0);
|
||||
const pageData = ref<UserItem[]>([]);
|
||||
const dialog = reactive({ visible: false });
|
||||
|
||||
const sortValue = ref(0);
|
||||
const sortOptions = ref([
|
||||
{ label: "默认排序", value: 0 },
|
||||
{ label: "最近创建", value: 1 },
|
||||
{ label: "最近更新", value: 2 },
|
||||
]);
|
||||
const initialFormData: UserForm = {
|
||||
id: undefined,
|
||||
roleIds: [],
|
||||
username: undefined,
|
||||
nickname: undefined,
|
||||
deptId: undefined,
|
||||
mobile: undefined,
|
||||
email: undefined,
|
||||
status: 1,
|
||||
};
|
||||
|
||||
const queryParams = reactive<UserPageQuery>({ pageNum: 1, pageSize: 10, keywords: "" });
|
||||
const total = ref(0);
|
||||
const pageData = ref<UserItem[]>([]);
|
||||
const dialog = reactive({ visible: false });
|
||||
const formData = reactive<UserForm>({ ...initialFormData });
|
||||
const roleOptions = ref<Record<string, any>[]>([]);
|
||||
const deptOptions = ref<OptionType[]>([]);
|
||||
|
||||
const initialFormData: UserForm = {
|
||||
id: undefined,
|
||||
roleIds: [],
|
||||
username: undefined,
|
||||
nickname: undefined,
|
||||
deptId: undefined,
|
||||
mobile: undefined,
|
||||
email: undefined,
|
||||
status: 1,
|
||||
};
|
||||
// 部门选择器状态
|
||||
const showDeptPicker = ref(false);
|
||||
const deptSelected = ref<(string | number)[]>([]);
|
||||
const deptLabel = ref("");
|
||||
|
||||
const formData = reactive<UserForm>({ ...initialFormData });
|
||||
const roleOptions = ref<Record<string, any>[]>([]);
|
||||
const deptOptions = ref<OptionType[]>([]);
|
||||
// 角色选择器状态
|
||||
const showRolePicker = ref(false);
|
||||
const roleLabel = ref("");
|
||||
|
||||
// 部门选择器状态
|
||||
const showDeptPicker = ref(false);
|
||||
const deptSelected = ref<(string | number)[]>([]);
|
||||
const deptLabel = ref("");
|
||||
// 格式化部门展示
|
||||
const displayDeptFormat = (selectedItems: Record<string, any>[]) => {
|
||||
return selectedItems.map((item) => item.label).join("/");
|
||||
};
|
||||
|
||||
// 角色选择器状态
|
||||
const showRolePicker = ref(false);
|
||||
const roleLabel = ref("");
|
||||
|
||||
// 格式化部门展示
|
||||
const displayDeptFormat = (selectedItems: Record<string, any>[]) => {
|
||||
return selectedItems.map((item) => item.label).join("/");
|
||||
};
|
||||
|
||||
// 查找部门在树中的完整路径
|
||||
function findDeptPath(data: any[], targetId: string, path: string[] = []): string[] | null {
|
||||
for (const item of data) {
|
||||
const currentPath = [...path, item.value];
|
||||
if (item.value === targetId) {
|
||||
return currentPath;
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
const found = findDeptPath(item.children, targetId, currentPath);
|
||||
if (found) return found;
|
||||
}
|
||||
// 查找部门在树中的完整路径
|
||||
function findDeptPath(data: any[], targetId: string, path: string[] = []): string[] | null {
|
||||
for (const item of data) {
|
||||
const currentPath = [...path, item.value];
|
||||
if (item.value === targetId) {
|
||||
return currentPath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// v2 Cascader 懒加载
|
||||
const handleDeptLazyLoad = ({ selectedItem, resolve, finish }: any) => {
|
||||
const children = selectedItem.children;
|
||||
if (children && children.length > 0) {
|
||||
resolve(children);
|
||||
} else {
|
||||
finish();
|
||||
if (item.children && item.children.length > 0) {
|
||||
const found = findDeptPath(item.children, targetId, currentPath);
|
||||
if (found) return found;
|
||||
}
|
||||
};
|
||||
|
||||
// 部门确认选择
|
||||
const handleDeptConfirm = ({ value, selectedItems }: any) => {
|
||||
deptSelected.value = value;
|
||||
formData.deptId = value[value.length - 1];
|
||||
deptLabel.value = selectedItems.map((item: any) => item.label).join("/");
|
||||
};
|
||||
|
||||
// 角色确认选择
|
||||
const handleRoleConfirm = ({ selectedItems }: any) => {
|
||||
roleLabel.value = selectedItems.map((item: any) => item.label).join("、");
|
||||
};
|
||||
|
||||
const rules = toFormSchema({
|
||||
username: [{ required: true, message: "请输入用户名" }],
|
||||
nickname: [{ required: true, message: "请输入昵称" }],
|
||||
roleIds: [{ required: true, message: "请选择角色" }],
|
||||
deptId: [{ required: true, message: "请选择部门" }],
|
||||
});
|
||||
|
||||
// 排序切换
|
||||
const handleSortChange = ({ value }: { value: string | number }) => {
|
||||
const num = Number(value);
|
||||
if (num === 1) {
|
||||
queryParams.field = "create_time";
|
||||
queryParams.direction = "desc";
|
||||
} else if (num === 2) {
|
||||
queryParams.field = "update_time";
|
||||
queryParams.direction = "desc";
|
||||
} else {
|
||||
queryParams.field = "";
|
||||
queryParams.direction = "";
|
||||
}
|
||||
loadUserList();
|
||||
};
|
||||
|
||||
const handleFilterOpen = () => { };
|
||||
|
||||
// 搜索触发
|
||||
const handleSearch = () => loadUserList();
|
||||
|
||||
// 加载列表
|
||||
function loadUserList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchUserList();
|
||||
}
|
||||
// 应用筛选并刷新
|
||||
function applyUserFilter() {
|
||||
closeOutside();
|
||||
loadUserList();
|
||||
return null;
|
||||
}
|
||||
|
||||
// v2 Cascader 懒加载
|
||||
const handleDeptLazyLoad = ({ selectedItem, resolve, finish }: any) => {
|
||||
const children = selectedItem.children;
|
||||
if (children && children.length > 0) {
|
||||
resolve(children);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
// 重置筛选并刷新
|
||||
function resetUserFilter() {
|
||||
queryParams.keywords = "";
|
||||
delete queryParams.createTime;
|
||||
};
|
||||
|
||||
// 部门确认选择
|
||||
const handleDeptConfirm = ({ value, selectedItems }: any) => {
|
||||
deptSelected.value = value;
|
||||
formData.deptId = value[value.length - 1];
|
||||
deptLabel.value = selectedItems.map((item: any) => item.label).join("/");
|
||||
};
|
||||
|
||||
// 角色确认选择
|
||||
const handleRoleConfirm = ({ selectedItems }: any) => {
|
||||
roleLabel.value = selectedItems.map((item: any) => item.label).join("、");
|
||||
};
|
||||
|
||||
const rules = toFormSchema({
|
||||
username: [{ required: true, message: "请输入用户名" }],
|
||||
nickname: [{ required: true, message: "请输入昵称" }],
|
||||
roleIds: [{ required: true, message: "请选择角色" }],
|
||||
deptId: [{ required: true, message: "请选择部门" }],
|
||||
});
|
||||
|
||||
// 排序切换
|
||||
const handleSortChange = ({ value }: { value: string | number }) => {
|
||||
const num = Number(value);
|
||||
if (num === 1) {
|
||||
queryParams.field = "create_time";
|
||||
queryParams.direction = "desc";
|
||||
} else if (num === 2) {
|
||||
queryParams.field = "update_time";
|
||||
queryParams.direction = "desc";
|
||||
} else {
|
||||
queryParams.field = "";
|
||||
queryParams.direction = "";
|
||||
sortValue.value = 0;
|
||||
applyUserFilter();
|
||||
}
|
||||
loadUserList();
|
||||
};
|
||||
|
||||
// 分页加载列表
|
||||
function fetchUserList() {
|
||||
loadMoreState.value = "loading";
|
||||
UserAPI.getPage(queryParams)
|
||||
.then((data: any) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
const handleFilterOpen = () => {};
|
||||
|
||||
// 搜索触发
|
||||
const handleSearch = () => loadUserList();
|
||||
|
||||
// 加载列表
|
||||
function loadUserList() {
|
||||
queryParams.pageNum = 1;
|
||||
fetchUserList();
|
||||
}
|
||||
// 应用筛选并刷新
|
||||
function applyUserFilter() {
|
||||
closeOutside();
|
||||
loadUserList();
|
||||
}
|
||||
// 重置筛选并刷新
|
||||
function resetUserFilter() {
|
||||
queryParams.keywords = "";
|
||||
delete queryParams.createTime;
|
||||
queryParams.field = "";
|
||||
queryParams.direction = "";
|
||||
sortValue.value = 0;
|
||||
applyUserFilter();
|
||||
}
|
||||
|
||||
// 分页加载列表
|
||||
function fetchUserList() {
|
||||
loadMoreState.value = "loading";
|
||||
UserAPI.getPage(queryParams)
|
||||
.then((data: any) => {
|
||||
pageData.value = data.list;
|
||||
total.value = data.total;
|
||||
queryParams.pageNum++;
|
||||
})
|
||||
.catch(() => {
|
||||
pageData.value = [];
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
});
|
||||
}
|
||||
|
||||
// 打开弹窗(新增/编辑)
|
||||
async function openUserDialog(id?: number) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
deptSelected.value = [];
|
||||
deptLabel.value = "";
|
||||
roleLabel.value = "";
|
||||
dialog.visible = true;
|
||||
roleOptions.value = await RoleAPI.getOptions();
|
||||
const deptData = await DeptAPI.getOptions();
|
||||
deptOptions.value = deptData;
|
||||
|
||||
if (id) {
|
||||
formData.id = id;
|
||||
const data = await UserAPI.getFormData(id);
|
||||
Object.assign(formData, data, { id });
|
||||
// 编辑时回显部门
|
||||
if (data.deptId) {
|
||||
const path = findDeptPath(deptData, String(data.deptId));
|
||||
if (path) {
|
||||
deptSelected.value = path;
|
||||
} else {
|
||||
deptSelected.value = [String(data.deptId)];
|
||||
}
|
||||
}
|
||||
// 回显角色
|
||||
if (data.roleIds && data.roleIds.length > 0) {
|
||||
const names = roleOptions.value
|
||||
.filter((r: any) => data.roleIds.includes(r.value))
|
||||
.map((r: any) => r.label);
|
||||
roleLabel.value = names.join("、");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function submitUserForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
isSubmitting.value = true;
|
||||
const action = formData.id ? UserAPI.update(formData.id, formData) : UserAPI.add(formData);
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeUserDialog();
|
||||
loadUserList();
|
||||
})
|
||||
.finally(() => {
|
||||
loadMoreState.value = "finished";
|
||||
isSubmitting.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 打开弹窗(新增/编辑)
|
||||
async function openUserDialog(id?: number) {
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
deptSelected.value = [];
|
||||
deptLabel.value = "";
|
||||
roleLabel.value = "";
|
||||
dialog.visible = true;
|
||||
roleOptions.value = await RoleAPI.getOptions();
|
||||
const deptData = await DeptAPI.getOptions();
|
||||
deptOptions.value = deptData;
|
||||
|
||||
if (id) {
|
||||
formData.id = id;
|
||||
const data = await UserAPI.getFormData(id);
|
||||
Object.assign(formData, data, { id });
|
||||
// 编辑时回显部门
|
||||
if (data.deptId) {
|
||||
const path = findDeptPath(deptData, String(data.deptId));
|
||||
if (path) {
|
||||
deptSelected.value = path;
|
||||
} else {
|
||||
deptSelected.value = [String(data.deptId)];
|
||||
}
|
||||
}
|
||||
// 回显角色
|
||||
if (data.roleIds && data.roleIds.length > 0) {
|
||||
const names = roleOptions.value
|
||||
.filter((r: any) => data.roleIds.includes(r.value))
|
||||
.map((r: any) => r.label);
|
||||
roleLabel.value = names.join("、");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function submitUserForm() {
|
||||
formRef.value.validate().then(({ valid }: { valid: boolean }) => {
|
||||
if (!valid) return;
|
||||
isSubmitting.value = true;
|
||||
const action = formData.id ? UserAPI.update(formData.id, formData) : UserAPI.add(formData);
|
||||
action
|
||||
.then(() => {
|
||||
toast.success("操作成功");
|
||||
closeUserDialog();
|
||||
loadUserList();
|
||||
})
|
||||
.finally(() => {
|
||||
isSubmitting.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function closeUserDialog() {
|
||||
dialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const pendingAction = ref<Record<string, () => void>>({});
|
||||
|
||||
const resetPwdDialog = reactive({
|
||||
visible: false,
|
||||
isSubmitting: false,
|
||||
userId: undefined as number | undefined,
|
||||
});
|
||||
const resetPwdForm = reactive({ password: "" });
|
||||
const resetPwdFormRef = ref();
|
||||
}
|
||||
|
||||
// 更多操作
|
||||
function showUserActions(item: UserItem) {
|
||||
const actions: { name: string; color?: string }[] = [];
|
||||
const actionMap: Record<string, () => void> = {};
|
||||
// 关闭弹窗
|
||||
function closeUserDialog() {
|
||||
dialog.visible = false;
|
||||
formRef.value?.reset();
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
if (hasPermission("sys:user:reset-password")) {
|
||||
actions.push({ name: "重置密码" });
|
||||
actionMap["重置密码"] = () => openResetPwdDialog(item);
|
||||
}
|
||||
const actionSheetVisible = ref(false);
|
||||
const actionSheetActions = ref<{ name: string; color?: string }[]>([]);
|
||||
const pendingAction = ref<Record<string, () => void>>({});
|
||||
|
||||
// 编辑
|
||||
if (hasPermission("sys:user:update")) {
|
||||
actions.push({ name: "编辑" });
|
||||
actionMap["编辑"] = () => openUserDialog(item.id);
|
||||
}
|
||||
const resetPwdDialog = reactive({
|
||||
visible: false,
|
||||
isSubmitting: false,
|
||||
userId: undefined as number | undefined,
|
||||
});
|
||||
const resetPwdForm = reactive({ password: "" });
|
||||
const resetPwdFormRef = ref();
|
||||
|
||||
// 删除
|
||||
if (hasPermission("sys:user:delete")) {
|
||||
actions.push({ name: "删除", color: "var(--color-danger)" });
|
||||
actionMap["删除"] = async () => {
|
||||
try {
|
||||
await confirm({
|
||||
title: "确认删除",
|
||||
msg: `确定要删除用户「${item.nickname}」吗?`,
|
||||
headerImage: "warning",
|
||||
});
|
||||
await UserAPI.deleteByIds(String(item.id));
|
||||
toast.success("删除成功");
|
||||
loadUserList();
|
||||
} catch {
|
||||
// 用户取消操作
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (actions.length === 0) {
|
||||
toast.warning("暂无操作权限");
|
||||
return;
|
||||
}
|
||||
|
||||
actionSheetActions.value = actions;
|
||||
pendingAction.value = actionMap;
|
||||
actionSheetVisible.value = true;
|
||||
}
|
||||
|
||||
function handleActionSelect({ item }: { item: any }) {
|
||||
pendingAction. | ||||