Merge remote-tracking branch 'origin/ui' into ui

This commit is contained in:
chenzifan 2024-03-11 13:46:46 +08:00
commit dca25dbb78
24 changed files with 421 additions and 107 deletions

View File

@ -65,6 +65,9 @@ importers:
'@gpt-vue/packages':
specifier: workspace:^1.0.0
version: link:../../packages
echarts:
specifier: ^5.5.0
version: 5.5.0
md-editor-v3:
specifier: ^2.2.1
version: 2.11.3(vue@3.4.21)
@ -93,6 +96,9 @@ importers:
'@vitejs/plugin-vue-jsx':
specifier: ^3.1.0
version: 3.1.0(vite@5.1.5)(vue@3.4.21)
'@vue/eslint-config-prettier':
specifier: ^7.0.0
version: 7.1.0(eslint@8.57.0)(prettier@3.2.5)
'@vue/eslint-config-typescript':
specifier: ^12.0.0
version: 12.0.0(eslint-plugin-vue@9.22.0)(eslint@8.57.0)(typescript@5.3.3)
@ -1283,6 +1289,18 @@ packages:
resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==}
dev: false
/@vue/eslint-config-prettier@7.1.0(eslint@8.57.0)(prettier@3.2.5):
resolution: {integrity: sha512-Pv/lVr0bAzSIHLd9iz0KnvAr4GKyCEl+h52bc4e5yWuDVtLgFwycF7nrbWTAQAS+FU6q1geVd07lc6EWfJiWKQ==}
peerDependencies:
eslint: '>= 7.28.0'
prettier: '>= 2.0.0'
dependencies:
eslint: 8.57.0
eslint-config-prettier: 8.10.0(eslint@8.57.0)
eslint-plugin-prettier: 4.2.1(eslint-config-prettier@8.10.0)(eslint@8.57.0)(prettier@3.2.5)
prettier: 3.2.5
dev: true
/@vue/eslint-config-typescript@12.0.0(eslint-plugin-vue@9.22.0)(eslint@8.57.0)(typescript@5.3.3):
resolution: {integrity: sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==}
engines: {node: ^14.17.0 || >=16.0.0}
@ -1637,6 +1655,13 @@ packages:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
dev: true
/echarts@5.5.0:
resolution: {integrity: sha512-rNYnNCzqDAPCr4m/fqyUFv7fD9qIsd50S6GDFgO1DxZhncCsNsG7IfUlAlvZe5oSEQxtsjnHiUuppzccry93Xw==}
dependencies:
tslib: 2.3.0
zrender: 5.5.0
dev: false
/electron-to-chromium@1.4.692:
resolution: {integrity: sha512-d5rZRka9n2Y3MkWRN74IoAsxR0HK3yaAt7T50e3iT9VZmCCQDT3geXUO5ZRMhDToa1pkCeQXuNo+0g+NfDOVPA==}
dev: true
@ -1708,6 +1733,15 @@ packages:
engines: {node: '>=10'}
dev: true
/eslint-config-prettier@8.10.0(eslint@8.57.0):
resolution: {integrity: sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==}
hasBin: true
peerDependencies:
eslint: '>=7.0.0'
dependencies:
eslint: 8.57.0
dev: true
/eslint-config-prettier@9.1.0(eslint@8.57.0):
resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
hasBin: true
@ -1717,6 +1751,23 @@ packages:
eslint: 8.57.0
dev: true
/eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0)(eslint@8.57.0)(prettier@3.2.5):
resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
eslint: '>=7.28.0'
eslint-config-prettier: '*'
prettier: '>=2.0.0'
peerDependenciesMeta:
eslint-config-prettier:
optional: true
dependencies:
eslint: 8.57.0
eslint-config-prettier: 8.10.0(eslint@8.57.0)
prettier: 3.2.5
prettier-linter-helpers: 1.0.0
dev: true
/eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5):
resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==}
engines: {node: ^14.18.0 || >=16.0.0}
@ -2797,6 +2848,10 @@ packages:
typescript: 5.3.3
dev: true
/tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
dev: false
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: true
@ -3008,3 +3063,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
dev: true
/zrender@5.5.0:
resolution: {integrity: sha512-O3MilSi/9mwoovx77m6ROZM7sXShR/O/JIanvzTwjN3FORfLSr81PsUGd7jlaYOeds9d8tw82oP44+3YucVo+w==}
dependencies:
tslib: 2.3.0
dev: false

View File

@ -14,6 +14,7 @@
"dependencies": {
"@arco-design/web-vue": "^2.54.6",
"@gpt-vue/packages": "workspace:^1.0.0",
"echarts": "^5.5.0",
"md-editor-v3": "^2.2.1",
"pinia": "^2.1.7",
"vue": "^3.4.15",

View File

@ -1,13 +1,17 @@
<script lang="ts" setup>
import { IconDown, IconExport } from "@arco-design/web-vue/es/icon";
import { useAuthStore } from "@/stores/auth";
import useState from "@/composables/useState";
import Logo from "/images/logo.png";
import avatar from "/images/user-info.jpg";
import donateImg from "/images/wechat-pay.png";
import SystemMenu from "./SystemMenu.vue";
import PageWrapper from "./PageWrapper.vue";
const logoWidth = "200px";
const authStore = useAuthStore();
const [visible, setVisible] = useState(false);
</script>
<template>
<ALayout class="custom-layout">
@ -19,18 +23,33 @@ const authStore = useAuthStore();
<div class="action">
<ADropdown>
<ASpace align="center" :size="4">
<span></span>
<a-avatar class="user-avatar" :size="30">
<img :src="avatar" />
</a-avatar>
<IconDown />
</ASpace>
<template #content>
<ADoption value="changeOwnPwd">更改密码</ADoption>
<a
class="dropdown-link"
href="https://github.com/yangjian102621/chatgpt-plus"
target="_blank"
>
<ADoption value="1">
<template #icon>
<icon-github />
</template>
<span>ChatPlus-AI 创作系统</span>
</ADoption>
</a>
<ADoption value="2" @click="setVisible(true)">
<template #icon>
<icon-wechatpay />
</template>
<span>打赏作者</span>
</ADoption>
</template>
<template #footer>
<APopconfirm
content="确认退出?"
position="br"
@ok="authStore.logout"
>
<APopconfirm content="确认退出?" position="br" @ok="authStore.logout">
<ASpace align="center" class="logout-area">
<IconExport size="16" />
<span>退出</span>
@ -49,6 +68,20 @@ const authStore = useAuthStore();
</ALayoutContent>
</ALayout>
</ALayout>
<a-modal
v-model:visible="visible"
class="donate-dialog"
width="400px"
title="请作者喝杯咖啡"
:footer="false"
>
<a-alert :closable="false" :show-icon="false">
如果你觉得这个项目对你有帮助并且情况允许的话可以请作者喝杯咖啡非常感谢你的支持
</a-alert>
<p>
<a-image :src="donateImg" />
</p>
</a-modal>
</template>
<style lang="less" scoped>
.custom-layout {
@ -81,6 +114,14 @@ const authStore = useAuthStore();
}
}
}
.dropdown-link {
text-decoration: none;
}
.donate-dialog {
p {
text-align: center;
}
}
.logout-area {
padding: 8px 0;
display: flex;

View File

@ -80,8 +80,8 @@ const optionsEvent = {
</slot>
</AFormItem>
</AGridItem>
<AGridItem suffix>
<ASpace class="flex-end">
<AGridItem>
<ASpace>
<slot name="search-options" :option="optionsEvent">
<AButton type="primary" html-type="submit" :size="size" :loading="submitting">
<icon-search />
@ -92,6 +92,10 @@ const optionsEvent = {
<span>重置</span>
</AButton>
</slot>
</ASpace>
</AGridItem>
<AGridItem suffix>
<ASpace class="flex-end">
<slot name="search-extra" />
</ASpace>
</AGridItem>
@ -100,7 +104,7 @@ const optionsEvent = {
</template>
<style scoped>
.search-form-conteiner {
padding: 16px 0;
padding: 8px 0px 0px;
}
.flex-end {
display: flex;

View File

@ -19,11 +19,13 @@ const requestParams = computed(() => ({
const [tableConfig, getList] = useAsyncTable(props.request, requestParams);
const _columns = computed(() => {
return props.columns.map((item) => ({
ellipsis: true,
tooltip: true,
...item,
}));
return props.columns
.filter((item) => !item.hideInTable)
.map((item) => ({
ellipsis: true,
tooltip: true,
...item,
}));
});
const handleSearch = async (tips?: boolean) => {
@ -46,7 +48,7 @@ onActivated(handleSearch);
<FormSection
v-model="formData"
:columns="columns"
:submitting="(tableConfig.loading as boolean)"
:submitting="tableConfig.loading as boolean"
@request="handleSearch"
>
<template v-for="slot in Object.keys($slots)" #[slot]="config">
@ -60,7 +62,7 @@ onActivated(handleSearch);
...tableConfig,
...props,
scroll: useTableScroll(_columns, tableContainerRef as HTMLElement),
columns: _columns
columns: _columns,
}"
>
<template v-for="slot in Object.keys($slots)" #[slot]="config">

View File

@ -33,12 +33,12 @@ const handleSearch = async (tips?: boolean) => {
onActivated(handleSearch);
</script>
<template>
<div class="simple-header">
<a-space>
<slot name="header" v-bind="{ reload: handleSearch }" />
</a-space>
</div>
<div class="simple-table">
<div class="simple-header">
<a-space class="flex-end">
<slot name="header" v-bind="{ reload: handleSearch }" />
</a-space>
</div>
<div ref="tableContainerRef" class="simple-table-container">
<ATable
v-bind="{
@ -46,7 +46,7 @@ onActivated(handleSearch);
...tableConfig,
...props,
scroll: useTableScroll(_columns || [], tableContainerRef as HTMLElement),
columns: _columns
columns: _columns,
}"
>
<template v-for="slot in Object.keys($slots)" #[slot]="config">
@ -71,6 +71,10 @@ onActivated(handleSearch);
justify-content: space-between;
}
.simple-header {
padding: 16px 0;
padding: 8px 0px 16px;
}
.flex-end {
display: flex;
justify-content: end;
}
</style>

View File

@ -0,0 +1,10 @@
import { ref, type Ref } from "vue";
function useState<T>(defaultValue?: T): [Ref<T>, (newValue: T) => void] {
const state = ref<T>(defaultValue) as Ref<T>;
const setState = (newValue: T) => {
state.value = newValue;
};
return [state, setState];
}
export default useState;

View File

@ -12,7 +12,7 @@ function useSubmit<T extends Record<string, any> = Record<string, any>, R = any>
const hasError = await formRef.value?.validate();
if (!hasError) {
const { data, message } = await api({ ...formData ?? {}, ...unref(params) });
Message.success(message);
message && Message.success(message);
return Promise.resolve({ formData, data });
}
return Promise.reject(false);

View File

@ -3,7 +3,7 @@ import { Notification } from "@arco-design/web-vue";
import createInstance from "@gpt-vue/packages/request"
import type { BaseResponse } from "@gpt-vue/packages/type";
export const uploadUrl = import.meta.env.VITE_PROXY_BASE_URL + "/api/upload";
export const uploadUrl = import.meta.env.VITE_PROXY_BASE_URL + "/api/admin/upload";
export const instance = createInstance()

View File

@ -32,4 +32,11 @@ export const loginLog = (params?: Record<string, unknown>) => {
method: "get",
params
})
}
}
export const captcha = () => {
return http({
url: "/api/admin/login/captcha",
method: "get",
});
};

View File

@ -19,6 +19,7 @@ const columns = [
{
title: "key",
dataIndex: "value",
slotName: "value",
},
{
title: "用途",
@ -96,7 +97,14 @@ const handleStatusChange = ({ filed, value, record, reload }) => {
</a-popconfirm>
</template>
<template #header="{ reload }">
<a-button @click="popup({ reload })" size="small"><icon-plus />新增</a-button>
<a-button @click="popup({ reload })" size="small" type="primary"
><template #icon> <icon-plus /> </template>新增
</a-button>
</template>
<template #value="{ record, column }">
<a-typography-text copyable ellipsis style="margin: 0">
{{ record[column.dataIndex] }}
</a-typography-text>
</template>
<template #status="{ record, reload }">
<a-switch

View File

@ -96,7 +96,9 @@ const handleStatusChange = ({ filed, value, record, reload }) => {
</a-popconfirm>
</template>
<template #header="{ reload }">
<a-button @click="popup({ reload })" size="small"><icon-plus />新增</a-button>
<a-button @click="popup({ reload })" size="small" type="primary"
><template #icon> <icon-plus /> </template>新增</a-button
>
</template>
<template #status="{ record, reload }">
<a-switch

View File

@ -1,6 +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 SearchTable from "@/components/SearchTable/SearchTable.vue";
import type { SearchTableColumns } from "@/components/SearchTable/type";
import app from "@/main";
@ -51,6 +52,7 @@ const columns: SearchTableColumns[] = [
search: {
valueType: "range",
},
render: ({ record }) => dateFormat(record.created_at),
},
{
title: "操作",

View File

@ -1,13 +1,11 @@
<script lang="ts" setup>
import http from "@/http/config";
import { ref } from "vue";
const dataSet = {
users: "今日新增用户",
chats: "今日新增对话",
tokens: "今日消耗 Tokens",
income: "今日入账",
};
import { ref, nextTick } from "vue";
import * as echarts from "echarts/core";
import { GridComponent, TitleComponent } from "echarts/components";
import { LineChart } from "echarts/charts";
import { UniversalTransition } from "echarts/features";
import { CanvasRenderer } from "echarts/renderers";
const icons = {
users: "icon-user",
@ -15,24 +13,81 @@ const icons = {
tokens: "icon-computer",
income: "icon-wechatpay",
};
const dataSet = {
users: "今日新增用户",
chats: "今日新增对话",
tokens: "今日消耗 Tokens",
income: "今日入账",
};
const data = ref<Record<string, number>>({});
const getData = () => {
http({
url: "api/admin/dashboard/stats",
method: "get",
}).then((res) => {
data.value = res.data;
handeChartData(res.data.chart);
});
};
getData();
//
const chartTitle = {
historyMessage: "对话",
orders: "订单",
users: "用户数",
};
echarts.use([GridComponent, LineChart, CanvasRenderer, UniversalTransition, TitleComponent]);
const chartDomRefs = [];
const chartData = ref({});
const data = ref<Record<string, number>>({});
const handeChartData = (data) => {
const _chartData = {};
for (let key in data) {
const type = data[key];
_chartData[key] = {
series: [],
xAxis: [],
};
for (let date in type) {
_chartData[key].series.push(type[date]);
_chartData[key].xAxis.push(date);
}
nextTick(() => {
const myChart = echarts.init(chartDomRefs.pop());
myChart.setOption(createOption(_chartData[key], key));
});
}
chartData.value = _chartData;
};
const createOption = (data, key) => {
const { xAxis, series } = data;
return {
title: {
left: "center",
text: chartTitle[key],
},
xAxis: {
type: "category",
data: xAxis,
},
yAxis: {
type: "value",
},
series: [
{
data: series,
type: "line",
},
],
};
};
</script>
<template>
<div class="dashboard">
<a-grid :cols="{ xs: 1, sm: 1, md: 2, lg: 3, xl: 4 }" :colGap="12" :rowGap="16" class="grid">
<a-grid-item v-for="(value, key) in dataSet" :key="key">
<div class="data-card">
<span :class="key" class="icon"><icon-user /></span>
<span :class="key" class="icon"><component :is="icons[key]" /> </span>
<span class="count"
><a-statistic :extra="value" :value="data[key]" :precision="0"
/></span>
@ -40,6 +95,27 @@ getData();
</a-grid-item>
</a-grid>
</div>
<div class="chart">
<a-grid
:cols="{ xs: 1, sm: 1, md: 1, lg: 2, xl: 2, xxl: 3 }"
:colGap="12"
:rowGap="16"
class="grid"
>
<a-grid-item v-for="(value, key, index) in chartData" :key="key">
<div
:ref="
(el) => {
chartDomRefs[index] = el;
}
"
class="chartDom"
>
{{ key }}
</div>
</a-grid-item>
</a-grid>
</div>
</template>
<style lang="less" scoped>
.dashboard {
@ -80,7 +156,16 @@ getData();
display: flex;
align-items: center;
justify-content: center;
background: #f3f3f3;
}
}
}
.chart {
margin-top: 15px;
.chartDom {
width: 450px;
height: 500px;
text-align: center;
}
}
</style>

View File

@ -57,7 +57,7 @@ const handleRemove = async (id, reload) => {
<template>
<SimpleTable :request="getList" :columns="columns" :pagination="false">
<template #header="{ reload }">
<a-button @click="openFormModal(reload, {})">
<a-button type="primary" @click="openFormModal(reload, {})">
<template #icon> <icon-plus /> </template>
新增
</a-button>

View File

@ -1,24 +1,82 @@
<script lang="ts" setup>
import { reactive } from "vue";
import { useRoute } from "vue-router";
import { onMounted, reactive } from "vue";
import { useAuthStore } from "@/stores/auth";
import { captcha } from "@/http/login";
import useState from "@/composables/useState";
import useRequest from "@/composables/useRequest";
const route = useRoute();
const authStore = useAuthStore();
const [loginRequest, _, loading] = useRequest(authStore.login);
const formData = reactive({
username: "",
password: "",
});
async function handleSubmit({ errors, values }: any) {
if (errors) return;
await loginRequest({
...values,
...route.query,
//
function useFormData() {
const formData = reactive({
username: "",
password: "",
captcha: "",
});
const rules = {
username: [{ required: true, message: "请输入您的账号" }],
password: [{ required: true, message: "请输入您的密码" }],
captcha: [{ required: true, message: "请输入验证码" }],
};
const authStore = useAuthStore();
const [loginRequest, _, submitting] = useRequest(authStore.login);
return { formData, loginRequest, submitting, rules };
}
//
function useCaptcha() {
const captchaImage = reactive({
pic_path: "",
captcha_id: "",
});
const getCaptchaImage = async () => {
const { data } = await captcha();
Object.assign(captchaImage, data);
};
onMounted(getCaptchaImage);
return { captchaImage, getCaptchaImage };
}
//
function useRemeberPWD(formData) {
const storageKey = "r-f";
const [isRemember, setIsRemember] = useState(false);
const onIsRememberChange = (v) => {
if (v) {
const value = {
username: formData.username,
password: formData.password,
};
localStorage.setItem(storageKey, JSON.stringify(value));
} else {
localStorage.removeItem(storageKey);
}
setIsRemember(v);
};
onMounted(() => {
const getter = localStorage.getItem(storageKey);
if (getter) {
setIsRemember(true);
Object.assign(formData, JSON.parse(getter));
}
});
return { isRemember, onIsRememberChange };
}
const { formData, loginRequest, submitting, rules } = useFormData();
const { captchaImage, getCaptchaImage } = useCaptcha();
const { isRemember, onIsRememberChange } = useRemeberPWD(formData);
//
async function handleSubmit({ errors }: any) {
if (errors) return;
try {
await loginRequest({
...formData,
captcha_id: captchaImage.captcha_id,
});
} catch (err) {
getCaptchaImage();
}
}
</script>
<template>
@ -33,44 +91,54 @@ async function handleSubmit({ errors, values }: any) {
<div class="form-box">
<div class="title">ChatGPT Plus Admin</div>
<a-form
ref="formRef"
:model="formData"
class="form"
size="medium"
auto-label-width
@submit="handleSubmit"
ref="formRef"
:model="formData"
class="form"
size="medium"
auto-label-width
:label-col-props="{ span: 0 }"
:wrapper-col-props="{ span: 24 }"
:rules="rules"
@submit="handleSubmit"
>
<a-space direction="vertical" style="width: 100%">
<a-form-item
field="username"
label="账号"
hide-label
hide-asterisk
:rules="[{ required: true, message: '请输入您的账号' }]"
>
<a-input
v-model="formData.username"
placeholder="请输入您的账号"
class="input"
></a-input>
<a-form-item field="username" label="账号">
<a-input v-model="formData.username" placeholder="请输入您的账号" class="input" />
</a-form-item>
<a-form-item
field="password"
label="密码"
hide-label
hide-asterisk
:rules="[{ required: true, message: '请输入您的密码' }]"
>
<a-form-item field="password" label="密码">
<a-input-password
v-model="formData.password"
placeholder="请输入您的密码"
class="input"
v-model="formData.password"
placeholder="请输入您的密码"
class="input"
/>
</a-form-item>
<a-form-item field="captcha" label="验证码">
<a-input v-model="formData.captcha" placeholder="请输入验证码" class="input">
<template #append>
<img
class="captcha-image"
:src="captchaImage.pic_path"
alt="验证码"
title="点击刷新验证码"
@click="getCaptchaImage()"
/>
</template>
</a-input>
</a-form-item>
<a-form-item hide-label>
<a-checkbox :model-value="isRemember" @change="onIsRememberChange"
>记住密码</a-checkbox
>
</a-input-password>
</a-form-item>
</a-space>
<a-form-item hide-label>
<a-button :disabled="loading" html-type="submit" long type="primary" class="sign-in-btn">
<a-button
:loading="submitting"
html-type="submit"
long
type="primary"
class="sign-in-btn"
>
登录
</a-button>
</a-form-item>

View File

@ -1,6 +1,7 @@
<script lang="ts" setup>
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";
const columns: SearchTableColumns[] = [
@ -10,6 +11,7 @@ const columns: SearchTableColumns[] = [
search: {
valueType: "input",
},
width: 280,
},
{
dataIndex: "username",
@ -30,6 +32,8 @@ const columns: SearchTableColumns[] = [
{
dataIndex: "created_at",
title: "下单时间",
render: ({ record }) => dateFormat(record.created_at),
width: 200,
},
{
dataIndex: "status",
@ -37,6 +41,7 @@ const columns: SearchTableColumns[] = [
hideInTable: true,
search: {
valueType: "select",
defaultValue: -1,
fieldProps: {
options: [
{ label: "全部", value: -1 },
@ -52,6 +57,8 @@ const columns: SearchTableColumns[] = [
search: {
valueType: "range",
},
slotName: "pay_time",
width: 200,
},
{
dataIndex: "pay_way",
@ -60,11 +67,17 @@ const columns: SearchTableColumns[] = [
{
title: "操作",
slotName: "actions",
fixed: "right",
width: 80,
},
];
</script>
<template>
<SearchTable :request="getList" :columns="columns">
<template #pay_time="{ record }">
<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>

View File

@ -102,7 +102,9 @@ const handleStatusChange = ({ value, record, reload }) => {
</a-popconfirm>
</template>
<template #header="{ reload }">
<a-button @click="popup({ reload })" size="small"><icon-plus />新增</a-button>
<a-button @click="popup({ reload })" size="small" type="primary"
><template #icon> <icon-plus /> </template>新增
</a-button>
</template>
<template #status="{ record, reload }">
<a-switch

View File

@ -105,7 +105,9 @@ const handleStatusChange = ({ value, record, reload }) => {
</a-popconfirm>
</template>
<template #header="{ reload }">
<a-button @click="popup({ reload })" size="small"><icon-plus />新增</a-button>
<a-button @click="popup({ reload })" size="small" type="primary"
><template #icon> <icon-plus /> </template>新增</a-button
>
</template>
<template #status="{ record, reload }">
<a-switch

View File

@ -148,7 +148,7 @@ onMounted(async () => {
</a-space>
</a-form-item>
<a-form-item>
<a-button type="primary" :loading="submitting" @click="handleSave">提交</a-button>
<a-button type="primary" :loading="submitting" @click="handleSave">保存</a-button>
</a-form-item>
</a-form>
</a-card>

View File

@ -141,7 +141,7 @@ onMounted(reload);
/>
</a-form-item>
<a-form-item>
<a-button type="primary" :loading="submitting" @click="handleSave">提交</a-button>
<a-button type="primary" :loading="submitting" @click="handleSave">保存</a-button>
</a-form-item>
</a-form>
</a-card>

View File

@ -61,7 +61,7 @@ onMounted(reload);
<md-editor v-model="formData.content" @on-upload-img="onUploadImg" />
</a-form-item>
<a-form-item>
<a-button type="primary" :loading="submitting" @click="handleSave">提交</a-button>
<a-button type="primary" :loading="submitting" @click="handleSave">保存</a-button>
</a-form-item>
</a-form>
</template>

View File

@ -1,5 +1,6 @@
<script lang="ts" setup>
import { computed } from "vue";
import { Message } from "@arco-design/web-vue";
import type { UploadInstance, FileItem } from "@arco-design/web-vue";
import { uploadUrl } from "@/http/config";
@ -18,20 +19,21 @@ const uploadProps = computed<UploadInstance["$props"]>(() => ({
}));
const handleChange = (_, file: FileItem) => {
console.log(file.response);
if (file?.response) {
emits("update:modelValue", file?.response?.data?.url);
Message.success("上传成功");
}
};
</script>
<template>
<a-space>
<a-input-group>
<a-input :model-value="modelValue" :placeholder="placeholder" readonly />
<a-upload v-bind="uploadProps" @change="handleChange">
<template #upload-button>
<a-button type="primary">
<icon-cloud />
</a-button>
</template>
</a-upload>
</a-input-group>
</a-space>
<a-upload v-bind="uploadProps" style="width: 100%" @change="handleChange">
<template #upload-button>
<a-input-group style="width: 100%">
<a-input :model-value="modelValue" :placeholder="placeholder" readonly />
<a-button type="primary" style="width: 100px">
<icon-cloud />
</a-button>
</a-input-group>
</template>
</a-upload>
</template>

View File

@ -80,9 +80,9 @@ const handleDelete = async ({ id }: { id: string }, reload) => {
<a-link @click="password({ record, reload })">重置密码</a-link>
</template>
<template #search-extra="{ reload }">
<a-button @click="editModal({ reload })" status="success" size="small"
><icon-plus />新增用户</a-button
>
<a-button @click="editModal({ reload })" size="small" type="primary">
<template #icon> <icon-plus /> </template>新增
</a-button>
</template>
</SearchTable>
</template>