0327 添加数据权限

main
kangwenjing 2026-03-27 12:22:00 +08:00
parent f0631c38b3
commit 2b0e477e39
73 changed files with 1328 additions and 190 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -4,7 +4,12 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="4c558d98-824e-4a48-ba48-bd2e6172f9f4" name="更改" comment="修改定位信息 0323" />
<list default="true" id="4c558d98-824e-4a48-ba48-bd2e6172f9f4" name="更改" comment="修改定位信息 0323">
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/mapper/ExpansionMapper.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/mapper/ExpansionMapper.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/java/com/unis/crm/mapper/OpportunityMapper.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/resources/mapper/expansion/ExpansionMapper.xml" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/resources/mapper/expansion/ExpansionMapper.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml" beforeDir="false" afterPath="$PROJECT_DIR$/backend/src/main/resources/mapper/opportunity/OpportunityMapper.xml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@ -24,29 +29,29 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RequestMappingsPanelOrder0": "0",
"RequestMappingsPanelOrder1": "1",
"RequestMappingsPanelWidth0": "75",
"RequestMappingsPanelWidth1": "75",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"WebServerToolWindowFactoryState": "false",
"git-widget-placeholder": "main",
"last_opened_file_path": "/Users/kangwenjing/Downloads/crm/unis_crm",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"project.structure.last.edited": "模块",
"project.structure.proportion": "0.0",
"project.structure.side.proportion": "0.0",
"ts.external.directory.path": "/Users/kangwenjing/Downloads/crm/unis_crm/frontend/node_modules/typescript/lib",
"vue.rearranger.settings.migration": "true"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;RequestMappingsPanelOrder0&quot;: &quot;0&quot;,
&quot;RequestMappingsPanelOrder1&quot;: &quot;1&quot;,
&quot;RequestMappingsPanelWidth0&quot;: &quot;75&quot;,
&quot;RequestMappingsPanelWidth1&quot;: &quot;75&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;WebServerToolWindowFactoryState&quot;: &quot;false&quot;,
&quot;git-widget-placeholder&quot;: &quot;main&quot;,
&quot;last_opened_file_path&quot;: &quot;/Users/kangwenjing/Downloads/crm/unis_crm&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;project.structure.last.edited&quot;: &quot;模块&quot;,
&quot;project.structure.proportion&quot;: &quot;0.0&quot;,
&quot;project.structure.side.proportion&quot;: &quot;0.0&quot;,
&quot;ts.external.directory.path&quot;: &quot;/Users/kangwenjing/Downloads/crm/unis_crm/frontend/node_modules/typescript/lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}]]></component>
}</component>
<component name="RunManager" selected="Spring Boot.UnisCrmBackendApplication">
<configuration name="unis-crm-backend中的所有" type="JUnit" factoryName="JUnit" temporary="true" nameIsGenerated="true">
<module name="unis-crm-backend" />
@ -81,6 +86,7 @@
<workItem from="1774235389841" duration="216000" />
<workItem from="1774244004381" duration="10126000" />
<workItem from="1774574410059" duration="200000" />
<workItem from="1774576185724" duration="1651000" />
</task>
<task id="LOCAL-00001" summary="修改定位信息 0323">
<option name="closed" value="true" />

View File

@ -19,6 +19,12 @@ public class CrmGlobalExceptionHandler {
return ApiResponse.fail(ex.getMessage());
}
@ExceptionHandler(com.unisbase.common.exception.BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Object> handleFrameworkBusinessException(com.unisbase.common.exception.BusinessException ex) {
return ApiResponse.fail(ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Object> handleValidationException(MethodArgumentNotValidException ex) {
@ -29,6 +35,12 @@ public class CrmGlobalExceptionHandler {
return ApiResponse.fail(message);
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Object> handleIllegalArgumentException(IllegalArgumentException ex) {
return ApiResponse.fail(ex.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, Object> handleUnexpectedException(Exception ex, HttpServletRequest request) {

View File

@ -16,6 +16,7 @@ import com.unis.crm.dto.expansion.UpdateSalesExpansionRequest;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.unisbase.annotation.DataScope;
@Mapper
public interface ExpansionMapper {
@ -24,18 +25,25 @@ public interface ExpansionMapper {
String selectNextChannelCode();
@DataScope(tableAlias = "s", ownerColumn = "owner_user_id")
List<SalesExpansionItemDTO> selectSalesExpansions(@Param("userId") Long userId, @Param("keyword") String keyword);
@DataScope(tableAlias = "c", ownerColumn = "owner_user_id")
List<ChannelExpansionItemDTO> selectChannelExpansions(@Param("userId") Long userId, @Param("keyword") String keyword);
@DataScope(tableAlias = "s", ownerColumn = "owner_user_id")
List<ExpansionFollowUpDTO> selectSalesFollowUps(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
@DataScope(tableAlias = "o", ownerColumn = "owner_user_id")
List<RelatedProjectSummaryDTO> selectSalesRelatedProjects(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
@DataScope(tableAlias = "c", ownerColumn = "owner_user_id")
List<ExpansionFollowUpDTO> selectChannelFollowUps(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
@DataScope(tableAlias = "c", ownerColumn = "owner_user_id")
List<ChannelExpansionContactDTO> selectChannelContacts(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
@DataScope(tableAlias = "o", ownerColumn = "owner_user_id")
List<ChannelRelatedProjectSummaryDTO> selectChannelRelatedProjects(@Param("userId") Long userId, @Param("bizIds") List<Long> bizIds);
int insertSalesExpansion(@Param("userId") Long userId, @Param("request") CreateSalesExpansionRequest request);

View File

@ -8,6 +8,7 @@ import com.unis.crm.dto.opportunity.OpportunityItemDTO;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.unisbase.annotation.DataScope;
@Mapper
public interface OpportunityMapper {
@ -18,11 +19,13 @@ public interface OpportunityMapper {
@Param("typeCode") String typeCode,
@Param("itemValue") String itemValue);
@DataScope(tableAlias = "o", ownerColumn = "owner_user_id")
List<OpportunityItemDTO> selectOpportunities(
@Param("userId") Long userId,
@Param("keyword") String keyword,
@Param("stage") String stage);
@DataScope(tableAlias = "o", ownerColumn = "owner_user_id")
List<OpportunityFollowUpDTO> selectOpportunityFollowUps(
@Param("userId") Long userId,
@Param("opportunityIds") List<Long> opportunityIds);

View File

@ -74,7 +74,7 @@
and industry_dict.item_value = s.industry
and industry_dict.status = 1
and coalesce(industry_dict.is_deleted, 0) = 0
where s.owner_user_id = #{userId}
where 1 = 1
<if test="keyword != null and keyword != ''">
and (
coalesce(s.employee_no, '') ilike concat('%', #{keyword}, '%')
@ -151,7 +151,7 @@
and internal_attribute_dict.item_value = c.internal_attribute
and internal_attribute_dict.status = 1
and coalesce(internal_attribute_dict.is_deleted, 0) = 0
where c.owner_user_id = #{userId}
where 1 = 1
<if test="keyword != null and keyword != ''">
and (
c.channel_name ilike concat('%', #{keyword}, '%')
@ -189,8 +189,7 @@
from crm_expansion_followup f
join crm_sales_expansion s on s.id = f.biz_id and f.biz_type = 'sales'
left join sys_user u on u.user_id = f.followup_user_id
where s.owner_user_id = #{userId}
and f.biz_id in
where f.biz_id in
<foreach collection="bizIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
@ -205,8 +204,7 @@
o.opportunity_name as opportunityName,
o.amount
from crm_opportunity o
where o.owner_user_id = #{userId}
and o.sales_expansion_id in
where o.sales_expansion_id in
<foreach collection="bizIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
@ -229,8 +227,7 @@
from crm_expansion_followup f
join crm_channel_expansion c on c.id = f.biz_id and f.biz_type = 'channel'
left join sys_user u on u.user_id = f.followup_user_id
where c.owner_user_id = #{userId}
and f.biz_id in
where f.biz_id in
<foreach collection="bizIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
@ -246,8 +243,7 @@
coalesce(cc.contact_title, '无') as title
from crm_channel_expansion_contact cc
join crm_channel_expansion c on c.id = cc.channel_expansion_id
where c.owner_user_id = #{userId}
and cc.channel_expansion_id in
where cc.channel_expansion_id in
<foreach collection="bizIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
@ -262,8 +258,7 @@
o.opportunity_name as opportunityName,
o.amount
from crm_opportunity o
where o.owner_user_id = #{userId}
and o.channel_expansion_id in
where o.channel_expansion_id in
<foreach collection="bizIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>

View File

@ -118,7 +118,7 @@
and operator_dict.item_value = o.operator_name
and operator_dict.status = 1
and coalesce(operator_dict.is_deleted, 0) = 0
where o.owner_user_id = #{userId}
where 1 = 1
<if test="keyword != null and keyword != ''">
and (
o.opportunity_name ilike concat('%', #{keyword}, '%')
@ -148,8 +148,7 @@
from crm_opportunity_followup f
join crm_opportunity o on o.id = f.opportunity_id
left join sys_user u on u.user_id = f.followup_user_id
where o.owner_user_id = #{userId}
and f.opportunity_id in
where f.opportunity_id in
<foreach collection="opportunityIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>

View File

@ -9,14 +9,20 @@
u.user_id as userId,
u.display_name as realName,
case
when u.created_at is null then 0
else greatest((current_date - u.created_at::date)::bigint, 0)
when coalesce(org_info.joined_date, u.created_at::date) is null then 0
else greatest((current_date - coalesce(org_info.joined_date, u.created_at::date))::bigint + 1, 0)
end as onboardingDays,
case
when u.status = 1 then '正常'
else '停用'
end as accountStatus
from sys_user u
left join lateral (
select min(tu.created_at)::date as joined_date
from sys_tenant_user tu
where tu.user_id = u.user_id
and tu.is_deleted = 0
) org_info on true
where u.user_id = #{userId}
and u.is_deleted = 0
limit 1

View File

@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { BarChart3, Building2, Check, TrendingUp, Users } from "lucide-react";
import { motion } from "motion/react";
import { useNavigate } from "react-router-dom";
import { completeDashboardTodo, getDashboardHome, type DashboardActivity, type DashboardHome, type DashboardStat, type DashboardTodo } from "@/lib/auth";
const DASHBOARD_PREVIEW_COUNT = 5;
@ -13,7 +14,15 @@ const baseStats = [
{ name: "本月打卡次数", metricKey: "monthlyCheckins", icon: BarChart3, color: "text-amber-600 dark:text-amber-400", bg: "bg-amber-100 dark:bg-amber-500/20" },
] as const;
const statRoutes: Record<(typeof baseStats)[number]["metricKey"], { pathname: string; state?: { tab: "sales" | "channel" } }> = {
monthlyOpportunities: { pathname: "/opportunities" },
pushedOmsProjects: { pathname: "/opportunities" },
monthlyChannels: { pathname: "/expansion", state: { tab: "channel" } },
monthlyCheckins: { pathname: "/work/checkin" },
};
export default function Dashboard() {
const navigate = useNavigate();
const [home, setHome] = useState<DashboardHome>({});
const [loading, setLoading] = useState(true);
const [showAllActivities, setShowAllActivities] = useState(false);
@ -97,6 +106,14 @@ export default function Dashboard() {
}
};
const handleStatCardClick = (metricKey: (typeof baseStats)[number]["metricKey"]) => {
const target = statRoutes[metricKey];
if (!target) {
return;
}
navigate(target.pathname, target.state ? { state: target.state } : undefined);
};
return (
<div className="crm-page-stack">
<header className="space-y-1 sm:space-y-2">
@ -124,12 +141,16 @@ export default function Dashboard() {
>
<div className="grid grid-cols-2 gap-x-3 gap-y-4 sm:gap-4 xl:grid-cols-4">
{stats.map((stat, i) => (
<motion.div
<motion.button
key={stat.name}
type="button"
onClick={() => handleStatCardClick(stat.metricKey)}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.1 }}
className="crm-card min-h-[88px] rounded-xl p-3 transition-all hover:shadow-md dark:hover:bg-slate-900 sm:min-h-[104px] sm:rounded-2xl sm:p-5"
className="crm-card min-h-[88px] rounded-xl p-3 text-left transition-all hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 focus-visible:ring-offset-2 dark:hover:bg-slate-900 sm:min-h-[104px] sm:rounded-2xl sm:p-5"
whileTap={{ scale: 0.98 }}
aria-label={`查看${stat.name}`}
>
<div className="flex items-center gap-2.5 sm:gap-4">
<div className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${stat.bg} sm:h-12 sm:w-12 sm:rounded-xl`}>
@ -140,7 +161,7 @@ export default function Dashboard() {
<p className="mt-1 text-lg font-bold leading-none text-slate-900 dark:text-white sm:text-2xl">{stat.value ?? 0}</p>
</div>
</div>
</motion.div>
</motion.button>
))}
</div>

View File

@ -1,6 +1,7 @@
import { useEffect, useState, type ReactNode } from "react";
import { Search, Plus, MapPin, Building2, User, Phone, X, Clock, FileText, Calendar, ChevronRight } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { useLocation } from "react-router-dom";
import {
createChannelExpansion,
createSalesExpansion,
@ -129,6 +130,7 @@ function DetailItem({
}
export default function Expansion() {
const location = useLocation();
const [activeTab, setActiveTab] = useState<ExpansionTab>("sales");
const [selectedItem, setSelectedItem] = useState<ExpansionItem | null>(null);
const [keyword, setKeyword] = useState("");
@ -155,6 +157,13 @@ export default function Expansion() {
const [editChannelForm, setEditChannelForm] = useState<CreateChannelExpansionPayload>(defaultChannelForm);
const hasForegroundModal = createOpen || editOpen;
useEffect(() => {
const requestedTab = (location.state as { tab?: ExpansionTab } | null)?.tab;
if (requestedTab === "sales" || requestedTab === "channel") {
setActiveTab(requestedTab);
}
}, [location.state]);
useEffect(() => {
let cancelled = false;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
import{d2 as c,bI as o}from"./index-CYM97J2V.js";const r=new o("antFadeIn",{"0%":{opacity:0},"100%":{opacity:1}}),s=new o("antFadeOut",{"0%":{opacity:1},"100%":{opacity:0}}),p=(t,a=!1)=>{const{antCls:e}=t,n=`${e}-fade`,i=a?"&":"";return[c(n,r,s,t.motionDurationMid,a),{[`
${i}${n}-enter,
${i}${n}-appear
`]:{opacity:0,animationTimingFunction:"linear"},[`${i}${n}-leave`]:{animationTimingFunction:"linear"}}]};export{p as i};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.dictionaries-page{padding:24px;height:100%;display:flex;flex-direction:column}.dictionaries-header{margin-bottom:24px}.dictionaries-title{margin-bottom:4px!important}.dictionaries-content{flex:1;min-height:0}.full-height{height:100%}.full-height-card{height:100%;display:flex;flex-direction:column}.full-height-card .ant-card-body{flex:1;overflow:hidden;padding:0}.scroll-container{height:100%;overflow-y:auto;padding:12px}.dict-type-row{transition:all .3s}.dict-type-row:hover{background-color:#f5f5f5}.dict-type-row-selected{background-color:#e6f7ff!important;border-right:3px solid #1890ff}.dict-type-item{display:flex;justify-content:space-between;align-items:center;padding:4px 0}.dict-type-name{font-weight:600;color:#262626}.dict-type-code{font-size:12px;color:#8c8c8c}.dict-type-actions{display:flex;gap:4px;opacity:.6;transition:opacity .3s}.dict-type-row:hover .dict-type-actions{opacity:1}.tabular-nums{font-variant-numeric:tabular-nums}.flex-center{display:flex;align-items:center;justify-content:center}.h-full{height:100%}

View File

@ -0,0 +1 @@
.devices-page{padding:24px}.devices-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:24px}.devices-title{margin-bottom:4px!important}.devices-table-card{border-radius:8px}.devices-table-toolbar{margin-bottom:20px}.devices-search-input{max-width:400px}.device-icon-placeholder{width:40px;height:40px;background-color:#f0f5ff;border-radius:8px;display:flex;align-items:center;justify-content:center;color:#1890ff;font-size:20px}.device-name{font-weight:600;color:#262626}.device-code{font-size:12px;color:#8c8c8c}.device-drawer-title{display:flex;align-items:center;font-size:16px;font-weight:600}.tabular-nums{font-variant-numeric:tabular-nums}

View File

@ -0,0 +1 @@
import{j as e,T as d}from"./index-CYM97J2V.js";const{Title:i,Text:n}=d,c=({title:r,subtitle:s,extra:a,className:l=""})=>e.jsxs("div",{className:`page-header flex justify-between items-end mb-6 ${l}`,children:[e.jsxs("div",{children:[e.jsx(i,{level:4,className:"mb-1",style:{margin:0},children:r}),s&&e.jsx(n,{type:"secondary",style:{display:"block"},children:s})]}),a&&e.jsx("div",{className:"page-header-extra",children:a})]});export{c as P};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.roles-page-v2{background:transparent;display:flex;flex-direction:column}.roles-layout{flex:1;min-height:0;display:flex;padding-bottom:24px}.roles-layout__row{width:100%;margin:0!important;height:100%}.roles-layout__side,.roles-layout__detail{height:100%;display:flex;flex-direction:column}.roles-side-card,.roles-detail-card{flex:1;min-height:0;border-radius:18px!important;display:flex;flex-direction:column;box-shadow:0 4px 20px #0000000a!important;border:1px solid rgba(226,232,240,.8)!important}.roles-side-card .ant-card-body,.roles-detail-card .ant-card-body{flex:1;min-height:0;display:flex;flex-direction:column;padding:16px!important;overflow:hidden}.role-search-panel{flex-shrink:0;display:flex;flex-direction:column;gap:12px;margin-bottom:16px}.role-search-bar{display:flex;gap:10px}.role-search-bar .ant-input-affix-wrapper{flex:1;border-radius:8px}.role-search-bar .ant-btn{border-radius:8px}.role-list-container-v3{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding-right:8px;margin-right:-8px}.role-list-container-v3::-webkit-scrollbar{width:6px}.role-list-container-v3::-webkit-scrollbar-track{background:transparent}.role-list-container-v3::-webkit-scrollbar-thumb{background:#e2e8f0;border-radius:10px}.role-list-container-v3::-webkit-scrollbar-thumb:hover{background:#cbd5e1}.role-item-card-v3{display:flex;align-items:center;gap:12px;padding:14px 16px;margin-bottom:10px;border:1px solid #f1f5f9;border-radius:12px;background:#fff;transition:all .2s ease;cursor:pointer;position:relative;overflow:hidden}.role-item-card-v3:hover{border-color:#cbd5e1;background:#f8fafc;transform:translateY(-1px);box-shadow:0 4px 12px #00000008}.role-item-card-v3.active{border-color:#bfdbfe;background:#eff6ff}.role-item-card-v3.active:before{content:"";position:absolute;left:0;top:0;bottom:0;width:4px;background:#3b82f6}.role-item-card-v3.active .role-name{color:#1e3a8a;font-weight:600}.role-item-card-v3.active .role-item-symbol{background:#3b82f6;color:#fff}.role-item-symbol{width:40px;height:40px;border-radius:10px;background:#f1f5f9;color:#64748b;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;transition:all .2s ease}.role-item-main{flex:1;min-width:0}.role-item-name-row{display:flex;align-items:center;gap:8px;margin-bottom:4px}.role-name{font-size:14px;font-weight:500;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.role-code{font-size:12px;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.role-item-actions{display:flex;gap:4px;opacity:0;transition:opacity .2s ease}.role-item-actions .ant-btn{color:#64748b}.role-item-actions .ant-btn:hover{color:#3b82f6;background:#e2e8f0}.role-item-actions .ant-btn.ant-btn-dangerous:hover{color:#ef4444;background:#fee2e2}.role-item-card-v3:hover .role-item-actions,.role-item-card-v3.active .role-item-actions{opacity:1}.role-list-pagination{flex-shrink:0;padding-top:16px;margin-top:auto;border-top:1px solid #f1f5f9;display:flex;justify-content:flex-end}.role-detail-header{display:flex;align-items:center;gap:16px}.role-detail-icon{width:48px;height:48px;border-radius:12px;background:#eff6ff;color:#3b82f6;display:flex;align-items:center;justify-content:center;font-size:24px}.role-detail-heading{display:flex;flex-direction:column;min-width:0}.role-detail-title{font-size:18px;font-weight:600;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.role-detail-code{font-size:13px;color:#64748b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.role-detail-tabs{flex:1;display:flex;flex-direction:column;min-height:0}.role-detail-tabs .ant-tabs-nav{margin-bottom:16px!important}.role-detail-tabs .ant-tabs-nav:before{border-bottom:1px solid #f1f5f9}.role-detail-tabs .ant-tabs-content-holder{flex:1;min-height:0;overflow-y:auto}.role-detail-tabs .ant-tabs-content-holder::-webkit-scrollbar{width:6px}.role-detail-tabs .ant-tabs-content-holder::-webkit-scrollbar-track{background:transparent}.role-detail-tabs .ant-tabs-content-holder::-webkit-scrollbar-thumb{background:#e2e8f0;border-radius:10px}.role-detail-pane{padding:4px}.permission-tree-wrapper{padding:16px;background:#f8fafc;border-radius:12px;border:1px solid #e2e8f0}.role-members-toolbar{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}.role-members-toolbar h5.ant-typography{color:#334155;font-weight:600}.roles-count-badge{background:#f1f5f9!important;color:#64748b!important;border:1px solid #e2e8f0}.app-page__empty-state{height:100%;display:flex;align-items:center;justify-content:center;background:#ffffffc7;border-radius:16px;border:1px dashed rgba(148,163,184,.5);margin:0}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.sys-params-page{min-height:100%}.sys-params-header{display:flex;justify-content:space-between;align-items:flex-end}.sys-params-table-card{border-radius:8px}.sys-params-table-toolbar{display:flex;justify-content:space-between;align-items:center}.sys-params-search-input{border-radius:6px}.param-key-text{font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-weight:500;color:#1890ff}.param-type-tag{border-radius:4px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.platform-settings-page{max-width:1280px;height:100%;min-height:0;overflow:hidden}.platform-settings-scroll{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:0 4px 32px 0}.platform-settings-form{width:100%}.platform-settings-page .app-page__content-card{overflow:visible}.platform-settings-page .ant-row{row-gap:24px}@media (max-height: 1080px){.platform-settings-scroll{padding-bottom:48px}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.permissions-page{display:flex;flex-direction:column;height:100%}.permissions-content-card{flex:1;min-height:0;display:flex;flex-direction:column;border-radius:18px!important;border:1px solid rgba(226,232,240,.8)!important;box-shadow:0 4px 20px #0000000a!important;overflow:hidden}.permissions-content-card .ant-card-body{flex:1;min-height:0;display:flex;flex-direction:column;padding:0!important;overflow:hidden}.permissions-table-full{flex:1;min-height:0;display:flex;flex-direction:column;width:100%}.permissions-table-full .ant-spin-nested-loading,.permissions-table-full .ant-spin-container,.permissions-table-full .ant-table,.permissions-table-full .ant-table-container{flex:1;min-height:0;display:flex;flex-direction:column}.permissions-table-full .ant-table-body{flex:1;min-height:0;max-height:none!important;overflow-y:auto!important}.permissions-table-full .ant-table-body::-webkit-scrollbar{width:6px;height:6px}.permissions-table-full .ant-table-body::-webkit-scrollbar-track{background:transparent}.permissions-table-full .ant-table-body::-webkit-scrollbar-thumb{background:#e2e8f0;border-radius:10px}.permissions-table-full .ant-table-body::-webkit-scrollbar-thumb:hover{background:#cbd5e1}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.list-table-container{width:100%}.list-table-container .row-selected{background-color:var(--item-hover-bg)}.list-table-container .row-selected:hover>td{background-color:var(--item-hover-bg)!important}.table-selection-info{display:inline-flex;align-items:center;gap:8px}.selection-count{color:var(--text-color-secondary);font-size:14px}.count-highlight{color:var(--link-color);font-weight:600}.selection-action{color:var(--link-color);font-size:14px;cursor:pointer;text-decoration:none;transition:color .3s;margin-left:4px}.selection-action:hover{color:var(--link-color);opacity:.8}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{x as F,r as s,H as $,ay as w,b$ as q,c0 as D,d7 as E,B as J,cQ as M,d8 as Q,C as S,A as U,av as Y,d9 as G,aJ as K}from"./index-CYM97J2V.js";import{A as Z}from"./useDict-BjF_A4fe.js";const ee=e=>{const{componentCls:n,iconCls:l,antCls:t,zIndexPopup:o,colorText:f,colorWarning:u,marginXXS:c,marginXS:i,fontSize:g,fontWeightStrong:v,colorTextHeading:y}=e;return{[n]:{zIndex:o,[`&${t}-popover`]:{fontSize:g},[`${n}-message`]:{marginBottom:i,display:"flex",flexWrap:"nowrap",alignItems:"start",[`> ${n}-message-icon ${l}`]:{color:u,fontSize:g,lineHeight:1,marginInlineEnd:i},[`${n}-title`]:{fontWeight:v,color:y,"&:only-child":{fontWeight:"normal"}},[`${n}-description`]:{marginTop:c,color:f}},[`${n}-buttons`]:{textAlign:"end",whiteSpace:"nowrap",button:{marginInlineStart:i}}}}},te=e=>{const{zIndexPopupBase:n}=e;return{zIndexPopup:n+60}},I=F("Popconfirm",e=>ee(e),te,{resetStyle:!1});var ne=function(e,n){var l={};for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&n.indexOf(t)<0&&(l[t]=e[t]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var o=0,t=Object.getOwnPropertySymbols(e);o<t.length;o++)n.indexOf(t[o])<0&&Object.prototype.propertyIsEnumerable.call(e,t[o])&&(l[t[o]]=e[t[o]]);return l};const k=e=>{const{prefixCls:n,okButtonProps:l,cancelButtonProps:t,title:o,description:f,cancelText:u,okText:c,okType:i="primary",icon:g=s.createElement($,null),showCancel:v=!0,close:y,onConfirm:C,onCancel:O,onPopupClick:m}=e,{getPrefixCls:p}=s.useContext(w),[d]=q("Popconfirm",D.Popconfirm),b=E(o),x=E(f);return s.createElement("div",{className:`${n}-inner-content`,onClick:m},s.createElement("div",{className:`${n}-message`},g&&s.createElement("span",{className:`${n}-message-icon`},g),s.createElement("div",{className:`${n}-message-text`},b&&s.createElement("div",{className:`${n}-title`},b),x&&s.createElement("div",{className:`${n}-description`},x))),s.createElement("div",{className:`${n}-buttons`},v&&s.createElement(J,Object.assign({onClick:O,size:"small"},t),u||(d==null?void 0:d.cancelText)),s.createElement(Z,{buttonProps:Object.assign(Object.assign({size:"small"},M(i)),l),actionFn:C,close:y,prefixCls:p("btn"),quitOnNullishReturnValue:!0,emitEvent:!0},c||(d==null?void 0:d.okText))))},oe=e=>{const{prefixCls:n,placement:l,className:t,style:o}=e,f=ne(e,["prefixCls","placement","className","style"]),{getPrefixCls:u}=s.useContext(w),c=u("popconfirm",n),[i]=I(c);return i(s.createElement(Q,{placement:l,className:S(c,t),style:o,content:s.createElement(k,Object.assign({prefixCls:c},f))}))};var se=function(e,n){var l={};for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&n.indexOf(t)<0&&(l[t]=e[t]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var o=0,t=Object.getOwnPropertySymbols(e);o<t.length;o++)n.indexOf(t[o])<0&&Object.prototype.propertyIsEnumerable.call(e,t[o])&&(l[t[o]]=e[t[o]]);return l};const le=s.forwardRef((e,n)=>{var l,t;const{prefixCls:o,placement:f="top",trigger:u="click",okType:c="primary",icon:i=s.createElement($,null),children:g,overlayClassName:v,onOpenChange:y,onVisibleChange:C,overlayStyle:O,styles:m,classNames:p}=e,d=se(e,["prefixCls","placement","trigger","okType","icon","children","overlayClassName","onOpenChange","onVisibleChange","overlayStyle","styles","classNames"]),{getPrefixCls:b,className:x,style:T,classNames:j,styles:h}=U("popconfirm"),[_,z]=Y(!1,{value:(l=e.open)!==null&&l!==void 0?l:e.visible,defaultValue:(t=e.defaultOpen)!==null&&t!==void 0?t:e.defaultVisible}),P=(a,r)=>{z(a,!0),C==null||C(a),y==null||y(a,r)},B=a=>{P(!1,a)},V=a=>{var r;return(r=e.onConfirm)===null||r===void 0?void 0:r.call(void 0,a)},W=a=>{var r;P(!1,a),(r=e.onCancel)===null||r===void 0||r.call(void 0,a)},A=(a,r)=>{const{disabled:X=!1}=e;X||P(a,r)},N=b("popconfirm",o),H=S(N,x,v,j.root,p==null?void 0:p.root),R=S(j.body,p==null?void 0:p.body),[L]=I(N);return L(s.createElement(G,Object.assign({},K(d,["title"]),{trigger:u,placement:f,onOpenChange:A,open:_,ref:n,classNames:{root:H,body:R},styles:{root:Object.assign(Object.assign(Object.assign(Object.assign({},h.root),T),O),m==null?void 0:m.root),body:Object.assign(Object.assign({},h.body),m==null?void 0:m.body)},content:s.createElement(k,Object.assign({okType:c,icon:i},e,{prefixCls:N,close:B,onConfirm:V,onCancel:W})),"data-popover-inject":!0}),g))}),ae=le;ae._InternalPanelDoNotUseOrYouWillBeFired=oe;export{ae as P};

View File

@ -0,0 +1 @@
.users-page{padding:24px}.users-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:24px}.users-title{margin-bottom:4px!important}.users-table-card{border-radius:8px;box-shadow:0 2px 8px #0000000d}.users-table-toolbar{margin-bottom:20px}.users-search-input{max-width:400px}.user-avatar-placeholder{width:40px;height:40px;background-color:#f0f2f5;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#1890ff;font-size:20px}.user-display-name{font-weight:600;color:#262626}.user-username{font-size:12px;color:#8c8c8c}.user-phone{color:#8c8c8c;font-size:13px}.user-drawer-title{display:flex;align-items:center;font-size:16px;font-weight:600}.user-drawer-footer{display:flex;justify-content:flex-end;gap:12px;padding:10px 0}.user-form .ant-form-item{margin-bottom:20px}.user-form .ant-row{margin-left:-8px!important;margin-right:-8px!important}.user-form .ant-col{padding-left:8px!important;padding-right:8px!important}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.stat-card{padding:16px;background:#fff;border-radius:8px;border:1px solid #f0f0f0;transition:all .3s ease}.stat-card:hover{border-color:#d9d9d9;box-shadow:0 2px 8px #00000014}.stat-card.stat-card-row{display:flex;align-items:center;gap:16px}.stat-card.stat-card-row .stat-card-header{flex:1;margin-bottom:0;display:flex;flex-direction:column;align-items:flex-start;gap:8px}.stat-card.stat-card-row .stat-card-body{flex-shrink:0;display:flex;flex-direction:column;align-items:flex-end;gap:4px}.stat-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.stat-card-title{font-size:13px;color:#000000a6;font-weight:500}.stat-card-icon{font-size:18px;display:flex;align-items:center}.stat-card-body{display:flex;align-items:flex-end;justify-content:space-between;gap:8px}.stat-card-value{font-size:24px;font-weight:600;line-height:1}.stat-card-suffix{font-size:14px;font-weight:400;margin-left:4px;color:#00000073}.stat-card-trend{display:flex;align-items:center;gap:4px;font-size:12px;font-weight:500;padding:2px 6px;border-radius:4px}.stat-card-trend.trend-up{color:#52c41a;background:#f6ffed}.stat-card-trend.trend-down{color:#ff4d4f;background:#fff1f0}.stat-card-trend svg{font-size:10px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{j as e,m as N,n as I,u as R,T as $,o as c,p as k,B as l,q as w,t as T,v as C,w as M,R as S,S as _}from"./index-CYM97J2V.js";import{P as D}from"./index-B7x062Ky.js";import{T as o}from"./index-DqftVe8j.js";import{R as h,C as r}from"./row-DNGQ3hV2.js";import{C as p}from"./index-Bcf8yIZZ.js";import{F as E}from"./Table-CAs2vM8c.js";import"./useForm-B5uILH8t.js";import"./index-M0Ae7f04.js";import"./Pagination-By0LqcoC.js";function d({title:a,value:n,icon:i,color:s="blue",trend:t,suffix:u="",layout:f="column",gridColumn:m,className:b="",onClick:g,style:y={}}){const x={blue:"#1677ff",green:"#52c41a",orange:"#faad14",red:"#ff4d4f",purple:"#722ed1",gray:"#8c8c8c"}[s]||s,v={...m?{gridColumn:m}:{},...y};return e.jsxs("div",{className:`stat-card stat-card-${f} ${b}`,style:v,onClick:g,children:[e.jsxs("div",{className:"stat-card-header",children:[e.jsx("span",{className:"stat-card-title",children:a}),i&&e.jsx("span",{className:"stat-card-icon",style:{color:x},"aria-hidden":"true",children:i})]}),e.jsxs("div",{className:"stat-card-body",children:[e.jsxs("div",{className:"stat-card-value tabular-nums",style:{color:x},children:[n,u&&e.jsx("span",{className:"stat-card-suffix",children:u})]}),t&&e.jsxs("div",{className:`stat-card-trend ${t.direction==="up"?"trend-up":"trend-down"} tabular-nums`,"aria-label":`${t.direction==="up"?"Increase":"Decrease"} of ${t.value}%`,children:[t.direction==="up"?e.jsx(N,{"aria-hidden":"true"}):e.jsx(I,{"aria-hidden":"true"}),e.jsxs("span",{children:[Math.abs(t.value),"%"]})]})]})]})}const{Text:j}=$;function J(){const{t:a}=R(),n=[{key:"1",name:"Product Sync",time:"2024-02-10 14:00",duration:"45min",status:"processing"},{key:"2",name:"Tech Review",time:"2024-02-10 10:00",duration:"60min",status:"success"},{key:"3",name:"Daily Standup",time:"2024-02-10 09:00",duration:"15min",status:"success"},{key:"4",name:"Client Call",time:"2024-02-10 16:30",duration:"30min",status:"default"}],i=[{title:a("dashboard.meetingName"),dataIndex:"name",key:"name",render:s=>e.jsx(j,{strong:!0,children:s})},{title:a("dashboard.startTime"),dataIndex:"time",key:"time",className:"tabular-nums",render:s=>e.jsx(j,{type:"secondary",children:s})},{title:a("dashboard.duration"),dataIndex:"duration",key:"duration",width:100,className:"tabular-nums"},{title:a("common.status"),dataIndex:"status",key:"status",width:120,render:s=>s==="processing"?e.jsx(o,{icon:e.jsx(c,{spin:!0,"aria-hidden":"true"}),color:"processing",children:a("dashboardExt.processing")}):s==="success"?e.jsx(o,{icon:e.jsx(k,{"aria-hidden":"true"}),color:"success",children:a("dashboardExt.completed")}):e.jsx(o,{color:"default",children:a("dashboardExt.pending")})},{title:a("common.action"),key:"action",width:80,render:()=>e.jsx(l,{type:"link",size:"small",icon:e.jsx(w,{"aria-hidden":"true"}),"aria-label":a("dashboard.viewAll")})}];return e.jsxs("div",{className:"app-page dashboard-page",children:[e.jsx(D,{title:a("dashboard.title"),subtitle:a("dashboard.subtitle")}),e.jsx("div",{className:"app-page__page-actions",children:e.jsx(l,{icon:e.jsx(c,{"aria-hidden":"true"}),size:"small",children:a("common.refresh")})}),e.jsxs(h,{gutter:[24,24],children:[e.jsx(r,{xs:24,sm:12,lg:6,children:e.jsx(d,{title:a("dashboard.todayMeetings"),value:12,icon:e.jsx(T,{"aria-hidden":"true"}),color:"blue",trend:{value:8,direction:"up"}})}),e.jsx(r,{xs:24,sm:12,lg:6,children:e.jsx(d,{title:a("dashboard.activeDevices"),value:45,icon:e.jsx(C,{"aria-hidden":"true"}),color:"green",trend:{value:2,direction:"up"}})}),e.jsx(r,{xs:24,sm:12,lg:6,children:e.jsx(d,{title:a("dashboard.transcriptionDuration"),value:1280,suffix:"min",icon:e.jsx(M,{"aria-hidden":"true"}),color:"orange",trend:{value:5,direction:"down"}})}),e.jsx(r,{xs:24,sm:12,lg:6,children:e.jsx(d,{title:a("dashboard.totalUsers"),value:320,icon:e.jsx(S,{"aria-hidden":"true"}),color:"purple",trend:{value:12,direction:"up"}})})]}),e.jsxs(h,{gutter:[24,24],className:"mt-6",children:[e.jsx(r,{xs:24,xl:16,children:e.jsx(p,{title:a("dashboard.recentMeetings"),bordered:!1,className:"app-page__content-card",extra:e.jsx(l,{type:"link",size:"small",children:a("dashboard.viewAll")}),styles:{body:{padding:0}},children:e.jsx(E,{dataSource:n,columns:i,pagination:!1,size:"middle",className:"roles-table"})})}),e.jsx(r,{xs:24,xl:8,children:e.jsx(p,{title:a("dashboard.deviceLoad"),bordered:!1,className:"app-page__content-card",children:e.jsxs("div",{className:"flex flex-col items-center justify-center py-12",children:[e.jsx(_,{active:!0,paragraph:{rows:4}}),e.jsxs("div",{className:"mt-4 text-gray-400 flex items-center gap-2",children:[e.jsx(c,{spin:!0,"aria-hidden":"true"}),e.jsx("span",{children:a("dashboardExt.chartLoading")})]})]})})})]})]})}export{J as default};

View File

@ -0,0 +1 @@
import{r as u,h as p,j as e,L as f,a as o,T as j,B as l,i as w,k as y,s as g}from"./index-CYM97J2V.js";import{F as s}from"./index-CacOxAQN.js";import{C as h}from"./index-Bcf8yIZZ.js";import{I as a}from"./index-DV4zjsPW.js";import"./useForm-B5uILH8t.js";import"./row-DNGQ3hV2.js";const{Title:P,Text:b}=j;function F(){const[d,t]=u.useState(!1),c=p(),[m]=s.useForm(),n=()=>{localStorage.clear(),sessionStorage.clear(),c("/login")},x=async r=>{t(!0);try{await y({oldPassword:r.oldPassword,newPassword:r.newPassword}),g.success("密码已更新,请重新登录"),n()}finally{t(!1)}};return e.jsx(f,{style:{minHeight:"100vh",background:"#f0f2f5",display:"flex",alignItems:"center",justifyContent:"center"},children:e.jsxs(h,{style:{width:420,borderRadius:8,boxShadow:"0 4px 12px rgba(0,0,0,0.1)"},children:[e.jsxs("div",{className:"text-center mb-6",children:[e.jsx(o,{style:{fontSize:40,color:"#1890ff"}}),e.jsx(P,{level:3,style:{marginTop:16},children:"首次登录请修改密码"}),e.jsx(b,{type:"secondary",children:"当前账号被要求更新初始密码,提交成功后会跳转到登录页。"})]}),e.jsxs(s,{form:m,layout:"vertical",onFinish:x,children:[e.jsx(s.Item,{label:"当前密码",name:"oldPassword",rules:[{required:!0,message:"请输入当前密码"}],children:e.jsx(a.Password,{prefix:e.jsx(o,{})})}),e.jsx(s.Item,{label:"新密码",name:"newPassword",rules:[{required:!0,min:6,message:"新密码至少 6 位"}],children:e.jsx(a.Password,{prefix:e.jsx(o,{})})}),e.jsx(s.Item,{label:"确认新密码",name:"confirmPassword",dependencies:["newPassword"],rules:[{required:!0,message:"请再次输入新密码"},({getFieldValue:r})=>({validator(T,i){return!i||r("newPassword")===i?Promise.resolve():Promise.reject(new Error("两次输入的新密码不一致"))}})],children:e.jsx(a.Password,{prefix:e.jsx(o,{})})}),e.jsx(l,{type:"primary",htmlType:"submit",block:!0,size:"large",loading:d,style:{marginTop:8},children:"提交并重新登录"}),e.jsx(l,{type:"link",block:!0,icon:e.jsx(w,{}),onClick:n,style:{marginTop:8},children:"退出登录"})]})]})})}export{F as default};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.login-page{min-height:100vh;display:flex;background:#fff}.login-left{flex:1.1;background:linear-gradient(140deg,#f0f5ff,#eef5ff 40%,#f7fbff);padding:56px 64px;display:flex;flex-direction:column;justify-content:space-between;position:relative;overflow:hidden}.login-brand{display:flex;align-items:center;gap:10px;z-index:1}.brand-logo-img{width:34px;height:34px;filter:drop-shadow(0 8px 16px rgba(45,107,255,.24))}.brand-name{font-size:20px;font-weight:600;color:#2f3a4f;letter-spacing:-.2px}.login-hero{margin-top:-40px;z-index:1}.hero-title{font-size:42px;font-weight:700;line-height:1.2;color:#1d2b3a;margin-bottom:24px;letter-spacing:-.5px;text-wrap:balance}.hero-accent{color:#1677ff;position:relative}.hero-desc{font-size:16px;line-height:1.8;color:#687489;max-width:440px}.login-left-footer{display:flex;align-items:center;gap:12px;color:#8c97a8;font-size:13px;z-index:1}.footer-divider{width:4px;height:4px;background:#c4ccd7;border-radius:50%}.login-right{flex:1;display:flex;align-items:center;justify-content:center;padding:40px;background:#fff}.login-container{width:100%;max-width:400px}.login-header{margin-bottom:40px}.login-header h2{font-size:28px!important;font-weight:700!important;color:#1f2a37!important;margin-bottom:8px!important;letter-spacing:-.5px}.login-header span{font-size:15px;color:#6b7280}.login-form .ant-form-item{margin-bottom:20px}.login-form .ant-input-affix-wrapper-lg{padding:10px 16px;border-radius:8px}.captcha-wrapper{display:flex;gap:12px}.captcha-image-btn{padding:0;width:120px;height:46px;flex-shrink:0;border-radius:8px;overflow:hidden;display:flex;align-items:center;justify-content:center;background:#f9fafb}.captcha-image-btn img{width:100%;height:100%;object-fit:cover;transition:opacity .2s}.captcha-image-btn:hover img{opacity:.8}.login-extra{display:flex;justify-content:space-between;align-items:center;margin-top:-4px;margin-bottom:24px}.forgot-password{font-size:14px;color:#1677ff}.login-submit-btn{height:48px;font-size:16px;font-weight:600;border-radius:8px}.login-footer{margin-top:32px;text-align:center;padding:16px;background:#f9fafb;border-radius:12px}.tabular-nums{font-variant-numeric:tabular-nums}@media (max-width: 1024px){.login-left{padding:48px}.hero-title{font-size:36px}}@media (max-width: 900px){.login-left{display:none}.login-right{background:#f3f4f6}.login-container{background:#fff;padding:48px 32px;border-radius:20px;box-shadow:0 20px 25px -5px #0000001a,0 10px 10px -5px #0000000a}}@media (max-width: 480px){.login-right{padding:20px}.login-container{padding:32px 20px}}

View File

@ -0,0 +1 @@
import{u as W,r as t,j as e,B as M,W as Y,a3 as $,Y as c,R as v,T as F,a5 as K,b3 as P,aU as D,aT as G,aW as H,s as w,a_ as q}from"./index-CYM97J2V.js";import{P as A}from"./index-B7x062Ky.js";import{R as b,C as d}from"./row-DNGQ3hV2.js";import{C as N}from"./index-Bcf8yIZZ.js";import{I as J}from"./index-DV4zjsPW.js";import{F as O}from"./Table-CAs2vM8c.js";import{T as u}from"./index-DqftVe8j.js";import{C as S}from"./index-M0Ae7f04.js";import"./useForm-B5uILH8t.js";import"./Pagination-By0LqcoC.js";const{Text:U}=F;function oe(){const{t:a}=W(),[o,C]=t.useState([]),[h,I]=t.useState([]),[T,m]=t.useState(!1),[p,x]=t.useState(!1),[f,g]=t.useState(!1),[l,y]=t.useState(null),[j,i]=t.useState([]),[n,_]=t.useState(""),R=t.useMemo(()=>o.find(s=>s.userId===l)||null,[o,l]),k=async()=>{m(!0);try{const s=await D();C(s||[])}finally{m(!1)}},z=async()=>{x(!0);try{const s=await G();I(s||[])}finally{x(!1)}},E=async s=>{try{const r=await H(s);i(r||[])}catch{i([])}};t.useEffect(()=>{k(),z()},[]),t.useEffect(()=>{l?E(l):i([])},[l]);const L=t.useMemo(()=>{if(!n)return o;const s=n.toLowerCase();return o.filter(r=>r.username.toLowerCase().includes(s)||r.displayName.toLowerCase().includes(s))},[o,n]),B=async()=>{if(!l){w.warning(a("userRole.selectUser"));return}g(!0);try{await q(l,j),w.success(a("common.success"))}finally{g(!1)}};return e.jsxs("div",{className:"app-page",children:[e.jsx(A,{title:a("userRole.title"),subtitle:a("userRole.subtitle")}),e.jsx("div",{className:"app-page__page-actions",children:e.jsx(M,{type:"primary",icon:e.jsx(Y,{"aria-hidden":"true"}),onClick:B,loading:f,disabled:!l,children:a(f?"common.loading":"common.save")})}),e.jsxs(b,{gutter:24,className:"app-page__split",style:{height:"calc(100vh - 180px)"},children:[e.jsx(d,{xs:24,lg:12,style:{height:"100%"},children:e.jsxs(N,{title:e.jsxs(c,{children:[e.jsx(v,{"aria-hidden":"true"}),e.jsx("span",{children:a("userRole.userList")})]}),className:"app-page__panel-card full-height-card",children:[e.jsx("div",{className:"mb-4",children:e.jsx(J,{placeholder:a("userRole.searchUser"),prefix:e.jsx($,{"aria-hidden":"true",className:"text-gray-400"}),value:n,onChange:s=>_(s.target.value),allowClear:!0,"aria-label":a("userRole.searchUser")})}),e.jsx("div",{style:{height:"calc(100% - 60px)",overflowY:"auto"},children:e.jsx(O,{rowKey:"userId",size:"middle",loading:T,dataSource:L,rowSelection:{type:"radio",selectedRowKeys:l?[l]:[],onChange:s=>y(s[0])},onRow:s=>({onClick:()=>y(s.userId),className:"cursor-pointer"}),pagination:{pageSize:10,showTotal:s=>a("common.total",{total:s})},columns:[{title:a("users.userInfo"),key:"user",render:(s,r)=>e.jsxs("div",{className:"min-w-0",children:[e.jsx("div",{style:{fontWeight:500},className:"truncate",children:r.displayName}),e.jsxs("div",{style:{fontSize:12,color:"#8c8c8c"},className:"truncate",children:["@",r.username]})]})},{title:a("common.status"),dataIndex:"status",width:80,render:s=>s===1?e.jsx(u,{color:"green",className:"m-0",children:"Enabled"}):e.jsx(u,{className:"m-0",children:"Disabled"})}]})})]})}),e.jsx(d,{xs:24,lg:12,style:{height:"100%"},children:e.jsx(N,{title:e.jsxs(c,{children:[e.jsx(P,{"aria-hidden":"true"}),e.jsx("span",{children:a("userRole.grantRoles")})]}),className:"app-page__panel-card full-height-card",extra:R?e.jsxs(u,{color:"blue",children:[a("userRole.editing"),": ",R.displayName]}):null,children:l?e.jsxs("div",{style:{padding:"8px 0",height:"100%",overflowY:"auto"},children:[e.jsx(S.Group,{style:{width:"100%"},value:j,onChange:s=>i(s),disabled:p,children:e.jsx(b,{gutter:[16,16],children:h.map(s=>e.jsx(d,{span:12,children:e.jsx(S,{value:s.roleId,className:"w-full",children:e.jsxs(c,{direction:"vertical",size:0,children:[e.jsx("span",{style:{fontWeight:500},children:s.roleName}),e.jsx(U,{type:"secondary",style:{fontSize:12},className:"tabular-nums",children:s.roleCode})]})})},s.roleId))})}),!h.length&&!p&&e.jsx(K,{description:"No roles available"})]}):e.jsxs("div",{className:"flex flex-col items-center justify-center py-20 bg-gray-50 rounded-lg border border-dashed border-gray-200",children:[e.jsx(v,{style:{fontSize:40,color:"#bfbfbf",marginBottom:16},"aria-hidden":"true"}),e.jsx(U,{type:"secondary",children:a("userRole.selectUser")})]})})})]})]})}export{oe as default};

View File

@ -0,0 +1 @@
import{bm as t}from"./index-CYM97J2V.js";async function n(a){return(await t.get("/sys/api/orgs",{params:{tenantId:a}})).data.data}async function p(a){return(await t.post("/sys/api/orgs",a)).data.data}async function o(a,s){return(await t.put(`/sys/api/orgs/${a}`,s)).data.data}async function c(a){return(await t.delete(`/sys/api/orgs/${a}`)).data.data}export{p as c,c as d,n as l,o as u};

View File

@ -0,0 +1 @@
import{cH as r}from"./index-CYM97J2V.js";const s=(t,o,a,e)=>({total:t,current:o,pageSize:a,onChange:e,showSizeChanger:!0,showQuickJumper:!0,showTotal:n=>r.t("common.total",{total:n}),pageSizeOptions:["10","20","50","100"]});export{s as g};

View File

@ -0,0 +1 @@
import{r as f,ay as k,C as A,bk as S,bj as I}from"./index-CYM97J2V.js";import{c as _,d as J}from"./useForm-B5uILH8t.js";const G=f.createContext({});var M=function(e,l){var n={};for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&l.indexOf(t)<0&&(n[t]=e[t]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var r=0,t=Object.getOwnPropertySymbols(e);r<t.length;r++)l.indexOf(t[r])<0&&Object.prototype.propertyIsEnumerable.call(e,t[r])&&(n[t[r]]=e[t[r]]);return n};function R(e){return e==="auto"?"1 1 auto":typeof e=="number"?`${e} ${e} auto`:/^\d+(\.\d+)?(px|em|rem|%)$/.test(e)?`0 0 ${e}`:e}const z=["xs","sm","md","lg","xl","xxl"],H=f.forwardRef((e,l)=>{const{getPrefixCls:n,direction:t}=f.useContext(k),{gutter:r,wrap:c}=f.useContext(G),{prefixCls:p,span:i,order:g,offset:m,push:h,pull:O,className:E,children:b,flex:x,style:C}=e,d=M(e,["prefixCls","span","order","offset","push","pull","className","children","flex","style"]),o=n("col",p),[N,P,y]=_(o),j={};let $={};z.forEach(a=>{let s={};const v=e[a];typeof v=="number"?s.span=v:typeof v=="object"&&(s=v||{}),delete d[a],$=Object.assign(Object.assign({},$),{[`${o}-${a}-${s.span}`]:s.span!==void 0,[`${o}-${a}-order-${s.order}`]:s.order||s.order===0,[`${o}-${a}-offset-${s.offset}`]:s.offset||s.offset===0,[`${o}-${a}-push-${s.push}`]:s.push||s.push===0,[`${o}-${a}-pull-${s.pull}`]:s.pull||s.pull===0,[`${o}-rtl`]:t==="rtl"}),s.flex&&($[`${o}-${a}-flex`]=!0,j[`--${o}-${a}-flex`]=R(s.flex))});const w=A(o,{[`${o}-${i}`]:i!==void 0,[`${o}-order-${g}`]:g,[`${o}-offset-${m}`]:m,[`${o}-push-${h}`]:h,[`${o}-pull-${O}`]:O},E,$,P,y),u={};if(r!=null&&r[0]){const a=typeof r[0]=="number"?`${r[0]/2}px`:`calc(${r[0]} / 2)`;u.paddingLeft=a,u.paddingRight=a}return x&&(u.flex=R(x),c===!1&&!u.minWidth&&(u.minWidth=0)),N(f.createElement("div",Object.assign({},d,{style:Object.assign(Object.assign(Object.assign({},u),C),j),className:w,ref:l}),b))});function B(e,l){const n=[void 0,void 0],t=Array.isArray(e)?e:[e,void 0],r=l||{xs:!0,sm:!0,md:!0,lg:!0,xl:!0,xxl:!0};return t.forEach((c,p)=>{if(typeof c=="object"&&c!==null)for(let i=0;i<S.length;i++){const g=S[i];if(r[g]&&c[g]!==void 0){n[p]=c[g];break}}else n[p]=c}),n}var L=function(e,l){var n={};for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&l.indexOf(t)<0&&(n[t]=e[t]);if(e!=null&&typeof Object.getOwnPropertySymbols=="function")for(var r=0,t=Object.getOwnPropertySymbols(e);r<t.length;r++)l.indexOf(t[r])<0&&Object.prototype.propertyIsEnumerable.call(e,t[r])&&(n[t[r]]=e[t[r]]);return n};function V(e,l){const[n,t]=f.useState(typeof e=="string"?e:""),r=()=>{if(typeof e=="string"&&t(e),typeof e=="object")for(let c=0;c<S.length;c++){const p=S[c];if(!l||!l[p])continue;const i=e[p];if(i!==void 0){t(i);return}}};return f.useEffect(()=>{r()},[JSON.stringify(e),l]),n}const q=f.forwardRef((e,l)=>{const{prefixCls:n,justify:t,align:r,className:c,style:p,children:i,gutter:g=0,wrap:m}=e,h=L(e,["prefixCls","justify","align","className","style","children","gutter","wrap"]),{getPrefixCls:O,direction:E}=f.useContext(k),b=I(!0,null),x=V(r,b),C=V(t,b),d=O("row",n),[o,N,P]=J(d),y=B(g,b),j=A(d,{[`${d}-no-wrap`]:m===!1,[`${d}-${C}`]:C,[`${d}-${x}`]:x,[`${d}-rtl`]:E==="rtl"},c,N,P),$={};if(y!=null&&y[0]){const s=typeof y[0]=="number"?`${y[0]/-2}px`:`calc(${y[0]} / -2)`;$.marginLeft=s,$.marginRight=s}const[w,u]=y;$.rowGap=u;const a=f.useMemo(()=>({gutter:[w,u],wrap:m}),[w,u,m]);return o(f.createElement(G.Provider,{value:a},f.createElement("div",Object.assign({},h,{className:j,style:Object.assign(Object.assign({},$),p),ref:l}),i)))});export{H as C,q as R};

View File

@ -0,0 +1 @@
import{bm as n}from"./index-CYM97J2V.js";async function r(t){return(await n.get("/sys/api/tenants",{params:t})).data.data}async function p(t){return(await n.post("/sys/api/tenants",t)).data.data}async function c(t,a){return(await n.put(`/sys/api/tenants/${t}`,a)).data.data}async function i(t){return(await n.delete(`/sys/api/tenants/${t}`)).data.data}export{p as c,i as d,r as l,c as u};

View File

@ -0,0 +1 @@
import{r as i,ds as S,B as $,cQ as E,bm as n}from"./index-CYM97J2V.js";const g=t=>typeof(t==null?void 0:t.then)=="function",x=t=>{const{type:e,children:c,prefixCls:y,buttonProps:d,close:r,autoFocus:m,emitEvent:o,isSilent:v,quitOnNullishReturnValue:D,actionFn:l}=t,u=i.useRef(!1),w=i.useRef(null),[I,h]=S(!1),p=(...a)=>{r==null||r.apply(void 0,a)};i.useEffect(()=>{let a=null;return m&&(a=setTimeout(()=>{var s;(s=w.current)===null||s===void 0||s.focus({preventScroll:!0})})),()=>{a&&clearTimeout(a)}},[m]);const T=a=>{g(a)&&(h(!0),a.then((...s)=>{h(!1,!0),p.apply(void 0,s),u.current=!1},s=>{if(h(!1,!0),u.current=!1,!(v!=null&&v()))return Promise.reject(s)}))},b=a=>{if(u.current)return;if(u.current=!0,!l){p();return}let s;if(o){if(s=l(a),D&&!g(s)){u.current=!1,p(a);return}}else if(l.length)s=l(r),u.current=!1;else if(s=l(),!g(s)){p();return}T(s)};return i.createElement($,Object.assign({},E(e),{onClick:b,loading:I,prefixCls:y},d,{ref:w}),c)};async function B(t){return(await n.get("/sys/api/dict-types",{params:t})).data.data}async function O(t){return(await n.post("/sys/api/dict-types",t)).data.data}async function P(t,e){return(await n.put(`/sys/api/dict-types/${t}`,e)).data.data}async function F(t){return(await n.delete(`/sys/api/dict-types/${t}`)).data.data}async function L(t){return(await n.get("/sys/api/dict-items",{params:{typeCode:t}})).data.data}async function j(t){return(await n.post("/sys/api/dict-items",t)).data.data}async function A(t,e){return(await n.put(`/sys/api/dict-items/${t}`,e)).data.data}async function q(t){return(await n.delete(`/sys/api/dict-items/${t}`)).data.data}async function R(t){return(await n.get(`/sys/api/dict-items/type/${t}`)).data.data}const f={};function M(t){const[e,c]=i.useState(f[t]||[]),[y,d]=i.useState(!f[t]);return i.useEffect(()=>{if(f[t]){c(f[t]),d(!1);return}let r=!0;return(async()=>{try{const o=await R(t);r&&(f[t]=o,c(o))}catch(o){console.error(`Failed to fetch dictionary ${t}:`,o)}finally{r&&d(!1)}})(),()=>{r=!1}},[t]),{items:e,loading:y}}export{x as A,L as a,P as b,O as c,F as d,q as e,B as f,A as g,j as h,M as u};

File diff suppressed because one or more lines are too long

14
frontend1/dist/index.html vendored 100644
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UnisBase - 智能会议系统</title>
<script type="module" crossorigin src="/assets/index-CYM97J2V.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CaWPk49l.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

14
frontend1/dist/logo.svg vendored 100644
View File

@ -0,0 +1,14 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="10" fill="#2D6BFF"/>
<g transform="translate(16 15.6) rotate(35)" stroke="white" fill="none" stroke-width="1.8">
<rect x="-5.2" y="-10.2" width="10.4" height="10.4" rx="5.2"/>
<path d="M-2.2 0.2 H2.2 L1.1 10.4 H-1.1 Z"/>
</g>
<path
d="M12.5 22.6 C14.2 21.6 16 21.6 17.8 22.6 S21.4 23.8 23.2 22.6"
stroke="white"
stroke-width="1.8"
stroke-linecap="round"
fill="none"
/>
</svg>

After

Width:  |  Height:  |  Size: 543 B

View File

@ -1,4 +1,4 @@
import axios from "axios";
import axios from "axios";
import { message } from "antd";
const http = axios.create({
@ -6,7 +6,104 @@ const http = axios.create({
timeout: 15000
});
http.interceptors.request.use((config) => {
const refreshClient = axios.create({
baseURL: "/",
timeout: 15000
});
const AUTH_WHITELIST = ["/sys/auth/login", "/sys/auth/refresh", "/sys/auth/captcha", "/sys/auth/device-code"];
const REFRESH_AHEAD_MS = 60 * 1000;
let refreshPromise: Promise<string | null> | null = null;
function getTokenPayload(token: string): Record<string, any> | null {
try {
const payload = token.split(".")[1];
if (!payload) {
return null;
}
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
return JSON.parse(decodeURIComponent(escape(window.atob(normalized))));
} catch {
return null;
}
}
function getTokenExpireAt(token: string): number | null {
const payload = getTokenPayload(token);
if (!payload || typeof payload.exp !== "number") {
return null;
}
return payload.exp * 1000;
}
function isTokenExpiringSoon(token: string): boolean {
const expireAt = getTokenExpireAt(token);
if (!expireAt) {
return false;
}
return expireAt - Date.now() <= REFRESH_AHEAD_MS;
}
function clearAuthStorage() {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
sessionStorage.removeItem("userProfile");
}
function persistTokens(data: { accessToken: string; refreshToken: string }) {
localStorage.setItem("accessToken", data.accessToken);
localStorage.setItem("refreshToken", data.refreshToken);
const payload = getTokenPayload(data.accessToken);
if (payload && payload.tenantId !== undefined && payload.tenantId !== null) {
localStorage.setItem("activeTenantId", String(payload.tenantId));
}
}
function isAuthWhitelistRequest(url?: string) {
return AUTH_WHITELIST.some((path) => (url || "").includes(path));
}
async function refreshAccessToken(): Promise<string | null> {
if (refreshPromise) {
return refreshPromise;
}
const refreshToken = localStorage.getItem("refreshToken");
if (!refreshToken) {
return null;
}
refreshPromise = refreshClient
.post("/sys/auth/refresh", { refreshToken })
.then((resp) => {
const body = resp.data;
if (!body || body.code !== "0" || !body.data?.accessToken || !body.data?.refreshToken) {
throw new Error(body?.msg || "刷新登录态失败");
}
persistTokens(body.data);
return body.data.accessToken as string;
})
.catch(() => {
clearAuthStorage();
return null;
})
.finally(() => {
refreshPromise = null;
});
return refreshPromise;
}
http.interceptors.request.use(async (config) => {
if (!isAuthWhitelistRequest(config.url)) {
const currentToken = localStorage.getItem("accessToken");
if (currentToken && isTokenExpiringSoon(currentToken)) {
await refreshAccessToken();
}
}
const token = localStorage.getItem("accessToken");
if (token) {
config.headers = config.headers || {};
@ -18,10 +115,9 @@ http.interceptors.request.use((config) => {
http.interceptors.response.use(
(resp) => {
const body = resp.data;
// 如果返回的 code 不是 0表示业务错误
if (body && body.code !== "0") {
const errorMsg = body.msg || "请求失败";
message.error(errorMsg); // 自动展示后端错误消息
message.error(errorMsg);
const err = new Error(errorMsg);
(err as any).code = body.code;
(err as any).msg = body.msg;
@ -29,27 +125,37 @@ http.interceptors.response.use(
}
return resp;
},
(error) => {
// 处理 HTTP 状态码错误 (4xx, 5xx)
async (error) => {
const originalRequest = error.config || {};
if (
error.response?.status === 401 &&
!originalRequest._retry &&
!isAuthWhitelistRequest(originalRequest.url)
) {
originalRequest._retry = true;
const newToken = await refreshAccessToken();
if (newToken) {
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return http(originalRequest);
}
}
if (error.response && (error.response.status === 401 || error.response.status === 403)) {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
sessionStorage.removeItem("userProfile");
clearAuthStorage();
window.location.href = "/login?timeout=1";
return Promise.reject(error);
}
const body = error.response?.data;
const errorMsg = body?.msg || error.message || "网络异常";
// 防止重复弹出相同的提示(可选逻辑,根据需要调整)
message.error(errorMsg);
if (body && body.msg) {
const err = new Error(body.msg);
(err as any).code = body.code;
(err as any).msg = body.msg;
return Promise.reject(err);
const err = new Error(body.msg);
(err as any).code = body.code;
(err as any).msg = body.msg;
return Promise.reject(err);
}
return Promise.reject(error);

View File

@ -1,6 +1,6 @@
import http from "./http";
import http from "./http";
import {
DeviceInfo, SysPermission, SysRole, SysUser, UserProfile, SysParamVO, SysParamQuery, PageResult,
BotCredential, DeviceInfo, RoleDataScope, SysPermission, SysRole, SysUser, UserProfile, SysParamVO, SysParamQuery, PageResult,
PermissionNode
} from "../types";
@ -29,7 +29,6 @@ export async function listUsers(params?: { tenantId?: number; orgId?: number })
return resp.data.data as SysUser[];
}
export async function createUser(payload: Partial<SysUser>) {
const resp = await http.post("/sys/api/users", payload);
return resp.data.data as boolean;
@ -110,6 +109,16 @@ export async function updateMyPassword(payload: any) {
return resp.data.data as boolean;
}
export async function getMyBotCredential() {
const resp = await http.get("/sys/api/users/bot-credential");
return resp.data.data as BotCredential;
}
export async function generateMyBotCredential() {
const resp = await http.post("/sys/api/users/bot-credential/generate");
return resp.data.data as BotCredential;
}
export async function createPermission(payload: Partial<SysPermission>) {
const resp = await http.post("/sys/api/permissions", payload);
return resp.data.data as boolean;
@ -165,6 +174,16 @@ export async function saveRolePermissions(roleId: number, permIds: number[]) {
return resp.data.data as boolean;
}
export async function getRoleDataScope(roleId: number) {
const resp = await http.get(`/sys/api/roles/${roleId}/data-scope`);
return resp.data.data as RoleDataScope;
}
export async function saveRoleDataScope(roleId: number, payload: RoleDataScope) {
const resp = await http.post(`/sys/api/roles/${roleId}/data-scope`, payload);
return resp.data.data as boolean;
}
export async function fetchUsersByRoleId(roleId: number) {
const resp = await http.get(`/sys/api/roles/${roleId}/users`);
return resp.data.data as SysUser[];
@ -194,5 +213,3 @@ export * from "./dict";
export * from "./tenant";
export * from "./org";
export * from "./platform";

View File

@ -62,6 +62,17 @@ export default function AppLayout() {
const [menus, setMenus] = useState<SysPermission[]>([]);
const [availableTenants, setAvailableTenants] = useState<TenantInfo[]>([]);
const [currentTenantId, setCurrentTenantId] = useState<number | null>(null);
const [currentUserLabel, setCurrentUserLabel] = useState<string>(() => {
try {
const profileStr = sessionStorage.getItem("userProfile");
if (profileStr) {
const profile = JSON.parse(profileStr) as { displayName?: string; username?: string };
return profile.displayName || profile.username || localStorage.getItem("username") || "";
}
} catch {
}
return localStorage.getItem("displayName") || localStorage.getItem("username") || "";
});
const [openKeys, setOpenKeys] = useState<string[]>([]);
const [platformConfig, setPlatformConfig] = useState<SysPlatformConfig | null>(() => {
const configStr = sessionStorage.getItem("platformConfig");
@ -86,6 +97,21 @@ export default function AppLayout() {
setCurrentTenantId(Number(activeTenantId));
}
try {
const profile = await getCurrentUser();
sessionStorage.setItem("userProfile", JSON.stringify(profile));
if (profile.username) {
localStorage.setItem("username", profile.username);
}
if (profile.displayName) {
localStorage.setItem("displayName", profile.displayName);
}
setCurrentUserLabel(profile.displayName || profile.username || "");
} catch {
const cached = localStorage.getItem("displayName") || localStorage.getItem("username") || "";
setCurrentUserLabel(cached);
}
const data = await listMyPermissions();
await loadPermissions();
@ -124,6 +150,13 @@ export default function AppLayout() {
const profile = await getCurrentUser();
sessionStorage.setItem("userProfile", JSON.stringify(profile));
if (profile.username) {
localStorage.setItem("username", profile.username);
}
if (profile.displayName) {
localStorage.setItem("displayName", profile.displayName);
}
setCurrentUserLabel(profile.displayName || profile.username || "");
message.success(t("common.success"));
window.location.reload();
@ -283,7 +316,7 @@ export default function AppLayout() {
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
<Space style={{ cursor: "pointer", color: "var(--app-text-main)" }}>
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: "var(--app-primary-color)" }} />
<span style={{ fontWeight: 500 }}>{localStorage.getItem("username") || t("layout.admin")}</span>
<span style={{ fontWeight: 500 }}>{currentUserLabel || t("layout.admin")}</span>
</Space>
</Dropdown>
</Space>

View File

@ -260,7 +260,18 @@
"saveChanges": "Save Changes",
"updatePassword": "Update Password",
"passwordsDoNotMatch": "Passwords do not match.",
"standardUser": "Standard User"
"standardUser": "Standard User",
"botCredentialTab": "Bot Credential",
"botCredentialHint": "Use this credential pair to access /mcp with X-Bot-Id and X-Bot-Secret.",
"botCredentialHintDesc": "The secret is shown only after generation. Store it securely after copying.",
"botBindStatus": "Binding Status",
"botBound": "Bound",
"botUnbound": "Not Generated",
"botSecretHidden": "Hidden. Generate or reset to get a new secret.",
"botLastAccessTime": "Last Access Time",
"botLastAccessIp": "Last Access IP",
"generateBotCredential": "Generate Credential",
"regenerateBotCredential": "Regenerate Credential"
},
"rolesExt": {
"roleList": "Role List",

View File

@ -260,7 +260,18 @@
"saveChanges": "保存修改",
"updatePassword": "更新密码",
"passwordsDoNotMatch": "两次输入的密码不一致。",
"standardUser": "普通用户"
"standardUser": "普通用户",
"botCredentialTab": "Bot 凭证",
"botCredentialHint": "使用这组凭证通过 X-Bot-Id 和 X-Bot-Secret 访问 /mcp。",
"botCredentialHintDesc": "Secret 只会在生成后显示一次,请复制后妥善保管。",
"botBindStatus": "绑定状态",
"botBound": "已绑定",
"botUnbound": "未生成",
"botSecretHidden": "已隐藏。如需查看新的 Secret请重新生成。",
"botLastAccessTime": "最近访问时间",
"botLastAccessIp": "最近访问 IP",
"generateBotCredential": "生成凭证",
"regenerateBotCredential": "重置凭证"
},
"rolesExt": {
"roleList": "角色列表",

View File

@ -1,7 +1,6 @@
import { Avatar, Badge, Button, Card, Col, Drawer, Empty, Form, Input, List, Pagination, message, Modal, Popconfirm, Select, Space, Table, Tabs, Tag, Tooltip, Tree, Typography, Row } from "antd";
import { Avatar, Button, Card, Col, Drawer, Empty, Form, Input, List, Pagination, Radio, message, Modal, Popconfirm, Select, Space, Table, Tabs, Tag, Tooltip, Tree, Typography, Row } from "antd";
import type { DataNode } from "antd/es/tree";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import {
ApartmentOutlined,
CheckCircleFilled,
@ -22,11 +21,14 @@ import {
createRole,
deleteRole,
fetchUsersByRoleId,
getRoleDataScope,
listOrgs,
listPermissions,
listRolePermissions,
listTenants,
listUsers,
pageRoles,
saveRoleDataScope,
saveRolePermissions,
unbindUserFromRole,
updateRole
@ -34,14 +36,25 @@ import {
import { useDict } from "@/hooks/useDict";
import { usePermission } from "@/hooks/usePermission";
import PageHeader from "@/components/shared/PageHeader";
import type { SysPermission, SysRole, SysTenant, SysUser } from "@/types";
import type { RoleDataScope, SysOrg, SysPermission, SysRole, SysTenant, SysUser } from "@/types";
import "./index.less";
const { Text, Title } = Typography;
type PermissionNode = SysPermission & { key: number; children?: PermissionNode[] };
type OrgTreeNode = SysOrg & { key: number; children?: OrgTreeNode[] };
type RoleTabKey = "permissions" | "dataScope" | "users";
const DEFAULT_STATUS = 1;
const DEFAULT_ROLE_PAGE_SIZE = 10;
const BUTTON_SHORT_LABEL = "按钮";
const DATA_SCOPE_OPTIONS = [
{ label: "全部", value: "ALL" },
{ label: "个人", value: "SELF" },
{ label: "本部门", value: "DEPT" },
{ label: "本部门及下级部门", value: "DEPT_AND_CHILD" },
{ label: "自定义部门", value: "CUSTOM" }
] as const;
function normalizeNumber(value: unknown): number | undefined {
if (typeof value === "number") {
@ -56,6 +69,7 @@ function normalizeNumber(value: unknown): number | undefined {
}
return undefined;
}
function buildPermissionTree(list: SysPermission[]): PermissionNode[] {
const active = (list || []).filter((permission) => permission.status !== 0);
const map = new Map<number, PermissionNode>();
@ -85,7 +99,7 @@ function buildPermissionTree(list: SysPermission[]): PermissionNode[] {
return roots;
}
function toTreeData(nodes: PermissionNode[], t: (key: string, options?: Record<string, unknown>) => string, buttonShortLabel: string): DataNode[] {
function toPermissionTreeData(nodes: PermissionNode[], buttonShortLabel: string): DataNode[] {
return nodes.map((node) => ({
key: node.permId,
title: (
@ -94,14 +108,63 @@ function toTreeData(nodes: PermissionNode[], t: (key: string, options?: Record<s
{node.permType === "button" ? <Tag color="cyan" style={{ fontSize: 10 }}>{buttonShortLabel}</Tag> : null}
</Space>
),
children: node.children?.length ? toTreeData(node.children, t, buttonShortLabel) : undefined
children: node.children?.length ? toPermissionTreeData(node.children, buttonShortLabel) : undefined
}));
}
function buildOrgTree(list: SysOrg[]): OrgTreeNode[] {
const map = new Map<number, OrgTreeNode>();
const roots: OrgTreeNode[] = [];
list.forEach((item) => {
map.set(item.id, { ...item, key: item.id, children: [] });
});
map.forEach((node) => {
if (node.parentId && map.has(node.parentId)) {
map.get(node.parentId)!.children!.push(node);
} else {
roots.push(node);
}
});
const sortNodes = (nodes: OrgTreeNode[]) => {
nodes.sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0));
nodes.forEach((node) => node.children && sortNodes(node.children));
};
sortNodes(roots);
return roots;
}
function toOrgTreeData(nodes: OrgTreeNode[]): DataNode[] {
return nodes.map((node) => ({
key: node.id,
title: node.orgName,
children: node.children?.length ? toOrgTreeData(node.children) : undefined
}));
}
function getDataScopeDescription(scopeType: string) {
switch (scopeType) {
case "ALL":
return "当前角色可访问当前租户全部数据。";
case "SELF":
return "当前角色仅可访问本人数据。";
case "DEPT":
return "当前角色可访问本人所在部门的数据。";
case "DEPT_AND_CHILD":
return "当前角色可访问本人所在部门及所有下级部门的数据。";
case "CUSTOM":
return "当前角色可访问选中部门的数据。";
default:
return "";
}
}
const generateRoleCode = () => `ROLE_${Date.now().toString(36).toUpperCase()}`;
export default function Roles() {
const { t } = useTranslation();
const { can } = usePermission();
const { items: statusDict } = useDict("sys_common_status");
const [loading, setLoading] = useState(false);
@ -119,21 +182,14 @@ export default function Roles() {
const [userSearchText, setUserSearchText] = useState("");
const [searchText, setSearchText] = useState("");
const [rolePage, setRolePage] = useState({ current: 1, size: DEFAULT_ROLE_PAGE_SIZE, total: 0 });
const handleSearch = () => {
setSearchText((value) => value.trim());
setRolePage((prev) => ({ ...prev, current: 1 }));
};
const handleResetSearch = () => {
setSearchText("");
setFilterTenantId(undefined);
setRolePage((prev) => ({ ...prev, current: 1 }));
};
const [filterTenantId, setFilterTenantId] = useState<number | undefined>(undefined);
const [drawerOpen, setDrawerOpen] = useState(false);
const [editing, setEditing] = useState<SysRole | null>(null);
const [tenants, setTenants] = useState<SysTenant[]>([]);
const [activeTab, setActiveTab] = useState<RoleTabKey>("permissions");
const [dataScopeType, setDataScopeType] = useState("SELF");
const [scopeOrgIds, setScopeOrgIds] = useState<number[]>([]);
const [scopeOrgTree, setScopeOrgTree] = useState<DataNode[]>([]);
const [form] = Form.useForm();
const isPlatformMode = useMemo(() => {
@ -144,12 +200,15 @@ export default function Roles() {
}, []);
const activeTenantId = useMemo(() => normalizeNumber(localStorage.getItem("activeTenantId")) ?? 0, []);
const buttonShortLabel = useMemo(() => {
const label = t("buttonShort");
return !label || label === "buttonShort" ? "BTN" : label;
}, [t]);
const permissionTreeData = useMemo(() => toTreeData(buildPermissionTree(permissions), t, buttonShortLabel), [buttonShortLabel, permissions, t]);
const permissionTreeData = useMemo(() => toPermissionTreeData(buildPermissionTree(permissions), BUTTON_SHORT_LABEL), [permissions]);
const filteredModalUsers = useMemo(() => {
const existingIds = new Set(roleUsers.map((user) => user.userId));
return allUsers.filter(
(user) =>
!existingIds.has(user.userId) &&
(user.username.toLowerCase().includes(userSearchText.toLowerCase()) || user.displayName.toLowerCase().includes(userSearchText.toLowerCase()))
);
}, [allUsers, roleUsers, userSearchText]);
useEffect(() => {
if (!isPlatformMode) return;
@ -160,23 +219,31 @@ export default function Roles() {
try {
const list = await listPermissions();
setPermissions(list || []);
return list || [];
} catch {
setPermissions([]);
return [] as SysPermission[];
}
};
const selectRole = async (role: SysRole) => {
const selectRole = async (role: SysRole, permissionList: SysPermission[] = permissions) => {
setSelectedRole(role);
setLoadingUsers(true);
try {
const ids = await listRolePermissions(role.roleId);
const [ids, users, dataScope, orgs] = await Promise.all([
listRolePermissions(role.roleId),
fetchUsersByRoleId(role.roleId),
getRoleDataScope(role.roleId),
listOrgs(role.tenantId)
]);
const normalized = (ids || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id));
const leafIds = normalized.filter((id) => !permissions.some((permission) => permission.parentId === id));
const leafIds = normalized.filter((id) => !permissionList.some((permission) => permission.parentId === id));
setSelectedPermIds(leafIds);
setHalfCheckedIds([]);
setLoadingUsers(true);
const users = await fetchUsersByRoleId(role.roleId);
setRoleUsers(users || []);
setDataScopeType(dataScope?.scopeType || role.dataScopeType || "SELF");
setScopeOrgIds((dataScope?.orgIds || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id)));
setScopeOrgTree(toOrgTreeData(buildOrgTree(orgs || [])));
} finally {
setLoadingUsers(false);
}
@ -185,6 +252,7 @@ export default function Roles() {
const loadRoles = async (page = rolePage.current, size = rolePage.size) => {
setLoading(true);
try {
const permissionList = await loadPermissions();
const response = await pageRoles({
current: page,
size,
@ -199,24 +267,26 @@ export default function Roles() {
setRoleUsers([]);
setSelectedPermIds([]);
setHalfCheckedIds([]);
setDataScopeType("SELF");
setScopeOrgIds([]);
setScopeOrgTree([]);
} else if (!selectedRole) {
await selectRole(roles[0]);
await selectRole(roles[0], permissionList);
} else {
const updated = roles.find((role) => role.roleId === selectedRole.roleId);
if (updated) {
setSelectedRole(updated);
await selectRole(updated, permissionList);
} else {
await selectRole(roles[0]);
await selectRole(roles[0], permissionList);
}
}
await loadPermissions();
} finally {
setLoading(false);
}
};
useEffect(() => {
loadRoles(rolePage.current, rolePage.size);
void loadRoles(rolePage.current, rolePage.size);
}, [filterTenantId, rolePage.current, rolePage.size, searchText]);
const loadAllUsers = async () => {
@ -229,7 +299,7 @@ export default function Roles() {
};
const openUserModal = () => {
loadAllUsers();
void loadAllUsers();
setSelectedUserKeys([]);
setUserModalOpen(true);
};
@ -237,33 +307,22 @@ export default function Roles() {
const handleAddUsers = async () => {
if (!selectedRole || selectedUserKeys.length === 0) return;
await bindUsersToRole(selectedRole.roleId, selectedUserKeys);
message.success(t("common.success"));
message.success("操作成功");
setUserModalOpen(false);
selectRole(selectedRole);
await selectRole(selectedRole);
};
const handleUnbindUser = async (userId: number) => {
if (!selectedRole) return;
if (selectedRole.roleCode === "TENANT_ADMIN" && roleUsers.length <= 1) {
message.warning(t("rolesExt.tenantAdminWarning"));
message.warning("租户管理员角色至少需要保留一个绑定用户");
return;
}
await unbindUserFromRole(selectedRole.roleId, userId);
message.success(t("common.success"));
selectRole(selectedRole);
message.success("操作成功");
await selectRole(selectedRole);
};
const filteredModalUsers = useMemo(() => {
const existingIds = new Set(roleUsers.map((user) => user.userId));
return allUsers.filter(
(user) =>
!existingIds.has(user.userId) &&
(user.username.toLowerCase().includes(userSearchText.toLowerCase()) || user.displayName.toLowerCase().includes(userSearchText.toLowerCase()))
);
}, [allUsers, roleUsers, userSearchText]);
const openCreate = () => {
setEditing(null);
form.resetFields();
@ -281,9 +340,9 @@ export default function Roles() {
const handleRemove = async (event: React.MouseEvent, id: number) => {
event.stopPropagation();
await deleteRole(id);
message.success(t("common.success"));
message.success("操作成功");
if (selectedRole?.roleId === id) setSelectedRole(null);
loadRoles(rolePage.current, rolePage.size);
await loadRoles(rolePage.current, rolePage.size);
};
const submitBasic = async () => {
@ -295,16 +354,17 @@ export default function Roles() {
roleName: values.roleName,
remark: values.remark,
status: values.status ?? DEFAULT_STATUS,
tenantId: values.tenantId
tenantId: values.tenantId,
dataScopeType: editing?.dataScopeType || "SELF"
};
if (editing) {
await updateRole(editing.roleId, payload);
} else {
await createRole(payload);
}
message.success(t("common.success"));
message.success("操作成功");
setDrawerOpen(false);
loadRoles(rolePage.current, rolePage.size);
await loadRoles(rolePage.current, rolePage.size);
} finally {
setSaving(false);
}
@ -319,33 +379,63 @@ export default function Roles() {
setSaving(true);
try {
await saveRolePermissions(selectedRole.roleId, Array.from(new Set([...selectedPermIds, ...halfCheckedIds])));
message.success(t("common.success"));
message.success("操作成功");
} finally {
setSaving(false);
}
};
const saveDataScope = async () => {
if (!selectedRole) return;
if (dataScopeType === "CUSTOM" && scopeOrgIds.length === 0) {
message.warning("请选择至少一个部门");
return;
}
setSaving(true);
try {
const payload: RoleDataScope = {
roleId: selectedRole.roleId,
scopeType: dataScopeType,
orgIds: dataScopeType === "CUSTOM" ? scopeOrgIds : []
};
await saveRoleDataScope(selectedRole.roleId, payload);
message.success("操作成功");
setSelectedRole((prev) => (prev ? { ...prev, dataScopeType } : prev));
setData((prev) => prev.map((item) => item.roleId === selectedRole.roleId ? { ...item, dataScopeType } : item));
} finally {
setSaving(false);
}
};
const handlePrimarySave = () => {
if (activeTab === "permissions") {
void savePermissions();
return;
}
if (activeTab === "dataScope") {
void saveDataScope();
}
};
const saveDisabled = !selectedRole || activeTab === "users" || (activeTab === "permissions" && !can("sys:role:permission:save")) || (activeTab === "dataScope" && !can("sys:role:update"));
const saveLabel = activeTab === "dataScope" ? "保存数据权限" : "保存";
return (
<div className="app-page roles-page-v2">
<PageHeader
title={t("roles.title")}
subtitle={t("roles.subtitle")}
/>
<PageHeader title="角色管理" subtitle="维护角色基础信息、功能权限、数据权限与成员绑定" />
<div className="app-page__page-actions">
{can("sys:role:create") && <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>{t("common.create")}</Button>}
{can("sys:role:create") && <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>{"新增角色"}</Button>}
</div>
<div className="roles-layout">
<Row gutter={24} className="roles-layout__row">
<Col span={7} className="roles-layout__side">
<Card title={<Space><ApartmentOutlined /><span>{t("rolesExt.roleList")}</span>
{/*<Badge count={rolePage.total} overflowCount={999} className="roles-count-badge" />*/}
</Space>} bordered={false} className="app-page__panel-card roles-side-card">
<Card title={<Space><ApartmentOutlined /><span>{"角色列表"}</span></Space>} bordered={false} className="app-page__panel-card roles-side-card">
<div className="role-search-panel">
{isPlatformMode && (
<Select
placeholder={t("rolesExt.filterTenant")}
placeholder="按租户筛选"
style={{ width: "100%" }}
allowClear
suffixIcon={<FilterOutlined />}
@ -355,8 +445,8 @@ export default function Roles() {
/>
)}
<div className="role-search-bar">
<Input placeholder={t("roles.searchPlaceholder")} prefix={<SearchOutlined style={{ color: "#94a3b8" }} />} value={searchText} onChange={(event) => setSearchText(event.target.value)} allowClear />
<Button type="default" onClick={handleResetSearch}>{t("common.reset")}</Button>
<Input placeholder="输入角色名称或编码搜索" prefix={<SearchOutlined style={{ color: "#94a3b8" }} />} value={searchText} onChange={(event) => setSearchText(event.target.value)} allowClear />
<Button type="default" onClick={() => { setSearchText(""); setFilterTenantId(undefined); setRolePage((prev) => ({ ...prev, current: 1 })); }}>{"重置"}</Button>
</div>
</div>
@ -365,17 +455,17 @@ export default function Roles() {
loading={loading}
dataSource={data}
pagination={false}
locale={{ emptyText: <Empty description={t("rolesExt.noRolesFound")} image={Empty.PRESENTED_IMAGE_SIMPLE} /> }}
locale={{ emptyText: <Empty description="暂无角色数据" image={Empty.PRESENTED_IMAGE_SIMPLE} /> }}
renderItem={(item) => (
<div key={item.roleId} className={`role-item-card-v3 ${selectedRole?.roleId === item.roleId ? "active" : ""}`} onClick={() => selectRole(item)}>
<div key={item.roleId} className={`role-item-card-v3 ${selectedRole?.roleId === item.roleId ? "active" : ""}`} onClick={() => void selectRole(item)}>
<div className="role-item-symbol" aria-hidden="true">
<SafetyCertificateOutlined />
</div>
<div className="role-item-main">
<div className="role-item-name-row">
<Text strong className="role-name">{item.roleName}</Text>
{isPlatformMode && <Tag color="blue" style={{ fontSize: 10, scale: "0.8", margin: "0 0 0 4px", borderRadius: "10px" }}>{item.tenantId === 0 ? t("rolesExt.systemTenant") : tenants.find((tenant) => tenant.id === item.tenantId)?.tenantName || `${t("rolesExt.tenantLabel")}:${item.tenantId}`}</Tag>}
{item.status === 0 && <Tag color="error" style={{ fontSize: 10, scale: "0.8", margin: 0 }}>{t("rolesExt.disabled")}</Tag>}
{isPlatformMode && <Tag color="blue" style={{ fontSize: 10, scale: "0.8", margin: "0 0 0 4px", borderRadius: "10px" }}>{item.tenantId === 0 ? "平台租户" : tenants.find((tenant) => tenant.id === item.tenantId)?.tenantName || `租户:${item.tenantId}`}</Tag>}
{item.status === 0 && <Tag color="error" style={{ fontSize: 10, scale: "0.8", margin: 0 }}>{"停用"}</Tag>}
</div>
<Text type="secondary" className="role-code">{item.roleCode}</Text>
</div>
@ -386,11 +476,11 @@ export default function Roles() {
) : null}
<div className="role-item-actions">
<Space size={4}>
<Tooltip title={t("common.edit")}>
<Tooltip title="编辑">
<Button type="text" size="small" icon={<EditOutlined />} onClick={(event) => openEditBasic(event, item)} />
</Tooltip>
{item.roleCode !== "ADMIN" && (
<Popconfirm title={t("rolesExt.deleteRole")} okText={t("common.confirm")} cancelText={t("common.cancel")} onConfirm={(event) => handleRemove(event!, item.roleId)}>
<Popconfirm title="确定删除该角色吗?" okText="确定" cancelText="取消" onConfirm={(event) => void handleRemove(event!, item.roleId)}>
<Button type="text" size="small" danger icon={<DeleteOutlined />} onClick={(event) => event.stopPropagation()} />
</Popconfirm>
)}
@ -420,10 +510,10 @@ export default function Roles() {
className="app-page__panel-card roles-detail-card"
bordered={false}
title={<div className="role-detail-header"><div className="role-detail-icon"><SafetyCertificateOutlined /></div><div className="role-detail-heading"><div className="role-detail-title">{selectedRole.roleName}</div><Text type="secondary" className="role-detail-code">{selectedRole.roleCode}</Text></div></div>}
extra={<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={savePermissions} disabled={!can("sys:role:permission:save") || (selectedRole.roleCode === "TENANT_ADMIN" && !isPlatformMode)} style={{ borderRadius: "6px" }}>{t("common.save")}</Button>}
extra={<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={handlePrimarySave} disabled={saveDisabled} style={{ borderRadius: "6px" }}>{saveLabel}</Button>}
>
<Tabs defaultActiveKey="permissions" className="role-detail-tabs">
<Tabs.TabPane tab={<Space><KeyOutlined />{t("roles.funcPerms")}</Space>} key="permissions">
<Tabs activeKey={activeTab} onChange={(key) => setActiveTab(key as RoleTabKey)} className="role-detail-tabs">
<Tabs.TabPane tab={<Space><KeyOutlined />{"功能权限"}</Space>} key="permissions">
<div className="role-detail-pane">
<div className="permission-tree-wrapper">
<Tree
@ -443,11 +533,40 @@ export default function Roles() {
</div>
</div>
</Tabs.TabPane>
<Tabs.TabPane tab={<Space><TeamOutlined />{t("rolesExt.membersTab")} ({roleUsers.length})</Space>} key="users">
<Tabs.TabPane tab={<Space><ApartmentOutlined />{"数据权限"}</Space>} key="dataScope">
<div className="role-detail-pane">
<div style={{ marginBottom: 16 }}>
<Radio.Group value={dataScopeType} onChange={(event) => setDataScopeType(event.target.value)} optionType="button" buttonStyle="solid">
{DATA_SCOPE_OPTIONS.map((item) => (
<Radio.Button key={item.value} value={item.value}>{item.label}</Radio.Button>
))}
</Radio.Group>
</div>
<div style={{ marginBottom: 16, color: "#64748b" }}>{getDataScopeDescription(dataScopeType)}</div>
{dataScopeType === "CUSTOM" ? (
<div className="permission-tree-wrapper">
<Tree
checkable
selectable={false}
treeData={scopeOrgTree}
checkedKeys={scopeOrgIds}
onCheck={(keys) => {
const checked = Array.isArray(keys) ? keys : keys.checked;
setScopeOrgIds(checked.map((key) => Number(key)));
}}
defaultExpandAll
/>
</div>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="当前范围不需要选择部门" />
)}
</div>
</Tabs.TabPane>
<Tabs.TabPane tab={<Space><TeamOutlined />{`成员管理 (${roleUsers.length})`}</Space>} key="users">
<div className="role-detail-pane">
<div className="role-members-toolbar">
<Title level={5} style={{ margin: 0 }}>{t("rolesExt.assignedUsers")}</Title>
<Button type="primary" ghost icon={<UserAddOutlined />} onClick={openUserModal} disabled={!can("sys:role:update")}>{t("rolesExt.bindUser")}</Button>
<Title level={5} style={{ margin: 0 }}>{"已绑定用户"}</Title>
<Button type="primary" ghost icon={<UserAddOutlined />} onClick={openUserModal} disabled={!can("sys:role:update")}>{"绑定用户"}</Button>
</div>
<Table
rowKey="userId"
@ -457,8 +576,8 @@ export default function Roles() {
pagination={{ pageSize: 10, size: "small" }}
columns={[
{
title: t("users.userInfo"),
render: (_: any, user: SysUser) => (
title: "用户信息",
render: (_: unknown, user: SysUser) => (
<Space>
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: "#f0f2f5", color: "#8c8c8c" }} />
<div>
@ -468,14 +587,14 @@ export default function Roles() {
</Space>
)
},
{ title: t("rolesExt.phone"), dataIndex: "phone", className: "tabular-nums" },
{ title: t("common.status"), dataIndex: "status", width: 80, render: (status: number) => <Tag color={status === 1 ? "green" : "red"}>{status === 1 ? t("logsExt.success") : t("rolesExt.disabled")}</Tag> },
{ title: "手机号", dataIndex: "phone", className: "tabular-nums" },
{ title: "状态", dataIndex: "status", width: 80, render: (status: number) => <Tag color={status === 1 ? "green" : "red"}>{status === 1 ? "启用" : "停用"}</Tag> },
{
title: t("common.action"),
title: "操作",
key: "action",
width: 80,
render: (_: any, user: SysUser) => (
<Popconfirm title={t("rolesExt.removeBinding")} okText={t("common.confirm")} cancelText={t("common.cancel")} onConfirm={() => handleUnbindUser(user.userId)} disabled={!can("sys:role:update")}>
render: (_: unknown, user: SysUser) => (
<Popconfirm title="确定解除该用户绑定吗?" okText="确定" cancelText="取消" onConfirm={() => void handleUnbindUser(user.userId)} disabled={!can("sys:role:update")}>
<Button type="text" danger size="small" icon={<DeleteOutlined />} disabled={!can("sys:role:update")} />
</Popconfirm>
)
@ -487,51 +606,38 @@ export default function Roles() {
</Tabs>
</Card>
) : (
<div className="app-page__empty-state"><Empty description={t("roles.selectRole")} /></div>
<div className="app-page__empty-state"><Empty description="请选择左侧角色查看详情" /></div>
)}
</Col>
</Row>
</div>
<Modal title={t("rolesExt.bindUsersToRole")} open={userModalOpen} onCancel={() => setUserModalOpen(false)} onOk={handleAddUsers} okText={t("common.confirm")} cancelText={t("common.cancel")} width={650} destroyOnClose>
<Modal title="绑定用户到角色" open={userModalOpen} onCancel={() => setUserModalOpen(false)} onOk={() => void handleAddUsers()} okText="确定" cancelText="取消" width={650} destroyOnClose>
<div style={{ marginBottom: 16 }}>
<Input placeholder={t("rolesExt.searchUser")} prefix={<SearchOutlined />} value={userSearchText} onChange={(event) => setUserSearchText(event.target.value)} allowClear />
<Input placeholder="搜索用户名或显示名称" prefix={<SearchOutlined />} value={userSearchText} onChange={(event) => setUserSearchText(event.target.value)} allowClear />
</div>
<Table rowKey="userId" size="small" dataSource={filteredModalUsers} pagination={{ pageSize: 6 }} rowSelection={{ selectedRowKeys: selectedUserKeys, onChange: (keys) => setSelectedUserKeys(keys as number[]) }} columns={[{ title: t("rolesExt.displayName"), dataIndex: "displayName" }, { title: t("users.username"), dataIndex: "username" }, { title: t("rolesExt.phone"), dataIndex: "phone" }]} />
<Table rowKey="userId" size="small" dataSource={filteredModalUsers} pagination={{ pageSize: 6 }} rowSelection={{ selectedRowKeys: selectedUserKeys, onChange: (keys) => setSelectedUserKeys(keys as number[]) }} columns={[{ title: "显示名称", dataIndex: "displayName" }, { title: "用户名", dataIndex: "username" }, { title: "手机号", dataIndex: "phone" }]} />
</Modal>
<Drawer title={editing ? t("roles.drawerTitleEdit") : t("roles.drawerTitleCreate")} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={420} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submitBasic}>{t("common.save")}</Button></div>}>
<Drawer title={editing ? "编辑角色" : "新增角色"} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={420} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{"取消"}</Button><Button type="primary" loading={saving} onClick={() => void submitBasic()}>{"保存"}</Button></div>}>
<Form form={form} layout="vertical">
<Form.Item label={t("rolesExt.tenantLabel")} name="tenantId" rules={[{ required: true }]} hidden={!isPlatformMode}>
<Form.Item label="租户" name="tenantId" rules={[{ required: true }]} hidden={!isPlatformMode}>
<Select options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} disabled={!!editing} />
</Form.Item>
<Form.Item label={t("roles.roleName")} name="roleName" rules={[{ required: true }]}>
<Input placeholder={t("rolesExt.enterRoleName")} />
<Form.Item label="角色名称" name="roleName" rules={[{ required: true }]}>
<Input placeholder="请输入角色名称" />
</Form.Item>
<Form.Item label={t("roles.roleCode")} name="roleCode" rules={[{ required: true }]}>
<Input placeholder={t("rolesExt.roleCodePlaceholder")} disabled={!!editing} />
<Form.Item label="角色编码" name="roleCode" rules={[{ required: true }]}>
<Input placeholder="请输入角色编码" disabled={!!editing} />
</Form.Item>
<Form.Item label={t("common.status")} name="status" initialValue={1}>
<Form.Item label="状态" name="status" initialValue={1}>
<Select options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} />
</Form.Item>
<Form.Item label={t("common.remark")} name="remark">
<Input.TextArea rows={4} placeholder={t("rolesExt.roleScopePlaceholder")} />
<Form.Item label="备注" name="remark">
<Input.TextArea rows={4} placeholder="请输入角色说明或适用范围" />
</Form.Item>
</Form>
</Drawer>
</div>
);
}

View File

@ -248,7 +248,7 @@ export default function Users() {
}
if (values.password) {
userPayload.passwordHash = values.password;
userPayload.password = values.password;
}
let userId = editing?.userId;

View File

@ -1,18 +1,21 @@
import { Avatar, Button, Card, Col, Form, Input, Row, Tabs, Tag, Typography, message } from "antd";
import { LockOutlined, SaveOutlined, SolutionOutlined, UserOutlined } from "@ant-design/icons";
import { Alert, Avatar, Button, Card, Col, Descriptions, Form, Input, Row, Space, Tabs, Tag, Typography, message } from "antd";
import { KeyOutlined, LockOutlined, ReloadOutlined, SaveOutlined, SolutionOutlined, UserOutlined } from "@ant-design/icons";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { getCurrentUser, updateMyPassword, updateMyProfile } from "@/api";
import { generateMyBotCredential, getCurrentUser, getMyBotCredential, updateMyPassword, updateMyProfile } from "@/api";
import PageHeader from "@/components/shared/PageHeader";
import type { UserProfile } from "@/types";
import type { BotCredential, UserProfile } from "@/types";
const { Title, Text } = Typography;
const { Paragraph, Title, Text } = Typography;
export default function Profile() {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [credentialLoading, setCredentialLoading] = useState(false);
const [credentialSaving, setCredentialSaving] = useState(false);
const [user, setUser] = useState<UserProfile | null>(null);
const [credential, setCredential] = useState<BotCredential | null>(null);
const [profileForm] = Form.useForm();
const [pwdForm] = Form.useForm();
@ -27,8 +30,19 @@ export default function Profile() {
}
};
const loadCredential = async () => {
setCredentialLoading(true);
try {
const data = await getMyBotCredential();
setCredential(data);
} finally {
setCredentialLoading(false);
}
};
useEffect(() => {
loadUser();
loadCredential();
}, []);
const handleUpdateProfile = async () => {
@ -55,6 +69,19 @@ export default function Profile() {
}
};
const handleGenerateCredential = async () => {
try {
setCredentialSaving(true);
const data = await generateMyBotCredential();
setCredential(data);
message.success(t("common.success"));
} finally {
setCredentialSaving(false);
}
};
const renderValue = (value?: string) => value || "-";
return (
<div className="app-page app-page--contained" style={{ maxWidth: 1024, width: "100%", margin: "0 auto" }}>
<PageHeader title={t("profile.title")} subtitle={t("profile.subtitle")} />
@ -134,6 +161,77 @@ export default function Profile() {
</div>
</Form>
)
},
{
key: "bot-credential",
label: <span><KeyOutlined /> {t("profile.botCredentialTab")}</span>,
children: (
<div style={{ marginTop: 16 }}>
<Space direction="vertical" size={16} style={{ width: "100%" }}>
<Alert
type="info"
showIcon
message={t("profile.botCredentialHint")}
description={t("profile.botCredentialHintDesc")}
/>
<Descriptions
bordered
size="middle"
column={1} items={[
{
key: "bind-status",
label: t("profile.botBindStatus"),
children: credential?.bound
? <Tag color="success">{t("profile.botBound")}</Tag>
: <Tag>{t("profile.botUnbound")}</Tag>
},
{
key: "bot-id",
label: "X-Bot-Id",
children: credential?.botId ? (
<Paragraph copyable={{ text: credential.botId }} style={{ marginBottom: 0 }}>
{credential.botId}
</Paragraph>
) : "-"
},
{
key: "bot-secret",
label: "X-Bot-Secret",
children: credential?.botSecret ? (
<Paragraph copyable={{ text: credential.botSecret }} style={{ marginBottom: 0 }}>
{credential.botSecret}
</Paragraph>
) : t("profile.botSecretHidden")
},
{
key: "last-access-time",
label: t("profile.botLastAccessTime"),
children: renderValue(credential?.lastAccessTime)
},
{
key: "last-access-ip",
label: t("profile.botLastAccessIp"),
children: renderValue(credential?.lastAccessIp)
}
]}
/>
<div className="app-page__page-actions" style={{ margin: "8px 0 0" }}>
<Button
type="primary"
icon={credential?.bound ? <ReloadOutlined /> : <KeyOutlined />}
loading={credentialSaving}
onClick={handleGenerateCredential}
>
{credential?.bound
? t("profile.regenerateBotCredential")
: t("profile.generateBotCredential")}
</Button>
</div>
</Space>
</div>
)
}
]}
/>
@ -143,3 +241,5 @@ export default function Profile() {
</div>
);
}

View File

@ -57,7 +57,7 @@ export default function SysParams() {
const openCreate = () => {
setEditing(null);
form.resetFields();
form.setFieldsValue({ isSystem: false, status: 1 });
form.setFieldsValue({ isSystem: 0, status: 1 });
setDrawerOpen(true);
};

View File

@ -12,6 +12,7 @@ export interface SysUser extends BaseEntity {
displayName: string;
email?: string;
phone?: string;
password?: string;
passwordHash?: string;
tenantId: number;
orgId?: number;
@ -33,12 +34,28 @@ export interface UserProfile {
pwdResetRequired?: number;
}
export interface BotCredential {
bound: boolean;
botId?: string;
botSecret?: string;
status?: string;
expireTime?: string;
lastAccessTime?: string;
lastAccessIp?: string;
}
export interface SysRole extends BaseEntity {
roleId: number;
roleCode: string;
roleName: string;
remark?: string;
dataScopeType?: string;
}
export interface RoleDataScope {
roleId?: number;
scopeType: string;
orgIds: number[];
}
export interface SysPermission extends BaseEntity {
@ -167,4 +184,3 @@ export interface MenuRoute {
element: ReactNode;
perm?: string;
}

8
node_modules/.vite/deps/_metadata.json generated vendored 100644
View File

@ -0,0 +1,8 @@
{
"hash": "87263cf7",
"configHash": "1878c139",
"lockfileHash": "e3b0c442",
"browserHash": "4129e467",
"optimized": {},
"chunks": {}
}

3
node_modules/.vite/deps/package.json generated vendored 100644
View File

@ -0,0 +1,3 @@
{
"type": "module"
}