Compare commits
6 Commits
fb5e218b43
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b4f5c393e | |||
| ac9712819a | |||
| 91708315f3 | |||
| cc01095107 | |||
| 096e78da4c | |||
| 2afd831d11 |
@@ -9,12 +9,12 @@ VITE_FILE_SERVER=https://your-file-server.com
|
||||
# AI 网关(OpenAI兼容)
|
||||
# - 开发环境推荐走同源反代:VITE_AI_API_URL=/ai-proxy(配合 vite.config.ts)
|
||||
# - 生产环境可直连(需 AI 服务允许 CORS),或在 Nginx 里配置 /ai-proxy 反代
|
||||
VITE_AI_API_URL=https://ai-api.websoft.top/api/v1
|
||||
VITE_AI_API_URL=/ai-proxy
|
||||
|
||||
# Ollama 原生接口(默认端口 11434)
|
||||
# - 开发环境推荐走同源反代:VITE_OLLAMA_API_URL=/proxy(配合 vite.config.ts)
|
||||
# - 生产环境不要直接用 http(会混合内容被拦截),建议 Nginx 反代成同源 https
|
||||
VITE_OLLAMA_API_URL=http://47.119.165.234:11434
|
||||
VITE_OLLAMA_API_URL=/proxy
|
||||
|
||||
# 仅用于本地开发反代注入(vite.config.ts 会读取并注入到 /ai-proxy 请求头)
|
||||
# 不要加 VITE_ 前缀,避免被打包到前端产物里
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
项目已在 `vite.config.ts` 配置(默认目标可通过 `AI_PROXY_TARGET` 调整):
|
||||
|
||||
- `/ai-proxy/*` -> `https://ai-api.websoft.top/api/v1/*`
|
||||
- `/ai-proxy/*` -> `http://127.0.0.1:11434/api/v1/*`
|
||||
|
||||
配合 `.env.development`:
|
||||
|
||||
@@ -20,7 +20,7 @@ VITE_AI_API_URL=/ai-proxy
|
||||
|
||||
```nginx
|
||||
location /ai-proxy/ {
|
||||
proxy_pass https://ai-api.websoft.top/api/v1/;
|
||||
proxy_pass http://127.0.0.1:11434/api/v1/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host ai-api.websoft.top;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -48,9 +48,9 @@ VITE_AI_API_URL=/ai-proxy
|
||||
|
||||
```nginx
|
||||
location /proxy/ {
|
||||
proxy_pass http://47.119.165.234:11434/;
|
||||
proxy_pass http://127.0.0.1:11434/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host 47.119.165.234;
|
||||
proxy_set_header Host 127.0.0.1;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
52
proxy.conf
Normal file
52
proxy.conf
Normal file
@@ -0,0 +1,52 @@
|
||||
server {
|
||||
listen 80 ;
|
||||
listen 443 ssl ;
|
||||
server_name ai-api.websoft.top;
|
||||
index index.php index.html index.htm default.php default.htm default.html;
|
||||
access_log /www/sites/ai-api.websoft.top/log/access.log main;
|
||||
error_log /www/sites/ai-api.websoft.top/log/error.log;
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) {
|
||||
return 404;
|
||||
}
|
||||
location ^~ /.well-known/acme-challenge {
|
||||
allow all;
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
http2 on;
|
||||
if ($scheme = http) {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
ssl_certificate /www/sites/ai-api.websoft.top/ssl/fullchain.pem;
|
||||
ssl_certificate_key /www/sites/ai-api.websoft.top/ssl/privkey.pem;
|
||||
ssl_protocols TLSv1.3 TLSv1.2;
|
||||
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
error_page 497 https://$host$request_uri;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
|
||||
# CORS: allow any *.websoft.top (and websoft.top) Origin; echo Origin for credentialed requests.
|
||||
# Keep wildcard for non-websoft origins without credentials.
|
||||
set $cors_origin "*";
|
||||
set $cors_credentials "false";
|
||||
if ($http_origin ~* '^https?://([a-z0-9-]+\.)*websoft\.top(?::[0-9]+)?$') {
|
||||
set $cors_origin $http_origin;
|
||||
set $cors_credentials "true";
|
||||
}
|
||||
add_header Access-Control-Allow-Origin $cors_origin always;
|
||||
add_header Access-Control-Allow-Credentials $cors_credentials always;
|
||||
add_header Access-Control-Allow-Methods "GET,POST,PUT,PATCH,DELETE,OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "$http_access_control_request_headers" always;
|
||||
add_header Vary "Origin" always;
|
||||
if ($request_method = OPTIONS) {
|
||||
add_header Content-Length 0;
|
||||
add_header Content-Type text/plain;
|
||||
return 204;
|
||||
}
|
||||
include /www/sites/ai-api.websoft.top/proxy/*.conf;
|
||||
}
|
||||
35
proxy_.conf
Normal file
35
proxy_.conf
Normal file
@@ -0,0 +1,35 @@
|
||||
server {
|
||||
listen 80 ;
|
||||
listen 443 ssl ;
|
||||
server_name ai-api.websoft.top;
|
||||
index index.php index.html index.htm default.php default.htm default.html;
|
||||
access_log /www/sites/ai-api.websoft.top/log/access.log main;
|
||||
error_log /www/sites/ai-api.websoft.top/log/error.log;
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) {
|
||||
return 404;
|
||||
}
|
||||
location ^~ /.well-known/acme-challenge {
|
||||
allow all;
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
|
||||
return 403;
|
||||
}
|
||||
|
||||
http2 on;
|
||||
if ($scheme = http) {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
ssl_certificate /www/sites/ai-api.websoft.top/ssl/fullchain.pem;
|
||||
ssl_certificate_key /www/sites/ai-api.websoft.top/ssl/privkey.pem;
|
||||
ssl_protocols TLSv1.3 TLSv1.2;
|
||||
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
error_page 497 https://$host$request_uri;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
include /www/sites/ai-api.websoft.top/proxy/*.conf;
|
||||
}
|
||||
@@ -48,7 +48,9 @@ export async function listOllamaModels(opts?: { baseURL?: string }) {
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(
|
||||
`listOllamaModels failed: ${res.status} ${res.statusText}${text ? ` - ${text}` : ''}`
|
||||
`listOllamaModels failed: ${res.status} ${res.statusText}${
|
||||
text ? ` - ${text}` : ''
|
||||
}`
|
||||
);
|
||||
}
|
||||
return (await res.json()) as OllamaTagsResponse;
|
||||
@@ -68,7 +70,9 @@ export async function ollamaChat(
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(
|
||||
`ollamaChat failed: ${res.status} ${res.statusText}${text ? ` - ${text}` : ''}`
|
||||
`ollamaChat failed: ${res.status} ${res.statusText}${
|
||||
text ? ` - ${text}` : ''
|
||||
}`
|
||||
);
|
||||
}
|
||||
return (await res.json()) as OllamaChatResponseChunk;
|
||||
@@ -97,7 +101,9 @@ export async function ollamaChatStream(
|
||||
if (!res.ok || !res.body) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(
|
||||
`ollamaChatStream failed: ${res.status} ${res.statusText}${text ? ` - ${text}` : ''}`
|
||||
`ollamaChatStream failed: ${res.status} ${res.statusText}${
|
||||
text ? ` - ${text}` : ''
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,4 +149,3 @@ export async function ollamaChatStream(
|
||||
|
||||
opts.onDone?.();
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,9 @@ export async function listModels(opts?: { apiKey?: string; baseURL?: string }) {
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(
|
||||
`listModels failed: ${res.status} ${res.statusText}${text ? ` - ${text}` : ''}`
|
||||
`listModels failed: ${res.status} ${res.statusText}${
|
||||
text ? ` - ${text}` : ''
|
||||
}`
|
||||
);
|
||||
}
|
||||
return (await res.json()) as OpenAIListModelsResponse;
|
||||
@@ -91,7 +93,9 @@ export async function chatCompletions(
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(
|
||||
`chatCompletions failed: ${res.status} ${res.statusText}${text ? ` - ${text}` : ''}`
|
||||
`chatCompletions failed: ${res.status} ${res.statusText}${
|
||||
text ? ` - ${text}` : ''
|
||||
}`
|
||||
);
|
||||
}
|
||||
return (await res.json()) as OpenAIChatCompletionResponse;
|
||||
@@ -124,7 +128,9 @@ export async function chatCompletionsStream(
|
||||
if (!res.ok || !res.body) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(
|
||||
`chatCompletionsStream failed: ${res.status} ${res.statusText}${text ? ` - ${text}` : ''}`
|
||||
`chatCompletionsStream failed: ${res.status} ${res.statusText}${
|
||||
text ? ` - ${text}` : ''
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@ export const AI_API_URL =
|
||||
// Note: browsers cannot call http from an https site (mixed-content); prefer same-origin proxy.
|
||||
export const OLLAMA_API_URL =
|
||||
import.meta.env.VITE_OLLAMA_API_URL ||
|
||||
(import.meta.env.DEV ? '/proxy' : 'http://47.119.165.234:11434');
|
||||
// Prefer same-origin reverse proxy in production too (avoid https mixed-content).
|
||||
'/proxy';
|
||||
|
||||
/**
|
||||
* 以下配置一般不需要修改
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { computed, onBeforeUnmount, ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
chatCompletions,
|
||||
chatCompletionsStream,
|
||||
listModels,
|
||||
type OpenAIChatMessage,
|
||||
type OpenAIModel
|
||||
} from '@/api/ai/openai';
|
||||
import { AI_API_URL, OLLAMA_API_URL } from '@/config/setting';
|
||||
import {
|
||||
listOllamaModels,
|
||||
ollamaChat,
|
||||
@@ -16,17 +8,19 @@
|
||||
type OllamaChatMessage
|
||||
} from '@/api/ai/ollama';
|
||||
|
||||
type Msg = OpenAIChatMessage;
|
||||
type Msg = OllamaChatMessage;
|
||||
|
||||
type Provider = 'openai' | 'ollama';
|
||||
const provider = ref<Provider>('ollama');
|
||||
|
||||
// Default to Ollama native API when provider is ollama.
|
||||
const baseURL = ref(provider.value === OLLAMA_API_URL);
|
||||
const apiKey = ref<string>('');
|
||||
// Hardcode endpoint to avoid going through mp.websoft.top `/proxy`.
|
||||
// The API methods append `/api/*` paths.
|
||||
//
|
||||
// IMPORTANT: do not use `127.0.0.1` in browser production builds:
|
||||
// it points to the visitor's machine, not your server.
|
||||
// If you want to use server-local Ollama (`127.0.0.1:11434`), put it behind an HTTPS reverse proxy
|
||||
// (e.g. `https://ai-api.websoft.top` or same-origin `/proxy`).
|
||||
const BASE_URL = 'https://ai-api.websoft.top';
|
||||
|
||||
const modelLoading = ref(false);
|
||||
const models = ref<OpenAIModel[]>([]);
|
||||
const models = ref<Array<{ id: string; name?: string }>>([]);
|
||||
const modelId = ref<string>('');
|
||||
|
||||
const systemPrompt = ref<string>('你是一个有帮助的助手。');
|
||||
@@ -40,7 +34,6 @@
|
||||
const errorText = ref<string>('');
|
||||
|
||||
const history = ref<Msg[]>([]);
|
||||
const historyJson = computed(() => JSON.stringify(history.value, null, 2));
|
||||
|
||||
const canSend = computed(() => {
|
||||
return !!modelId.value && !!userPrompt.value.trim() && !sending.value;
|
||||
@@ -58,46 +51,19 @@
|
||||
modelLoading.value = true;
|
||||
errorText.value = '';
|
||||
try {
|
||||
if (provider.value === 'openai') {
|
||||
if (!baseURL.value.trim()) {
|
||||
baseURL.value = AI_API_URL;
|
||||
}
|
||||
const res = await listModels({
|
||||
baseURL: baseURL.value.trim() || AI_API_URL,
|
||||
apiKey: apiKey.value.trim() || undefined
|
||||
});
|
||||
models.value = res.data ?? [];
|
||||
if (!modelId.value && models.value.length) {
|
||||
modelId.value = models.value[0].id;
|
||||
}
|
||||
} else {
|
||||
if (!baseURL.value.trim()) {
|
||||
baseURL.value = OLLAMA_API_URL;
|
||||
}
|
||||
const res = await listOllamaModels({
|
||||
baseURL: baseURL.value.trim() || OLLAMA_API_URL
|
||||
baseURL: BASE_URL
|
||||
});
|
||||
models.value = (res.models ?? []).map((m) => ({
|
||||
id: m.name,
|
||||
name: m.name,
|
||||
object: 'model'
|
||||
name: m.name
|
||||
}));
|
||||
if (!modelId.value && models.value.length) {
|
||||
modelId.value = models.value[0].id;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
errorText.value = e?.message ?? String(e);
|
||||
if (
|
||||
provider.value === 'openai' &&
|
||||
String(errorText.value).includes('401')
|
||||
) {
|
||||
message.error(
|
||||
'未认证(401):请填写 API Key,或在本地用 AI_API_KEY 通过 /ai-proxy 注入'
|
||||
);
|
||||
} else {
|
||||
message.error('加载模型列表失败');
|
||||
}
|
||||
} finally {
|
||||
modelLoading.value = false;
|
||||
}
|
||||
@@ -128,52 +94,15 @@
|
||||
abortController.value = controller;
|
||||
|
||||
try {
|
||||
if (provider.value === 'openai') {
|
||||
if (stream.value) {
|
||||
await chatCompletionsStream(
|
||||
{
|
||||
model: modelId.value,
|
||||
messages,
|
||||
temperature: temperature.value
|
||||
},
|
||||
{
|
||||
baseURL: baseURL.value.trim() || AI_API_URL,
|
||||
apiKey: apiKey.value.trim() || undefined,
|
||||
signal: controller.signal,
|
||||
onDelta: (t) => {
|
||||
assistantText.value += t;
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const res = await chatCompletions(
|
||||
{
|
||||
model: modelId.value,
|
||||
messages,
|
||||
temperature: temperature.value
|
||||
},
|
||||
{
|
||||
baseURL: baseURL.value.trim() || AI_API_URL,
|
||||
apiKey: apiKey.value.trim() || undefined,
|
||||
signal: controller.signal
|
||||
}
|
||||
);
|
||||
assistantText.value = res.choices?.[0]?.message?.content ?? '';
|
||||
}
|
||||
} else {
|
||||
const ollamaMessages: OllamaChatMessage[] = messages.map((m) => ({
|
||||
role: m.role as any,
|
||||
content: m.content
|
||||
}));
|
||||
if (stream.value) {
|
||||
await ollamaChatStream(
|
||||
{
|
||||
model: modelId.value,
|
||||
messages: ollamaMessages,
|
||||
messages,
|
||||
options: { temperature: temperature.value }
|
||||
},
|
||||
{
|
||||
baseURL: baseURL.value.trim() || OLLAMA_API_URL,
|
||||
baseURL: BASE_URL,
|
||||
signal: controller.signal,
|
||||
onDelta: (t) => {
|
||||
assistantText.value += t;
|
||||
@@ -184,17 +113,16 @@
|
||||
const res = await ollamaChat(
|
||||
{
|
||||
model: modelId.value,
|
||||
messages: ollamaMessages,
|
||||
messages,
|
||||
options: { temperature: temperature.value }
|
||||
},
|
||||
{
|
||||
baseURL: baseURL.value.trim() || OLLAMA_API_URL,
|
||||
baseURL: BASE_URL,
|
||||
signal: controller.signal
|
||||
}
|
||||
);
|
||||
assistantText.value = res.message?.content ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
history.value = [
|
||||
...messages,
|
||||
@@ -223,19 +151,6 @@
|
||||
// Load once for convenience; if the gateway blocks CORS you can still paste output from curl.
|
||||
loadModels();
|
||||
|
||||
watch(
|
||||
() => provider.value,
|
||||
(p) => {
|
||||
stop();
|
||||
clearChat();
|
||||
models.value = [];
|
||||
modelId.value = '';
|
||||
errorText.value = '';
|
||||
baseURL.value = p === 'openai' ? AI_API_URL : OLLAMA_API_URL;
|
||||
loadModels();
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stop();
|
||||
});
|
||||
@@ -252,33 +167,6 @@
|
||||
description="支持Qwen3.5、DeepSeek、Gemini3等主流的开源大模型,免费使用"
|
||||
/>
|
||||
|
||||
<a-row :gutter="12">
|
||||
<a-col :xs="24" :md="6">
|
||||
<a-select
|
||||
v-model:value="provider"
|
||||
:options="[
|
||||
{ label: 'OpenAI兼容(/v1)', value: 'openai' },
|
||||
{ label: 'Ollama原生(/api)', value: 'ollama' }
|
||||
]"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-input
|
||||
v-model:value="baseURL"
|
||||
addon-before="BaseURL"
|
||||
placeholder="https://ai-api.websoft.top/api/v1"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12" v-if="provider === 'openai'">
|
||||
<a-input-password
|
||||
v-model:value="apiKey"
|
||||
addon-before="API Key"
|
||||
placeholder="可选(不建议在前端保存)"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="12">
|
||||
<a-col :xs="24" :md="12">
|
||||
<a-select
|
||||
|
||||
@@ -56,15 +56,7 @@
|
||||
// 导出
|
||||
const handleExport = async () => {
|
||||
const array: (string | number)[][] = [
|
||||
[
|
||||
'订单号',
|
||||
'用户',
|
||||
'收益类型',
|
||||
'金额',
|
||||
'描述',
|
||||
'创建时间',
|
||||
'租户ID'
|
||||
]
|
||||
['订单号', '用户', '收益类型', '金额', '描述', '创建时间', '租户ID']
|
||||
];
|
||||
|
||||
// 按搜索结果导出
|
||||
|
||||
@@ -91,7 +91,8 @@
|
||||
? `\n${d.nickname ?? '-'}(${d.userId ?? '-'})`
|
||||
: '');
|
||||
|
||||
const firstDividendUserName = (d as any)?.firstDividendUserName ?? '-';
|
||||
const firstDividendUserName =
|
||||
(d as any)?.firstDividendUserName ?? '-';
|
||||
const firstDividend = (d as any)?.firstDividend ?? 0;
|
||||
const secondDividendUserName =
|
||||
(d as any)?.secondDividendUserName ?? '-';
|
||||
|
||||
@@ -11,9 +11,7 @@
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
/>
|
||||
<search @search="reload" />
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'title'">
|
||||
@@ -55,16 +53,16 @@
|
||||
|
||||
<template v-if="column.key === 'firstDividendUserName'">
|
||||
<div>{{ record.firstDividend }}</div>
|
||||
<div class="text-gray-400"
|
||||
>{{ record.firstDividendUserName || '-' }}</div
|
||||
>
|
||||
<div class="text-gray-400">{{
|
||||
record.firstDividendUserName || '-'
|
||||
}}</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'secondDividendUserName'">
|
||||
<div>{{ record.secondDividend }}</div>
|
||||
<div class="text-gray-400"
|
||||
>{{ record.secondDividendUserName || '-' }}</div
|
||||
>
|
||||
<div class="text-gray-400">{{
|
||||
record.secondDividendUserName || '-'
|
||||
}}</div>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'dealerInfo'">
|
||||
@@ -175,9 +173,7 @@
|
||||
ShopDealerOrder,
|
||||
ShopDealerOrderParam
|
||||
} from '@/api/shop/shopDealerOrder/model';
|
||||
import {
|
||||
updateShopDealerOrder
|
||||
} from '@/api/shop/shopDealerOrder';
|
||||
import { updateShopDealerOrder } from '@/api/shop/shopDealerOrder';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
||||
import {importSdyDealerOrder} from "@/api/sdy/sdyDealerOrder";
|
||||
import { importSdyDealerOrder } from '@/api/sdy/sdyDealerOrder';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
|
||||
@@ -17,11 +17,14 @@
|
||||
import { ref, watch } from 'vue';
|
||||
import { utils, writeFile } from 'xlsx';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {ShopDealerCapital} from "@/api/shop/shopDealerCapital/model";
|
||||
import {getTenantId} from "@/utils/domain";
|
||||
import useSearch from "@/utils/use-search";
|
||||
import {ShopDealerOrder, ShopDealerOrderParam} from "@/api/sdy/sdyDealerOrder/model";
|
||||
import {pageShopDealerOrder} from "@/api/shop/shopDealerOrder";
|
||||
import { ShopDealerCapital } from '@/api/shop/shopDealerCapital/model';
|
||||
import { getTenantId } from '@/utils/domain';
|
||||
import useSearch from '@/utils/use-search';
|
||||
import {
|
||||
ShopDealerOrder,
|
||||
ShopDealerOrderParam
|
||||
} from '@/api/sdy/sdyDealerOrder/model';
|
||||
import { pageShopDealerOrder } from '@/api/shop/shopDealerOrder';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -124,17 +127,13 @@
|
||||
];
|
||||
message.loading('正在导出...');
|
||||
setTimeout(() => {
|
||||
writeFile(
|
||||
workbook,
|
||||
`${sheetName}.xlsx`
|
||||
);
|
||||
writeFile(workbook, `${sheetName}.xlsx`);
|
||||
}, 1000);
|
||||
})
|
||||
.catch((msg) => {
|
||||
message.error(msg);
|
||||
})
|
||||
.finally(() => {
|
||||
});
|
||||
.finally(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
>
|
||||
<!-- 订单基本信息 -->
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600;">基本信息</span>
|
||||
<span style="color: #1890ff; font-weight: 600">基本信息</span>
|
||||
</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
@@ -68,13 +68,25 @@
|
||||
<div class="font-bold text-gray-400 bg-gray-50">开发调试</div>
|
||||
<div class="text-gray-400 bg-gray-50">
|
||||
<div>业务员({{ form.userId }}):{{ form.nickname }}</div>
|
||||
<div>一级分销商({{ form.firstUserId }}):{{ form.firstNickname }},一级佣金30%:{{ form.firstMoney }}</div>
|
||||
<div>二级分销商({{ form.secondUserId }}):{{ form.secondNickname }},二级佣金10%:{{ form.secondMoney }}</div>
|
||||
<div>三级分销商({{ form.thirdUserId }}):{{ form.thirdNickname }},三级佣金60%:{{ form.thirdMoney }}</div>
|
||||
<div
|
||||
>一级分销商({{ form.firstUserId }}):{{
|
||||
form.firstNickname
|
||||
}},一级佣金30%:{{ form.firstMoney }}</div
|
||||
>
|
||||
<div
|
||||
>二级分销商({{ form.secondUserId }}):{{
|
||||
form.secondNickname
|
||||
}},二级佣金10%:{{ form.secondMoney }}</div
|
||||
>
|
||||
<div
|
||||
>三级分销商({{ form.thirdUserId }}):{{
|
||||
form.thirdNickname
|
||||
}},三级佣金60%:{{ form.thirdMoney }}</div
|
||||
>
|
||||
</div>
|
||||
<!-- 分销商信息 -->
|
||||
<a-divider orientation="left">
|
||||
<span style="color: #1890ff; font-weight: 600;">收益计算</span>
|
||||
<span style="color: #1890ff; font-weight: 600">收益计算</span>
|
||||
</a-divider>
|
||||
|
||||
<!-- 一级分销商 -->
|
||||
@@ -117,9 +129,7 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="占比" name="rate">
|
||||
10%
|
||||
</a-form-item>
|
||||
<a-form-item label="占比" name="rate"> 10% </a-form-item>
|
||||
<a-form-item label="获取收益" name="firstMoney">
|
||||
{{ form.secondMoney }}
|
||||
</a-form-item>
|
||||
@@ -152,11 +162,14 @@
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<a-form-item label="结算时间" name="settleTime" v-if="form.isSettled === 1">
|
||||
<a-form-item
|
||||
label="结算时间"
|
||||
name="settleTime"
|
||||
v-if="form.isSettled === 1"
|
||||
>
|
||||
{{ form.settleTime }}
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
@@ -166,7 +179,7 @@ import {Form, message} from 'ant-design-vue';
|
||||
import { assignObject } from 'ele-admin-pro';
|
||||
import { ShopDealerOrder } from '@/api/shop/shopDealerOrder/model';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import {updateSdyDealerOrder} from "@/api/sdy/sdyDealerOrder";
|
||||
import { updateSdyDealerOrder } from '@/api/sdy/sdyDealerOrder';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
@@ -236,10 +249,9 @@ const rules = reactive({
|
||||
message: '请选择用户ID',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
/* 保存编辑 */
|
||||
@@ -275,11 +287,10 @@ const save = () => {
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
});
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
console.log(localStorage.getItem(''))
|
||||
console.log(localStorage.getItem(''));
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
|
||||
<template v-if="column.key === 'title'">
|
||||
<div>{{ record.title }}</div>
|
||||
<div class="text-gray-400">业务员:{{ record.userId }}</div>
|
||||
@@ -51,15 +50,21 @@
|
||||
<div class="dealer-info">
|
||||
<div v-if="record.firstUserId" class="dealer-level">
|
||||
<a-tag color="red">一级</a-tag>
|
||||
用户{{ record.firstUserId }} - ¥{{ parseFloat(record.firstMoney || '0').toFixed(2) }}
|
||||
用户{{ record.firstUserId }} - ¥{{
|
||||
parseFloat(record.firstMoney || '0').toFixed(2)
|
||||
}}
|
||||
</div>
|
||||
<div v-if="record.secondUserId" class="dealer-level">
|
||||
<a-tag color="orange">二级</a-tag>
|
||||
用户{{ record.secondUserId }} - ¥{{ parseFloat(record.secondMoney || '0').toFixed(2) }}
|
||||
用户{{ record.secondUserId }} - ¥{{
|
||||
parseFloat(record.secondMoney || '0').toFixed(2)
|
||||
}}
|
||||
</div>
|
||||
<div v-if="record.thirdUserId" class="dealer-level">
|
||||
<a-tag color="gold">三级</a-tag>
|
||||
用户{{ record.thirdUserId }} - ¥{{ parseFloat(record.thirdMoney || '0').toFixed(2) }}
|
||||
用户{{ record.thirdUserId }} - ¥{{
|
||||
parseFloat(record.thirdMoney || '0').toFixed(2)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -109,9 +114,7 @@
|
||||
@confirm="remove(record)"
|
||||
placement="topRight"
|
||||
>
|
||||
<a class="text-red-500">
|
||||
删除
|
||||
</a>
|
||||
<a class="text-red-500"> 删除 </a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
@@ -119,7 +122,11 @@
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ShopDealerOrderEdit v-model:visible="showEdit" :data="current" @done="reload"/>
|
||||
<ShopDealerOrderEdit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
@@ -128,7 +135,7 @@ import {createVNode, ref} from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import {
|
||||
ExclamationCircleOutlined,
|
||||
DollarOutlined,
|
||||
DollarOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
@@ -138,9 +145,19 @@ import type {
|
||||
import Search from './components/search.vue';
|
||||
import { getPageTitle } from '@/utils/common';
|
||||
import ShopDealerOrderEdit from './components/shopDealerOrderEdit.vue';
|
||||
import {pageShopDealerOrder, removeShopDealerOrder, removeBatchShopDealerOrder} from '@/api/shop/shopDealerOrder';
|
||||
import type {ShopDealerOrder, ShopDealerOrderParam} from '@/api/shop/shopDealerOrder/model';
|
||||
import {exportSdyDealerOrder, updateSdyDealerOrder} from "@/api/sdy/sdyDealerOrder";
|
||||
import {
|
||||
pageShopDealerOrder,
|
||||
removeShopDealerOrder,
|
||||
removeBatchShopDealerOrder
|
||||
} from '@/api/shop/shopDealerOrder';
|
||||
import type {
|
||||
ShopDealerOrder,
|
||||
ShopDealerOrderParam
|
||||
} from '@/api/shop/shopDealerOrder/model';
|
||||
import {
|
||||
exportSdyDealerOrder,
|
||||
updateSdyDealerOrder
|
||||
} from '@/api/sdy/sdyDealerOrder';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
@@ -283,9 +300,11 @@ const reload = (where?: ShopDealerOrderParam) => {
|
||||
|
||||
/* 结算单个订单 */
|
||||
const settleOrder = (row: ShopDealerOrder) => {
|
||||
const totalCommission = (parseFloat(row.firstMoney || '0') +
|
||||
const totalCommission = (
|
||||
parseFloat(row.firstMoney || '0') +
|
||||
parseFloat(row.secondMoney || '0') +
|
||||
parseFloat(row.thirdMoney || '0')).toFixed(2);
|
||||
parseFloat(row.thirdMoney || '0')
|
||||
).toFixed(2);
|
||||
|
||||
Modal.confirm({
|
||||
title: '确认结算',
|
||||
@@ -300,7 +319,7 @@ const settleOrder = (row: ShopDealerOrder) => {
|
||||
updateSdyDealerOrder({
|
||||
...row,
|
||||
isSettled: 1
|
||||
})
|
||||
});
|
||||
setTimeout(() => {
|
||||
hide();
|
||||
message.success('结算成功');
|
||||
@@ -317,8 +336,8 @@ const batchSettle = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const validOrders = selection.value.filter(order =>
|
||||
order.isSettled === 0 && order.isInvalid === 0
|
||||
const validOrders = selection.value.filter(
|
||||
(order) => order.isSettled === 0 && order.isInvalid === 0
|
||||
);
|
||||
|
||||
if (!validOrders.length) {
|
||||
@@ -326,11 +345,16 @@ const batchSettle = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const totalCommission = validOrders.reduce((sum, order) => {
|
||||
return sum + parseFloat(order.firstMoney || '0') +
|
||||
const totalCommission = validOrders
|
||||
.reduce((sum, order) => {
|
||||
return (
|
||||
sum +
|
||||
parseFloat(order.firstMoney || '0') +
|
||||
parseFloat(order.secondMoney || '0') +
|
||||
parseFloat(order.thirdMoney || '0');
|
||||
}, 0).toFixed(2);
|
||||
parseFloat(order.thirdMoney || '0')
|
||||
);
|
||||
}, 0)
|
||||
.toFixed(2);
|
||||
|
||||
Modal.confirm({
|
||||
title: '批量结算确认',
|
||||
|
||||
@@ -190,7 +190,10 @@
|
||||
import { computed, ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, toDateString, uuid } from 'ele-admin-pro';
|
||||
import { addShopDealerUser, updateShopDealerUser } from '@/api/shop/shopDealerUser';
|
||||
import {
|
||||
addShopDealerUser,
|
||||
updateShopDealerUser
|
||||
} from '@/api/shop/shopDealerUser';
|
||||
import { ShopDealerUser } from '@/api/shop/shopDealerUser/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
@@ -260,11 +263,15 @@
|
||||
};
|
||||
|
||||
const createTimeText = computed(() => {
|
||||
return form.createTime ? toDateString(form.createTime, 'yyyy-MM-dd HH:mm:ss') : '';
|
||||
return form.createTime
|
||||
? toDateString(form.createTime, 'yyyy-MM-dd HH:mm:ss')
|
||||
: '';
|
||||
});
|
||||
|
||||
const updateTimeText = computed(() => {
|
||||
return form.updateTime ? toDateString(form.updateTime, 'yyyy-MM-dd HH:mm:ss') : '';
|
||||
return form.updateTime
|
||||
? toDateString(form.updateTime, 'yyyy-MM-dd HH:mm:ss')
|
||||
: '';
|
||||
});
|
||||
|
||||
const selectedUserText = ref<string>('');
|
||||
@@ -369,13 +376,7 @@
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
// 不在弹窗里编辑的字段不提交,避免误更新(如自增ID、删除标识等)
|
||||
const {
|
||||
isDelete,
|
||||
tenantId,
|
||||
createTime,
|
||||
updateTime,
|
||||
...rest
|
||||
} = form;
|
||||
const { isDelete, tenantId, createTime, updateTime, ...rest } = form;
|
||||
const formData: ShopDealerUser = { ...rest };
|
||||
// userId 新增需要,编辑不允许修改
|
||||
if (isUpdate.value) {
|
||||
@@ -385,7 +386,9 @@
|
||||
if (isUpdate.value && !formData.payPassword) {
|
||||
delete formData.payPassword;
|
||||
}
|
||||
const saveOrUpdate = isUpdate.value ? updateShopDealerUser : addShopDealerUser;
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateShopDealerUser
|
||||
: addShopDealerUser;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
@@ -417,7 +420,7 @@
|
||||
uid: uuid(),
|
||||
url: props.data.image,
|
||||
status: 'done'
|
||||
})
|
||||
});
|
||||
}
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
|
||||
@@ -29,7 +29,10 @@
|
||||
<!-- <a-tag v-if="record.type === 2" color="purple">集团</a-tag>-->
|
||||
</template>
|
||||
<template v-if="column.key === 'qrcode'">
|
||||
<QrcodeOutlined :style="{fontSize: '24px'}" @click="openQrCode(record)" />
|
||||
<QrcodeOutlined
|
||||
:style="{ fontSize: '24px' }"
|
||||
@click="openQrCode(record)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
|
||||
@@ -52,7 +55,11 @@
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ShopDealerUserEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
<ShopDealerUserEdit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
|
||||
<!-- 二维码预览 -->
|
||||
<a-modal
|
||||
@@ -64,7 +71,12 @@
|
||||
destroy-on-close
|
||||
>
|
||||
<div style="display: flex; justify-content: center">
|
||||
<a-image v-if="qrModalUrl" :src="qrModalUrl" :width="280" :preview="false" />
|
||||
<a-image
|
||||
v-if="qrModalUrl"
|
||||
:src="qrModalUrl"
|
||||
:width="280"
|
||||
:preview="false"
|
||||
/>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center; margin-top: 12px">
|
||||
<a-space>
|
||||
@@ -79,7 +91,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref, computed } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined, QrcodeOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
ExclamationCircleOutlined,
|
||||
QrcodeOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
@@ -89,8 +104,15 @@
|
||||
import Search from './components/search.vue';
|
||||
import { getPageTitle } from '@/utils/common';
|
||||
import ShopDealerUserEdit from './components/shopDealerUserEdit.vue';
|
||||
import { pageShopDealerUser, removeShopDealerUser, removeBatchShopDealerUser } from '@/api/shop/shopDealerUser';
|
||||
import type { ShopDealerUser, ShopDealerUserParam } from '@/api/shop/shopDealerUser/model';
|
||||
import {
|
||||
pageShopDealerUser,
|
||||
removeShopDealerUser,
|
||||
removeBatchShopDealerUser
|
||||
} from '@/api/shop/shopDealerUser';
|
||||
import type {
|
||||
ShopDealerUser,
|
||||
ShopDealerUserParam
|
||||
} from '@/api/shop/shopDealerUser/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
@@ -111,7 +133,9 @@
|
||||
const loading = ref(true);
|
||||
|
||||
const getQrCodeUrl = (userId?: number) => {
|
||||
return `https://mp-api.websoft.top/api/wx-login/getOrderQRCodeUnlimited/uid_${userId ?? ''}`;
|
||||
return `https://mp-api.websoft.top/api/wx-login/getOrderQRCodeUnlimited/uid_${
|
||||
userId ?? ''
|
||||
}`;
|
||||
};
|
||||
|
||||
const openQrCode = (row: ShopDealerUser) => {
|
||||
@@ -120,7 +144,9 @@
|
||||
return;
|
||||
}
|
||||
qrModalUrl.value = getQrCodeUrl(row.userId);
|
||||
qrModalTitle.value = row.realName ? `${row.realName} 的二维码` : `UID_${row.userId} 二维码`;
|
||||
qrModalTitle.value = row.realName
|
||||
? `${row.realName} 的二维码`
|
||||
: `UID_${row.userId} 二维码`;
|
||||
showQrModal.value = true;
|
||||
};
|
||||
|
||||
@@ -168,7 +194,7 @@
|
||||
title: '用户ID',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
width: 90,
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
|
||||
@@ -101,7 +101,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
// GET /ai-proxy/models -> https://ai.websoft.top/api/v1/models
|
||||
// POST /ai-proxy/chat/completions -> https://ai.websoft.top/api/v1/chat/completions
|
||||
'/ai-proxy': {
|
||||
target: env.AI_PROXY_TARGET || 'https://ai-api.websoft.top',
|
||||
target: env.AI_PROXY_TARGET || 'http://127.0.0.1:11434',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path) =>
|
||||
@@ -124,10 +124,10 @@ export default defineConfig(({ command, mode }) => {
|
||||
}
|
||||
},
|
||||
// Ollama native API reverse proxy (dev only).
|
||||
// GET /proxy/api/tags -> http://47.119.165.234:11434/api/tags
|
||||
// POST /proxy/api/chat -> http://47.119.165.234:11434/api/chat
|
||||
// GET /proxy/api/tags -> http://127.0.0.1:11434/api/tags
|
||||
// POST /proxy/api/chat -> http://127.0.0.1:11434/api/chat
|
||||
'/proxy': {
|
||||
target: 'http://47.119.165.234:11434',
|
||||
target: 'http://127.0.0.1:11434',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path) => path.replace(/^\/proxy/, '')
|
||||
|
||||
Reference in New Issue
Block a user