restore new ui files

This commit is contained in:
RockYang
2024-03-15 11:13:02 +08:00
344 changed files with 56769 additions and 887 deletions

View File

@@ -6,4 +6,4 @@ declare module "*.vue" {
export default component;
}
declare const __AUTH_KEY: string;
declare const __AUTH_KEY: string;

View File

@@ -1,6 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<head>
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

View File

@@ -1,5 +1,5 @@
{
"name": "@gpt-vue-projects/vue-admin",
"name": "@chatgpt-plus-projects/admin",
"version": "0.0.0",
"private": true,
"type": "module",
@@ -13,7 +13,7 @@
},
"dependencies": {
"@arco-design/web-vue": "^2.54.6",
"@gpt-vue/packages": "workspace:^1.0.0",
"@chatgpt-plus/packages": "workspace:^1.0.0",
"echarts": "^5.5.0",
"md-editor-v3": "^2.2.1",
"pinia": "^2.1.7",

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { computed } from "vue";
import { Message, type SwitchInstance } from "@arco-design/web-vue";
import type { BaseResponse } from "@gpt-vue/packages/type";
import type { BaseResponse } from "@chatgpt-plus/packages/type";
type OriginProps = SwitchInstance["$props"];

View File

@@ -0,0 +1,10 @@
<script lang="ts" setup>
import { hasPermission } from "@/directives/permission";
defineProps<{
permission: string | string[] | true;
}>();
</script>
<template>
<slot v-if="hasPermission(permission)" />
<slot v-else name="none" />
</template>

View File

@@ -38,30 +38,32 @@ onActivated(handleSearch);
<template>
<div class="search-table">
<div class="search-table-header">
<div>
<slot name="header-title">{{ props.headerTitle }}</slot>
</div>
<div class="header-option">
<slot name="header-option" :formData="formData" :reload="handleSearch" />
<div class="search-table-header-option">
<div>
<slot name="header-title">{{ props.headerTitle }}</slot>
</div>
<div class="header-option">
<slot name="header-option" :formData="formData" :reload="handleSearch" />
</div>
</div>
<FormSection
v-model="formData"
:columns="columns"
:submitting="tableConfig.loading as boolean"
@request="handleSearch"
>
<template v-for="slot in Object.keys($slots)" #[slot]="config">
<slot :name="slot" v-bind="{ ...config, reload: handleSearch }" />
</template>
</FormSection>
</div>
<FormSection
v-model="formData"
:columns="columns"
:submitting="tableConfig.loading as boolean"
@request="handleSearch"
>
<template v-for="slot in Object.keys($slots)" #[slot]="config">
<slot :name="slot" v-bind="{ ...config, reload: handleSearch }" />
</template>
</FormSection>
<div ref="tableContainerRef" class="search-table-container">
<ATable
v-bind="{
...$attrs,
...tableConfig,
...props,
scroll: useTableScroll(_columns, tableContainerRef as HTMLElement),
scroll: useTableScroll(_columns),
columns: _columns,
}"
>
@@ -74,14 +76,18 @@ onActivated(handleSearch);
</template>
<style scoped>
.search-table {
display: flex;
flex-direction: column;
position: relative;
height: 100%;
}
.search-table-container {
flex: 1;
position: relative;
z-index: 1;
}
.search-table-header {
background: #fff;
z-index: 2;
}
.search-table-header-option {
display: flex;
align-items: center;
justify-content: space-between;

View File

@@ -1,6 +1,6 @@
import { computed, onMounted, reactive, unref, type Ref } from "vue";
import type { TableInstance } from "@arco-design/web-vue";
import type { BaseResponse, ListResponse } from "@gpt-vue/packages/type";
import type { BaseResponse, ListResponse } from "@chatgpt-plus/packages/type";
export type TableOriginalProps = TableInstance["$props"];
export type TableRequest<T extends Record<string, unknown>> = (params?: any) => Promise<BaseResponse<ListResponse<T>>>

View File

@@ -1,20 +1,11 @@
import type { TableColumnData } from "@arco-design/web-vue";
import type { SearchTableColumns, SearchColumns } from "./type";
export function useTableXScroll(columns: TableColumnData[]) {
return columns.reduce((prev, curr) => {
const width = curr.width ?? 150;
return prev + width;
}, 0);
}
export function useTableScroll(columns: SearchTableColumns[], container?: HTMLElement) {
export function useTableScroll(columns: SearchTableColumns[]) {
const x = columns.reduce((prev, curr) => {
const width = curr.hideInTable ? 0 : curr.width ?? 150;
return prev + width;
}, 0);
const y = container?.clientHeight ?? undefined;
return { x, y };
return { x };
}
export function getDefaultFormData(columns: SearchTableColumns[]) {

View File

@@ -1,6 +1,6 @@
import { computed, onMounted, reactive, unref } from "vue";
import type { TableInstance } from "@arco-design/web-vue";
import type { BaseResponse } from "@gpt-vue/packages/type";
import type { BaseResponse } from "@chatgpt-plus/packages/type";
export type TableOriginalProps = TableInstance["$props"];
export type TableRequest<T extends Record<string, unknown>> = (

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import { defineComponent, h } from "vue";
import type { Component, PropType } from "vue";
import type { RouteRecordRaw } from "vue-router";
import { SubMenu, MenuItem } from "@arco-design/web-vue";
const CustomMenuItem: Component = defineComponent({
props: {
tree: {
type: Array as PropType<RouteRecordRaw[]>,
default: () => [],
},
},
setup: (props) => {
return () =>
props.tree?.map((item) => {
const _icon = item.meta?.icon ? h(item.meta.icon) : undefined;
const hasChildren = Array.isArray(item.children) && item.children.length;
if (hasChildren) {
return h(
SubMenu,
{ title: item.meta.title, key: item.name },
{
default: () => h(CustomMenuItem, { tree: item.children }),
icon: () => _icon,
}
);
}
return h(
MenuItem,
{ key: item.name },
{ default: () => item.meta.title, icon: () => _icon }
);
});
},
});
</script>
<script lang="ts" setup>
import { computed } from "vue";
import { useRoute } from "vue-router";
import router from "@/router";
import menu from "@/router/menu";
import { hasPermission } from "@/directives/permission";
defineProps({
width: {
type: [Number, String],
default: 200,
},
});
const route = useRoute();
const goto = (name: string) => router.push({ name });
const selectedKeys = computed(() => [route.name]);
const showMenu = computed(() => menu.filter((item: any) => hasPermission(item.meta?.permission)));
</script>
<template>
<ALayoutSider :style="{ width, height: '100%' }">
<AMenu :selectedKeys="selectedKeys" auto-open-selected @menu-item-click="goto">
<CustomMenuItem :tree="showMenu" />
</AMenu>
</ALayoutSider>
</template>

View File

@@ -1,44 +1,44 @@
import usePopup, { type Config } from "./usePopup";
import { Message } from "@arco-design/web-vue";
import type { Component } from "vue";
import type { BaseResponse } from "@gpt-vue/packages/type";
interface Arg {
reload?: () => void;
record?: Record<string, any>;
}
export default function (
node: Component,
api: (params?: any) => Promise<BaseResponse<any>>,
config?: Config
): (arg: Arg) => void {
const nodeProps = (arg: Arg[]) => {
return {
data: arg[0].record || {},
...config.nodeProps?.(arg),
};
};
const popupProps = (arg: Arg[], getExposed) => {
return {
width: 750,
maskClosable: false,
onBeforeOk: async () => {
const exposed = getExposed();
const validateRes = await exposed?.formRef.value.validate();
if (validateRes) {
return false;
}
const { code } = await api(exposed?.form.value);
if (code === 0) {
Message.success("操作成功");
}
arg[0]?.reload?.();
return code === 0;
},
...config.popupProps?.(arg, getExposed),
};
};
return usePopup(node, { nodeProps, popupProps });
}
import usePopup, { type Config } from "./usePopup";
import { Message } from "@arco-design/web-vue";
import type { Component } from "vue";
import type { BaseResponse } from "@chatgpt-plus/packages/type";
interface Arg {
reload?: () => void;
record?: Record<string, any>;
}
export default function (
node: Component,
api: (params?: any) => Promise<BaseResponse<any>>,
config?: Config
): (arg: Arg) => void {
const nodeProps = (arg: Arg[]) => {
return {
data: arg[0].record || {},
...config.nodeProps?.(arg),
};
};
const popupProps = (arg: Arg[], getExposed) => {
return {
width: 750,
maskClosable: false,
onBeforeOk: async () => {
const exposed = getExposed();
const validateRes = await exposed?.formRef.value.validate();
if (validateRes) {
return false;
}
const { code } = await api(exposed?.form.value);
if (code === 0) {
Message.success("操作成功");
}
arg[0]?.reload?.();
return code === 0;
},
...config.popupProps?.(arg, getExposed),
};
};
return usePopup(node, { nodeProps, popupProps });
}

View File

@@ -1,6 +1,6 @@
import { ref } from "vue";
import type { Ref } from "vue";
import type { BaseResponse } from "@gpt-vue/packages/type";
import type { BaseResponse } from "@chatgpt-plus/packages/type";
type Request<T> = (params?: any) => Promise<BaseResponse<T>>
function useRequest<T>(request: Request<T>) {

View File

@@ -1,6 +1,5 @@
import { ref, reactive, unref } from "vue";
import { Message } from "@arco-design/web-vue";
import type { BaseResponse } from "@gpt-vue/packages/type";
import type { BaseResponse } from "@chatgpt-plus/packages/type";
function useSubmit<T extends Record<string, any> = Record<string, any>, R = any>(defaultData?: T) {
const formRef = ref();
const formData = reactive<T | Record<string, any>>({ ...defaultData ?? {} });
@@ -10,14 +9,18 @@ function useSubmit<T extends Record<string, any> = Record<string, any>, R = any>
submitting.value = true;
try {
const hasError = await formRef.value?.validate();
if (!hasError) {
const { data, message } = await api({ ...formData ?? {}, ...unref(params) });
message && Message.success(message);
return Promise.resolve({ formData, data });
if (hasError) {
return Promise.reject({ validateErrors: hasError });
}
return Promise.reject(false);
const { data, code, message } = await api({ ...formData ?? {}, ...unref(params) });
if (code) {
return Promise.reject({ requestErrors: message })
}
return Promise.resolve({ formData, data });
} catch (err) {
return Promise.reject(err);
return Promise.reject({ errors: err });
} finally {
submitting.value = false;
}

View File

@@ -0,0 +1,32 @@
import { useAuthStore } from "@/stores/auth";
// 判断操作权限
export function hasPermission(permissionTag: string | string[] | boolean) {
const authStore = useAuthStore();
const { is_super_admin, permissions = [] } = authStore;
if (is_super_admin) {
return true;
}
if (Array.isArray(permissionTag)) {
return permissionTag.every((tag) => permissions.includes(tag));
}
if (typeof permissionTag === "string") {
return permissions.includes(permissionTag);
}
return permissionTag;
}
function checkPermission(el, binding) {
if (!hasPermission(binding.value)) {
el.parentNode && el.parentNode.removeChild(el);
}
}
export const permission = {
mounted(el, binding) {
checkPermission(el, binding);
},
updated(el, binding) {
checkPermission(el, binding);
},
};

View File

@@ -1,25 +1,33 @@
import router from "@/router";
import { Notification } from "@arco-design/web-vue";
import createInstance from "@gpt-vue/packages/request"
import type { BaseResponse } from "@gpt-vue/packages/type";
import createInstance from "@chatgpt-plus/packages/request"
import type { BaseResponse } from "@chatgpt-plus/packages/type";
export const uploadUrl = import.meta.env.VITE_PROXY_BASE_URL + "/api/admin/upload";
export const instance = createInstance(import.meta.env.VITE_PROXY_BASE_URL)
instance.interceptors.request.use((config) => {
config.headers[__AUTH_KEY] = localStorage.getItem(__AUTH_KEY);
config.headers["Authorization"] = localStorage.getItem(__AUTH_KEY);
const TOKEN = JSON.parse(localStorage.getItem(__AUTH_KEY))?.token
config.headers[__AUTH_KEY] = TOKEN;
config.headers["Authorization"] = TOKEN;
return config;
});
instance.interceptors.response.use(
(response) => {
const { data }: { data: BaseResponse<unknown> } = response
if (data && typeof data === "object" && data.code !== 0) {
if (data.code === 400) {
localStorage.removeItem(__AUTH_KEY);
router.push({ name: "Login" })
if (data && typeof data === "object" && data.code > 0) {
switch (data.code) {
case 400: {
localStorage.removeItem(__AUTH_KEY);
router.push({ name: "Login" })
break;
}
case 403: {
router.replace({ name: "403" })
break;
}
}
Notification.error(data.message ?? '未知错误')
}

View File

@@ -1,9 +1,6 @@
import http from "@/http/config";
export const userLogin = (data: {
username: string;
password: string;
}) => {
export const userLogin = (data) => {
return http({
url: "/api/admin/login",
method: "post",
@@ -26,7 +23,7 @@ export const getSession = () => {
};
export const loginLog = (params?: Record<string, unknown>) => {
export const loginLog = (params) => {
return http({
url: "/api/admin/user/loginLog",
method: "get",

View File

@@ -3,6 +3,8 @@ import { createPinia } from "pinia";
import ArcoVue from "@arco-design/web-vue";
import ArcoVueIcon from "@arco-design/web-vue/es/icon";
import "@arco-design/web-vue/dist/arco.css";
import PermissionRender from "@/components/PermissionRender.vue";
import { permission } from "@/directives/permission";
import App from "./App.vue";
import router from "./router";
@@ -14,6 +16,9 @@ app.use(router);
app.use(ArcoVue);
app.use(ArcoVueIcon);
app.component("PermissionRender", PermissionRender);
app.directive("permission", permission);
app.mount("#app");
app.config.warnHandler = (msg, vm, trace) => {
if (msg.includes('Invalid prop name: "key" is a reserved property.')) {

View File

@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
import { useAuthStore } from "@/stores/auth";
import CustomLayout from '@/components/CustomLayout.vue'
import { hasPermission } from "@/directives/permission";
import menu from './menu'
const whiteListRoutes = [
@@ -17,20 +18,27 @@ const whiteListRoutes = [
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: CustomLayout,
redirect: () => menu[0].path,
children: menu
children: [
{
path: "403",
name: "403",
component: () => import("@/views/NoPermission.vue"),
},
...menu
]
},
...whiteListRoutes
]
})
const whiteList = whiteListRoutes.map((i) => i.name);
const whiteList = [...whiteListRoutes.map((i) => i.name), "403"];
router.beforeEach((to, _, next) => {
const authStore = useAuthStore();
@@ -44,9 +52,13 @@ router.beforeEach((to, _, next) => {
return;
}
if (!authStore.token) {
authStore.$reset();
next({ name: "Login" });
return;
}
if (to.meta.permission) {
next(!hasPermission(to.meta.permission) ? { name: "403" } : undefined);
}
next();
});

View File

@@ -1,20 +1,22 @@
import type { RouteRecordRaw } from "vue-router";
import {
IconUser,
IconDashboard,
IconOrderedList,
IconCalendar,
IconHeartFill,
IconCodeSandbox,
IconCodeSquare,
IconMessage,
IconSettings,
IconUserGroup,
IconLock,
IconCodepen,
IconWechatpay,
IconRobot,
} from "@arco-design/web-vue/es/icon";
const menu = [
import system from "./system";
const menu: RouteRecordRaw[] = [
{
path: "/dashboard",
name: "Dashboard",
@@ -30,6 +32,7 @@ const menu = [
meta: {
title: "用户管理",
icon: IconUser,
permission: "api_admin_user_list",
},
component: () => import("@/views/User/UserContainer.vue"),
},
@@ -37,8 +40,9 @@ const menu = [
path: "/role",
name: "Role",
meta: {
title: "角色管理",
icon: IconUserGroup,
title: "角色模型",
icon: IconCodeSandbox,
permission: "api_admin_role_list",
},
component: () => import("@/views/Role/RoleContainer.vue"),
},
@@ -48,6 +52,7 @@ const menu = [
meta: {
title: "语言模型",
icon: IconCodepen,
permission: "api_admin_model_list",
},
component: () => import("@/views/ChatModel/ChatModelContainer.vue"),
},
@@ -57,6 +62,7 @@ const menu = [
meta: {
title: "充值产品",
icon: IconWechatpay,
permission: "api_admin_product_list",
},
component: () => import("@/views/Product/ProductContainer.vue"),
},
@@ -66,6 +72,7 @@ const menu = [
meta: {
title: "APIKEY",
icon: IconLock,
permission: "api_admin_apikey_list",
},
component: () => import("@/views/ApiKey/ApiKeyContainer.vue"),
},
@@ -75,6 +82,7 @@ const menu = [
meta: {
title: "充值订单",
icon: IconOrderedList,
permission: "api_admin_order_list",
},
component: () => import("@/views/Order/OrderContainer.vue"),
},
@@ -85,6 +93,7 @@ const menu = [
meta: {
title: "众筹管理",
icon: IconHeartFill,
permission: "api_admin_reward_list",
},
component: () => import("@/views/Reward/RewardContainer.vue"),
},
@@ -94,6 +103,7 @@ const menu = [
meta: {
title: "函数管理",
icon: IconCodeSquare,
permission: "api_admin_function_list",
},
component: () => import("@/views/Functions/FunctionsContainer.vue"),
},
@@ -103,6 +113,7 @@ const menu = [
meta: {
title: "对话管理",
icon: IconMessage,
permission: "api_admin_chat_list",
},
component: () => import("@/views/Chats/ChatsContainer.vue"),
},
@@ -110,29 +121,23 @@ const menu = [
path: "/system",
name: "System",
meta: {
title: "系统设置",
title: "网站设置",
icon: IconSettings,
permission: "api_admin_config_get",
},
component: () => import("@/views/System/SystemContainer.vue"),
},
{
path: "/loginLog",
name: "LoginLog",
path: "/sys",
name: "Sys",
meta: {
title: "登录日志",
icon: IconCalendar,
},
component: () => import("@/views/LoginLog.vue"),
},
{
path: "/sysAdmin",
name: "SysAdmin",
meta: {
title: "系统管理员",
title: "系统设置",
icon: IconRobot,
},
component: () => import("@/views/SysAdmin/SysAdminContainer.vue"),
redirect: () => system[0].path,
children: system
},
];
export default menu;

View File

@@ -0,0 +1,40 @@
import type { RouteRecordRaw } from "vue-router";
const system: RouteRecordRaw[] = [
{
path: "admin",
name: "SysAdmin",
meta: {
title: "系统管理员",
permission: "api_admin_sysUser_list",
},
component: () => import("@/views/SysAdmin/SysAdminContainer.vue"),
},
{
path: "permission",
name: "SysPermission",
meta: {
title: "权限配置",
permission: "api_admin_sysPermission_list",
},
component: () => import("@/views/SysPermission/SysPermissionContainer.vue"),
},
{
path: "role",
name: "SysRole",
meta: {
title: "角色管理",
permission: "api_admin_sysRole_list",
},
component: () => import("@/views/SysRole/SysRoleContainer.vue"),
},
{
path: "loginLog",
name: "LoginLog",
meta: {
title: "登录日志",
},
component: () => import("@/views/LoginLog.vue"),
},
]
export default system

View File

@@ -0,0 +1,8 @@
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
title?: string
icon?: any
permission?: boolean | string | string[]
}
}

View File

@@ -3,19 +3,29 @@ import { Message } from '@arco-design/web-vue'
import { userLogin, userLogout } from '@/http/login'
import router from '@/router'
const defaultState: {
token: string
is_super_admin?: boolean;
permissions?: string[]
} = {
token: null,
is_super_admin: false,
permissions: []
}
export const useAuthStore = defineStore({
id: __AUTH_KEY,
state: () => ({ token: null } as { token: string | null }),
id: Symbol(__AUTH_KEY).toString(),
state: () => ({ ...defaultState }),
actions: {
init() {
this.$state.token = localStorage.getItem(__AUTH_KEY);
this.$state = JSON.parse(localStorage.getItem(__AUTH_KEY));
},
async login(params: any) {
try {
const { data } = await userLogin(params)
if (data) {
this.$state.token = data;
localStorage.setItem(__AUTH_KEY, data)
this.$state = data;
localStorage.setItem(__AUTH_KEY, JSON.stringify(data))
Message.success('登录成功');
router.replace({ name: 'home' })
return Promise.resolve(data)

View File

@@ -1,11 +1,10 @@
<script lang="ts" setup>
import { getList, save, deleting, setStatus } from "./api";
import { ref } from "vue";
import ApiKeyForm from "./ApiKeyForm.vue";
import useCustomFormPopup from "@/composables/useCustomFormPopup";
import { Message } from "@arco-design/web-vue";
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
import { dateFormat } from "@gpt-vue/packages/utils";
import { dateFormat } from "@chatgpt-plus/packages/utils";
// table
const columns = [
{
@@ -29,10 +28,13 @@ const columns = [
title: "使用代理",
dataIndex: "use_proxy",
slotName: "proxy",
align: "center",
width: 100,
},
{
title: "最后使用时间",
dataIndex: "last_used_at",
width: 180,
render: ({ record }) => {
return dateFormat(record.last_used_at);
},
@@ -41,24 +43,17 @@ const columns = [
title: "启用状态",
dataIndex: "enabled",
slotName: "status",
align: "center",
width: 100,
},
{
title: "操作",
slotName: "action",
width: 120,
fixed: "right",
},
];
//
const tableData = ref([]);
const getData = () => {
getList().then(({ code, data }) => {
if (code === 0) {
tableData.value = data;
}
});
};
getData();
//
const popup = useCustomFormPopup(ApiKeyForm, save, {
popupProps: (arg) => ({ title: arg[0].record ? "编辑ApiKey" : "新增ApiKey" }),

View File

@@ -1,119 +1,119 @@
<template>
<a-alert type="warning">
<div class="warning">
{{
`注意如果是百度文心一言平台API-KEY 为 APIKey|SecretKey中间用竖线|)连接\n注意如果是讯飞星火大模型API-KEY 为 AppId|APIKey|APISecret中间用竖线|)连接`
}}
</div>
</a-alert>
<a-form
ref="formRef"
:model="form"
:style="{ width: '600px', 'margin-top': '10px' }"
@submit="handleSubmit"
>
<a-form-item
field="platform"
label="所属平台"
:rules="[{ required: true, message: '请输入所属平台' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.platform" placeholder="请输入所属平台" />
</a-form-item>
<a-form-item
field="name"
label="名称"
:rules="[{ required: true, message: '请输入名称' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input v-model="form.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item
field="type"
label="用途"
:rules="[{ required: true, message: '请输入用途' }]"
:validate-trigger="['change', 'input']"
>
<a-select v-model="form.type" placeholder="请输入用途" :options="typeOPtions"> </a-select>
</a-form-item>
<a-form-item
field="value"
label="API KEY"
:rules="[{ required: true, message: '请输入API KEY' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.value" placeholder="请输入API KEY" />
</a-form-item>
<a-form-item
field="api_url"
label="API URL"
:rules="[{ required: true, message: '请输入API URL' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.api_url" placeholder="请输入API URL" />
</a-form-item>
<a-form-item field="use_proxy" label="使用代理">
<a-switch v-model="form.use_proxy" />
<a-tooltip
content="是否使用代理访问 API URLOpenAI 官方API需要开启代理访问"
position="right"
>
<icon-info-circle-fill />
</a-tooltip>
</a-form-item>
<a-form-item field="enable" label="启用状态">
<a-switch v-model="form.enable" />
</a-form-item>
</a-form>
</template>
<script setup>
import { ref, defineExpose, defineProps } from "vue";
const props = defineProps({
data: {},
});
const formRef = ref();
const form = ref({});
if (props.data?.id) {
form.value = Object.assign({}, props.data);
}
defineExpose({
formRef,
form,
});
const typeOPtions = [
{
label: "聊天",
value: "chart",
},
{
label: "绘图",
value: "img",
},
];
</script>
<style lang="less" scoped>
.content-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 350px;
}
.content-cell {
display: flex;
align-items: center;
width: 350px;
svg {
margin-left: 10px;
}
}
.warning {
color: #e6a23c;
white-space: pre;
}
</style>
<template>
<a-alert type="warning">
<div class="warning">
{{
`注意如果是百度文心一言平台API-KEY 为 APIKey|SecretKey中间用竖线|)连接\n注意如果是讯飞星火大模型API-KEY 为 AppId|APIKey|APISecret中间用竖线|)连接`
}}
</div>
</a-alert>
<a-form
ref="formRef"
:model="form"
:style="{ width: '600px', 'margin-top': '10px' }"
@submit="handleSubmit"
>
<a-form-item
field="platform"
label="所属平台"
:rules="[{ required: true, message: '请输入所属平台' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.platform" placeholder="请输入所属平台" />
</a-form-item>
<a-form-item
field="name"
label="名称"
:rules="[{ required: true, message: '请输入名称' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input v-model="form.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item
field="type"
label="用途"
:rules="[{ required: true, message: '请输入用途' }]"
:validate-trigger="['change', 'input']"
>
<a-select v-model="form.type" placeholder="请输入用途" :options="typeOPtions"> </a-select>
</a-form-item>
<a-form-item
field="value"
label="API KEY"
:rules="[{ required: true, message: '请输入API KEY' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.value" placeholder="请输入API KEY" />
</a-form-item>
<a-form-item
field="api_url"
label="API URL"
:rules="[{ required: true, message: '请输入API URL' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.api_url" placeholder="请输入API URL" />
</a-form-item>
<a-form-item field="use_proxy" label="使用代理">
<a-switch v-model="form.use_proxy" />
<a-tooltip
content="是否使用代理访问 API URLOpenAI 官方API需要开启代理访问"
position="right"
>
<icon-info-circle-fill />
</a-tooltip>
</a-form-item>
<a-form-item field="enable" label="启用状态">
<a-switch v-model="form.enable" />
</a-form-item>
</a-form>
</template>
<script setup>
import { ref, defineExpose, defineProps } from "vue";
const props = defineProps({
data: {},
});
const formRef = ref();
const form = ref({});
if (props.data?.id) {
form.value = Object.assign({}, props.data);
}
defineExpose({
formRef,
form,
});
const typeOPtions = [
{
label: "聊天",
value: "chart",
},
{
label: "绘图",
value: "img",
},
];
</script>
<style lang="less" scoped>
.content-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 350px;
}
.content-cell {
display: flex;
align-items: center;
width: 350px;
svg {
margin-left: 10px;
}
}
.warning {
color: #e6a23c;
white-space: pre;
}
</style>

View File

@@ -16,8 +16,9 @@ export const save = (data?: Record<string, unknown>) => {
};
export const deleting = (id: string | number) => {
return http({
url: `/api/admin/apikey/remove?id=${id}`,
method: "get",
url: `/api/admin/apikey/remove`,
method: "post",
data: { id },
});
};
export const setStatus = (data) => {

View File

@@ -5,7 +5,7 @@ import ChatModelForm from "./ChatModelForm.vue";
import useCustomFormPopup from "@/composables/useCustomFormPopup";
import { Message } from "@arco-design/web-vue";
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
import { dateFormat } from "@gpt-vue/packages/utils";
import { dateFormat } from "@chatgpt-plus/packages/utils";
// table
const columns = [
{
@@ -44,6 +44,8 @@ const columns = [
{
title: "操作",
slotName: "action",
width: 120,
fixed: "right",
},
];

View File

@@ -1,94 +1,94 @@
<template>
<a-form ref="formRef" :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
<a-form-item
field="platform"
label="所属平台"
:rules="[{ required: true, message: '请输入所属平台' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.platform" placeholder="请输入所属平台" />
</a-form-item>
<a-form-item
field="name"
label="名称"
:rules="[{ required: true, message: '请输入名称' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input v-model="form.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item
field="value"
label="模型值"
:rules="[{ required: true, message: '请输入名称' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input v-model="form.value" placeholder="请输入名称" />
</a-form-item>
<a-form-item
field="weight"
label="对话权重"
:rules="[{ required: true, message: '请输入对话权重' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input-number v-model="form.weight" placeholder="请输入对话权重" />
<a-tooltip content="对话权重,每次对话扣减多少次对话额度" position="right">
<icon-info-circle-fill />
</a-tooltip>
</a-form-item>
<a-form-item field="open" label="开放状态代理">
<a-switch v-model="form.open" />
</a-form-item>
<a-form-item field="enabled" label="启用状态">
<a-switch v-model="form.enabled" />
</a-form-item>
</a-form>
</template>
<script setup>
import { ref, defineExpose, defineProps } from "vue";
const props = defineProps({
data: {},
});
const formRef = ref();
const form = ref({});
if (props.data?.id) {
form.value = Object.assign({}, props.data);
}
defineExpose({
formRef,
form,
});
const typeOPtions = [
{
label: "聊天",
value: "chart",
},
{
label: "绘图",
value: "img",
},
];
</script>
<style lang="less" scoped>
.content-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 350px;
}
.content-cell {
display: flex;
align-items: center;
width: 350px;
svg {
margin-left: 10px;
}
}
</style>
<template>
<a-form ref="formRef" :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
<a-form-item
field="platform"
label="所属平台"
:rules="[{ required: true, message: '请输入所属平台' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.platform" placeholder="请输入所属平台" />
</a-form-item>
<a-form-item
field="name"
label="名称"
:rules="[{ required: true, message: '请输入名称' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input v-model="form.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item
field="value"
label="模型值"
:rules="[{ required: true, message: '请输入名称' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input v-model="form.value" placeholder="请输入名称" />
</a-form-item>
<a-form-item
field="weight"
label="对话权重"
:rules="[{ required: true, message: '请输入对话权重' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input-number v-model="form.weight" placeholder="请输入对话权重" />
<a-tooltip content="对话权重,每次对话扣减多少次对话额度" position="right">
<icon-info-circle-fill />
</a-tooltip>
</a-form-item>
<a-form-item field="open" label="开放状态代理">
<a-switch v-model="form.open" />
</a-form-item>
<a-form-item field="enabled" label="启用状态">
<a-switch v-model="form.enabled" />
</a-form-item>
</a-form>
</template>
<script setup>
import { ref, defineExpose, defineProps } from "vue";
const props = defineProps({
data: {},
});
const formRef = ref();
const form = ref({});
if (props.data?.id) {
form.value = Object.assign({}, props.data);
}
defineExpose({
formRef,
form,
});
const typeOPtions = [
{
label: "聊天",
value: "chart",
},
{
label: "绘图",
value: "img",
},
];
</script>
<style lang="less" scoped>
.content-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 350px;
}
.content-cell {
display: flex;
align-items: center;
width: 350px;
svg {
margin-left: 10px;
}
}
</style>

View File

@@ -16,8 +16,9 @@ export const save = (data?: Record<string, unknown>) => {
};
export const deleting = (id: string | number) => {
return http({
url: `/api/admin/model/remove?id=${id}`,
method: "get",
url: `/api/admin/model/remove`,
method: "post",
data: { id },
});
};
export const setStatus = (data) => {

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { ref, h } from "vue";
import { Message, Modal } from "@arco-design/web-vue";
import { dateFormat } from "@gpt-vue/packages/utils";
import { dateFormat } from "@chatgpt-plus/packages/utils";
import SearchTable from "@/components/SearchTable/SearchTable.vue";
import type { SearchTableColumns } from "@/components/SearchTable/type";
import app from "@/main";

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { onMounted } from "vue";
import { Message } from "@arco-design/web-vue";
import { dateFormat } from "@gpt-vue/packages/utils";
import { dateFormat } from "@chatgpt-plus/packages/utils";
import useRequest from "@/composables/useRequest";
import { history } from "./api";

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { dateFormat } from "@gpt-vue/packages/utils";
import { dateFormat } from "@chatgpt-plus/packages/utils";
import SearchTable from "@/components/SearchTable/SearchTable.vue";
import type { SearchTableColumns } from "@/components/SearchTable/type";
import { loginLog } from "@/http/login";

View File

@@ -0,0 +1,11 @@
<script setup lang="ts"></script>
<template>
<AResult status="403" subtitle="没有权限">
<template #extra>
<MButton preset="back" type="primary" />
</template>
</AResult>
</template>
<style scoped lang="less"></style>

View File

@@ -1,8 +1,9 @@
<script lang="ts" setup>
import { Message } from "@arco-design/web-vue";
import SearchTable from "@/components/SearchTable/SearchTable.vue";
import type { SearchTableColumns } from "@/components/SearchTable/type";
import { dateFormat } from "@gpt-vue/packages/utils";
import { getList } from "./api";
import { dateFormat } from "@chatgpt-plus/packages/utils";
import { getList, remove } from "./api";
const columns: SearchTableColumns[] = [
{
@@ -71,6 +72,12 @@ const columns: SearchTableColumns[] = [
width: 80,
},
];
const handleRemove = async (id, reload) => {
await remove({ id });
Message.success("删除成功");
await reload();
return true;
};
</script>
<template>
<SearchTable :request="getList" :columns="columns">
@@ -78,8 +85,15 @@ const columns: SearchTableColumns[] = [
<a-tag v-if="!record.pay_time" color="blue">未支付</a-tag>
<span v-else>{{ dateFormat(record.pay_time) }}</span>
</template>
<template #actions="{ record }">
<a-link :key="record.id" status="danger">删除</a-link>
<template #actions="{ record, reload }">
<a-popconfirm
content="是否删除?"
position="left"
type="warning"
:on-before-ok="() => handleRemove(record.id, reload)"
>
<a-link status="danger">删除</a-link>
</a-popconfirm>
</template>
</SearchTable>
</template>

View File

@@ -6,4 +6,12 @@ export const getList = (data?: Record<string, unknown>) => {
method: "post",
data
})
}
export const remove = (data) => {
return http({
url: "/api/admin/order/remove",
method: "post",
data
})
}

View File

@@ -5,7 +5,7 @@ import ProductForm from "./ProductForm.vue";
import useCustomFormPopup from "@/composables/useCustomFormPopup";
import { Message } from "@arco-design/web-vue";
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
import { dateFormat } from "@gpt-vue/packages/utils";
import { dateFormat } from "@chatgpt-plus/packages/utils";
// table
const columns = [
{
@@ -40,10 +40,13 @@ const columns = [
title: "启用状态",
dataIndex: "enabled",
slotName: "status",
align: "center",
width: 100,
},
{
title: "更新时间",
dataIndex: "updated_at",
width: 180,
render: ({ record }) => {
return dateFormat(record.updated_at);
},
@@ -51,6 +54,8 @@ const columns = [
{
title: "操作",
slotName: "action",
width: 120,
fixed: "right",
},
];

View File

@@ -1,87 +1,87 @@
<template>
<a-form ref="formRef" :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
<a-form-item
field="name"
label="产品名称"
:rules="[{ required: true, message: '请输入产品名称' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.name" placeholder="请输入产品名称" />
</a-form-item>
<a-form-item
field="price"
label="产品价格"
:rules="[{ required: true, message: '请输入产品价格' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input-number v-model="form.price" placeholder="请输入产品价格" />
</a-form-item>
<a-form-item
field="discount"
label="优惠金额"
:rules="[{ required: true, message: '请输入优惠金额' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input-number v-model="form.discount" placeholder="请输入优惠金额" />
</a-form-item>
<a-form-item
field="days"
label="有效期(天)"
:rules="[{ required: true, message: '请输入有效期(天)' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input-number v-model="form.days" placeholder="请输入有效期(天)" />
</a-form-item>
<a-form-item field="calls" label="对话次数" :validate-trigger="['change', 'input']" showable>
<a-input-number v-model="form.calls" placeholder="请输入对话次数" />
</a-form-item>
<a-form-item
field="img_calls"
label="绘图次数"
:validate-trigger="['change', 'input']"
showable
>
<a-input-number v-model="form.img_calls" placeholder="请输入绘图次数" />
</a-form-item>
<a-form-item field="enabled" label="启用状态">
<a-switch v-model="form.enabled" />
</a-form-item>
</a-form>
</template>
<script setup>
import { ref, defineExpose, defineProps } from "vue";
const props = defineProps({
data: {},
});
const formRef = ref();
const form = ref({});
if (props.data?.id) {
form.value = Object.assign({}, props.data);
}
defineExpose({
formRef,
form,
});
</script>
<style lang="less" scoped>
.content-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 350px;
}
.content-cell {
display: flex;
align-items: center;
width: 350px;
svg {
margin-left: 10px;
}
}
</style>
<template>
<a-form ref="formRef" :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
<a-form-item
field="name"
label="产品名称"
:rules="[{ required: true, message: '请输入产品名称' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.name" placeholder="请输入产品名称" />
</a-form-item>
<a-form-item
field="price"
label="产品价格"
:rules="[{ required: true, message: '请输入产品价格' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input-number v-model="form.price" placeholder="请输入产品价格" />
</a-form-item>
<a-form-item
field="discount"
label="优惠金额"
:rules="[{ required: true, message: '请输入优惠金额' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input-number v-model="form.discount" placeholder="请输入优惠金额" />
</a-form-item>
<a-form-item
field="days"
label="有效期(天)"
:rules="[{ required: true, message: '请输入有效期(天)' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input-number v-model="form.days" placeholder="请输入有效期(天)" />
</a-form-item>
<a-form-item field="calls" label="对话次数" :validate-trigger="['change', 'input']" showable>
<a-input-number v-model="form.calls" placeholder="请输入对话次数" />
</a-form-item>
<a-form-item
field="img_calls"
label="绘图次数"
:validate-trigger="['change', 'input']"
showable
>
<a-input-number v-model="form.img_calls" placeholder="请输入绘图次数" />
</a-form-item>
<a-form-item field="enabled" label="启用状态">
<a-switch v-model="form.enabled" />
</a-form-item>
</a-form>
</template>
<script setup>
import { ref, defineExpose, defineProps } from "vue";
const props = defineProps({
data: {},
});
const formRef = ref();
const form = ref({});
if (props.data?.id) {
form.value = Object.assign({}, props.data);
}
defineExpose({
formRef,
form,
});
</script>
<style lang="less" scoped>
.content-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 350px;
}
.content-cell {
display: flex;
align-items: center;
width: 350px;
svg {
margin-left: 10px;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { Message, type TableColumnData } from "@arco-design/web-vue";
import { dateFormat } from "@gpt-vue/packages/utils";
import { dateFormat } from "@chatgpt-plus/packages/utils";
import SimpleTable from "@/components/SimpleTable/SimpleTable.vue";
import { getList, remove } from "./api";

View File

@@ -8,10 +8,10 @@ export const getList = (params?: Record<string, unknown>) => {
})
}
export const remove = (params?: Record<string, unknown>) => {
export const remove = (data?: Record<string, unknown>) => {
return http({
url: "/api/admin/reward/remove",
method: "get",
params
method: "post",
data
})
}

View File

@@ -1,116 +1,116 @@
<template>
<a-form ref="formRef" :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
<a-form-item
field="name"
label="角色名称"
:rules="[{ required: true, message: '请输入角色名称' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.name" placeholder="请输入角色名称" />
</a-form-item>
<a-form-item
field="key"
label="角色标志"
:rules="[{ required: true, message: '请输入角色标志' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input v-model="form.key" placeholder="请输入角色标志" />
</a-form-item>
<a-form-item
field="icon"
label="角色图标"
:rules="[{ required: true, message: '请输入角色图标' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.icon" placeholder="请输入角色图标" />
</a-form-item>
<a-form-item
field="hello_msg"
label="打招呼信息"
:rules="[{ required: true, message: '请输入打招呼信息' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.hello_msg" placeholder="请输入打招呼信息" />
</a-form-item>
<a-form-item field="username" label="上下文信息" :validate-trigger="['change', 'input']">
<a-table :data="form.context || []" :pagination="false">
<template #columns>
<a-table-column title="对话角色">
<template #cell="{ record }">
<a-input v-model="record.role" />
</template>
</a-table-column>
<a-table-column width="350">
<template #title>
<div class="content-title">
<span>对话内容</span>
<a-button @click="addContext" type="primary">增加一行</a-button>
</div>
</template>
<template #cell="{ record }">
<div class="content-cell">
<a-input v-model="record.content" /><icon-minus-circle
@click="removeContext(record)"
/>
</div>
</template>
</a-table-column>
</template>
</a-table>
</a-form-item>
<a-form-item field="enable" label="启用状态">
<a-switch v-model="form.enable" />
</a-form-item>
</a-form>
</template>
<script setup>
import { ref, defineExpose, defineProps } from "vue";
const props = defineProps({
data: {},
});
const formRef = ref();
const form = ref({});
if (props.data?.id) {
form.value = Object.assign({}, props.data);
}
const addContext = () => {
form.value.context = form.value.context || [];
form.value.context.push({
role: "",
content: "",
});
};
const removeContext = (record) => {
const index = form.value.context.findIndex((item) => {
return item === record;
});
if (index > -1) {
form.value.context.splice(index, 1);
}
};
defineExpose({
formRef,
form,
});
</script>
<style lang="less" scoped>
.content-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 350px;
}
.content-cell {
display: flex;
align-items: center;
width: 350px;
svg {
margin-left: 10px;
}
}
</style>
<template>
<a-form ref="formRef" :model="form" :style="{ width: '600px' }" @submit="handleSubmit">
<a-form-item
field="name"
label="角色名称"
:rules="[{ required: true, message: '请输入角色名称' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.name" placeholder="请输入角色名称" />
</a-form-item>
<a-form-item
field="key"
label="角色标志"
:rules="[{ required: true, message: '请输入角色标志' }]"
:validate-trigger="['change', 'input']"
showable
>
<a-input v-model="form.key" placeholder="请输入角色标志" />
</a-form-item>
<a-form-item
field="icon"
label="角色图标"
:rules="[{ required: true, message: '请输入角色图标' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.icon" placeholder="请输入角色图标" />
</a-form-item>
<a-form-item
field="hello_msg"
label="打招呼信息"
:rules="[{ required: true, message: '请输入打招呼信息' }]"
:validate-trigger="['change', 'input']"
>
<a-input v-model="form.hello_msg" placeholder="请输入打招呼信息" />
</a-form-item>
<a-form-item field="username" label="上下文信息" :validate-trigger="['change', 'input']">
<a-table :data="form.context || []" :pagination="false">
<template #columns>
<a-table-column title="对话角色">
<template #cell="{ record }">
<a-input v-model="record.role" />
</template>
</a-table-column>
<a-table-column width="350">
<template #title>
<div class="content-title">
<span>对话内容</span>
<a-button @click="addContext" type="primary">增加一行</a-button>
</div>
</template>
<template #cell="{ record }">
<div class="content-cell">
<a-input v-model="record.content" /><icon-minus-circle
@click="removeContext(record)"
/>
</div>
</template>
</a-table-column>
</template>
</a-table>
</a-form-item>
<a-form-item field="enable" label="启用状态">
<a-switch v-model="form.enable" />
</a-form-item>
</a-form>
</template>
<script setup>
import { ref, defineExpose, defineProps } from "vue";
const props = defineProps({
data: {},
});
const formRef = ref();
const form = ref({});
if (props.data?.id) {
form.value = Object.assign({}, props.data);
}
const addContext = () => {
form.value.context = form.value.context || [];
form.value.context.push({
role: "",
content: "",
});
};
const removeContext = (record) => {
const index = form.value.context.findIndex((item) => {
return item === record;
});
if (index > -1) {
form.value.context.splice(index, 1);
}
};
defineExpose({
formRef,
form,
});
</script>
<style lang="less" scoped>
.content-title {
display: flex;
justify-content: space-between;
align-items: center;
width: 350px;
}
.content-cell {
display: flex;
align-items: center;
width: 350px;
svg {
margin-left: 10px;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More