From 9ed91eed18ea963e2d7b2a0d7c17d48d4cd126c7 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Fri, 17 Jul 2026 17:38:38 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=E6=99=BA=E8=83=BD=E6=9F=9C=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/hard/components/hard-door-log.vue | 5 +++++ src/views/hard/hard-after-sale/order-refund.vue | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/views/hard/components/hard-door-log.vue b/src/views/hard/components/hard-door-log.vue index 1a9d0318..f07aa7d9 100644 --- a/src/views/hard/components/hard-door-log.vue +++ b/src/views/hard/components/hard-door-log.vue @@ -135,6 +135,7 @@ diff --git a/src/router/route.ts b/src/router/route.ts index a04053d8..08777913 100644 --- a/src/router/route.ts +++ b/src/router/route.ts @@ -57,6 +57,15 @@ export const dynamicRoutes: Array = [ isHide: true, }, }, + { + path: "/admin/system/user/relation-tree", + name: "router.userRelationTree", + component: () => import("/@/views/admin/system/user/relation-tree.vue"), + meta: { + isHide: true, + isKeepAlive: false, + }, + }, ] /** diff --git a/src/views/admin/system/relation-type/form.vue b/src/views/admin/system/relation-type/form.vue new file mode 100644 index 00000000..61dfc790 --- /dev/null +++ b/src/views/admin/system/relation-type/form.vue @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ item.label }} + + + + + + + + + + + + + + 取消 + 确认 + + + + + + diff --git a/src/views/admin/system/relation-type/index.vue b/src/views/admin/system/relation-type/index.vue new file mode 100644 index 00000000..bbbe82b5 --- /dev/null +++ b/src/views/admin/system/relation-type/index.vue @@ -0,0 +1,119 @@ + + + + + + + 新 增 + + + 删除 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 编辑 + + + 删除 + + + + + + + + + + + + diff --git a/src/views/admin/system/user/components/user-info-form.ts b/src/views/admin/system/user/components/user-info-form.ts new file mode 100644 index 00000000..8011d338 --- /dev/null +++ b/src/views/admin/system/user/components/user-info-form.ts @@ -0,0 +1,70 @@ +export const DEFAULT_USER_IDENTITY = "DEFAULT" +export const STUDENT_USER_IDENTITY = "STUDENT" + +export interface UserStudentInfo { + studentNo: string + schoolName: string + className: string + extJson: string +} + +export interface UserInfoFormModel { + userId: string + username: string + password?: string + salt: string + wxOpenid: string + qqOpenid: string + lockFlag: string + phone?: string + deptId: string + roleList: any[] + postList: any[] + nickname: string + name: string + email: string + avatar?: string + gender: number + staffingType: string + identityTypes: string[] + studentInfo: UserStudentInfo + post: Array + role: Array +} + +export const createEmptyStudentInfo = (): UserStudentInfo => ({ + studentNo: "", + schoolName: "", + className: "", + extJson: "", +}) + +export const createEmptyUserInfo = (deptId = ""): UserInfoFormModel => ({ + userId: "", + username: "", + password: "", + salt: "", + wxOpenid: "", + qqOpenid: "", + lockFlag: "0", + phone: "", + deptId, + roleList: [], + postList: [], + nickname: "", + name: "", + email: "", + avatar: "", + gender: 0, + staffingType: "2", + identityTypes: [DEFAULT_USER_IDENTITY], + studentInfo: createEmptyStudentInfo(), + post: [], + role: [], +}) + +export const getUserIdentityPayload = (model: UserInfoFormModel) => ({ + identityTypes: [...model.identityTypes], + phone: model.identityTypes.includes(DEFAULT_USER_IDENTITY) ? model.phone : undefined, + studentInfo: model.identityTypes.includes(STUDENT_USER_IDENTITY) ? { ...model.studentInfo } : undefined, +}) diff --git a/src/views/admin/system/user/components/user-info-form.vue b/src/views/admin/system/user/components/user-info-form.vue new file mode 100644 index 00000000..bcc679fb --- /dev/null +++ b/src/views/admin/system/user/components/user-info-form.vue @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + 未知 + 男 + 女 + + + + + + + + + + + + + + + + + + + + + + + + {{ node.label }} + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ item.label }} + + + + + + 身份信息 + + + + 身份选择 + + + + + + + + + + + + 默认身份 + + + + + 该手机号与默认身份绑定 + + + + + + + + + 学生身份 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/views/admin/system/user/form.vue b/src/views/admin/system/user/form.vue index 26887880..1bb33887 100644 --- a/src/views/admin/system/user/form.vue +++ b/src/views/admin/system/user/form.vue @@ -1,106 +1,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - 未知 - 男 - 女 - - - - - - - - - - - - - - - - - - - - - - - - {{ node.label }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ item.label }} - - - - - - + + {{ $t("common.cancelButtonText") }} - {{ $t("common.confirmButtonText") - }} + {{ $t("common.confirmButtonText") }} @@ -108,35 +18,11 @@ diff --git a/src/views/admin/system/user/index.vue b/src/views/admin/system/user/index.vue index 814ff5f4..fb06a2ca 100644 --- a/src/views/admin/system/user/index.vue +++ b/src/views/admin/system/user/index.vue @@ -127,35 +127,42 @@ - 密码 + 密码 修改 + icon="SetUp" text type="primary" size="small" + @click="tagTableRef.openDialog(row.userId)"> 用户标签 + v-auth="'userFace_sysUserFace_view'" icon="User" size="small" text + type="primary" @click="faceManageRef.openDialog(row.userId)"> 人脸管理 开始处理 + + 关系图谱 + - 删除 @@ -202,6 +209,7 @@ const BatchDeal = defineAsyncComponent(() => import("./batchDeal.vue")) const TagTable = defineAsyncComponent(() => import("./tag-table.vue")) const { t } = useI18n() +const { push } = useRouter() const userDialogRef = ref() const faceManageRef = ref() @@ -278,6 +286,16 @@ const doBatchDeal = () => { batchDealRef.value.openDialog(processIds) } +// 关系图谱 +const toRelationTree = (row: any) => { + push({ + path: "/admin/system/user/relation-tree", + query: { + userId: row.userId, + }, + }) +} + // 审批提交事件 const taskSubmitEvent = () => { getDataList() diff --git a/src/views/admin/system/user/relation-create-user.vue b/src/views/admin/system/user/relation-create-user.vue new file mode 100644 index 00000000..f81700e1 --- /dev/null +++ b/src/views/admin/system/user/relation-create-user.vue @@ -0,0 +1,361 @@ + + + + {{ getInitial(targetUser?.displayName) }} + + 当前用户:{{ targetUser?.displayName }} + 可选择当前用户与新建或已有用户之间的关系方向 + + + + + 关系信息 + + + + + + + + + + + + 关联用户 → 当前用户 + 当前用户 → 关联用户 + + + + + + + + + + + + + + + + + + + + + {{ relationDirectionDescription }} + + + + 用户信息 + + + + + + + + + + + + + + + 取消 + + {{ createMode === "new" ? "新增用户并建立关系" : "建立关系" }} + + + + + + + + diff --git a/src/views/admin/system/user/relation-tree.vue b/src/views/admin/system/user/relation-tree.vue new file mode 100644 index 00000000..0617fa8f --- /dev/null +++ b/src/views/admin/system/user/relation-tree.vue @@ -0,0 +1,935 @@ + + + + + + 返回用户列表 + + {{ graphRoot?.displayName || "用户" }}的关系图谱 + 节点布局固定,拖动画布或缩放可查看完整关系 + + + + 适应画布 + 刷新 + 新增关系用户 + + + + + + + + + + + + {{ graphRoot.userCount || 1 }} + 图谱用户 + + + + + + + + + + {{ graphRoot.relationCount || 0 }} + 用户关系 + + + + + + + + + + {{ graphDepth }} + 关系层级 + + + + + + + + + + {{ selectedNode?.displayName || "-" }} + 当前选中 + + + + + + + + + + + 核心用户 + 重复引用 + + + {{ getInitial(data.graph.displayName) }} + + + {{ data.graph.displayName }} + @{{ data.graph.username || data.graph.userId }} + + {{ data.graph.userRoleName || data.graph.relationName || "关联用户" }} + + + + + + + + + + + + + + + + + + + + 重新加载 + + + + + + + + + + + + + + From c69e87d1dbdacb3ed03bf13748ee2284f9df9180 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Mon, 20 Jul 2026 16:50:24 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=E5=95=86=E6=88=B7=E9=85=8D=E7=BD=AE-?= =?UTF-8?q?=E4=BB=A3=E8=AE=A2=E9=A4=90=E7=94=A8=E6=88=B7=E5=85=B3=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/admin/sysUserRelationType.ts | 8 + .../Select/SelectUserRelationType.vue | 252 ++++++++++++++++++ .../Select/UserRelationTypeDisplay.vue | 121 +++++++++ src/components/index.ts | 4 + .../table-reservation-conf-form.vue | 8 + .../components/take-out-setting.vue | 7 + 6 files changed, 400 insertions(+) create mode 100644 src/components/Select/SelectUserRelationType.vue create mode 100644 src/components/Select/UserRelationTypeDisplay.vue diff --git a/src/api/admin/sysUserRelationType.ts b/src/api/admin/sysUserRelationType.ts index 3147da96..07c0ada7 100755 --- a/src/api/admin/sysUserRelationType.ts +++ b/src/api/admin/sysUserRelationType.ts @@ -1,5 +1,13 @@ import request from "/@/utils/request" +export function selectList(query?: Object) { + return request({ + url: "/admin/sysUserRelationType/select", + method: "get", + params: query, + }) +} + export function fetchList(query?: Object) { return request({ url: "/admin/sysUserRelationType/page", diff --git a/src/components/Select/SelectUserRelationType.vue b/src/components/Select/SelectUserRelationType.vue new file mode 100644 index 00000000..be269fc0 --- /dev/null +++ b/src/components/Select/SelectUserRelationType.vue @@ -0,0 +1,252 @@ + + + + + {{ item.relationName }} + + {{ formatRelationRoles(item) }} + + + + + + + diff --git a/src/components/Select/UserRelationTypeDisplay.vue b/src/components/Select/UserRelationTypeDisplay.vue new file mode 100644 index 00000000..65dce94e --- /dev/null +++ b/src/components/Select/UserRelationTypeDisplay.vue @@ -0,0 +1,121 @@ + + + + + {{ formatRelationType(item) }} + + + 加载中... + {{ props.emptyText }} + + + + + + diff --git a/src/components/index.ts b/src/components/index.ts index 3b54e438..0b9ba71e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -40,6 +40,8 @@ import SelectCanteenSupplier from "/@/components/Select/SelectCanteenSupplier.vu import SelectWalletInvoice from "/@/components/Select/SelectWalletInvoice.vue" import SelectNotice from "/@/components/Select/SelectNotice.vue" import SelectAct from "/@/components/Select/SelectAct.vue" +import SelectUserRelationType from "/@/components/Select/SelectUserRelationType.vue" +import UserRelationTypeDisplay from "/@/components/Select/UserRelationTypeDisplay.vue" // 地图组件 import MapSelect from "/@/components/map/map-select.vue" @@ -102,6 +104,8 @@ export default { app.component("SelectWalletInvoice", SelectWalletInvoice) app.component("SelectNotice", SelectNotice) app.component("SelectAct", SelectAct) + app.component("SelectUserRelationType", SelectUserRelationType) + app.component("UserRelationTypeDisplay", UserRelationTypeDisplay) app.component("MapSelect", MapSelect) diff --git a/src/views/mch/mch-manage/components/table-reservation-conf-form.vue b/src/views/mch/mch-manage/components/table-reservation-conf-form.vue index fa818ab1..43223f6d 100644 --- a/src/views/mch/mch-manage/components/table-reservation-conf-form.vue +++ b/src/views/mch/mch-manage/components/table-reservation-conf-form.vue @@ -8,6 +8,11 @@ inactive-text="否" /> + + + + + @@ -37,6 +42,7 @@ const form = reactive({ id: "", tableReservationConf: createDefaultTableReservationConf(), singleProductOnlyInCart: false, + userRelationTypeIdList: [], }) // 打开弹框并回填当前定餐配置 @@ -71,6 +77,7 @@ const onSubmit = async () => { ...createDefaultTableReservationConf(), ...(data?.tableReservationConf || {}), singleProductOnlyInCart: form.singleProductOnlyInCart, + userRelationTypeIdList: form.userRelationTypeIdList, } } const updateResult = await mchManageApi.putObj(payload) @@ -101,6 +108,7 @@ const getTableReservationConfData = async (mchId: string) => { } form.tableReservationConf = tableReservationConf form.singleProductOnlyInCart = tableReservationConf.singleProductOnlyInCart + form.userRelationTypeIdList = tableReservationConf.userRelationTypeIdList || [] } catch (error: any) { useMessage().error(error?.msg || error?.message || "获取定餐配置失败") } finally { diff --git a/src/views/mch/mch-manage/components/take-out-setting.vue b/src/views/mch/mch-manage/components/take-out-setting.vue index 175c9d20..54181c08 100644 --- a/src/views/mch/mch-manage/components/take-out-setting.vue +++ b/src/views/mch/mch-manage/components/take-out-setting.vue @@ -104,6 +104,13 @@ {{ state.tableReservationConf.singleProductOnlyInCart ? "是" : "否" }} + + 代订餐用户关系 + + + + From dcf3d2fd6ad8bc9965ec768e97ac4d27e7a2d488 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Mon, 20 Jul 2026 17:51:14 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=E5=85=B3=E7=B3=BB=E5=9B=BE=E8=B0=B1?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/admin/sysUserRelation.ts | 13 + .../admin/system/relation-type/index.vue | 29 ++ .../system/user/relation-create-user.vue | 3 +- .../admin/system/user/relation-edit-form.vue | 350 ++++++++++++++++++ src/views/admin/system/user/relation-tree.vue | 144 +++++-- 5 files changed, 515 insertions(+), 24 deletions(-) create mode 100644 src/views/admin/system/user/relation-edit-form.vue diff --git a/src/api/admin/sysUserRelation.ts b/src/api/admin/sysUserRelation.ts index 9eb2ef22..b9b02bea 100755 --- a/src/api/admin/sysUserRelation.ts +++ b/src/api/admin/sysUserRelation.ts @@ -70,6 +70,10 @@ export interface RelationCreateForm { remark?: string } +export interface RelationUpdateForm extends RelationCreateForm { + relationId: string +} + // 关系图谱 export function getRelationGraph(userId?: string) { return request({ @@ -95,3 +99,12 @@ export function createRelation(data: RelationCreateForm) { data, }) } + +// 修改已有用户关系 +export function updateRelation(data: RelationUpdateForm) { + return request({ + url: "/admin/sysUserRelation/update", + method: "put", + data, + }) +} diff --git a/src/views/admin/system/relation-type/index.vue b/src/views/admin/system/relation-type/index.vue index bbbe82b5..6cec3e58 100644 --- a/src/views/admin/system/relation-type/index.vue +++ b/src/views/admin/system/relation-type/index.vue @@ -1,6 +1,24 @@ + + + + + + + + + + + {{ $t("common.queryBtn") }} + + + {{ $t("common.resetBtn") }} + + + + { multiple.value = !rows.length } +// 清空搜索条件 +const resetQuery = () => { + // 清空搜索条件 + queryRef.value?.resetFields() + // 清空多选 + selectObjs.value = [] + getDataList() +} + const handleDelete = async (ids: string[]) => { try { await useMessageBox().confirm("此操作将永久删除") diff --git a/src/views/admin/system/user/relation-create-user.vue b/src/views/admin/system/user/relation-create-user.vue index f81700e1..70d47d20 100644 --- a/src/views/admin/system/user/relation-create-user.vue +++ b/src/views/admin/system/user/relation-create-user.vue @@ -58,7 +58,8 @@ - + + + + + {{ getInitial(subjectUserName) }} + + {{ subjectUserName }} + 关系主语 + + + + + + + {{ getInitial(objectUserName) }} + + {{ objectUserName }} + 关系对象 + + + + + + 绑定用户 + + + + + + + + + + + + + + + + + + + 关系信息 + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ relationDescription }} + + + + + 取消 + 保存修改 + + + + + + + diff --git a/src/views/admin/system/user/relation-tree.vue b/src/views/admin/system/user/relation-tree.vue index 0617fa8f..bd8aa2a2 100644 --- a/src/views/admin/system/user/relation-tree.vue +++ b/src/views/admin/system/user/relation-tree.vue @@ -5,8 +5,8 @@ 返回用户列表 - {{ graphRoot?.displayName || "用户" }}的关系图谱 - 节点布局固定,拖动画布或缩放可查看完整关系 + {{ entryNode?.displayName || "用户" }}的关系图谱 + 绿色标记当前进入用户,蓝色标记图谱根节点 @@ -90,13 +90,17 @@ class="relation-node" :class="{ 'is-root': data.graph.root, + 'is-entry': isEntryNode(data.graph), 'is-repeated': data.graph.repeated, 'is-selected': selectedNode?.nodeId === data.graph.nodeId, }" > - 核心用户 - 重复引用 + + 图谱根节点 + 当前用户 + 重复引用 + {{ getInitial(data.graph.displayName) }} @@ -108,13 +112,22 @@ {{ data.graph.userRoleName || data.graph.relationName || "关联用户" }} - - - - - - - + + + + + + + + + + + + + + + + @@ -151,7 +164,8 @@ {{ selectedNode.displayName }} 用户ID:{{ selectedNode.userId }} - 根节点 + 当前用户 + 图谱根节点 重复节点 @@ -209,9 +223,14 @@ - - 以此人为对象新增关系用户 - + + + 修改关系 + + + 新增关系用户 + + @@ -219,6 +238,7 @@ + @@ -230,6 +250,7 @@ import { MiniMap } from "@vue-flow/minimap" import { getRelationGraph, type RelationGraphNode } from "/@/api/admin/sysUserRelation" import { useMessage } from "/@/hooks/message" import RelationCreateUser from "./relation-create-user.vue" +import RelationEditForm from "./relation-edit-form.vue" import "@vue-flow/core/dist/style.css" import "@vue-flow/core/dist/theme-default.css" @@ -245,12 +266,14 @@ const route = useRoute() const router = useRouter() const loading = ref(false) const graphRoot = ref() +const entryNode = ref() const selectedNode = ref() const flowNodes = ref[]>([]) const flowEdges = ref([]) const graphDepth = ref(0) const flowStore = ref() const createUserRef = ref>() +const editFormRef = ref>() const nodeWidth = 230 const levelGap = 320 @@ -271,15 +294,17 @@ const loadGraph = async () => { try { const { data } = await getRelationGraph(String(userId.value)) graphRoot.value = data - selectedNode.value = data const graph = buildFlowElements(data) flowNodes.value = graph.nodes flowEdges.value = graph.edges graphDepth.value = graph.depth + entryNode.value = findGraphNodeByUserId(data, String(userId.value)) || data + selectedNode.value = entryNode.value await nextTick() fitGraph() } catch (error: any) { graphRoot.value = undefined + entryNode.value = undefined selectedNode.value = undefined flowNodes.value = [] flowEdges.value = [] @@ -374,10 +399,25 @@ const openCreateDialog = (target = selectedNode.value) => { createUserRef.value?.openDialog(target) } +const openEditDialog = (target = selectedNode.value) => { + if (!target || target.root) { + useMessage().warning("请选择一条需要修改的关系") + return + } + editFormRef.value?.openDialog(target, { + subjectUserName: getGraphUserName(target.subjectUserId), + objectUserName: getGraphUserName(target.objectUserId), + }) +} + const handleCreateSuccess = async () => { await loadGraph() } +const handleEditSuccess = async () => { + await loadGraph() +} + const goBack = () => { if (window.history.length > 1) { router.back() @@ -388,6 +428,23 @@ const goBack = () => { const getInitial = (name?: string) => name?.trim().slice(0, 1) || "用" +const isEntryNode = (node: RelationGraphNode) => entryNode.value?.nodeId === node.nodeId + +const findGraphNodeByUserId = (node: RelationGraphNode, targetUserId: string): RelationGraphNode | undefined => { + if (String(node.userId) === targetUserId) return node + for (const child of node.children || []) { + const targetNode = findGraphNodeByUserId(child, targetUserId) + if (targetNode) return targetNode + } + return undefined +} + +const getGraphUserName = (targetUserId?: string) => { + if (!targetUserId) return "用户" + const graphNode = flowNodes.value.find((node) => String((node.data as RelationNodeData).graph.userId) === String(targetUserId)) + return (graphNode?.data as RelationNodeData | undefined)?.graph.displayName || String(targetUserId) +} + const resolveImageUrl = (url?: string) => { if (!url) return "" return url.includes("http") ? url : `${import.meta.env.VITE_IMAGE_BASE_PATH}${url}` @@ -617,7 +674,7 @@ watch(userId, loadGraph, { immediate: true }) gap: 12px; width: 230px; height: 96px; - padding: 15px 38px 15px 14px; + padding: 15px 68px 15px 14px; box-sizing: border-box; background: rgba(255, 255, 255, 0.96); border: 2px solid #cbd5e1; @@ -633,7 +690,18 @@ watch(userId, loadGraph, { immediate: true }) box-shadow: 0 12px 30px rgba(37, 99, 235, 0.18); } +.relation-node.is-entry { + border-color: #10b981; + box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.16), 0 12px 30px rgba(5, 150, 105, 0.2); +} + +.relation-node.is-entry.is-selected { + transform: translateY(-2px); + box-shadow: 0 0 0 5px rgba(16, 185, 129, 0.2), 0 16px 34px rgba(5, 150, 105, 0.24); +} + .relation-node.is-root { + padding-right: 38px; color: #fff; background: linear-gradient(135deg, #2563eb 0%, #4f46e5 100%); border-color: #1d4ed8; @@ -646,11 +714,17 @@ watch(userId, loadGraph, { immediate: true }) opacity: 0.82; } -.root-badge, -.repeat-badge { +.node-badges { position: absolute; top: -10px; left: 14px; + display: flex; + gap: 5px; +} + +.root-badge, +.entry-badge, +.repeat-badge { padding: 3px 9px; font-size: 11px; font-weight: 600; @@ -662,6 +736,11 @@ watch(userId, loadGraph, { immediate: true }) background: #dbeafe; } +.entry-badge { + color: #047857; + background: #d1fae5; +} + .repeat-badge { color: #475569; background: #e2e8f0; @@ -748,9 +827,15 @@ watch(userId, loadGraph, { immediate: true }) border-radius: 10px; } -.node-add { +.node-actions { position: absolute; right: 8px; + display: flex; + flex-direction: column; + gap: 7px; +} + +.node-action { display: flex; align-items: center; justify-content: center; @@ -764,6 +849,12 @@ watch(userId, loadGraph, { immediate: true }) cursor: pointer; } +.node-edit { + color: #7c3aed; + background: #f5f3ff; + border-color: #ddd6fe; +} + .is-root .node-add { color: #fff; background: rgba(255, 255, 255, 0.16); @@ -888,10 +979,17 @@ watch(userId, loadGraph, { immediate: true }) margin-top: 7px; } -.detail-add-button { - width: 100%; - height: 34px; +.detail-actions { + display: flex; + gap: 8px; margin-top: 12px; +} + +.detail-action-button { + flex: 1; + height: 34px; + margin-left: 0; + padding: 0 10px; border-radius: 999px; } From 1c54a7bc1ead324b32f087ecf618865f58d3fd4d Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Tue, 21 Jul 2026 21:04:31 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=E5=91=A8=E6=9C=9F=E9=A4=90=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/hard/hard-appt-cycle-rule.ts | 42 +++ src/views/hard/hard-appt/constants.ts | 43 +++ src/views/hard/hard-appt/cycle-rule-form.vue | 319 ++++++++++++++++++ src/views/hard/hard-appt/cycle-rule-list.vue | 123 +++++++ src/views/hard/hard-appt/form.vue | 135 ++++---- src/views/hard/hard-appt/index.vue | 33 +- src/views/integrated/meeting/booking/form.vue | 216 ++++++------ .../components/take-out-setting.vue | 33 +- 8 files changed, 760 insertions(+), 184 deletions(-) create mode 100644 src/api/hard/hard-appt-cycle-rule.ts create mode 100644 src/views/hard/hard-appt/constants.ts create mode 100644 src/views/hard/hard-appt/cycle-rule-form.vue create mode 100644 src/views/hard/hard-appt/cycle-rule-list.vue diff --git a/src/api/hard/hard-appt-cycle-rule.ts b/src/api/hard/hard-appt-cycle-rule.ts new file mode 100644 index 00000000..d01d9c9a --- /dev/null +++ b/src/api/hard/hard-appt-cycle-rule.ts @@ -0,0 +1,42 @@ +import request from "/@/utils/request" + +const baseUrl = "/hard/hard-appt-cycle-rule" + +export function fetchList(query?: Object) { + return request({ + url: `${baseUrl}/page`, + method: "get", + params: query, + }) +} + +export function getObj(id: string) { + return request({ + url: `${baseUrl}/${id}`, + method: "get", + }) +} + +export function addObj(data: Object) { + return request({ + url: baseUrl, + method: "post", + data, + }) +} + +export function putObj(data: Object) { + return request({ + url: baseUrl, + method: "put", + data, + }) +} + +export function delObjs(ids: Array) { + return request({ + url: baseUrl, + method: "delete", + data: ids, + }) +} diff --git a/src/views/hard/hard-appt/constants.ts b/src/views/hard/hard-appt/constants.ts new file mode 100644 index 00000000..658e5991 --- /dev/null +++ b/src/views/hard/hard-appt/constants.ts @@ -0,0 +1,43 @@ +export const RESERVE_MODE = { + TIME_WINDOW: "TIME_WINDOW", + WEEKLY_CYCLE: "WEEKLY_CYCLE", +} as const + +export const RESERVE_MODE_OPTIONS = [ + { label: "动态时间窗", value: RESERVE_MODE.TIME_WINDOW }, + { label: "按周集中预约", value: RESERVE_MODE.WEEKLY_CYCLE }, +] + +export const WEEKDAY_OPTIONS = [ + { label: "周一", value: 1 }, + { label: "周二", value: 2 }, + { label: "周三", value: 3 }, + { label: "周四", value: 4 }, + { label: "周五", value: 5 }, + { label: "周六", value: 6 }, + { label: "周日", value: 7 }, +] + +export function normalizeReserveMode(value?: string) { + return value === RESERVE_MODE.WEEKLY_CYCLE ? RESERVE_MODE.WEEKLY_CYCLE : RESERVE_MODE.TIME_WINDOW +} + +export function reserveModeLabel(value?: string) { + return normalizeReserveMode(value) === RESERVE_MODE.WEEKLY_CYCLE ? "按周集中预约" : "动态时间窗" +} + +export function weekdayLabel(value?: number | string) { + return WEEKDAY_OPTIONS.find((item) => item.value === Number(value))?.label ?? "-" +} + +export function formatTime(value?: string) { + return value ? value.slice(0, 5) : "-" +} + +export function formatCycleWindow(rule: any) { + return `${weekdayLabel(rule?.openWeekday)} ${formatTime(rule?.openTime)} 至 ${weekdayLabel(rule?.closeWeekday)} ${formatTime(rule?.closeTime)}` +} + +export function formatCycleTarget(rule: any) { + return `截止后第 ${rule?.targetStartOffsetDays ?? "-"} 天起,连续 ${rule?.targetDayCount ?? "-"} 天` +} diff --git a/src/views/hard/hard-appt/cycle-rule-form.vue b/src/views/hard/hard-appt/cycle-rule-form.vue new file mode 100644 index 00000000..2f75fce2 --- /dev/null +++ b/src/views/hard/hard-appt/cycle-rule-form.vue @@ -0,0 +1,319 @@ + + + + + + + + + + + + + + + + + + 开放窗口 + + + + + + + + + + + + + + + + + + + + + + + + 目标用餐日 + + + + + 截止后第 + + 天 + + + + + + + + + + + + + 逐日餐段 + + 复制首日 + 清空 + + + + + + + + 截止后第 {{ Number(form.targetStartOffsetDays) + row.targetDayOffset }} 天 + {{ previewDate(row.targetDayOffset) }} + + + + + + + + + + + + + 其他设置 + + + + + + {{ item.label }} + + + + + + + + + + + + + + + + + + + 取消 + 确认 + + + + + diff --git a/src/views/hard/hard-appt/cycle-rule-list.vue b/src/views/hard/hard-appt/cycle-rule-list.vue new file mode 100644 index 00000000..41b45d53 --- /dev/null +++ b/src/views/hard/hard-appt/cycle-rule-list.vue @@ -0,0 +1,123 @@ + + + + + + + + + + + 查询 + 重置 + + + + + 新增 + 删除 + + + + + + + + + + {{ formatCycleWindow(row) }} + + + {{ formatCycleTarget(row) }} + + + {{ row.apptCount ?? 0 }} 个 + + + + + + + + 编辑 + 删除 + + + + + + + + + + diff --git a/src/views/hard/hard-appt/form.vue b/src/views/hard/hard-appt/form.vue index 00ae0783..cae39301 100644 --- a/src/views/hard/hard-appt/form.vue +++ b/src/views/hard/hard-appt/form.vue @@ -20,30 +20,37 @@ - - - - - - - - - - - - - - - - - - + + 动态时间窗 + + + + + + + + + + + + + + + + + + + + + + + @@ -74,6 +81,7 @@ import { useDict } from "/@/hooks/dict" import { useMessage } from "/@/hooks/message" import * as hardApptApi from "/@/api/hard/hard-appt" +import { RESERVE_MODE } from "./constants" const emit = defineEmits(["refresh"]) // 定义变量内容 @@ -85,24 +93,23 @@ const { yes_no_type } = useDict("yes_no_type") // 提交表单数据 const currentDeptId = ref() -const defaultForm = { +const createDefaultForm = () => ({ id: "", - mchId: '', + mchId: "", title: "", hourType: "", + reserveMode: RESERVE_MODE.TIME_WINDOW as string, startTimeType: "2", endTimeType: "2", startTime: "", endTime: "", status: "1", remark: "", - reserveTimeList: ['2'], -} - -const form = reactive({ - ...defaultForm + reserveTimeList: ["2"], }) +const form = reactive(createDefaultForm()) + function toHHmm(value?: string) { const raw = (value ?? "").trim() if (!raw) return "" @@ -132,47 +139,57 @@ function toHHmmss(value?: string) { } function toTotalMinutes(type: any, time: any) { - const offset = Number(type) - const base = Number.isFinite(offset) ? offset * 24 * 60 : 0 + const dayOffset: Record = { "1": -1, "2": 0, "3": 1, "4": 2 } + const base = (dayOffset[String(type)] ?? 0) * 24 * 60 const hhmm = toHHmm(time) if (!hhmm) return base const [h, m] = hhmm.split(":").map((v) => Number(v)) return base + h * 60 + m } +const requiredForTimeWindow = (message: string) => + (_rule: any, value: any, callback: (error?: Error) => void) => { + if (form.reserveMode !== RESERVE_MODE.TIME_WINDOW || (Array.isArray(value) ? value.length : value)) { + return callback() + } + callback(new Error(message)) + } + +const validateStartTime = (_rule: any, _value: string, callback: (error?: Error) => void) => { + if (form.reserveMode !== RESERVE_MODE.TIME_WINDOW) return callback() + if (!form.startTimeType) return callback(new Error("开始时间类型不能为空")) + if (!form.startTime) return callback(new Error("开始时间不能为空")) + callback() +} + +const validateEndTime = (_rule: any, _value: string, callback: (error?: Error) => void) => { + if (form.reserveMode !== RESERVE_MODE.TIME_WINDOW) return callback() + if (!form.endTimeType) return callback(new Error("结束时间类型不能为空")) + if (!form.endTime) return callback(new Error("结束时间不能为空")) + if (toTotalMinutes(form.startTimeType, form.startTime) >= toTotalMinutes(form.endTimeType, form.endTime)) { + return callback(new Error("结束时间必须大于开始时间")) + } + callback() +} + // 定义校验规则 -const dataRules = ref({ +const dataRules = { mchId: [{ required: true, message: "商户不能为空", trigger: "change" }], title: [{ required: true, message: "标题不能为空", trigger: "blur" }], hourType: [{ required: true, message: "类型不能为空", trigger: "blur" }], - startApptTimeType: [{ required: true, message: "开始时间类型不能为空", trigger: "change" }], - startTime: [{ required: true, message: "开始时间不能为空", trigger: "change" }], - endApptTimeType: [{ required: true, message: "结束时间类型不能为空", trigger: "change" }], - reserveTimeList: [{ required: true, message: "可预约时间不能为空", trigger: "change" }], - endTime: [ - { required: true, message: "结束时间不能为空", trigger: "change" }, - { - validator: (_rule: any, _value: string, callback: (error?: Error) => void) => { - if (!form.startTime || !form.endTime) return callback() - if (toTotalMinutes(form.startTimeType, form.startTime) >= toTotalMinutes(form.endTimeType, form.endTime)) { - return callback(new Error("结束时间必须大于开始时间")) - } - callback() - }, - trigger: "change", - }, - ], + startTime: [{ validator: validateStartTime, trigger: "change" }], + reserveTimeList: [{ validator: requiredForTimeWindow("可预约时间不能为空"), trigger: "change" }], + endTime: [{ validator: validateEndTime, trigger: "change" }], status: [{ required: true, message: "是否启用不能为空", trigger: "change" }], -}) +} // 打开弹窗 -const openDialog = (id: string, deptId: string) => { +const openDialog = (id?: string | null, deptId?: string | number) => { visible.value = true currentDeptId.value = deptId - form.id = "" - // 重置表单数据 + Object.assign(form, createDefaultForm()) nextTick(() => { - dataFormRef.value?.resetFields() + dataFormRef.value?.clearValidate() }) // 获取hardAppt信息 if (id) { @@ -189,8 +206,9 @@ const onSubmit = async () => { loading.value = true const _pa = { ...form, - startTime: toHHmmss(form.startTime), - endTime: toHHmmss(form.endTime), + startTime: form.reserveMode === RESERVE_MODE.TIME_WINDOW ? toHHmmss(form.startTime) : undefined, + endTime: form.reserveMode === RESERVE_MODE.TIME_WINDOW ? toHHmmss(form.endTime) : undefined, + reserveTimeList: form.reserveMode === RESERVE_MODE.TIME_WINDOW ? form.reserveTimeList : [], } form.id ? await hardApptApi.putObj(_pa) : await hardApptApi.addObj(_pa) useMessage().success(form.id ? "修改成功" : "添加成功") @@ -209,11 +227,10 @@ const gethardApptData = async (id: string) => { loading.value = true try { const { data } = await hardApptApi.getObj(id) - Object.assign(form, data, { + Object.assign(form, createDefaultForm(), data, { + reserveMode: RESERVE_MODE.TIME_WINDOW, startTime: toHHmm(data.startTime), endTime: toHHmm(data.endTime), - startApptTimeType: form.startTimeType, - endApptTimeType: form.endTimeType, }) } catch (error) { console.error(error) diff --git a/src/views/hard/hard-appt/index.vue b/src/views/hard/hard-appt/index.vue index b8588017..cb0de385 100644 --- a/src/views/hard/hard-appt/index.vue +++ b/src/views/hard/hard-appt/index.vue @@ -1,6 +1,8 @@ + + @@ -39,11 +41,24 @@ + + + + {{ reserveModeLabel(row.reserveMode) }} + + + - {{ row.startTime }} - - - {{ row.endTime }} + + 由周期预约规则配置 + + + {{ row.startTime }} + - + {{ row.endTime }} + @@ -63,6 +78,11 @@ + + + + + @@ -75,13 +95,20 @@ import { BasicTableProps, useTable } from "/@/hooks/table" import * as hardApptApi from "/@/api/hard/hard-appt" import { useMessage, useMessageBox } from "/@/hooks/message" import { useDict } from "/@/hooks/dict" +import { + RESERVE_MODE, + normalizeReserveMode, + reserveModeLabel, +} from "./constants" // 引入组件 const FormDialog = defineAsyncComponent(() => import("./form.vue")) +const CycleRuleList = defineAsyncComponent(() => import("./cycle-rule-list.vue")) // 定义查询字典 const { yes_no_type, daily_menu } = useDict("yes_no_type", "daily_menu") // 定义变量内容 const formDialogRef = ref() +const activeTab = ref("segments") // 搜索变量 const queryRef = ref() const showSearch = ref(true) diff --git a/src/views/integrated/meeting/booking/form.vue b/src/views/integrated/meeting/booking/form.vue index beff1542..175efd3a 100644 --- a/src/views/integrated/meeting/booking/form.vue +++ b/src/views/integrated/meeting/booking/form.vue @@ -1,90 +1,68 @@ - - - - - - - - + + + + + + + + - - - - - - - + + + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - + 可预约时间段 @@ -103,10 +81,8 @@ + :plain="!selectedSlots.includes(slot.startTime + '-' + slot.endTime)" size="small" + @click="toggleSlot(slot)"> {{ selectedSlots.includes(slot.startTime + '-' + slot.endTime) ? '已选中' : '可预约' }} @@ -120,14 +96,14 @@ 请先选择会议室 - - - - 取消 - 确认 - - - + + + + 取消 + 确认 + + +