perf: 用户名支持修改

v2
xiaojunnuo 2024-12-23 14:47:27 +08:00
parent b150b2f034
commit 89c7f07034
5 changed files with 333 additions and 274 deletions

View File

@ -75,7 +75,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
{ max: 50, message: "最大50个字符" } { max: 50, message: "最大50个字符" }
] ]
}, },
editForm: { component: { disabled: true } }, editForm: { component: { disabled: false } },
column: { column: {
sorter: true, sorter: true,
width: 200 width: 200
@ -107,11 +107,35 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
sorter: true sorter: true
} }
}, },
email: {
title: "邮箱",
type: "text",
search: { show: true }, // 开启查询
form: {
rules: [{ max: 50, message: "最大50个字符" }]
},
column: {
sorter: true,
width: 160
}
},
mobile: {
title: "手机号",
type: "text",
search: { show: true }, // 开启查询
form: {
rules: [{ max: 50, message: "最大50个字符" }]
},
column: {
sorter: true,
width: 130
}
},
avatar: { avatar: {
title: "头像", title: "头像",
type: "cropper-uploader", type: "cropper-uploader",
column: { column: {
width: 100, width: 70,
component: { component: {
//设置高度,修复操作列错位的问题 //设置高度,修复操作列错位的问题
style: { style: {

View File

@ -1,75 +1,54 @@
import { request } from "/src/api/service"; import { request } from "/src/api/service";
const apiPrefix = "/sys/suite/userSuite"; export function createApi() {
const apiPrefix = "/sys/suite/userSuites";
export async function GetList(query: any) { return {
async GetList(query: any) {
return await request({ return await request({
url: apiPrefix + "/page", url: apiPrefix + "/page",
method: "post", method: "post",
data: query data: query
}); });
} },
export async function AddObj(obj: any) { async AddObj(obj: any) {
return await request({ return await request({
url: apiPrefix + "/add", url: apiPrefix + "/add",
method: "post", method: "post",
data: obj data: obj
}); });
} },
export async function UpdateObj(obj: any) { async UpdateObj(obj: any) {
return await request({ return await request({
url: apiPrefix + "/update", url: apiPrefix + "/update",
method: "post", method: "post",
data: obj data: obj
}); });
} },
export async function DelObj(id: any) { async DelObj(id: number) {
return await request({ return await request({
url: apiPrefix + "/delete", url: apiPrefix + "/delete",
method: "post", method: "post",
params: { id } params: { id }
}); });
} },
export async function GetObj(id: any) { async GetObj(id: number) {
return await request({ return await request({
url: apiPrefix + "/info", url: apiPrefix + "/info",
method: "post", method: "post",
params: { id } params: { id }
}); });
},
async ListAll() {
return await request({
url: apiPrefix + "/all",
method: "post"
});
}
};
} }
export async function GetDetail(id: any) { export const pipelineGroupApi = createApi();
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id }
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids }
});
}
export async function SetDefault(id: any) {
return await request({
url: apiPrefix + "/setDefault",
method: "post",
data: { id }
});
}
export async function SetDisabled(id: any, disabled: boolean) {
return await request({
url: apiPrefix + "/setDisabled",
method: "post",
data: { id, disabled }
});
}

View File

@ -1,249 +1,321 @@
import * as api from "./api"; import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n"; import { pipelineGroupApi } from "./api";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud"; import SuiteValueEdit from "/@/views/sys/suite/product/suite-value-edit.vue";
import { useUserStore } from "/@/store/modules/user"; import SuiteValue from "/@/views/sys/suite/product/suite-value.vue";
import { useSettingStore } from "/@/store/modules/settings"; import DurationValue from "/@/views/sys/suite/product/duration-value.vue";
import { Modal } from "ant-design-vue"; import dayjs from "dayjs";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet { export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter(); const api = pipelineGroupApi;
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => { const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query); return await api.GetList(query);
}; };
const editRequest = async ({ form, row }: EditReq) => { const editRequest = async (req: EditReq) => {
const { form, row } = req;
form.id = row.id; form.id = row.id;
const res = await api.UpdateObj(form); const res = await api.UpdateObj(form);
return res; return res;
}; };
const delRequest = async ({ row }: DelReq) => { const delRequest = async (req: DelReq) => {
const { row } = req;
return await api.DelObj(row.id); return await api.DelObj(row.id);
}; };
const addRequest = async ({ form }: AddReq) => { const addRequest = async (req: AddReq) => {
const { form } = req;
const res = await api.AddObj(form); const res = await api.AddObj(form);
return res; return res;
}; };
const userStore = useUserStore(); const router = useRouter();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return { return {
crudOptions: { crudOptions: {
settings: {
plugins: {
//这里使用行选择插件生成行选择crudOptions配置最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys
}
}
}
},
request: { request: {
pageRequest, pageRequest,
addRequest, addRequest,
editRequest, editRequest,
delRequest delRequest
}, },
form: {
labelCol: {
//固定label宽度
span: null,
style: {
width: "100px"
}
},
col: {
span: 22
},
wrapper: {
width: 600
}
},
actionbar: {
buttons: {
add: { show: false },
buy: {
text: "购买",
type: "primary",
click() {
router.push({
path: "/certd/suite/buy"
});
}
}
}
},
rowHandle: { rowHandle: {
minWidth: 200, width: 200,
fixed: "right" fixed: "right",
buttons: {
view: { show: false },
copy: { show: false },
edit: { show: false },
remove: { show: false }
// continue:{
// text:"续期",
// type:"link",
// click(){
// console.log("续期");
// }
// }
}
}, },
columns: { columns: {
id: { id: {
title: "ID", title: "ID",
key: "id", key: "id",
type: "number", type: "number",
search: {
show: false
},
column: { column: {
width: 100 width: 100,
editable: {
disabled: true
}
}, },
form: { form: {
show: false show: false
} }
}, },
domain: { title: {
title: "CNAME域名", title: "套餐名称",
type: "text", type: "text",
editForm: {
component: {
disabled: true
}
},
search: { search: {
show: true show: true
}, },
form: { form: {
component: {
placeholder: "cname.handsfree.work"
},
helper: "需要一个右边DNS提供商注册的域名也可以将其他域名的dns服务器转移到这几家来。\nCNAME域名一旦确定不可修改建议使用一级子域名",
rules: [{ required: true, message: "此项必填" }] rules: [{ required: true, message: "此项必填" }]
}, },
column: { column: {
width: 200 width: 200
} }
}, },
dnsProviderType: { productType: {
title: "DNS提供商", title: "类型",
type: "dict-select", type: "dict-select",
search: { editForm: {
show: true component: {
disabled: true
}
}, },
dict: dict({ dict: dict({
url: "pi/dnsProvider/list", data: [
value: "key", { label: "套餐", value: "suite", color: "green" },
label: "title" { label: "加量包", value: "addon", color: "blue" }
]
}), }),
form: { form: {
rules: [{ required: true, message: "此项必填" }] rules: [{ required: true, message: "此项必填" }]
}, },
column: { column: {
width: 150, width: 80,
component: { align: "center"
color: "auto" },
valueBuilder: ({ row }) => {
if (row.content) {
row.content = JSON.parse(row.content);
}
},
valueResolve: ({ form }) => {
if (form.content) {
form.content = JSON.stringify(form.content);
} }
} }
}, },
accessId: { "content.maxDomainCount": {
title: "DNS提供商授权", title: "域名数量",
type: "dict-select", type: "text",
dict: dict({
url: "/pi/access/list",
value: "id",
label: "name"
}),
form: { form: {
key: ["content", "maxDomainCount"],
component: { component: {
name: "access-selector", name: SuiteValueEdit,
vModel: "modelValue", vModel: "modelValue",
type: compute(({ form }) => { unit: "个"
return form.dnsProviderType;
})
}, },
rules: [{ required: true, message: "此项必填" }] rules: [{ required: true, message: "此项必填" }]
}, },
column: { column: {
width: 150, width: 100,
component: { component: {
color: "auto" name: SuiteValue,
vModel: "modelValue",
unit: "个"
},
align: "center"
}
},
"content.maxPipelineCount": {
title: "流水线数量",
type: "text",
form: {
key: ["content", "maxPipelineCount"],
component: {
name: SuiteValueEdit,
vModel: "modelValue",
unit: "条"
},
rules: [{ required: true, message: "此项必填" }]
},
column: {
width: 100,
component: {
name: SuiteValue,
vModel: "modelValue",
unit: "条"
},
align: "center"
}
},
"content.maxDeployCount": {
title: "部署次数",
type: "text",
form: {
key: ["content", "maxDeployCount"],
component: {
name: SuiteValueEdit,
vModel: "modelValue",
unit: "次"
},
rules: [{ required: true, message: "此项必填" }]
},
column: {
width: 100,
component: {
name: SuiteValue,
vModel: "modelValue",
unit: "次"
},
align: "center"
}
},
"content.maxMonitorCount": {
title: "证书监控数量",
type: "text",
form: {
key: ["content", "maxMonitorCount"],
component: {
name: SuiteValueEdit,
vModel: "modelValue",
unit: "个"
},
rules: [{ required: true, message: "此项必填" }]
},
column: {
width: 120,
component: {
name: SuiteValue,
vModel: "modelValue",
unit: "个"
},
align: "center"
}
},
duration: {
title: "时长",
type: "text",
form: {},
column: {
component: {
name: DurationValue,
vModel: "modelValue"
},
width: 100,
align: "center"
}
},
status: {
title: "状态",
type: "text",
form: { show: false },
column: {
width: 100,
align: "center",
conditionalRender: {
match() {
return false;
}
},
cellRender({ row }) {
if (row.activeTime == null) {
return <a-tag color="blue">使</a-tag>;
}
const now = dayjs().valueOf();
//已过期
const isExpired = row.expiresTime != -1 && now > row.expiresTime;
if (isExpired) {
return <a-tag color="error"></a-tag>;
}
//如果在激活时间之前
if (now < row.activeTime) {
return <a-tag color="blue"></a-tag>;
}
// 是否在激活时间和过期时间之间
if (now > row.activeTime && (row.expiresTime == -1 || now < row.expiresTime)) {
return <a-tag color="success"></a-tag>;
}
} }
} }
}, },
isDefault: { activeTime: {
title: "是否默认", title: "激活时间",
type: "date",
column: {
width: 150
}
},
expiresTime: {
title: "过期时间",
type: "date",
column: {
width: 150,
component: {
name: "expires-time-text",
vModel: "value",
mode: "tag"
}
}
},
isPresent: {
title: "是否赠送",
type: "dict-switch", type: "dict-switch",
dict: dict({ dict: dict({
data: [ data: [
{ label: "是", value: true, color: "success" }, { label: "是", value: true, color: "success" },
{ label: "否", value: false, color: "default" } { label: "否", value: false, color: "blue" }
] ]
}), }),
form: { form: {
value: false, value: true
rules: [{ required: true, message: "请选择是否默认" }]
},
column: {
align: "center",
width: 100
}
},
setDefault: {
title: "设置默认",
type: "text",
form: {
show: false
}, },
column: { column: {
width: 100, width: 100,
align: "center",
conditionalRenderDisabled: true,
cellRender: ({ row }) => {
if (row.isDefault) {
return;
}
const onClick = async () => {
Modal.confirm({
title: "提示",
content: `确定要设置为默认吗?`,
onOk: async () => {
await api.SetDefault(row.id);
await crudExpose.doRefresh();
}
});
};
return (
<a-button type={"link"} size={"small"} onClick={onClick}>
</a-button>
);
}
}
},
disabled: {
title: "禁用/启用",
type: "dict-switch",
dict: dict({
data: [
{ label: "启用", value: false, color: "success" },
{ label: "禁用", value: true, color: "error" }
]
}),
form: {
value: false
},
column: {
width: 100,
component: {
title: "点击可禁用/启用",
on: {
async click({ value, row }) {
Modal.confirm({
title: "提示",
content: `确定要${!value ? "禁用" : "启用"}吗?`,
onOk: async () => {
await api.SetDisabled(row.id, !value);
await crudExpose.doRefresh();
}
});
}
}
}
}
},
createTime: {
title: "创建时间",
type: "datetime",
form: {
show: false
},
column: {
sorter: true,
width: 160,
align: "center" align: "center"
} }
},
updateTime: {
title: "更新时间",
type: "datetime",
form: {
show: false
},
column: {
show: true,
width: 160
}
} }
} }
} }

View File

@ -1,57 +1,30 @@
<template> <template>
<fs-page class="page-cert"> <fs-page>
<template #header> <template #header>
<div class="title"> <div class="title">
CNAME服务配置 用户套餐
<span class="sub"> <span class="sub">管理用户的套餐</span>
此处配置的域名作为其他域名校验的代理当别的域名需要申请证书时通过CNAME映射到此域名上来验证所有权好处是任何域名都可以通过此方式申请证书也无需填写AccessSecret
<a href="https://certd.docmirror.cn/guide/feature/cname/" target="_blank">CNAME功能原理及使用说明</a>
</span>
</div> </div>
</template> </template>
<fs-crud ref="crudRef" v-bind="crudBinding"> <fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<template #pagination-left>
<a-tooltip title="批量删除">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page> </fs-page>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted } from "vue"; import { defineComponent, onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud"; import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud"; import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue"; import { createApi } from "./api";
import { DeleteBatch } from "./api";
defineOptions({ defineOptions({
name: "CnameProvider" name: "UserSuites"
}); });
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions }); const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: "确认",
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info("删除成功");
crudExpose.doRefresh();
selectedRowKeys.value = [];
}
});
} else {
message.error("请先勾选记录");
}
};
// //
onMounted(() => { onMounted(() => {
crudExpose.doRefresh(); crudExpose.doRefresh();
}); });
onActivated(() => {
crudExpose.doRefresh();
});
</script> </script>
<style lang="less"></style>

View File

@ -1,6 +1,6 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core'; import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm'; import { InjectEntityModel } from '@midwayjs/typeorm';
import { MoreThan, Repository } from 'typeorm'; import { MoreThan, Not, Repository } from 'typeorm';
import { UserEntity } from '../entity/user.js'; import { UserEntity } from '../entity/user.js';
import * as _ from 'lodash-es'; import * as _ from 'lodash-es';
import { BaseService, CommonException, Constants, FileService, SysInstallInfo, SysSettingsService } from '@certd/lib-server'; import { BaseService, CommonException, Constants, FileService, SysInstallInfo, SysSettingsService } from '@certd/lib-server';
@ -100,7 +100,18 @@ export class UserService extends BaseService<UserEntity> {
throw new CommonException('用户不存在'); throw new CommonException('用户不存在');
} }
delete param.username; if (param.username) {
const username = param.username;
const id = param.id;
const old = await this.findOne([
{ username: username, id: Not(id) },
{ mobile: username, id: Not(id) },
{ email: username, id: Not(id) },
]);
if (old != null) {
throw new CommonException('用户名已被占用');
}
}
if (!_.isEmpty(param.password)) { if (!_.isEmpty(param.password)) {
param.passwordVersion = 2; param.passwordVersion = 2;
param.password = await this.genPassword(param.password, param.passwordVersion); param.password = await this.genPassword(param.password, param.passwordVersion);