style: 代码格式优化

This commit is contained in:
Ray.Hao
2026-06-08 07:53:07 +08:00
parent a422f9f962
commit 55086c6757
35 changed files with 4045 additions and 3515 deletions

View File

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

View File

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

View File

@@ -125,4 +125,4 @@ export interface DictDataItem {
sort?: number;
status?: number;
remark?: string;
}
}

View File

@@ -37,4 +37,4 @@ export const themeColorOptions: ThemeColorOption[] = [
export interface ProviderThemeVars extends ConfigProviderThemeVars {
colorTheme: string;
}
}

View File

@@ -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 () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
& + & {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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