mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 08:13:43 +08:00 
			
		
		
		
	Merge remote-tracking branch 'origin/ui' into ui
This commit is contained in:
		
							
								
								
									
										61
									
								
								gpt-vue/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										61
									
								
								gpt-vue/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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">
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								gpt-vue/projects/vue-admin/src/composables/useState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								gpt-vue/projects/vue-admin/src/composables/useState.ts
									
									
									
									
									
										Normal 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;
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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: "操作",
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user