mobile version 2.0

This commit is contained in:
Chuck1sn
2025-06-10 13:04:38 +08:00
parent 45c921fa24
commit 5068e7ff58
11 changed files with 410 additions and 141 deletions

View File

@@ -1,28 +1,24 @@
<template> <template>
<nav class="flex mb-3 sm:mb-4 md:mb-5" aria-label="Breadcrumb"> <nav class="flex mb-4" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 text-xs sm:text-sm font-medium md:space-x-2"> <ol class="inline-flex items-center space-x-1 sm:space-x-2 text-sm">
<li class="inline-flex items-center"> <li class="inline-flex items-center">
<RouterLink :to="{name: RouteName.USERVIEW}" <RouterLink to="/" class="inline-flex items-center font-medium text-gray-500 hover:text-blue-600">
class="inline-flex items-center text-gray-700 hover:text-primary-600"> <svg class="w-3.5 h-3.5 mr-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor"
<svg class="w-4 h-4 sm:w-5 sm:h-5 me-1.5 sm:me-2 md:me-2.5" fill="currentColor" viewBox="0 0 20 20" viewBox="0 0 20 20">
xmlns="http://www.w3.org/2000/svg">
<path <path
d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"> d="m19.707 9.293-2-2-7-7a1 1 0 0 0-1.414 0l-7 7-2 2a1 1 0 0 0 1.414 1.414L2 10.414V18a2 2 0 0 0 2 2h3a1 1 0 0 0 1-1v-4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v4a1 1 0 0 0 1 1h3a2 2 0 0 0 2-2v-7.586l.293.293a1 1 0 0 0 1.414-1.414Z" />
</path>
</svg> </svg>
首页 首页
</RouterLink> </RouterLink>
</li> </li>
<li v-for="name in names" :key="name"> <li v-for="(name, index) in names" :key="index">
<div class="flex items-center"> <div class="flex items-center">
<svg class="w-3 h-3 sm:w-4 sm:h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20" <svg class="w-3 h-3 text-gray-400 mx-1.5 sm:mx-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"> fill="none" viewBox="0 0 6 10">
<path fill-rule="evenodd" <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="m1 9 4-4-4-4" />
clip-rule="evenodd"></path>
</svg> </svg>
<span class="ms-1 text-gray-400 hover:text-primary-600 md:ms-2">{{ <span class="font-medium text-gray-500 truncate">{{ name }}</span>
name }}</span>
</div> </div>
</li> </li>
</ol> </ol>

View File

@@ -59,9 +59,9 @@ const sizeClasses = computed(() => {
} }
switch (props.size) { switch (props.size) {
case "xs": case "xs":
return "text-xs px-2.5 py-1.5"; return "text-xs px-3 py-1.5";
case "sm": case "sm":
return "text-xs px-3 py-2"; return "text-sm px-3.5 py-2";
case "lg": case "lg":
return "text-base px-5 py-3"; return "text-base px-5 py-3";
case "xl": case "xl":
@@ -74,7 +74,7 @@ const sizeClasses = computed(() => {
const iconSizeClasses = computed(() => { const iconSizeClasses = computed(() => {
switch (props.size) { switch (props.size) {
case "xs": case "xs":
return "w-3 h-3"; return "w-3.5 h-3.5";
case "sm": case "sm":
return "w-4 h-4"; return "w-4 h-4";
case "lg": case "lg":
@@ -82,7 +82,7 @@ const iconSizeClasses = computed(() => {
case "xl": case "xl":
return "w-6 h-6"; return "w-6 h-6";
default: default:
return "w-5 h-5"; return "w-4.5 h-4.5";
} }
}); });

View File

@@ -0,0 +1,43 @@
<template>
<div class="space-y-4">
<slot name="empty" v-if="items.length === 0"></slot>
<div v-else v-for="(item, index) in items" :key="index"
class="p-4 bg-white rounded-lg shadow relative border border-gray-100">
<div class="flex justify-between items-start mb-3">
<!-- 标题区域 -->
<div class="flex items-center">
<slot name="checkbox" :item="item" v-if="hasCheckbox"></slot>
<div class="font-medium text-gray-900">
<slot name="title" :item="item"></slot>
</div>
</div>
<!-- 状态区域 -->
<div>
<slot name="status" :item="item"></slot>
</div>
</div>
<!-- 内容区域 -->
<div class="text-sm text-gray-600 mb-3 space-y-2">
<slot name="content" :item="item"></slot>
</div>
<!-- 标签/分类区域 -->
<div v-if="$slots.tags" class="flex flex-wrap gap-2 mb-3">
<slot name="tags" :item="item"></slot>
</div>
<!-- 操作按钮区域 -->
<div class="flex justify-between items-center mt-4">
<slot name="actions" :item="item"></slot>
</div>
</div>
</div>
</template>
<script setup generic="T" lang="ts">
defineProps<{
items: T[];
hasCheckbox?: boolean;
}>();
</script>

View File

@@ -2,7 +2,7 @@
<div :id tabindex="-1" <div :id tabindex="-1"
class="bg-gray-900/50 hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full h-[calc(100%-1rem)] max-h-full"> class="bg-gray-900/50 hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full max-w-xs sm:max-w-sm md:max-w-md max-h-full"> <div class="relative p-4 w-full max-w-xs sm:max-w-sm md:max-w-md max-h-full">
<div class="relative bg-white rounded-lg shadow-sm"> <div class="relative bg-white rounded-lg shadow">
<button type="button" @click="closeModal" <button type="button" @click="closeModal"
class="absolute top-3 end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center"> class="absolute top-3 end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14"> <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
@@ -11,22 +11,22 @@
</svg> </svg>
<span class="sr-only">Close modal</span> <span class="sr-only">Close modal</span>
</button> </button>
<div class="p-4 md:p-5 text-center flex flex-col items-center gap-y-3 sm:gap-y-4"> <div class="p-5 md:p-6 text-center">
<svg class="w-12 h-12 sm:w-16 sm:h-16 mx-auto text-red-600 mt-4 sm:mt-5" fill="none" stroke="currentColor" <svg class="w-14 h-14 sm:w-16 sm:h-16 mx-auto text-red-600 mb-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg> </svg>
<h3 class="mb-2 sm:mb-3 text-base sm:text-lg font-normal text-gray-500"> <h3 class="mb-4 text-base sm:text-lg font-medium text-gray-800">
{{ title }} {{ title }}
</h3> </h3>
<div class="flex flex-col sm:flex-row sm:justify-center gap-2 w-full sm:w-auto"> <div class="flex justify-center items-center space-x-3 sm:space-x-4">
<button type="button" @click="onSubmit" <button type="button" @click="onSubmit"
class="w-full sm:w-auto text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-xs sm:text-sm px-4 py-2 sm:px-5 sm:py-2.5 text-center"> class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center min-w-[80px]">
</button> </button>
<button type="button" @click="closeModal" <button type="button" @click="closeModal"
class="w-full sm:w-auto py-2 sm:py-2.5 px-4 sm:px-5 text-xs sm:text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100"></button> class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 min-w-[80px]"></button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<template> <template>
<nav class="flex items-center flex-col md:flex-row flex-wrap justify-between py-3 sm:py-4 px-3 sm:px-5" <nav class="flex items-center flex-col md:flex-row flex-wrap justify-between py-4 px-3 sm:px-5"
aria-label="Table navigation"> aria-label="Table navigation">
<span class="text-xs sm:text-sm font-normal text-gray-500 mb-3 md:mb-0 block w-full md:inline md:w-auto"> <span class="text-xs sm:text-sm font-normal text-gray-500 mb-4 md:mb-0 block w-full md:inline md:w-auto">
显示 显示
<span class="font-semibold text-gray-900"> <span class="font-semibold text-gray-900">
{{ displayRange.start }}-{{ displayRange.end }} {{ displayRange.start }}-{{ displayRange.end }}
@@ -9,25 +9,25 @@
<span class="font-semibold text-gray-900">{{ total }}</span> <span class="font-semibold text-gray-900">{{ total }}</span>
</span> </span>
<ul class="inline-flex -space-x-px rtl:space-x-reverse text-xs sm:text-sm"> <ul class="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
<li> <li>
<a href="#" @click.prevent="handlePageChangeClick(currentPage - 1)" :class="[ <a href="#" @click.prevent="handlePageChangeClick(currentPage - 1)" :class="[
'flex items-center justify-center px-2.5 h-7 sm:px-3 sm:h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700', 'flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700',
{ 'opacity-50 cursor-not-allowed': isFirstPage } { 'opacity-50 cursor-not-allowed': isFirstPage }
]">上一页</a> ]">上一页</a>
</li> </li>
<li v-for="page in pageNumbers" :key="page"> <li v-for="page in pageNumbers" :key="page">
<button @click.prevent="handlePageChangeClick(page)" :class="[ <button @click.prevent="handlePageChangeClick(page)" :class="[
'flex items-center justify-center px-2.5 h-7 sm:px-3 sm:h-8 leading-tight border border-gray-300 hover:bg-gray-100 hover:text-gray-700', 'flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 hover:bg-gray-100 hover:text-gray-700',
currentPage === page currentPage === page
? 'text-blue-600 bg-blue-50 hover:bg-blue-100 hover:text-blue-700' ? 'text-blue-600 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 font-medium'
: 'text-gray-500 bg-white' : 'text-gray-500 bg-white'
]">{{ page }}</button> ]">{{ page }}</button>
</li> </li>
<li> <li>
<button @click.prevent="handlePageChangeClick(currentPage + 1)" :class="[ <button @click.prevent="handlePageChangeClick(currentPage + 1)" :class="[
'flex items-center justify-center px-2.5 h-7 sm:px-3 sm:h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700', 'flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700',
{ 'opacity-50 cursor-not-allowed': isLastPage } { 'opacity-50 cursor-not-allowed': isLastPage }
]">下一页</button> ]">下一页</button>
</li> </li>

View File

@@ -4,7 +4,7 @@
class="bg-gray-900/50 hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full"> class="bg-gray-900/50 hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative p-4 w-full max-w-xs sm:max-w-sm md:max-w-md max-h-full"> <div class="relative p-4 w-full max-w-xs sm:max-w-sm md:max-w-md max-h-full">
<!-- Modal content --> <!-- Modal content -->
<div class="relative bg-white rounded-lg shadow-sm"> <div class="relative bg-white rounded-lg shadow">
<!-- Modal header --> <!-- Modal header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200"> <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t border-gray-200">
<h3 class="text-base sm:text-lg font-semibold text-gray-900"> <h3 class="text-base sm:text-lg font-semibold text-gray-900">
@@ -21,27 +21,27 @@
</div> </div>
<!-- Modal body --> <!-- Modal body -->
<form class="p-4 md:p-5"> <form class="p-4 md:p-5">
<div class="grid gap-4 mb-4 grid-cols-1 sm:grid-cols-2"> <div class="space-y-4">
<div class="col-span-full sm:col-span-2"> <div class="w-full">
<label for="name" class="block mb-2 text-sm font-medium text-gray-900">用户名</label> <label for="name" class="block mb-2 text-sm font-medium text-gray-900">用户名</label>
<input type="text" name="用户名" id="name" v-model="formData.username" <input type="text" name="用户名" id="name" v-model="formData.username"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5"
required="true"> required="true">
</div> </div>
<div class="col-span-full sm:col-span-2"> <div class="w-full">
<label for="password" class="block mb-2 text-sm font-medium autocompletetext-gray-900">密码</label> <label for="password" class="block mb-2 text-sm font-medium text-gray-900">密码</label>
<input type="password" id="password" autocomplete="new-password" v-model="formData.password" <input type="password" id="password" autocomplete="new-password" v-model="formData.password"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="编辑时非必填" required /> placeholder="编辑时非必填" required />
</div> </div>
<div class="col-span-full sm:col-span-2"> <div class="w-full">
<label for="confirm_password" class="block mb-2 text-sm font-medium text-gray-900">确认密码</label> <label for="confirm_password" class="block mb-2 text-sm font-medium text-gray-900">确认密码</label>
<input type="password" id="confirm_password" autocomplete="new-password" <input type="password" id="confirm_password" autocomplete="new-password"
v-model="formData.confirmPassword" v-model="formData.confirmPassword"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
required placeholder="编辑时非必填" /> required placeholder="编辑时非必填" />
</div> </div>
<div class="col-span-full sm:col-span-1"> <div class="w-full">
<label for="status" class="block mb-2 text-sm font-medium text-gray-900">状态</label> <label for="status" class="block mb-2 text-sm font-medium text-gray-900">状态</label>
<select id="status" v-model="formData.enable" <select id="status" v-model="formData.enable"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5"> class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5">
@@ -51,7 +51,7 @@
</div> </div>
</div> </div>
<button type="submit" @click.prevent="handleSubmit" <button type="submit" @click.prevent="handleSubmit"
class="w-auto text-sm px-4 py-2 text-white flex items-center bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-center self-start mt-5"> class="mt-5 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">
保存 保存
</button> </button>
</form> </form>

View File

@@ -0,0 +1,67 @@
/**
* 移动端样式hook提供通用的移动端样式类
*/
export function useMobileStyles() {
// 移动端卡片容器样式
const cardContainerClass =
"p-4 bg-white rounded-lg shadow border border-gray-100";
// 卡片头部样式
const cardHeaderClass = "flex justify-between items-start mb-3";
// 卡片标题样式
const cardTitleClass = "font-medium text-gray-900";
// 卡片内容容器样式
const cardContentClass = "text-sm text-gray-600 mb-3 space-y-2";
// 标签名称样式
const labelClass = "text-xs text-gray-500";
// 标签值样式
const valueClass = "text-sm";
// 卡片网格布局
const gridContainerClass = "grid grid-cols-2 gap-2";
// 按钮容器样式
const actionContainerClass = "flex justify-between items-center mt-4";
// 行动按钮组样式
const buttonGroupClass = "flex gap-x-2";
// 主要按钮样式
const primaryButtonClass =
"flex items-center justify-center gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-1.5";
// 危险按钮样式
const dangerButtonClass =
"flex items-center justify-center gap-x-1 bg-red-700 hover:bg-red-800 focus:ring-red-300 text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-xs px-3 py-1.5";
// 次要按钮样式
const secondaryButtonClass =
"flex items-center justify-center gap-x-1 text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-xs px-3 py-1.5";
// 状态指示器样式
const statusIndicatorClass = "flex items-center";
// 状态指示点样式
const statusDotClass = "h-2.5 w-2.5 rounded-full me-2";
return {
cardContainerClass,
cardHeaderClass,
cardTitleClass,
cardContentClass,
labelClass,
valueClass,
gridContainerClass,
actionContainerClass,
buttonGroupClass,
primaryButtonClass,
dangerButtonClass,
secondaryButtonClass,
statusIndicatorClass,
statusDotClass,
};
}

View File

@@ -42,7 +42,7 @@
roleBindModal?.show(); roleBindModal?.show();
} }
}" }"
class="flex items-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs sm:text-sm px-3 py-2 sm:px-4 sm:py-2.5 text-center" class="flex items-center text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center"
type="button"> type="button">
绑定 绑定
</button> </button>
@@ -56,45 +56,68 @@
roleUnbindModal?.show(); roleUnbindModal?.show();
} }
}" }"
class="flex items-center text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-xs sm:text-sm px-3 py-2 sm:px-4 sm:py-2.5 text-center" class="flex items-center text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center"
type="button"> type="button">
解绑 解绑
</button> </button>
</div> </div>
</div> </div>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg"> <!-- 移动端卡片布局 -->
<div class="md:hidden space-y-4">
<div v-for="role in roles" :key="role.id" class="p-4 bg-white rounded-lg shadow">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<input :id="'mobile-checkbox-' + role.id" :value="role.id" type="checkbox" v-model="checkedRoleIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2 mr-3">
<div class="font-medium text-gray-900">{{ role.name }}</div>
</div>
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="role.isBound ? 'bg-green-500' : 'bg-red-500'"></div>
<span class="text-sm">{{ role.isBound === true ? "已绑定" : "未绑定" }}</span>
</div>
</div>
<div class="text-xs text-gray-500">
角色编码: {{ role.code }}
</div>
</div>
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full text-sm text-left rtl:text-right text-gray-500"> <table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50"> <thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr> <tr>
<th scope="col" class="p-2 sm:p-4 w-4"> <th scope="col" class="p-4 w-4">
<div class="flex items-center"> <div class="flex items-center">
<input id="checkbox-all-search" type="checkbox" v-model="allChecked" <input id="checkbox-all-search" type="checkbox" v-model="allChecked"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2"> class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label> <label for="checkbox-all-search" class="sr-only">checkbox</label>
</div> </div>
</th> </th>
<th scope="col" class="px-3 py-2 md:px-4 md:py-3 hidden md:table-cell">角色编码</th> <th scope="col" class="px-4 py-3">角色编码</th>
<th scope="col" class="px-3 py-2 md:px-4 md:py-3">角色名称</th> <th scope="col" class="px-4 py-3">角色名称</th>
<th scope="col" class="px-3 py-2 md:px-4 md:py-3">绑定状态</th> <th scope="col" class="px-4 py-3">绑定状态</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="role in roles" :key="role.id" class="bg-white border-b border-gray-200 hover:bg-gray-50"> <tr v-for="role in roles" :key="role.id" class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-2 sm:p-4"> <td class="w-4 p-4">
<div class="flex items-center"> <div class="flex items-center">
<input :id="'checkbox-table-search-' + role.id" :value="role.id" type="checkbox" v-model="checkedRoleIds" <input :id="'checkbox-table-search-' + role.id" :value="role.id" type="checkbox"
v-model="checkedRoleIds"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2"> class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + role.id" class="sr-only">checkbox</label> <label :for="'checkbox-table-search-' + role.id" class="sr-only">checkbox</label>
</div> </div>
</td> </td>
<td scope="row" class="px-3 py-2 md:px-4 md:py-3 font-medium text-gray-900 whitespace-nowrap hidden md:table-cell"> <td scope="row" class="px-4 py-3 font-medium text-gray-900 whitespace-nowrap">
{{ role.code }} {{ role.code }}
</td> </td>
<td scope="row" class="px-3 py-2 md:px-4 md:py-3 whitespace-nowrap"> <td scope="row" class="px-4 py-3 whitespace-nowrap">
{{ role.name }} {{ role.name }}
</td> </td>
<td class="px-3 py-2 md:px-4 md:py-3 max-w-xs sm:max-w-sm overflow-hidden text-ellipsis"> <td class="px-4 py-3 max-w-xs sm:max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center"> <div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="role.isBound ? 'bg-green-500' : 'bg-red-500'"> <div class="h-2.5 w-2.5 rounded-full me-2" :class="role.isBound ? 'bg-green-500' : 'bg-red-500'">
</div> {{ </div> {{

View File

@@ -25,76 +25,119 @@
</form> </form>
</div> </div>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg"> <!-- 移动端卡片布局 -->
<div class="md:hidden">
<MobileCardList :items="llms as Array<components['schemas']['LlmVm']>">
<template #title="{ item }">
{{ item.name }}
</template>
<template #status="{ item }">
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="item.enable ? 'bg-blue-500' : 'bg-red-500'"></div>
<span class="text-sm">{{ item.enable === true ? "启用" : "禁用" }}</span>
</div>
</template>
<template #content="{ item }">
<div class="grid grid-cols-2 gap-2">
<div>
<p class="text-xs text-gray-500">模型名称</p>
<p>{{ item.modelName }}</p>
</div>
<div>
<p class="text-xs text-gray-500">类型</p>
<p>{{ item.type === 'CHAT' ? '聊天' : '嵌入' }}</p>
</div>
<div>
<p class="text-xs text-gray-500">优先级</p>
<p>{{ item.priority }}</p>
</div>
<div class="col-span-2">
<p class="text-xs text-gray-500">API Key</p>
<p class="truncate">{{ item.apiKey }}</p>
</div>
<div class="col-span-2">
<p class="text-xs text-gray-500">URL</p>
<p class="truncate">{{ item.url }}</p>
</div>
</div>
</template>
<template #actions="{ item }">
<button @click="handleLlmUpdateClick(item)"
class="flex items-center justify-center gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-1.5"
type="button">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
</template>
</MobileCardList>
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full whitespace-nowrap text-sm text-left rtl:text-right text-gray-500"> <table class="w-full whitespace-nowrap text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50"> <thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr> <tr>
<th scope="col" class="p-2 sm:p-4 hidden sm:table-cell"> <th scope="col" class="p-4">
<div class="flex items-center"> <div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox" <input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2"> class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label> <label for="checkbox-all-search" class="sr-only">checkbox</label>
</div> </div>
</th> </th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3">名称</th> <th scope="col" class="px-6 py-3">名称</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3">模型名称</th> <th scope="col" class="px-6 py-3">模型名称</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3 hidden md:table-cell">类型</th> <th scope="col" class="px-6 py-3">类型</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3 hidden lg:table-cell">apiKey</th> <th scope="col" class="px-6 py-3 hidden lg:table-cell">apiKey</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3 hidden lg:table-cell">url</th> <th scope="col" class="px-6 py-3 hidden lg:table-cell">url</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3">状态</th> <th scope="col" class="px-6 py-3">状态</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3 hidden md:table-cell">优先级</th> <th scope="col" class="px-6 py-3">优先级</th>
<th scope="col" class="px-3 py-2 sm:px-4 md:px-6 sm:py-3">操作</th> <th scope="col" class="px-6 py-3">操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="llm in llms" :key="llm.id" class="bg-white border-b border-gray-200 hover:bg-gray-50"> <tr v-for="llm in llms" :key="llm.id" class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-2 sm:p-4 hidden sm:table-cell"> <td class="w-4 p-4">
<div class="flex items-center"> <div class="flex items-center">
<input :id="'checkbox-table-search-' + llm.id" type="checkbox" disabled <input :id="'checkbox-table-search-' + llm.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2"> class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + llm.id" class="sr-only">checkbox</label> <label :for="'checkbox-table-search-' + llm.id" class="sr-only">checkbox</label>
</div> </div>
</td> </td>
<td <td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis font-medium text-gray-900">
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 max-w-[100px] sm:max-w-xs md:max-w-sm overflow-hidden text-ellipsis font-medium text-gray-900"> {{ llm.name }}
{{
`${llm.name}` }}</td>
<td
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 max-w-[120px] sm:max-w-xs md:max-w-sm overflow-hidden text-ellipsis">
{{
`${llm.modelName}` }}
</td> </td>
<td <td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 max-w-[80px] sm:max-w-xs md:max-w-sm overflow-hidden text-ellipsis hidden md:table-cell"> {{ llm.modelName }}
{{
llm.type === 'CHAT' ? '聊天' : '嵌入' }}
</td> </td>
<td <td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 max-w-[150px] sm:max-w-xs md:max-w-sm overflow-hidden text-ellipsis hidden lg:table-cell"> {{ llm.type === 'CHAT' ? '聊天' : '嵌入' }}
{{
llm.apiKey }}
</td> </td>
<td <td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis hidden lg:table-cell">
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 max-w-[150px] sm:max-w-xs md:max-w-sm overflow-hidden text-ellipsis hidden lg:table-cell"> {{ llm.apiKey }}
{{ llm.url }}</td> </td>
<td <td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis hidden lg:table-cell">
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 max-w-[80px] sm:max-w-xs md:max-w-sm overflow-hidden text-ellipsis"> {{ llm.url }}
</td>
<td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
<div class="flex items-center"> <div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="llm.enable ? 'bg-blue-500' : 'bg-red-500'"></div> {{ <div class="h-2.5 w-2.5 rounded-full me-2" :class="llm.enable ? 'bg-blue-500' : 'bg-red-500'"></div> {{
llm.enable === true ? "启用" : "禁用" }} llm.enable === true ? "启用" : "禁用" }}
</div> </div>
</td> </td>
<td <td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
class="px-3 py-2 sm:px-4 md:px-6 sm:py-4 max-w-[80px] sm:max-w-xs md:max-w-sm overflow-hidden text-ellipsis hidden md:table-cell">
{{ llm.priority }} {{ llm.priority }}
</td> </td>
<td class="px-3 py-2 sm:px-4 md:px-6 sm:py-4"> <td class="px-6 py-4">
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
<button @click="handleLlmUpdateClick(llm)" <button @click="handleLlmUpdateClick(llm)"
class="flex items-center justify-center whitespace-nowrap gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5" class="flex items-center justify-center whitespace-nowrap gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5"
type="button"> type="button">
<svg class="w-3 h-3 sm:w-4 sm:h-4" fill="currentColor" viewBox="0 0 20 20" <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path> <path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
@@ -119,14 +162,15 @@
<script setup lang="ts"> <script setup lang="ts">
import Breadcrumbs from "@/components/Breadcrumbs.vue"; import Breadcrumbs from "@/components/Breadcrumbs.vue";
import LlmUpdateModal from "@/components/LlmUpdateModal.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import TablePagination from "@/components/TablePagination.vue"; import TablePagination from "@/components/TablePagination.vue";
import { useLlmQuery } from "@/composables/ai/useLlmQuery";
import { useLlmUpdate } from "@/composables/ai/useLlmUpdate";
import useAlertStore from "@/composables/store/useAlertStore"; import useAlertStore from "@/composables/store/useAlertStore";
import { Modal, type ModalInterface, initFlowbite } from "flowbite"; import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, ref } from "vue"; import { nextTick, onMounted, ref } from "vue";
import type { components } from "../api/types/schema"; import type { components } from "../api/types/schema";
import { useLlmQuery } from "@/composables/ai/useLlmQuery";
import { useLlmUpdate } from "@/composables/ai/useLlmUpdate";
import LlmUpdateModal from "@/components/LlmUpdateModal.vue";
const llmUpdateModal = ref<ModalInterface>(); const llmUpdateModal = ref<ModalInterface>();
const selectedLlm = ref<components["schemas"]["LlmVm"]>(); const selectedLlm = ref<components["schemas"]["LlmVm"]>();

View File

@@ -34,45 +34,85 @@
</template> </template>
</Button> </Button>
</div> </div>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<!-- 移动端卡片布局 -->
<div class="md:hidden">
<MobileCardList :items="permissions as Array<components['schemas']['PermissionRespDto']>">
<template #title="{ item }">
{{ item.name }}
</template>
<template #content="{ item }">
<div>
<p class="text-xs text-gray-500">权限编码</p>
<p>{{ item.code }}</p>
</div>
</template>
<template #actions="{ item }">
<div class="flex gap-x-2">
<button @click="handleUpsertPermissionClick(item)"
class="flex items-center justify-center gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-1.5"
type="button">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
<button
class="flex items-center justify-center gap-x-1 bg-red-700 hover:bg-red-800 focus:outline-none focus:ring-red-300 text-white focus:ring-4 font-medium rounded-lg text-xs px-3 py-1.5"
@click="handleDeletePermissionClick(item)" type="button">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
<span>删除</span>
</button>
</div>
</template>
</MobileCardList>
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full text-sm text-left rtl:text-right text-gray-500"> <table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50"> <thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr> <tr>
<th scope="col" class="p-2 sm:p-4 hidden sm:table-cell"> <th scope="col" class="p-4">
<div class="flex items-center"> <div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox" <input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2"> class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label> <label for="checkbox-all-search" class="sr-only">checkbox</label>
</div> </div>
</th> </th>
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3">权限名称</th> <th scope="col" class="px-6 py-3">权限名称</th>
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 hidden md:table-cell">权限编码</th> <th scope="col" class="px-6 py-3">权限编码</th>
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3">操作</th> <th scope="col" class="px-6 py-3">操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="permission in permissions" :key="permission.id" <tr v-for="permission in permissions" :key="permission.id"
class="bg-white border-b border-gray-200 hover:bg-gray-50"> class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-2 sm:p-4 hidden sm:table-cell"> <td class="w-4 p-4">
<div class="flex items-center"> <div class="flex items-center">
<input :id="'checkbox-table-search-' + permission.id" type="checkbox" disabled <input :id="'checkbox-table-search-' + permission.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2"> class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label :for="'checkbox-table-search-' + permission.id" class="sr-only">checkbox</label> <label :for="'checkbox-table-search-' + permission.id" class="sr-only">checkbox</label>
</div> </div>
</td> </td>
<td scope="row" class="px-3 py-2 sm:px-6 sm:py-4 font-medium text-gray-900 whitespace-nowrap"> <td scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
{{ permission.name }} {{ permission.name }}
</td> </td>
<td <td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
class="px-3 py-2 sm:px-6 sm:py-4 max-w-xs sm:max-w-sm overflow-hidden text-ellipsis hidden md:table-cell">
{{ permission.code }}</td> {{ permission.code }}</td>
<td class="px-3 py-2 sm:px-6 sm:py-4 max-w-xs sm:max-w-sm overflow-hidden text-ellipsis"> <td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-y-2 sm:gap-y-0 sm:gap-x-2"> <div class="flex items-center gap-x-2">
<button @click="handleUpsertPermissionClick(permission)" <button @click="handleUpsertPermissionClick(permission)"
class="flex items-center justify-center whitespace-nowrap gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5" class="flex items-center justify-center whitespace-nowrap gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5"
type="button"> type="button">
<svg class="w-3 h-3 sm:w-4 sm:h-4" fill="currentColor" viewBox="0 0 20 20" <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path> <path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
@@ -81,10 +121,9 @@
<span>编辑</span> <span>编辑</span>
</button> </button>
<button <button
class="flex items-center justify-center whitespace-nowrap gap-x-1 bg-red-700 hover:bg-red-800 focus:outline-none focus:ring-red-300 text-white focus:ring-4 font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5" class="flex items-center justify-center whitespace-nowrap gap-x-1 bg-red-700 hover:bg-red-800 focus:outline-none focus:ring-red-300 text-white focus:ring-4 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleDeletePermissionClick(permission)" type="button"> @click="handleDeletePermissionClick(permission)" type="button">
<svg class="w-3 h-3 sm:w-4 sm:h-4" fill="currentColor" viewBox="0 0 20 20" <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path> clip-rule="evenodd"></path>
@@ -118,6 +157,7 @@ import usePermissionDelete from "@/composables/permission/usePermissionDelete";
import type { components } from "@/api/types/schema"; import type { components } from "@/api/types/schema";
import Breadcrumbs from "@/components/Breadcrumbs.vue"; import Breadcrumbs from "@/components/Breadcrumbs.vue";
import MobileCardList from "@/components/MobileCardList.vue";
import TablePagination from "@/components/TablePagination.vue"; import TablePagination from "@/components/TablePagination.vue";
import { Modal, type ModalInterface, initFlowbite } from "flowbite"; import { Modal, type ModalInterface, initFlowbite } from "flowbite";
import { nextTick, onMounted, ref } from "vue"; import { nextTick, onMounted, ref } from "vue";

View File

@@ -34,38 +34,100 @@
</template> </template>
</Button> </Button>
</div> </div>
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
<!-- 移动端卡片布局 -->
<div class="md:hidden space-y-4">
<div v-for="user in users" :key="user.id" class="p-4 bg-white rounded-lg shadow">
<div class="flex justify-between items-start mb-3">
<div class="font-medium text-gray-900">{{ user.username }}</div>
<div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="user.enable ? 'bg-blue-500' : 'bg-red-500'"></div>
<span class="text-sm">{{ user.enable === true ? "启用" : "禁用" }}</span>
</div>
</div>
<div class="text-xs text-gray-500 mb-3">
创建时间: {{ dayjs(user.createTime).format("llll") }}
</div>
<div class="space-y-2">
<div class="flex flex-wrap gap-2">
<button
class="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-xs px-3 py-1.5"
@click="handleBindRoleClick(user)" type="button">
分配角色
</button>
<button
class="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-xs px-3 py-1.5"
@click="handleBindPositionClick(user)" type="button">
分配岗位
</button>
<button
class="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-xs px-3 py-1.5"
@click="handleBindDepartmentClick(user)" type="button">
分配部门
</button>
</div>
</div>
<div class="flex justify-between mt-4">
<button @click="handleUpsertUserClick(user)"
class="flex items-center justify-center gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-1.5"
type="button">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
clip-rule="evenodd"></path>
</svg>
<span>编辑</span>
</button>
<button
class="flex items-center justify-center gap-x-1 bg-red-700 hover:bg-red-800 focus:ring-red-300 text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-xs px-3 py-1.5"
@click="handleDeleteUserClick(user)" type="button">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path>
</svg>
<span>删除</span>
</button>
</div>
</div>
</div>
<!-- PC端表格布局 -->
<div class="relative overflow-x-auto shadow-md sm:rounded-lg hidden md:block">
<table class="w-full text-sm text-left rtl:text-right text-gray-500"> <table class="w-full text-sm text-left rtl:text-right text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50"> <thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr> <tr>
<th scope="col" class="p-2 sm:p-4"> <th scope="col" class="p-4">
<div class="flex items-center"> <div class="flex items-center">
<input id="checkbox-all-search" disabled type="checkbox" <input id="checkbox-all-search" disabled type="checkbox"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2"> class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
<label for="checkbox-all-search" class="sr-only">checkbox</label> <label for="checkbox-all-search" class="sr-only">checkbox</label>
</div> </div>
</th> </th>
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 cursor-pointer" @click="handleSortClick('username')"> <th scope="col" class="px-6 py-3 cursor-pointer" @click="handleSortClick('username')">
<div class="flex items-center"> <div class="flex items-center">
<span>用户名</span> <span>用户名</span>
<SortIcon :sortField="getSortField('username')" /> <SortIcon :sortField="getSortField('username')" />
</div> </div>
</th> </th>
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 cursor-pointer hidden md:table-cell" <th scope="col" class="px-6 py-3 cursor-pointer" @click="handleSortClick('createTime')">
@click="handleSortClick('createTime')">
<div class="flex items-center"> <div class="flex items-center">
<span>创建时间</span> <span>创建时间</span>
<SortIcon :sortField="getSortField('createTime')" /> <SortIcon :sortField="getSortField('createTime')" />
</div> </div>
</th> </th>
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3 hidden sm:table-cell">状态</th> <th scope="col" class="px-6 py-3">状态</th>
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3">分配</th> <th scope="col" class="px-6 py-3">分配</th>
<th scope="col" class="px-3 py-2 sm:px-6 sm:py-3">操作</th> <th scope="col" class="px-6 py-3">操作</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="user in users" :key="user.id" class="bg-white border-b border-gray-200 hover:bg-gray-50"> <tr v-for="user in users" :key="user.id" class="bg-white border-b border-gray-200 hover:bg-gray-50">
<td class="w-4 p-2 sm:p-4"> <td class="w-4 p-4">
<div class="flex items-center"> <div class="flex items-center">
<input :id="'checkbox-table-search-' + user.id" type="checkbox" disabled <input :id="'checkbox-table-search-' + user.id" type="checkbox" disabled
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2"> class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
@@ -73,47 +135,44 @@
</div> </div>
</td> </td>
<td scope="row" <td scope="row"
class="px-3 py-2 sm:px-6 sm:py-4 font-medium text-gray-900 whitespace-nowrap max-w-xs sm:max-w-sm overflow-hidden text-ellipsis"> class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap max-w-sm overflow-hidden text-ellipsis">
{{ user.username }} {{ user.username }}
</td> </td>
<td <td class="px-6 py-4 max-w-sm whitespace-nowrap overflow-hidden text-ellipsis">
class="px-3 py-2 sm:px-6 sm:py-4 max-w-xs sm:max-w-sm whitespace-nowrap overflow-hidden text-ellipsis hidden md:table-cell">
{{ dayjs(user.createTime).format("llll") }} {{ dayjs(user.createTime).format("llll") }}
</td> </td>
<td <td class="px-6 py-4 max-w-sm whitespace-nowrap overflow-hidden text-ellipsis">
class="px-3 py-2 sm:px-6 sm:py-4 max-w-xs sm:max-w-sm whitespace-nowrap overflow-hidden text-ellipsis hidden sm:table-cell">
<div class="flex items-center"> <div class="flex items-center">
<div class="h-2.5 w-2.5 rounded-full me-2" :class="user.enable ? 'bg-blue-500' : 'bg-red-500'"></div> {{ <div class="h-2.5 w-2.5 rounded-full me-2" :class="user.enable ? 'bg-blue-500' : 'bg-red-500'"></div> {{
user.enable === true ? "启用" : "禁用" }} user.enable === true ? "启用" : "禁用" }}
</div> </div>
</td> </td>
<td class="px-3 py-2 sm:px-6 sm:py-4 max-w-xs sm:max-w-sm overflow-hidden text-ellipsis"> <td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-y-2 sm:gap-y-0 sm:gap-x-2"> <div class="flex items-center gap-x-2">
<button <button
class="text-gray-900 bg-white border whitespace-nowrap border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5" class="text-gray-900 bg-white border whitespace-nowrap border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleBindRoleClick(user)" type="button"> @click="handleBindRoleClick(user)" type="button">
分配角色 分配角色
</button> </button>
<button <button
class="text-gray-900 bg-white border whitespace-nowrap border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5" class="text-gray-900 bg-white border whitespace-nowrap border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleBindPositionClick(user)" type="button"> @click="handleBindPositionClick(user)" type="button">
分配岗位 分配岗位
</button> </button>
<button <button
class="text-gray-900 bg-white border whitespace-nowrap border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5" class="text-gray-900 bg-white border whitespace-nowrap border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-4 py-2.5"
@click="handleBindDepartmentClick(user)" type="button"> @click="handleBindDepartmentClick(user)" type="button">
分配部门 分配部门
</button> </button>
</div> </div>
</td> </td>
<td class="px-3 py-2 sm:px-6 sm:py-4 max-w-xs sm:max-w-sm overflow-hidden text-ellipsis"> <td class="px-6 py-4 max-w-sm overflow-hidden text-ellipsis">
<!-- Edit Modal toggle --> <!-- Edit Modal toggle -->
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-y-2 sm:gap-y-0 sm:gap-x-2"> <div class="flex items-center gap-x-2">
<button @click="handleUpsertUserClick(user)" <button @click="handleUpsertUserClick(user)"
class="flex items-center justify-center whitespace-nowrap gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5" class="flex items-center justify-center whitespace-nowrap gap-x-1 text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5"
type="button"> type="button">
<svg class="w-3 h-3 sm:w-4 sm:h-4" fill="currentColor" viewBox="0 0 20 20" <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
xmlns="http://www.w3.org/2000/svg">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path> <path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"></path>
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"
@@ -122,12 +181,9 @@
<span>编辑</span> <span>编辑</span>
</button> </button>
<button <button
class="flex items-center justify-center whitespace-nowrap gap-x-1 class="flex items-center justify-center whitespace-nowrap gap-x-1 bg-red-700 hover:bg-red-800 focus:ring-red-300 text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-4 py-2.5"
bg-red-700 hover:bg-red-800 focus:ring-red-300
text-white focus:ring-4 focus:outline-none font-medium rounded-lg text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2.5"
@click="handleDeleteUserClick(user)" type="button"> @click="handleDeleteUserClick(user)" type="button">
<svg class="w-3 h-3 sm:w-4 sm:h-4" fill="currentColor" viewBox="0 0 20 20" <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clip-rule="evenodd"></path> clip-rule="evenodd"></path>