@adep/cli 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2884 -575
- package/dist/vite.js +11 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1931,6 +1931,17 @@ var init_sql_engine = __esm({
|
|
|
1931
1931
|
}
|
|
1932
1932
|
return { changes: 0 };
|
|
1933
1933
|
}
|
|
1934
|
+
if (/^CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?/i.test(stmt)) {
|
|
1935
|
+
const idx = /^CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s+ON\s+((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))/i.exec(
|
|
1936
|
+
stmt
|
|
1937
|
+
);
|
|
1938
|
+
if (idx === null) throw new SimDbError("DB_UNSAFE_OP", `\u65E0\u6CD5\u89E3\u6790 CREATE INDEX \u8BED\u53E5`);
|
|
1939
|
+
const tableName = ident(idx[3]);
|
|
1940
|
+
if (this.tables[tableName] === void 0) {
|
|
1941
|
+
throw new SimDbError("DB_UNSAFE_OP", `CREATE INDEX \u76EE\u6807\u8868\u4E0D\u5B58\u5728\uFF1A${tableName}`);
|
|
1942
|
+
}
|
|
1943
|
+
return { changes: 0 };
|
|
1944
|
+
}
|
|
1934
1945
|
if (/^INSERT\s+INTO/i.test(stmt)) return this.execInsert(stmt, params);
|
|
1935
1946
|
if (/^UPDATE\s+/i.test(stmt)) return this.execUpdate(stmt, params);
|
|
1936
1947
|
if (/^DELETE\s+FROM/i.test(stmt)) return this.execDelete(stmt, params);
|
|
@@ -4706,6 +4717,260 @@ var init_dev = __esm({
|
|
|
4706
4717
|
}
|
|
4707
4718
|
});
|
|
4708
4719
|
|
|
4720
|
+
// packages/cli/src/client.ts
|
|
4721
|
+
var client_exports = {};
|
|
4722
|
+
__export(client_exports, {
|
|
4723
|
+
createClient: () => createClient,
|
|
4724
|
+
resolveSlug: () => resolveSlug
|
|
4725
|
+
});
|
|
4726
|
+
async function envelopeError(response, fallbackCode, parsed) {
|
|
4727
|
+
const payload = parsed ?? null;
|
|
4728
|
+
return new CliError(
|
|
4729
|
+
payload?.error?.code ?? fallbackCode,
|
|
4730
|
+
payload?.error?.message ?? `\u5E73\u53F0\u8FD4\u56DE HTTP ${response.status}`
|
|
4731
|
+
);
|
|
4732
|
+
}
|
|
4733
|
+
async function parseBody(response) {
|
|
4734
|
+
const text = await response.text();
|
|
4735
|
+
if (text.length === 0) return null;
|
|
4736
|
+
try {
|
|
4737
|
+
return JSON.parse(text);
|
|
4738
|
+
} catch {
|
|
4739
|
+
return null;
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4742
|
+
async function createClient(paths) {
|
|
4743
|
+
const credentials = await loadCredentials(paths);
|
|
4744
|
+
if (credentials === null) {
|
|
4745
|
+
throw new CliError("NOT_LOGGED_IN", "\u672A\u767B\u5F55\uFF1A\u8BF7\u5148\u6267\u884C adep login");
|
|
4746
|
+
}
|
|
4747
|
+
const server = credentials.server;
|
|
4748
|
+
const cookie = `better-auth.session_token=${decodeToken(credentials.encodedToken)}`;
|
|
4749
|
+
const request = async (path, init = {}, fallbackCode = "API_REQUEST_FAILED") => {
|
|
4750
|
+
const method = init.method ?? (init.body === void 0 ? "GET" : "POST");
|
|
4751
|
+
let response;
|
|
4752
|
+
try {
|
|
4753
|
+
response = await fetch(`${server}${path}`, {
|
|
4754
|
+
method,
|
|
4755
|
+
headers: { "content-type": "application/json", cookie },
|
|
4756
|
+
...init.body === void 0 ? {} : { body: JSON.stringify(init.body) }
|
|
4757
|
+
});
|
|
4758
|
+
} catch (error) {
|
|
4759
|
+
throw new CliError(
|
|
4760
|
+
"SERVER_UNREACHABLE",
|
|
4761
|
+
`\u65E0\u6CD5\u8FDE\u63A5\u5E73\u53F0 ${server}\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
4762
|
+
);
|
|
4763
|
+
}
|
|
4764
|
+
const parsed = await parseBody(response);
|
|
4765
|
+
if (response.status < 200 || response.status >= 300) {
|
|
4766
|
+
throw await envelopeError(response, fallbackCode, parsed);
|
|
4767
|
+
}
|
|
4768
|
+
return parsed;
|
|
4769
|
+
};
|
|
4770
|
+
const upload = async (path, form, fallbackCode = "UPLOAD_FAILED") => {
|
|
4771
|
+
let response;
|
|
4772
|
+
try {
|
|
4773
|
+
response = await fetch(`${server}${path}`, {
|
|
4774
|
+
method: "POST",
|
|
4775
|
+
headers: { cookie },
|
|
4776
|
+
body: form
|
|
4777
|
+
});
|
|
4778
|
+
} catch (error) {
|
|
4779
|
+
throw new CliError(
|
|
4780
|
+
"SERVER_UNREACHABLE",
|
|
4781
|
+
`\u65E0\u6CD5\u8FDE\u63A5\u5E73\u53F0 ${server}\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
4782
|
+
);
|
|
4783
|
+
}
|
|
4784
|
+
if (response.status < 200 || response.status >= 300) {
|
|
4785
|
+
throw await envelopeError(response, fallbackCode, await parseBody(response));
|
|
4786
|
+
}
|
|
4787
|
+
return response;
|
|
4788
|
+
};
|
|
4789
|
+
const download = async (url, fallbackCode = "DOWNLOAD_FAILED") => {
|
|
4790
|
+
const target = url.startsWith("http") ? url : `${server}${url}`;
|
|
4791
|
+
let response;
|
|
4792
|
+
try {
|
|
4793
|
+
response = await fetch(target, { headers: { cookie } });
|
|
4794
|
+
} catch (error) {
|
|
4795
|
+
throw new CliError(
|
|
4796
|
+
"SERVER_UNREACHABLE",
|
|
4797
|
+
`\u65E0\u6CD5\u8FDE\u63A5\u5E73\u53F0 ${server}\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
4798
|
+
);
|
|
4799
|
+
}
|
|
4800
|
+
if (response.status < 200 || response.status >= 300) {
|
|
4801
|
+
throw await envelopeError(response, fallbackCode, await parseBody(response));
|
|
4802
|
+
}
|
|
4803
|
+
return response;
|
|
4804
|
+
};
|
|
4805
|
+
return { server, cookie, request, upload, download };
|
|
4806
|
+
}
|
|
4807
|
+
async function resolveSlug(cwd, slug) {
|
|
4808
|
+
if (slug !== void 0 && slug.length > 0) return slug;
|
|
4809
|
+
const config = await loadConfig(cwd);
|
|
4810
|
+
return config.name;
|
|
4811
|
+
}
|
|
4812
|
+
var init_client = __esm({
|
|
4813
|
+
"packages/cli/src/client.ts"() {
|
|
4814
|
+
"use strict";
|
|
4815
|
+
init_auth();
|
|
4816
|
+
init_credentials();
|
|
4817
|
+
init_dev();
|
|
4818
|
+
}
|
|
4819
|
+
});
|
|
4820
|
+
|
|
4821
|
+
// packages/cli/src/db.ts
|
|
4822
|
+
var db_exports = {};
|
|
4823
|
+
__export(db_exports, {
|
|
4824
|
+
dbExec: () => dbExec,
|
|
4825
|
+
dbMigrate: () => dbMigrate,
|
|
4826
|
+
dbRollback: () => dbRollback,
|
|
4827
|
+
dbSnapshotCreate: () => dbSnapshotCreate,
|
|
4828
|
+
dbSnapshotList: () => dbSnapshotList,
|
|
4829
|
+
dbSnapshotRestore: () => dbSnapshotRestore,
|
|
4830
|
+
dbStart: () => dbStart,
|
|
4831
|
+
dbStatus: () => dbStatus,
|
|
4832
|
+
dbStop: () => dbStop,
|
|
4833
|
+
splitSqlStatements: () => splitSqlStatements
|
|
4834
|
+
});
|
|
4835
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
4836
|
+
import { resolve as resolve5 } from "node:path";
|
|
4837
|
+
async function open2(paths, options) {
|
|
4838
|
+
const client = await createClient(paths);
|
|
4839
|
+
const project2 = await resolveSlug(resolve5(options.cwd), options.slug);
|
|
4840
|
+
return { client, project: project2 };
|
|
4841
|
+
}
|
|
4842
|
+
async function dbStart(paths, options) {
|
|
4843
|
+
const { client, project: project2 } = await open2(paths, options);
|
|
4844
|
+
return client.request(`/api/v1/projects/${project2}/database`, { method: "POST" });
|
|
4845
|
+
}
|
|
4846
|
+
async function dbStatus(paths, options) {
|
|
4847
|
+
const { client, project: project2 } = await open2(paths, options);
|
|
4848
|
+
return client.request(`/api/v1/projects/${project2}/database`);
|
|
4849
|
+
}
|
|
4850
|
+
async function dbStop(paths, options) {
|
|
4851
|
+
const { client, project: project2 } = await open2(paths, options);
|
|
4852
|
+
return client.request(`/api/v1/projects/${project2}/database`, {
|
|
4853
|
+
method: "DELETE"
|
|
4854
|
+
});
|
|
4855
|
+
}
|
|
4856
|
+
async function dbExec(paths, options) {
|
|
4857
|
+
const { client, project: project2 } = await open2(paths, options);
|
|
4858
|
+
return client.request(
|
|
4859
|
+
`/api/v1/projects/${project2}/database/console/sql`,
|
|
4860
|
+
{
|
|
4861
|
+
method: "POST",
|
|
4862
|
+
body: {
|
|
4863
|
+
sql: options.sql,
|
|
4864
|
+
...options.params === void 0 ? {} : { params: options.params },
|
|
4865
|
+
...options.confirmTable === void 0 ? {} : { confirmTable: options.confirmTable }
|
|
4866
|
+
}
|
|
4867
|
+
},
|
|
4868
|
+
"SQL_EXEC_FAILED"
|
|
4869
|
+
);
|
|
4870
|
+
}
|
|
4871
|
+
async function dbSnapshotList(paths, options) {
|
|
4872
|
+
const { client, project: project2 } = await open2(paths, options);
|
|
4873
|
+
return client.request(
|
|
4874
|
+
`/api/v1/projects/${project2}/database/snapshots`
|
|
4875
|
+
);
|
|
4876
|
+
}
|
|
4877
|
+
async function dbSnapshotCreate(paths, options) {
|
|
4878
|
+
const { client, project: project2 } = await open2(paths, options);
|
|
4879
|
+
return client.request(`/api/v1/projects/${project2}/database/snapshots`, {
|
|
4880
|
+
method: "POST"
|
|
4881
|
+
});
|
|
4882
|
+
}
|
|
4883
|
+
async function dbSnapshotRestore(paths, options) {
|
|
4884
|
+
const { client, project: project2 } = await open2(paths, options);
|
|
4885
|
+
return client.request(
|
|
4886
|
+
`/api/v1/projects/${project2}/database/snapshots/${options.snapshotId}/restore`,
|
|
4887
|
+
{ method: "POST" }
|
|
4888
|
+
);
|
|
4889
|
+
}
|
|
4890
|
+
async function dbRollback(paths, options) {
|
|
4891
|
+
const { client, project: project2 } = await open2(paths, options);
|
|
4892
|
+
return client.request(
|
|
4893
|
+
`/api/v1/projects/${project2}/database/rollback`,
|
|
4894
|
+
{ method: "POST", body: { to: options.to } }
|
|
4895
|
+
);
|
|
4896
|
+
}
|
|
4897
|
+
function splitSqlStatements(source) {
|
|
4898
|
+
const statements = [];
|
|
4899
|
+
let buffer = "";
|
|
4900
|
+
let inQuote = false;
|
|
4901
|
+
const lines = source.split("\n");
|
|
4902
|
+
for (const rawLine of lines) {
|
|
4903
|
+
const line = rawLine.trim();
|
|
4904
|
+
if (line.length === 0) continue;
|
|
4905
|
+
if (line.startsWith("--")) continue;
|
|
4906
|
+
for (let i = 0; i < line.length; i++) {
|
|
4907
|
+
const ch = line[i];
|
|
4908
|
+
if (ch === "'") {
|
|
4909
|
+
if (inQuote && line[i + 1] === "'") {
|
|
4910
|
+
buffer += ch + line[i + 1];
|
|
4911
|
+
i++;
|
|
4912
|
+
continue;
|
|
4913
|
+
}
|
|
4914
|
+
inQuote = !inQuote;
|
|
4915
|
+
buffer += ch;
|
|
4916
|
+
continue;
|
|
4917
|
+
}
|
|
4918
|
+
if (ch === ";" && !inQuote) {
|
|
4919
|
+
const statement = buffer.trim();
|
|
4920
|
+
if (statement.length > 0) statements.push(statement);
|
|
4921
|
+
buffer = "";
|
|
4922
|
+
continue;
|
|
4923
|
+
}
|
|
4924
|
+
buffer += ch;
|
|
4925
|
+
}
|
|
4926
|
+
buffer += " ";
|
|
4927
|
+
}
|
|
4928
|
+
if (inQuote) {
|
|
4929
|
+
throw new Error(`SQL \u8FC1\u79FB\u6587\u4EF6\u5F15\u53F7\u672A\u95ED\u5408\uFF1A${buffer.trim().slice(0, 80)}\u2026`);
|
|
4930
|
+
}
|
|
4931
|
+
const tail = buffer.trim();
|
|
4932
|
+
if (tail.length > 0) statements.push(tail);
|
|
4933
|
+
return statements;
|
|
4934
|
+
}
|
|
4935
|
+
async function dbMigrate(paths, options) {
|
|
4936
|
+
const source = await readFile8(resolve5(options.file), "utf8");
|
|
4937
|
+
const statements = splitSqlStatements(source);
|
|
4938
|
+
if (statements.length === 0) {
|
|
4939
|
+
throw new Error(`\u8FC1\u79FB\u6587\u4EF6\u6CA1\u6709\u53EF\u6267\u884C\u8BED\u53E5\uFF1A${options.file}`);
|
|
4940
|
+
}
|
|
4941
|
+
const { project: project2 } = await open2(paths, options);
|
|
4942
|
+
const applied = [];
|
|
4943
|
+
const changes = [];
|
|
4944
|
+
for (const statement of statements) {
|
|
4945
|
+
try {
|
|
4946
|
+
const result = await dbExec(paths, {
|
|
4947
|
+
cwd: options.cwd,
|
|
4948
|
+
...options.slug === void 0 ? {} : { slug: options.slug },
|
|
4949
|
+
sql: statement
|
|
4950
|
+
});
|
|
4951
|
+
applied.push(statement);
|
|
4952
|
+
changes.push(result.changes);
|
|
4953
|
+
} catch (error) {
|
|
4954
|
+
const code = error instanceof CliError ? error.code : "MIGRATE_FAILED";
|
|
4955
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4956
|
+
return {
|
|
4957
|
+
projectId: project2,
|
|
4958
|
+
applied,
|
|
4959
|
+
changes,
|
|
4960
|
+
failed: { statement, code, message }
|
|
4961
|
+
};
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
return { projectId: project2, applied, changes };
|
|
4965
|
+
}
|
|
4966
|
+
var init_db2 = __esm({
|
|
4967
|
+
"packages/cli/src/db.ts"() {
|
|
4968
|
+
"use strict";
|
|
4969
|
+
init_auth();
|
|
4970
|
+
init_client();
|
|
4971
|
+
}
|
|
4972
|
+
});
|
|
4973
|
+
|
|
4709
4974
|
// packages/cli/src/serve/banner.ts
|
|
4710
4975
|
function isLoopback(host) {
|
|
4711
4976
|
return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "[::1]";
|
|
@@ -4727,7 +4992,7 @@ function renderBanner(options) {
|
|
|
4727
4992
|
lines.push("");
|
|
4728
4993
|
lines.push(" \u80FD\u529B:");
|
|
4729
4994
|
if (functions.length > 0) {
|
|
4730
|
-
const route = options.functionsPrefix === void 0 || options.functionsPrefix.length === 0 ? `${baseUrl}/api/{fn}` : `${baseUrl}
|
|
4995
|
+
const route = options.functionsPrefix === void 0 || options.functionsPrefix.length === 0 ? `${baseUrl}/api/{fn}` : `${baseUrl}/${options.functionsPrefix.replace(/^\/+|\/+$/g, "")}/{fn}`;
|
|
4731
4996
|
lines.push(` - \u51FD\u6570\u8DEF\u7531: ${route}\uFF08${functions.length} \u4E2A\u51FD\u6570: ${functions.join(", ")}\uFF09`);
|
|
4732
4997
|
} else {
|
|
4733
4998
|
lines.push(" - \u51FD\u6570\u8DEF\u7531: \u65E0\uFF08functions/ \u76EE\u5F55\u4E3A\u7A7A\uFF09");
|
|
@@ -4778,8 +5043,8 @@ __export(server_exports, {
|
|
|
4778
5043
|
});
|
|
4779
5044
|
import { createServer as createServer2 } from "node:http";
|
|
4780
5045
|
import { watch as watch2 } from "node:fs";
|
|
4781
|
-
import { mkdir as mkdir7, readdir as readdir4, readFile as
|
|
4782
|
-
import { join as join11, relative as relative2, resolve as
|
|
5046
|
+
import { mkdir as mkdir7, readdir as readdir4, readFile as readFile9, stat as stat6 } from "node:fs/promises";
|
|
5047
|
+
import { extname, join as join11, relative as relative2, resolve as resolve6 } from "node:path";
|
|
4783
5048
|
function envNumber(name) {
|
|
4784
5049
|
const raw = process.env[name];
|
|
4785
5050
|
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
@@ -4805,7 +5070,7 @@ async function collectFunctions2(dir) {
|
|
|
4805
5070
|
if (entry.isDirectory()) {
|
|
4806
5071
|
await walk(full, rel);
|
|
4807
5072
|
} else if (entry.isFile() && entry.name.endsWith(".ts")) {
|
|
4808
|
-
files[rel] = await
|
|
5073
|
+
files[rel] = await readFile9(full, "utf8");
|
|
4809
5074
|
}
|
|
4810
5075
|
}
|
|
4811
5076
|
};
|
|
@@ -4840,12 +5105,12 @@ function contentTypeFor(path) {
|
|
|
4840
5105
|
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
4841
5106
|
}
|
|
4842
5107
|
async function startServeServer(options) {
|
|
4843
|
-
const cwd =
|
|
5108
|
+
const cwd = resolve6(options.cwd);
|
|
4844
5109
|
const log = options.log ?? ((line) => process.stdout.write(`${line}
|
|
4845
5110
|
`));
|
|
4846
5111
|
const host = options.host ?? envString("HOST") ?? "127.0.0.1";
|
|
4847
5112
|
const port = options.port ?? envNumber("PORT") ?? 8787;
|
|
4848
|
-
const staticDir =
|
|
5113
|
+
const staticDir = resolve6(
|
|
4849
5114
|
cwd,
|
|
4850
5115
|
options.staticDir ?? envString("ADEP_SERVE_STATIC_DIR") ?? "public"
|
|
4851
5116
|
);
|
|
@@ -4860,7 +5125,7 @@ async function startServeServer(options) {
|
|
|
4860
5125
|
}
|
|
4861
5126
|
let files = await collectFunctions2(functionsDir);
|
|
4862
5127
|
let functionNames = listFunctionNames(files);
|
|
4863
|
-
const envText = await
|
|
5128
|
+
const envText = await readFile9(join11(cwd, ".env.local"), "utf8").catch(() => "");
|
|
4864
5129
|
let env = parseEnvFile(envText);
|
|
4865
5130
|
let simEnv = await loadSimEnv(cwd).catch(() => ({}));
|
|
4866
5131
|
const runtime = await createSimRuntime({
|
|
@@ -4869,7 +5134,35 @@ async function startServeServer(options) {
|
|
|
4869
5134
|
baseUrl: `http://${host}:${port}`,
|
|
4870
5135
|
log
|
|
4871
5136
|
});
|
|
4872
|
-
|
|
5137
|
+
if (options.schemaFile !== void 0) {
|
|
5138
|
+
const schemaPath = resolve6(cwd, options.schemaFile);
|
|
5139
|
+
const schemaSource = await readFile9(schemaPath, "utf8").catch((error) => {
|
|
5140
|
+
if (error.code === "ENOENT") {
|
|
5141
|
+
throw new Error(`schema \u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${schemaPath}`);
|
|
5142
|
+
}
|
|
5143
|
+
throw error;
|
|
5144
|
+
});
|
|
5145
|
+
const statements = splitSqlStatements(schemaSource);
|
|
5146
|
+
let applied = 0;
|
|
5147
|
+
let skipped = 0;
|
|
5148
|
+
for (const statement of statements) {
|
|
5149
|
+
try {
|
|
5150
|
+
await runtime.db.engine.run(statement);
|
|
5151
|
+
applied += 1;
|
|
5152
|
+
} catch (error) {
|
|
5153
|
+
const isUnsupported = error instanceof Error && "code" in error && error.code === "DB_UNSAFE_OP";
|
|
5154
|
+
if (isUnsupported) {
|
|
5155
|
+
skipped += 1;
|
|
5156
|
+
log(`[serve] \u8DF3\u8FC7 schema \u8BED\u53E5\uFF08sim \u5F15\u64CE\u4E0D\u652F\u6301\uFF09\uFF1A${statement.slice(0, 60)}\u2026`);
|
|
5157
|
+
} else {
|
|
5158
|
+
throw error;
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
}
|
|
5162
|
+
const skipNote = skipped > 0 ? `\uFF0C\u8DF3\u8FC7 ${skipped} \u6761` : "";
|
|
5163
|
+
log(`[serve] \u5DF2\u5E94\u7528\u6570\u636E\u5E93\u7ED3\u6784\uFF08--schema ${options.schemaFile}\uFF09\uFF1A${applied} \u6761\u8BED\u53E5${skipNote}`);
|
|
5164
|
+
}
|
|
5165
|
+
const executor = new WorkerFunctionExecutor({
|
|
4873
5166
|
deps: { rootDir: cwd, builtin: LOCAL_BUILTIN_DEPS }
|
|
4874
5167
|
});
|
|
4875
5168
|
const dbBundle = runtime.bundle;
|
|
@@ -4879,7 +5172,7 @@ async function startServeServer(options) {
|
|
|
4879
5172
|
const reload = async () => {
|
|
4880
5173
|
const [nextFiles, envLocalText] = await Promise.all([
|
|
4881
5174
|
collectFunctions2(functionsDir),
|
|
4882
|
-
|
|
5175
|
+
readFile9(join11(cwd, ".env.local"), "utf8").catch(() => "")
|
|
4883
5176
|
]);
|
|
4884
5177
|
files = nextFiles;
|
|
4885
5178
|
env = parseEnvFile(envLocalText);
|
|
@@ -4957,7 +5250,7 @@ async function startServeServer(options) {
|
|
|
4957
5250
|
try {
|
|
4958
5251
|
const indexStat = await stat6(indexPath);
|
|
4959
5252
|
if (indexStat.isFile()) {
|
|
4960
|
-
const content2 = await
|
|
5253
|
+
const content2 = await readFile9(indexPath);
|
|
4961
5254
|
res.writeHead(200, { "content-type": contentTypeFor(indexPath) });
|
|
4962
5255
|
res.end(content2);
|
|
4963
5256
|
return true;
|
|
@@ -4966,20 +5259,33 @@ async function startServeServer(options) {
|
|
|
4966
5259
|
}
|
|
4967
5260
|
return false;
|
|
4968
5261
|
}
|
|
4969
|
-
const content = await
|
|
5262
|
+
const content = await readFile9(resolvedPath);
|
|
4970
5263
|
res.writeHead(200, { "content-type": contentTypeFor(resolvedPath) });
|
|
4971
5264
|
res.end(content);
|
|
4972
5265
|
return true;
|
|
4973
5266
|
} catch {
|
|
5267
|
+
if (options.spaFallback === true) {
|
|
5268
|
+
const ext = extname(pathname);
|
|
5269
|
+
if (ext === "" || ext === ".html") {
|
|
5270
|
+
try {
|
|
5271
|
+
const indexContent = await readFile9(join11(staticDir, "index.html"));
|
|
5272
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
5273
|
+
res.end(indexContent);
|
|
5274
|
+
return true;
|
|
5275
|
+
} catch {
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
4974
5279
|
return false;
|
|
4975
5280
|
}
|
|
4976
5281
|
};
|
|
4977
5282
|
const handle = async (req, res) => {
|
|
4978
5283
|
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
4979
5284
|
const pathname = url.pathname;
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
5285
|
+
const normalizedPrefix = config.functionsPrefix.replace(/^\/+|\/+$/g, "");
|
|
5286
|
+
const fnRest = normalizedPrefix.length > 0 ? pathname === `/${normalizedPrefix}` || pathname.startsWith(`/${normalizedPrefix}/`) ? pathname.slice(normalizedPrefix.length + 1) : null : pathname === "/api" ? "" : pathname.startsWith("/api/") ? pathname.slice("/api/".length) : null;
|
|
5287
|
+
if (fnRest !== null) {
|
|
5288
|
+
const { fnName, error: routeError } = resolveRouteFnName(fnRest, "");
|
|
4983
5289
|
if (routeError !== void 0) {
|
|
4984
5290
|
writeJson2(res, 400, { error: routeError });
|
|
4985
5291
|
return;
|
|
@@ -5113,6 +5419,7 @@ var init_server = __esm({
|
|
|
5113
5419
|
init_http_envelope();
|
|
5114
5420
|
init_worker_executor();
|
|
5115
5421
|
init_builtin_deps();
|
|
5422
|
+
init_db2();
|
|
5116
5423
|
init_runtime();
|
|
5117
5424
|
init_env();
|
|
5118
5425
|
init_invoke();
|
|
@@ -5142,107 +5449,6 @@ var init_server = __esm({
|
|
|
5142
5449
|
}
|
|
5143
5450
|
});
|
|
5144
5451
|
|
|
5145
|
-
// packages/cli/src/client.ts
|
|
5146
|
-
var client_exports = {};
|
|
5147
|
-
__export(client_exports, {
|
|
5148
|
-
createClient: () => createClient,
|
|
5149
|
-
resolveSlug: () => resolveSlug
|
|
5150
|
-
});
|
|
5151
|
-
async function envelopeError(response, fallbackCode, parsed) {
|
|
5152
|
-
const payload = parsed ?? null;
|
|
5153
|
-
return new CliError(
|
|
5154
|
-
payload?.error?.code ?? fallbackCode,
|
|
5155
|
-
payload?.error?.message ?? `\u5E73\u53F0\u8FD4\u56DE HTTP ${response.status}`
|
|
5156
|
-
);
|
|
5157
|
-
}
|
|
5158
|
-
async function parseBody(response) {
|
|
5159
|
-
const text = await response.text();
|
|
5160
|
-
if (text.length === 0) return null;
|
|
5161
|
-
try {
|
|
5162
|
-
return JSON.parse(text);
|
|
5163
|
-
} catch {
|
|
5164
|
-
return null;
|
|
5165
|
-
}
|
|
5166
|
-
}
|
|
5167
|
-
async function createClient(paths) {
|
|
5168
|
-
const credentials = await loadCredentials(paths);
|
|
5169
|
-
if (credentials === null) {
|
|
5170
|
-
throw new CliError("NOT_LOGGED_IN", "\u672A\u767B\u5F55\uFF1A\u8BF7\u5148\u6267\u884C adep login");
|
|
5171
|
-
}
|
|
5172
|
-
const server = credentials.server;
|
|
5173
|
-
const cookie = `better-auth.session_token=${decodeToken(credentials.encodedToken)}`;
|
|
5174
|
-
const request = async (path, init = {}, fallbackCode = "API_REQUEST_FAILED") => {
|
|
5175
|
-
const method = init.method ?? (init.body === void 0 ? "GET" : "POST");
|
|
5176
|
-
let response;
|
|
5177
|
-
try {
|
|
5178
|
-
response = await fetch(`${server}${path}`, {
|
|
5179
|
-
method,
|
|
5180
|
-
headers: { "content-type": "application/json", cookie },
|
|
5181
|
-
...init.body === void 0 ? {} : { body: JSON.stringify(init.body) }
|
|
5182
|
-
});
|
|
5183
|
-
} catch (error) {
|
|
5184
|
-
throw new CliError(
|
|
5185
|
-
"SERVER_UNREACHABLE",
|
|
5186
|
-
`\u65E0\u6CD5\u8FDE\u63A5\u5E73\u53F0 ${server}\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
5187
|
-
);
|
|
5188
|
-
}
|
|
5189
|
-
const parsed = await parseBody(response);
|
|
5190
|
-
if (response.status < 200 || response.status >= 300) {
|
|
5191
|
-
throw await envelopeError(response, fallbackCode, parsed);
|
|
5192
|
-
}
|
|
5193
|
-
return parsed;
|
|
5194
|
-
};
|
|
5195
|
-
const upload = async (path, form, fallbackCode = "UPLOAD_FAILED") => {
|
|
5196
|
-
let response;
|
|
5197
|
-
try {
|
|
5198
|
-
response = await fetch(`${server}${path}`, {
|
|
5199
|
-
method: "POST",
|
|
5200
|
-
headers: { cookie },
|
|
5201
|
-
body: form
|
|
5202
|
-
});
|
|
5203
|
-
} catch (error) {
|
|
5204
|
-
throw new CliError(
|
|
5205
|
-
"SERVER_UNREACHABLE",
|
|
5206
|
-
`\u65E0\u6CD5\u8FDE\u63A5\u5E73\u53F0 ${server}\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
5207
|
-
);
|
|
5208
|
-
}
|
|
5209
|
-
if (response.status < 200 || response.status >= 300) {
|
|
5210
|
-
throw await envelopeError(response, fallbackCode, await parseBody(response));
|
|
5211
|
-
}
|
|
5212
|
-
return response;
|
|
5213
|
-
};
|
|
5214
|
-
const download = async (url, fallbackCode = "DOWNLOAD_FAILED") => {
|
|
5215
|
-
const target = url.startsWith("http") ? url : `${server}${url}`;
|
|
5216
|
-
let response;
|
|
5217
|
-
try {
|
|
5218
|
-
response = await fetch(target, { headers: { cookie } });
|
|
5219
|
-
} catch (error) {
|
|
5220
|
-
throw new CliError(
|
|
5221
|
-
"SERVER_UNREACHABLE",
|
|
5222
|
-
`\u65E0\u6CD5\u8FDE\u63A5\u5E73\u53F0 ${server}\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
5223
|
-
);
|
|
5224
|
-
}
|
|
5225
|
-
if (response.status < 200 || response.status >= 300) {
|
|
5226
|
-
throw await envelopeError(response, fallbackCode, await parseBody(response));
|
|
5227
|
-
}
|
|
5228
|
-
return response;
|
|
5229
|
-
};
|
|
5230
|
-
return { server, cookie, request, upload, download };
|
|
5231
|
-
}
|
|
5232
|
-
async function resolveSlug(cwd, slug) {
|
|
5233
|
-
if (slug !== void 0 && slug.length > 0) return slug;
|
|
5234
|
-
const config = await loadConfig(cwd);
|
|
5235
|
-
return config.name;
|
|
5236
|
-
}
|
|
5237
|
-
var init_client = __esm({
|
|
5238
|
-
"packages/cli/src/client.ts"() {
|
|
5239
|
-
"use strict";
|
|
5240
|
-
init_auth();
|
|
5241
|
-
init_credentials();
|
|
5242
|
-
init_dev();
|
|
5243
|
-
}
|
|
5244
|
-
});
|
|
5245
|
-
|
|
5246
5452
|
// packages/cli/src/deploy.ts
|
|
5247
5453
|
var deploy_exports = {};
|
|
5248
5454
|
__export(deploy_exports, {
|
|
@@ -5251,8 +5457,8 @@ __export(deploy_exports, {
|
|
|
5251
5457
|
deploy: () => deploy
|
|
5252
5458
|
});
|
|
5253
5459
|
import { createHash as createHash2 } from "node:crypto";
|
|
5254
|
-
import { readdir as readdir5, readFile as
|
|
5255
|
-
import { join as join12, resolve as
|
|
5460
|
+
import { readdir as readdir5, readFile as readFile10 } from "node:fs/promises";
|
|
5461
|
+
import { join as join12, resolve as resolve7, basename as basename3, relative as relative3, sep } from "node:path";
|
|
5256
5462
|
function functionUrlBase(prefix) {
|
|
5257
5463
|
const raw = (prefix ?? "/api").trim();
|
|
5258
5464
|
const normalized = raw === "/" ? "/" : raw.replace(/\/+$/, "");
|
|
@@ -5268,7 +5474,7 @@ async function collectEntries(functionsDir) {
|
|
|
5268
5474
|
return entries;
|
|
5269
5475
|
}
|
|
5270
5476
|
for (const name of names) {
|
|
5271
|
-
entries[name] = await
|
|
5477
|
+
entries[name] = await readFile10(join12(functionsDir, `${name}.ts`), "utf8");
|
|
5272
5478
|
}
|
|
5273
5479
|
return entries;
|
|
5274
5480
|
}
|
|
@@ -5294,7 +5500,7 @@ async function collectShared(functionsDir) {
|
|
|
5294
5500
|
if (!entry.name.endsWith(".ts") || entry.name.endsWith(".test.ts")) continue;
|
|
5295
5501
|
const full = join12(dir, entry.name);
|
|
5296
5502
|
const rel = relative3(sharedDir, full).split(sep).join("/");
|
|
5297
|
-
files[`_shared/${rel}`] = await
|
|
5503
|
+
files[`_shared/${rel}`] = await readFile10(full, "utf8");
|
|
5298
5504
|
}
|
|
5299
5505
|
};
|
|
5300
5506
|
await walk(sharedDir);
|
|
@@ -5305,10 +5511,10 @@ async function deploy(paths, options) {
|
|
|
5305
5511
|
const log = options.silent === true ? () => void 0 : options.log ?? ((line) => process.stdout.write(`${line}
|
|
5306
5512
|
`));
|
|
5307
5513
|
const client = await createClient(paths);
|
|
5308
|
-
const cwd =
|
|
5514
|
+
const cwd = resolve7(options.cwd);
|
|
5309
5515
|
const config = await loadConfig(cwd);
|
|
5310
5516
|
const slug = options.slug ?? config.name;
|
|
5311
|
-
const functionsDir = options.functionsDir === void 0 ? join12(cwd, config.functionsDir) :
|
|
5517
|
+
const functionsDir = options.functionsDir === void 0 ? join12(cwd, config.functionsDir) : resolve7(cwd, options.functionsDir);
|
|
5312
5518
|
const local = await collectEntries(functionsDir);
|
|
5313
5519
|
if (Object.keys(local).length === 0) {
|
|
5314
5520
|
throw new CliError("NO_FUNCTIONS", `${functionsDir} \u4E0B\u6CA1\u6709\u51FD\u6570\u6587\u4EF6`);
|
|
@@ -5401,7 +5607,7 @@ var frontend_exports = {};
|
|
|
5401
5607
|
__export(frontend_exports, {
|
|
5402
5608
|
frontendSync: () => frontendSync
|
|
5403
5609
|
});
|
|
5404
|
-
import { readdir as readdir6, readFile as
|
|
5610
|
+
import { readdir as readdir6, readFile as readFile11, stat as stat7 } from "node:fs/promises";
|
|
5405
5611
|
import { join as join13, relative as relative4, sep as sep2 } from "node:path";
|
|
5406
5612
|
async function resolveProjectId(client, cwd, slug) {
|
|
5407
5613
|
const resolvedSlug = await resolveSlug(cwd, slug);
|
|
@@ -5435,7 +5641,7 @@ async function collectFiles(root, dir) {
|
|
|
5435
5641
|
continue;
|
|
5436
5642
|
}
|
|
5437
5643
|
try {
|
|
5438
|
-
const content = await
|
|
5644
|
+
const content = await readFile11(full, "utf8");
|
|
5439
5645
|
if (content.includes("\0")) {
|
|
5440
5646
|
skipped.push(rel);
|
|
5441
5647
|
continue;
|
|
@@ -5663,11 +5869,11 @@ var fs_exports = {};
|
|
|
5663
5869
|
__export(fs_exports, {
|
|
5664
5870
|
createExportFileSystem: () => createExportFileSystem
|
|
5665
5871
|
});
|
|
5666
|
-
import { mkdir as mkdir8, readFile as
|
|
5872
|
+
import { mkdir as mkdir8, readFile as readFile12, rm as rm3, stat as stat9, writeFile as writeFile7 } from "node:fs/promises";
|
|
5667
5873
|
import { dirname as dirname7, join as join15 } from "node:path";
|
|
5668
5874
|
function createExportFileSystem() {
|
|
5669
5875
|
return {
|
|
5670
|
-
readFile: (path) =>
|
|
5876
|
+
readFile: (path) => readFile12(path),
|
|
5671
5877
|
writeFile: (path, content) => writeFile7(path, content),
|
|
5672
5878
|
// 同时用于清理解压临时目录(${zip}.tmp 是个目录)——递归强制删除,缺失不报错。
|
|
5673
5879
|
deleteFile: async (path) => {
|
|
@@ -5685,7 +5891,7 @@ function createExportFileSystem() {
|
|
|
5685
5891
|
await mkdir8(path, { recursive: true });
|
|
5686
5892
|
},
|
|
5687
5893
|
unzip: async (zipPath, targetDir) => {
|
|
5688
|
-
const entries = zipRead(await
|
|
5894
|
+
const entries = zipRead(await readFile12(zipPath));
|
|
5689
5895
|
for (const [name, data] of entries) {
|
|
5690
5896
|
const dest = join15(targetDir, name);
|
|
5691
5897
|
await mkdir8(dirname7(dest), { recursive: true });
|
|
@@ -6196,246 +6402,93 @@ ${detail}`);
|
|
|
6196
6402
|
}
|
|
6197
6403
|
});
|
|
6198
6404
|
|
|
6199
|
-
// packages/cli/src/
|
|
6200
|
-
var
|
|
6201
|
-
__export(
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
dbSnapshotRestore: () => dbSnapshotRestore,
|
|
6208
|
-
dbStart: () => dbStart,
|
|
6209
|
-
dbStatus: () => dbStatus,
|
|
6210
|
-
dbStop: () => dbStop,
|
|
6211
|
-
splitSqlStatements: () => splitSqlStatements
|
|
6405
|
+
// packages/cli/src/storage.ts
|
|
6406
|
+
var storage_exports = {};
|
|
6407
|
+
__export(storage_exports, {
|
|
6408
|
+
contentTypeOf: () => contentTypeOf,
|
|
6409
|
+
storageDownload: () => storageDownload,
|
|
6410
|
+
storageList: () => storageList,
|
|
6411
|
+
storageRemove: () => storageRemove,
|
|
6412
|
+
storageUpload: () => storageUpload
|
|
6212
6413
|
});
|
|
6213
|
-
import { readFile as
|
|
6214
|
-
import { resolve as
|
|
6215
|
-
async function
|
|
6414
|
+
import { mkdir as mkdir9, readFile as readFile13, stat as stat10, writeFile as writeFile8 } from "node:fs/promises";
|
|
6415
|
+
import { basename as basename4, dirname as dirname8, extname as extname2, join as join16, resolve as resolve9 } from "node:path";
|
|
6416
|
+
async function open3(paths, options) {
|
|
6216
6417
|
const client = await createClient(paths);
|
|
6217
|
-
const project2 = await resolveSlug(
|
|
6418
|
+
const project2 = await resolveSlug(resolve9(options.cwd), options.slug);
|
|
6218
6419
|
return { client, project: project2 };
|
|
6219
6420
|
}
|
|
6220
|
-
|
|
6221
|
-
const
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6421
|
+
function contentTypeOf(path) {
|
|
6422
|
+
const map = {
|
|
6423
|
+
".html": "text/html; charset=utf-8",
|
|
6424
|
+
".htm": "text/html; charset=utf-8",
|
|
6425
|
+
".css": "text/css; charset=utf-8",
|
|
6426
|
+
".js": "text/javascript; charset=utf-8",
|
|
6427
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
6428
|
+
".json": "application/json; charset=utf-8",
|
|
6429
|
+
".png": "image/png",
|
|
6430
|
+
".jpg": "image/jpeg",
|
|
6431
|
+
".jpeg": "image/jpeg",
|
|
6432
|
+
".gif": "image/gif",
|
|
6433
|
+
".svg": "image/svg+xml",
|
|
6434
|
+
".webp": "image/webp",
|
|
6435
|
+
".ico": "image/x-icon",
|
|
6436
|
+
".txt": "text/plain; charset=utf-8",
|
|
6437
|
+
".md": "text/markdown; charset=utf-8",
|
|
6438
|
+
".xml": "application/xml",
|
|
6439
|
+
".wasm": "application/wasm",
|
|
6440
|
+
".pdf": "application/pdf",
|
|
6441
|
+
".zip": "application/zip",
|
|
6442
|
+
".woff": "font/woff",
|
|
6443
|
+
".woff2": "font/woff2",
|
|
6444
|
+
".ttf": "font/ttf",
|
|
6445
|
+
".otf": "font/otf"
|
|
6446
|
+
};
|
|
6447
|
+
return map[extname2(path).toLowerCase()] ?? "application/octet-stream";
|
|
6233
6448
|
}
|
|
6234
|
-
async function
|
|
6235
|
-
const { client, project: project2 } = await
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6449
|
+
async function storageUpload(paths, options) {
|
|
6450
|
+
const { client, project: project2 } = await open3(paths, options);
|
|
6451
|
+
const local = resolve9(options.file);
|
|
6452
|
+
const info = await stat10(local).catch(() => null);
|
|
6453
|
+
if (info === null || !info.isFile()) {
|
|
6454
|
+
throw new CliError("FILE_NOT_FOUND", `\u672C\u5730\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${local}`);
|
|
6455
|
+
}
|
|
6456
|
+
const visibility = options.visibility ?? "private";
|
|
6457
|
+
const bytes = await readFile13(local);
|
|
6458
|
+
const form = new FormData();
|
|
6459
|
+
form.set("path", options.path);
|
|
6460
|
+
form.set("visibility", visibility);
|
|
6461
|
+
form.set(
|
|
6462
|
+
"file",
|
|
6463
|
+
new Blob([bytes], { type: contentTypeOf(options.path) }),
|
|
6464
|
+
basename4(local)
|
|
6247
6465
|
);
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
`/api/v1/projects/${project2}/database/snapshots`
|
|
6466
|
+
const response = await client.upload(
|
|
6467
|
+
`/api/v1/projects/${project2}/files`,
|
|
6468
|
+
form,
|
|
6469
|
+
"STORAGE_UPLOAD_FAILED"
|
|
6253
6470
|
);
|
|
6471
|
+
const body = await response.json();
|
|
6472
|
+
return { ...body.file, ...body.signedUrl === void 0 ? {} : { signedUrl: body.signedUrl } };
|
|
6254
6473
|
}
|
|
6255
|
-
async function
|
|
6256
|
-
const { client, project: project2 } = await
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
}
|
|
6261
|
-
async function dbSnapshotRestore(paths, options) {
|
|
6262
|
-
const { client, project: project2 } = await open2(paths, options);
|
|
6263
|
-
return client.request(
|
|
6264
|
-
`/api/v1/projects/${project2}/database/snapshots/${options.snapshotId}/restore`,
|
|
6265
|
-
{ method: "POST" }
|
|
6474
|
+
async function storageList(paths, options) {
|
|
6475
|
+
const { client, project: project2 } = await open3(paths, options);
|
|
6476
|
+
const query = options.prefix === void 0 ? "" : `?prefix=${encodeURIComponent(options.prefix)}`;
|
|
6477
|
+
const body = await client.request(
|
|
6478
|
+
`/api/v1/projects/${project2}/files${query}`
|
|
6266
6479
|
);
|
|
6480
|
+
return { project: project2, files: body.files };
|
|
6267
6481
|
}
|
|
6268
|
-
async function
|
|
6269
|
-
const
|
|
6270
|
-
|
|
6271
|
-
`/api/v1/projects/${project2}/database/rollback`,
|
|
6272
|
-
{ method: "POST", body: { to: options.to } }
|
|
6482
|
+
async function resolveDownloadUrl(client, project2, path) {
|
|
6483
|
+
const body = await client.request(
|
|
6484
|
+
`/api/v1/projects/${project2}/files?prefix=${encodeURIComponent(path)}`
|
|
6273
6485
|
);
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
for (const rawLine of lines) {
|
|
6281
|
-
const line = rawLine.trim();
|
|
6282
|
-
if (line.length === 0) continue;
|
|
6283
|
-
if (line.startsWith("--")) continue;
|
|
6284
|
-
for (let i = 0; i < line.length; i++) {
|
|
6285
|
-
const ch = line[i];
|
|
6286
|
-
if (ch === "'") {
|
|
6287
|
-
if (inQuote && line[i + 1] === "'") {
|
|
6288
|
-
buffer += ch + line[i + 1];
|
|
6289
|
-
i++;
|
|
6290
|
-
continue;
|
|
6291
|
-
}
|
|
6292
|
-
inQuote = !inQuote;
|
|
6293
|
-
buffer += ch;
|
|
6294
|
-
continue;
|
|
6295
|
-
}
|
|
6296
|
-
if (ch === ";" && !inQuote) {
|
|
6297
|
-
const statement = buffer.trim();
|
|
6298
|
-
if (statement.length > 0) statements.push(statement);
|
|
6299
|
-
buffer = "";
|
|
6300
|
-
continue;
|
|
6301
|
-
}
|
|
6302
|
-
buffer += ch;
|
|
6303
|
-
}
|
|
6304
|
-
buffer += " ";
|
|
6305
|
-
}
|
|
6306
|
-
if (inQuote) {
|
|
6307
|
-
throw new Error(`SQL \u8FC1\u79FB\u6587\u4EF6\u5F15\u53F7\u672A\u95ED\u5408\uFF1A${buffer.trim().slice(0, 80)}\u2026`);
|
|
6308
|
-
}
|
|
6309
|
-
const tail = buffer.trim();
|
|
6310
|
-
if (tail.length > 0) statements.push(tail);
|
|
6311
|
-
return statements;
|
|
6312
|
-
}
|
|
6313
|
-
async function dbMigrate(paths, options) {
|
|
6314
|
-
const source = await readFile12(resolve8(options.file), "utf8");
|
|
6315
|
-
const statements = splitSqlStatements(source);
|
|
6316
|
-
if (statements.length === 0) {
|
|
6317
|
-
throw new Error(`\u8FC1\u79FB\u6587\u4EF6\u6CA1\u6709\u53EF\u6267\u884C\u8BED\u53E5\uFF1A${options.file}`);
|
|
6318
|
-
}
|
|
6319
|
-
const { project: project2 } = await open2(paths, options);
|
|
6320
|
-
const applied = [];
|
|
6321
|
-
const changes = [];
|
|
6322
|
-
for (const statement of statements) {
|
|
6323
|
-
try {
|
|
6324
|
-
const result = await dbExec(paths, {
|
|
6325
|
-
cwd: options.cwd,
|
|
6326
|
-
...options.slug === void 0 ? {} : { slug: options.slug },
|
|
6327
|
-
sql: statement
|
|
6328
|
-
});
|
|
6329
|
-
applied.push(statement);
|
|
6330
|
-
changes.push(result.changes);
|
|
6331
|
-
} catch (error) {
|
|
6332
|
-
const code = error instanceof CliError ? error.code : "MIGRATE_FAILED";
|
|
6333
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
6334
|
-
return {
|
|
6335
|
-
projectId: project2,
|
|
6336
|
-
applied,
|
|
6337
|
-
changes,
|
|
6338
|
-
failed: { statement, code, message }
|
|
6339
|
-
};
|
|
6340
|
-
}
|
|
6341
|
-
}
|
|
6342
|
-
return { projectId: project2, applied, changes };
|
|
6343
|
-
}
|
|
6344
|
-
var init_db2 = __esm({
|
|
6345
|
-
"packages/cli/src/db.ts"() {
|
|
6346
|
-
"use strict";
|
|
6347
|
-
init_auth();
|
|
6348
|
-
init_client();
|
|
6349
|
-
}
|
|
6350
|
-
});
|
|
6351
|
-
|
|
6352
|
-
// packages/cli/src/storage.ts
|
|
6353
|
-
var storage_exports = {};
|
|
6354
|
-
__export(storage_exports, {
|
|
6355
|
-
contentTypeOf: () => contentTypeOf,
|
|
6356
|
-
storageDownload: () => storageDownload,
|
|
6357
|
-
storageList: () => storageList,
|
|
6358
|
-
storageRemove: () => storageRemove,
|
|
6359
|
-
storageUpload: () => storageUpload
|
|
6360
|
-
});
|
|
6361
|
-
import { mkdir as mkdir9, readFile as readFile13, stat as stat10, writeFile as writeFile8 } from "node:fs/promises";
|
|
6362
|
-
import { basename as basename4, dirname as dirname8, extname, join as join16, resolve as resolve9 } from "node:path";
|
|
6363
|
-
async function open3(paths, options) {
|
|
6364
|
-
const client = await createClient(paths);
|
|
6365
|
-
const project2 = await resolveSlug(resolve9(options.cwd), options.slug);
|
|
6366
|
-
return { client, project: project2 };
|
|
6367
|
-
}
|
|
6368
|
-
function contentTypeOf(path) {
|
|
6369
|
-
const map = {
|
|
6370
|
-
".html": "text/html; charset=utf-8",
|
|
6371
|
-
".htm": "text/html; charset=utf-8",
|
|
6372
|
-
".css": "text/css; charset=utf-8",
|
|
6373
|
-
".js": "text/javascript; charset=utf-8",
|
|
6374
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
6375
|
-
".json": "application/json; charset=utf-8",
|
|
6376
|
-
".png": "image/png",
|
|
6377
|
-
".jpg": "image/jpeg",
|
|
6378
|
-
".jpeg": "image/jpeg",
|
|
6379
|
-
".gif": "image/gif",
|
|
6380
|
-
".svg": "image/svg+xml",
|
|
6381
|
-
".webp": "image/webp",
|
|
6382
|
-
".ico": "image/x-icon",
|
|
6383
|
-
".txt": "text/plain; charset=utf-8",
|
|
6384
|
-
".md": "text/markdown; charset=utf-8",
|
|
6385
|
-
".xml": "application/xml",
|
|
6386
|
-
".wasm": "application/wasm",
|
|
6387
|
-
".pdf": "application/pdf",
|
|
6388
|
-
".zip": "application/zip",
|
|
6389
|
-
".woff": "font/woff",
|
|
6390
|
-
".woff2": "font/woff2",
|
|
6391
|
-
".ttf": "font/ttf",
|
|
6392
|
-
".otf": "font/otf"
|
|
6393
|
-
};
|
|
6394
|
-
return map[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
6395
|
-
}
|
|
6396
|
-
async function storageUpload(paths, options) {
|
|
6397
|
-
const { client, project: project2 } = await open3(paths, options);
|
|
6398
|
-
const local = resolve9(options.file);
|
|
6399
|
-
const info = await stat10(local).catch(() => null);
|
|
6400
|
-
if (info === null || !info.isFile()) {
|
|
6401
|
-
throw new CliError("FILE_NOT_FOUND", `\u672C\u5730\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${local}`);
|
|
6402
|
-
}
|
|
6403
|
-
const visibility = options.visibility ?? "private";
|
|
6404
|
-
const bytes = await readFile13(local);
|
|
6405
|
-
const form = new FormData();
|
|
6406
|
-
form.set("path", options.path);
|
|
6407
|
-
form.set("visibility", visibility);
|
|
6408
|
-
form.set(
|
|
6409
|
-
"file",
|
|
6410
|
-
new Blob([bytes], { type: contentTypeOf(options.path) }),
|
|
6411
|
-
basename4(local)
|
|
6412
|
-
);
|
|
6413
|
-
const response = await client.upload(
|
|
6414
|
-
`/api/v1/projects/${project2}/files`,
|
|
6415
|
-
form,
|
|
6416
|
-
"STORAGE_UPLOAD_FAILED"
|
|
6417
|
-
);
|
|
6418
|
-
const body = await response.json();
|
|
6419
|
-
return { ...body.file, ...body.signedUrl === void 0 ? {} : { signedUrl: body.signedUrl } };
|
|
6420
|
-
}
|
|
6421
|
-
async function storageList(paths, options) {
|
|
6422
|
-
const { client, project: project2 } = await open3(paths, options);
|
|
6423
|
-
const query = options.prefix === void 0 ? "" : `?prefix=${encodeURIComponent(options.prefix)}`;
|
|
6424
|
-
const body = await client.request(
|
|
6425
|
-
`/api/v1/projects/${project2}/files${query}`
|
|
6426
|
-
);
|
|
6427
|
-
return { project: project2, files: body.files };
|
|
6428
|
-
}
|
|
6429
|
-
async function resolveDownloadUrl(client, project2, path) {
|
|
6430
|
-
const body = await client.request(
|
|
6431
|
-
`/api/v1/projects/${project2}/files?prefix=${encodeURIComponent(path)}`
|
|
6432
|
-
);
|
|
6433
|
-
const entry = body.files.find((file) => file.path === path);
|
|
6434
|
-
const url = entry?.url ?? entry?.signedUrl;
|
|
6435
|
-
if (entry === void 0 || url === void 0 || url.length === 0) {
|
|
6436
|
-
throw new CliError("STORAGE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${path}`);
|
|
6437
|
-
}
|
|
6438
|
-
return url;
|
|
6486
|
+
const entry = body.files.find((file) => file.path === path);
|
|
6487
|
+
const url = entry?.url ?? entry?.signedUrl;
|
|
6488
|
+
if (entry === void 0 || url === void 0 || url.length === 0) {
|
|
6489
|
+
throw new CliError("STORAGE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${path}`);
|
|
6490
|
+
}
|
|
6491
|
+
return url;
|
|
6439
6492
|
}
|
|
6440
6493
|
async function storageDownload(paths, options) {
|
|
6441
6494
|
const { client, project: project2 } = await open3(paths, options);
|
|
@@ -7284,215 +7337,2354 @@ interface AdepExecutorUser {
|
|
|
7284
7337
|
|
|
7285
7338
|
/* ---------- \u5168\u5C40 ctx\uFF08FunctionContext \u955C\u50CF\uFF09 ---------- */
|
|
7286
7339
|
|
|
7287
|
-
interface AdepContext {
|
|
7288
|
-
/** \u8BF7\u6C42\u65B9\u6CD5\u3002 */
|
|
7289
|
-
readonly method: AdepHttpMethod
|
|
7290
|
-
/** \u89E6\u53D1\u8DEF\u5F84\uFF08\u4E0D\u542B host\uFF09\uFF0C\u5982 /hello\u3002 */
|
|
7291
|
-
readonly path: string
|
|
7292
|
-
/** \u89E3\u6790\u540E\u7684 query\uFF1B\u540C\u540D\u591A\u503C\u6536\u4E3A\u6570\u7EC4\u3002 */
|
|
7293
|
-
readonly query: Readonly<Record<string, string | readonly string[]>>
|
|
7294
|
-
/** \u8BF7\u6C42\u5934\uFF08\u952E\u4E3A\u5C0F\u5199\uFF09\u3002 */
|
|
7295
|
-
readonly headers: Readonly<Record<string, string>>
|
|
7296
|
-
/** \u8BF7\u6C42\u4F53\uFF1B\u5F62\u72B6\u7531\u7528\u6237\u7528\u7C7B\u578B\u65AD\u8A00\u6536\u7A84\uFF08\u5982 ctx.body as { name: string }\uFF09\u3002 */
|
|
7297
|
-
readonly body: unknown
|
|
7298
|
-
/** multipart \u4E0A\u4F20\u7684\u6587\u4EF6\uFF08\u65E0\u6587\u4EF6\u65F6\u4E3A\u7A7A\u6570\u7EC4\uFF09\u3002 */
|
|
7299
|
-
readonly files: readonly AdepFunctionFile[]
|
|
7300
|
-
/** \u5E73\u53F0\u80FD\u529B\u6302\u8F7D\u70B9\uFF1Acloud.db / cloud.storage / cloud.fetch / cloud.realtime\u3002 */
|
|
7301
|
-
readonly cloud: AdepCloud
|
|
7302
|
-
/** \u8C03\u7528\u8005\u8EAB\u4EFD\u6295\u5F71\uFF1Bnull = \u533F\u540D / \u672A\u6CE8\u5165\u3002 */
|
|
7303
|
-
readonly user: AdepExecutorUser | null
|
|
7340
|
+
interface AdepContext {
|
|
7341
|
+
/** \u8BF7\u6C42\u65B9\u6CD5\u3002 */
|
|
7342
|
+
readonly method: AdepHttpMethod
|
|
7343
|
+
/** \u89E6\u53D1\u8DEF\u5F84\uFF08\u4E0D\u542B host\uFF09\uFF0C\u5982 /hello\u3002 */
|
|
7344
|
+
readonly path: string
|
|
7345
|
+
/** \u89E3\u6790\u540E\u7684 query\uFF1B\u540C\u540D\u591A\u503C\u6536\u4E3A\u6570\u7EC4\u3002 */
|
|
7346
|
+
readonly query: Readonly<Record<string, string | readonly string[]>>
|
|
7347
|
+
/** \u8BF7\u6C42\u5934\uFF08\u952E\u4E3A\u5C0F\u5199\uFF09\u3002 */
|
|
7348
|
+
readonly headers: Readonly<Record<string, string>>
|
|
7349
|
+
/** \u8BF7\u6C42\u4F53\uFF1B\u5F62\u72B6\u7531\u7528\u6237\u7528\u7C7B\u578B\u65AD\u8A00\u6536\u7A84\uFF08\u5982 ctx.body as { name: string }\uFF09\u3002 */
|
|
7350
|
+
readonly body: unknown
|
|
7351
|
+
/** multipart \u4E0A\u4F20\u7684\u6587\u4EF6\uFF08\u65E0\u6587\u4EF6\u65F6\u4E3A\u7A7A\u6570\u7EC4\uFF09\u3002 */
|
|
7352
|
+
readonly files: readonly AdepFunctionFile[]
|
|
7353
|
+
/** \u5E73\u53F0\u80FD\u529B\u6302\u8F7D\u70B9\uFF1Acloud.db / cloud.storage / cloud.fetch / cloud.realtime\u3002 */
|
|
7354
|
+
readonly cloud: AdepCloud
|
|
7355
|
+
/** \u8C03\u7528\u8005\u8EAB\u4EFD\u6295\u5F71\uFF1Bnull = \u533F\u540D / \u672A\u6CE8\u5165\u3002 */
|
|
7356
|
+
readonly user: AdepExecutorUser | null
|
|
7357
|
+
}
|
|
7358
|
+
|
|
7359
|
+
/** \u5168\u5C40\u5F00\u53D1\u671F\u4E0A\u4E0B\u6587\uFF08\u7F16\u8F91\u5668\u8865\u5168\u7528\uFF1B\u51FD\u6570\u8FD0\u884C\u671F\u7ECF\u53C2\u6570\u6CE8\u5165\uFF0C\u4E0D\u8BFB\u53D6\u5168\u5C40\uFF09\u3002 */
|
|
7360
|
+
declare const ctx: AdepContext
|
|
7361
|
+
`;
|
|
7362
|
+
var ADEP_TSCONFIG_JSON = `{
|
|
7363
|
+
"compilerOptions": {
|
|
7364
|
+
"target": "ES2021",
|
|
7365
|
+
"module": "ESNext",
|
|
7366
|
+
"moduleResolution": "Bundler",
|
|
7367
|
+
"strict": true,
|
|
7368
|
+
"noEmit": true,
|
|
7369
|
+
"skipLibCheck": true,
|
|
7370
|
+
"types": [],
|
|
7371
|
+
"lib": ["ES2021", "DOM"]
|
|
7372
|
+
},
|
|
7373
|
+
"include": ["**/*.ts", "**/*.d.ts"]
|
|
7374
|
+
}
|
|
7375
|
+
`;
|
|
7376
|
+
|
|
7377
|
+
// packages/cli/src/init.ts
|
|
7378
|
+
init_adep_config();
|
|
7379
|
+
|
|
7380
|
+
// shared/sdk/web-project-template.ts
|
|
7381
|
+
var ADEP_CLIENT_VERSION = "^0.2.0";
|
|
7382
|
+
var ADEP_VITE_PLUGIN_VERSION = "^0.1.0";
|
|
7383
|
+
var WEB_DEPENDENCIES = {
|
|
7384
|
+
vue: "^3.3.4",
|
|
7385
|
+
// @adep/client:浏览器端云函数调用 SDK(IDE-031)。模板默认在 web/src/lib/adep.ts
|
|
7386
|
+
// 封装并由 App.vue 示例调用;运行时 fetch('/api/{name}') 在 dev 态被 @adep/vite-plugin
|
|
7387
|
+
// 拦截(CLI 代理到本地 adep dev / Web IDE 经 postMessage 转发到 IDE 主线程),
|
|
7388
|
+
// 生产态由平台网关路由到已发布云函数。
|
|
7389
|
+
"@adep/client": ADEP_CLIENT_VERSION
|
|
7390
|
+
};
|
|
7391
|
+
var WEB_DEV_DEPENDENCIES = {
|
|
7392
|
+
vite: "^4.4.0",
|
|
7393
|
+
"@vitejs/plugin-vue": "^4.3.0",
|
|
7394
|
+
// vite 内部动态 import('esbuild-wasm'),必须在 package.json 显式声明才能被 Nodebox 解析
|
|
7395
|
+
"esbuild-wasm": "0.18.20",
|
|
7396
|
+
// @adep/vite-plugin:云函数 Vite 插件(IDE-030)。dev 时自动注入 /api/* fetch 拦截器
|
|
7397
|
+
// (Web IDE 预览经 postMessage 转发到 IDE 主线程执行云函数草稿),若注入 createDevServer
|
|
7398
|
+
// (CLI 场景)则启动本地 adep dev server 并按 functions_prefix 代理 /{prefix}/* 到云函数。
|
|
7399
|
+
"@adep/vite-plugin": ADEP_VITE_PLUGIN_VERSION
|
|
7400
|
+
};
|
|
7401
|
+
function webProjectFiles() {
|
|
7402
|
+
return [
|
|
7403
|
+
{
|
|
7404
|
+
path: "package.json",
|
|
7405
|
+
content: JSON.stringify(
|
|
7406
|
+
{
|
|
7407
|
+
name: "adep-web",
|
|
7408
|
+
version: "0.0.0",
|
|
7409
|
+
type: "module",
|
|
7410
|
+
scripts: { dev: "vite" },
|
|
7411
|
+
dependencies: WEB_DEPENDENCIES,
|
|
7412
|
+
devDependencies: WEB_DEV_DEPENDENCIES
|
|
7413
|
+
},
|
|
7414
|
+
null,
|
|
7415
|
+
2
|
|
7416
|
+
)
|
|
7417
|
+
},
|
|
7418
|
+
{
|
|
7419
|
+
path: "index.html",
|
|
7420
|
+
content: [
|
|
7421
|
+
`<!doctype html>`,
|
|
7422
|
+
`<html lang="zh-CN">`,
|
|
7423
|
+
`<head>`,
|
|
7424
|
+
` <meta charset="utf-8" />`,
|
|
7425
|
+
` <meta name="viewport" content="width=device-width, initial-scale=1" />`,
|
|
7426
|
+
` <title>adep web</title>`,
|
|
7427
|
+
`</head>`,
|
|
7428
|
+
`<body>`,
|
|
7429
|
+
` <div id="app"></div>`,
|
|
7430
|
+
` <script type="module" src="/src/main.ts"></script>`,
|
|
7431
|
+
`</body>`,
|
|
7432
|
+
`</html>`,
|
|
7433
|
+
``
|
|
7434
|
+
].join("\n")
|
|
7435
|
+
},
|
|
7436
|
+
{
|
|
7437
|
+
path: "vite.config.ts",
|
|
7438
|
+
content: [
|
|
7439
|
+
`// web/vite.config.ts \u2014\u2014 \u6807\u51C6 Vite \u914D\u7F6E\uFF08Vue 3 + @adep/vite-plugin \u4E91\u51FD\u6570\u4EE3\u7406\uFF09\u3002`,
|
|
7440
|
+
`// @adep/vite-plugin \u5728 dev \u65F6\uFF1A`,
|
|
7441
|
+
`// 1. \u6CE8\u5165 /api/* fetch \u62E6\u622A\u5668\uFF08Web IDE \u9884\u89C8\u7ECF postMessage \u8F6C\u53D1\u5230 IDE \u4E3B\u7EBF\u7A0B\u6267\u884C\u4E91\u51FD\u6570\u8349\u7A3F\uFF09\uFF1B`,
|
|
7442
|
+
`// 2. \u82E5\u6CE8\u5165 createDevServer\uFF08CLI \u573A\u666F\uFF09\uFF0C\u542F\u52A8\u672C\u5730 adep dev \u5E76\u4EE3\u7406 /api/* \u5230\u4E91\u51FD\u6570\u3002`,
|
|
7443
|
+
`// \u6D4F\u89C8\u5668\u5185 vite-dev \u9884\u89C8\uFF08@adep/web-container buildDocument\uFF09\u6CE8\u5165\u540C\u4E00\u4EFD\u811A\u672C\uFF0C\u65E0\u9700\u5728\u6B64\u5185\u8054\u3002`,
|
|
7444
|
+
`import { defineConfig } from 'vite'`,
|
|
7445
|
+
`import vue from '@vitejs/plugin-vue'`,
|
|
7446
|
+
`import { adepPlugin } from '@adep/vite-plugin'`,
|
|
7447
|
+
``,
|
|
7448
|
+
`export default defineConfig({`,
|
|
7449
|
+
` plugins: [vue(), adepPlugin()],`,
|
|
7450
|
+
`})`,
|
|
7451
|
+
``
|
|
7452
|
+
].join("\n")
|
|
7453
|
+
},
|
|
7454
|
+
{
|
|
7455
|
+
path: "src/main.ts",
|
|
7456
|
+
content: [
|
|
7457
|
+
`// web/src/main.ts \u2014\u2014 \u5E94\u7528\u5165\u53E3\uFF08Vue 3 + Vite\uFF09\u3002`,
|
|
7458
|
+
`// \u5F00\u53D1\uFF1A\u5E95\u90E8\u300C\u6D4F\u89C8\u5668\u7EC8\u7AEF\u300D\u8FD0\u884C npm run dev\uFF08\u6216\u7B49 Nodebox \u81EA\u52A8\u62C9\u8D77\uFF09\uFF0C\u4FDD\u5B58\u540E\u9884\u89C8 HMR \u5237\u65B0\u3002`,
|
|
7459
|
+
`import { createApp } from 'vue'`,
|
|
7460
|
+
`import App from './App.vue'`,
|
|
7461
|
+
``,
|
|
7462
|
+
`createApp(App).mount('#app')`,
|
|
7463
|
+
``
|
|
7464
|
+
].join("\n")
|
|
7465
|
+
},
|
|
7466
|
+
{
|
|
7467
|
+
path: "src/lib/adep.ts",
|
|
7468
|
+
content: [
|
|
7469
|
+
`// web/src/lib/adep.ts \u2014\u2014 @adep/client \u6D4F\u89C8\u5668\u7AEF\u5C01\u88C5\uFF08IDE-032\uFF09\u3002`,
|
|
7470
|
+
`// \u5F00\u53D1\u6001\uFF08vite dev + @adep/vite-plugin\uFF09\uFF1AinvokeFunction \u8D70 fetch('/api/*')\uFF0C`,
|
|
7471
|
+
`// \u7531 vite \u63D2\u4EF6\u62E6\u622A\u5230\u672C\u5730\u4E91\u51FD\u6570\u6267\u884C\uFF08CLI\uFF09\u6216\u7ECF postMessage \u8F6C\u53D1\u5230 IDE \u4E3B\u7EBF\u7A0B\uFF08Web IDE\uFF09\u3002`,
|
|
7472
|
+
`// \u751F\u4EA7\u6001\uFF08\u5DF2\u90E8\u7F72\u7AD9\u70B9\uFF09\uFF1Afetch('/api/*') \u7531\u5E73\u53F0\u7F51\u5173\u8DEF\u7531\u5230\u5DF2\u53D1\u5E03\u4E91\u51FD\u6570\u3002`,
|
|
7473
|
+
`import { createAdepClient } from '@adep/client'`,
|
|
7474
|
+
``,
|
|
7475
|
+
`export const adep = createAdepClient()`,
|
|
7476
|
+
``,
|
|
7477
|
+
`export const { invokeFunction } = adep`,
|
|
7478
|
+
``
|
|
7479
|
+
].join("\n")
|
|
7480
|
+
},
|
|
7481
|
+
{
|
|
7482
|
+
path: "src/App.vue",
|
|
7483
|
+
content: [
|
|
7484
|
+
`<script setup lang="ts">`,
|
|
7485
|
+
`import { ref } from 'vue'`,
|
|
7486
|
+
`import { invokeFunction } from './lib/adep'`,
|
|
7487
|
+
``,
|
|
7488
|
+
`const title = ref('adep web')`,
|
|
7489
|
+
`const clicks = ref(0)`,
|
|
7490
|
+
`const fnResult = ref<string>('')`,
|
|
7491
|
+
`const fnLoading = ref(false)`,
|
|
7492
|
+
``,
|
|
7493
|
+
`async function callHello() {`,
|
|
7494
|
+
` fnLoading.value = true`,
|
|
7495
|
+
` fnResult.value = ''`,
|
|
7496
|
+
` try {`,
|
|
7497
|
+
` const result = await invokeFunction<{ message: string }>('hello')`,
|
|
7498
|
+
` fnResult.value = result.message`,
|
|
7499
|
+
` } catch (err) {`,
|
|
7500
|
+
` fnResult.value = \`\u8C03\u7528\u5931\u8D25\uFF1A\${err instanceof Error ? err.message : String(err)}\``,
|
|
7501
|
+
` } finally {`,
|
|
7502
|
+
` fnLoading.value = false`,
|
|
7503
|
+
` }`,
|
|
7504
|
+
`}`,
|
|
7505
|
+
`</script>`,
|
|
7506
|
+
``,
|
|
7507
|
+
`<template>`,
|
|
7508
|
+
` <main>`,
|
|
7509
|
+
` <h1>{{ title }}</h1>`,
|
|
7510
|
+
` <p>\u5728 web/src/App.vue \u91CC\u7F16\u8F91\uFF0C\u4FDD\u5B58\u540E\u9884\u89C8\u81EA\u52A8\u5237\u65B0\uFF08HMR\uFF09\u3002</p>`,
|
|
7511
|
+
` <button @click="clicks++">clicks: {{ clicks }}</button>`,
|
|
7512
|
+
` <div style="margin-top: 16px;">`,
|
|
7513
|
+
` <button @click="callHello" :disabled="fnLoading">`,
|
|
7514
|
+
` {{ fnLoading ? '\u8C03\u7528\u4E2D\u2026' : '\u8C03\u7528 hello \u51FD\u6570' }}`,
|
|
7515
|
+
` </button>`,
|
|
7516
|
+
` <p v-if="fnResult" style="margin-top: 8px; color: #059669;">\u7ED3\u679C\uFF1A{{ fnResult }}</p>`,
|
|
7517
|
+
` </div>`,
|
|
7518
|
+
` </main>`,
|
|
7519
|
+
`</template>`,
|
|
7520
|
+
``,
|
|
7521
|
+
`<style scoped>`,
|
|
7522
|
+
`main {`,
|
|
7523
|
+
` margin: 24px;`,
|
|
7524
|
+
` font-family: system-ui, -apple-system, sans-serif;`,
|
|
7525
|
+
`}`,
|
|
7526
|
+
`button {`,
|
|
7527
|
+
` padding: 6px 14px;`,
|
|
7528
|
+
` border: 1px solid #6366f1;`,
|
|
7529
|
+
` border-radius: 6px;`,
|
|
7530
|
+
` background: #eef2ff;`,
|
|
7531
|
+
` cursor: pointer;`,
|
|
7532
|
+
`}`,
|
|
7533
|
+
`button:disabled {`,
|
|
7534
|
+
` opacity: 0.6;`,
|
|
7535
|
+
` cursor: not-allowed;`,
|
|
7536
|
+
`}`,
|
|
7537
|
+
`</style>`,
|
|
7538
|
+
``
|
|
7539
|
+
].join("\n")
|
|
7540
|
+
}
|
|
7541
|
+
];
|
|
7542
|
+
}
|
|
7543
|
+
function webSrcFiles() {
|
|
7544
|
+
return webProjectFiles().filter((file) => file.path.startsWith("src/"));
|
|
7545
|
+
}
|
|
7546
|
+
|
|
7547
|
+
// packages/cli/src/templates/countdown-template.ts
|
|
7548
|
+
var COUNTDOWN_TEMPLATE_FILES = [
|
|
7549
|
+
{ path: "functions/_shared/auth.ts", content: "/**\n * \u8BF7\u6C42\u9274\u6743\uFF08\u79FB\u690D\u81EA cf-backend src/middleware/auth.ts + src/lib/jwt.ts \u7684\u8BED\u4E49\uFF09\u3002\n *\n * \u8FC1\u79FB\u70B9\uFF1A\u5FAE\u4FE1 code2Session \u2192 openid \u2192 JWT \u7684\u8EAB\u4EFD\u94FE\u8DEF\uFF0C\u66FF\u6362\u4E3A adep web \u7AEF\n * \u300C\u8BBE\u5907\u8EAB\u4EFD device_id\u300D\u4F5C\u4E3A Bearer \u51ED\u636E\uFF08\u524D\u7AEF\u6301\u4E45\u5316\u4E8E localStorage\uFF0C\u89C1\n * web/src/lib/device.ts\uFF09\u3002device_id \u5373\u7528\u6237\u8868\u552F\u4E00\u952E\uFF0C\u7B49\u4EF7 API Key\uFF1A\u67E5\u5F97\u5230 \u2192 \u8EAB\u4EFD\u6210\u7ACB\u3002\n *\n * \u5931\u8D25\u8FD4\u56DE\u4E0E\u5C0F\u7A0B\u5E8F\u540E\u7AEF\u4E00\u81F4\u7684\u4E1A\u52A1\u7801 40001\uFF08\u524D\u7AEF request.ts \u4F1A\u636E\u6B64\u9759\u9ED8\u91CD\u767B\uFF09\u3002\n */\nimport type { CloudDb, FunctionContext } from '@adep/types'\nimport { fail, CODE } from './response'\nimport { findUserByDeviceId, type UserRow } from './store'\n\n/** \u89E3\u6790 Authorization: Bearer xxx\uFF08\u5E26\u524D\u5BFC\u5927\u5C0F\u5199/\u591A\u4F59\u7A7A\u683C\u5BB9\u9519\uFF09 */\nexport function parseBearer(ctx: FunctionContext): string {\n const header = ctx.headers?.authorization ?? ctx.headers?.Authorization ?? ''\n const match = /^Bearer\\s+(.+)$/i.exec(String(header).trim())\n return match?.[1]?.trim() ?? ''\n}\n\n/** \u4ECE\u8BF7\u6C42\u5934\u89E3\u6790\u51FA\u7528\u6237\uFF1B\u7F3A\u51ED\u636E/\u7528\u6237\u4E0D\u5B58\u5728 \u2192 40001 \u54CD\u5E94\u4FE1\u5C01\u3002 */\nexport async function requireUser(\n db: CloudDb,\n ctx: FunctionContext\n): Promise<{ user: UserRow } | AdepHttpEnvelopeLike> {\n const token = parseBearer(ctx)\n if (!token) return fail(CODE.NO_AUTH, '\u672A\u767B\u5F55')\n const user = await findUserByDeviceId(db, token)\n if (!user) return fail(CODE.NO_AUTH, '\u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55')\n return { user }\n}\n\n/** \u5224\u65AD\u8FD4\u56DE\u662F\u5426\u54CD\u5E94\u4FE1\u5C01\uFF08\u7528\u4E8E requireUser \u6536\u7A84\uFF09\u3002 */\nexport function isEnvelope<T>(value: T | AdepHttpEnvelopeLike): value is AdepHttpEnvelopeLike {\n return typeof value === 'object' && value !== null && '__adepHttp' in value\n}\n\nexport interface AdepHttpEnvelopeLike {\n __adepHttp: { status: number; body?: unknown }\n}\n" },
|
|
7550
|
+
{ path: "functions/_shared/date.test.ts", content: "/**\n * \u670D\u52A1\u7AEF\u65E5\u671F/\u6392\u5E8F/\u54CD\u5E94\u4FE1\u5C01\u5355\u5143\u6D4B\u8BD5\uFF08\u79FB\u690D\u903B\u8F91\u7684\u56DE\u5F52\u4FDD\u62A4\uFF09\u3002\n * \u8FD0\u884C\uFF1Anpm run test\n */\nimport { describe, it, expect } from 'vitest'\nimport { isValidDate, calcDays, toDTO, sortEvents, type EventRow } from './date'\nimport { ok, fail, CODE } from './response'\n\ndescribe('isValidDate', () => {\n it('\u63A5\u53D7\u5408\u6CD5 YYYY-MM-DD', () => {\n expect(isValidDate('2026-09-20')).toBe(true)\n expect(isValidDate('2024-02-29')).toBe(true)\n })\n it('\u62D2\u7EDD\u975E\u6CD5\u65E5\u671F\u4E0E\u683C\u5F0F', () => {\n expect(isValidDate('2026-13-01')).toBe(false)\n expect(isValidDate('2026-02-30')).toBe(false)\n expect(isValidDate('2026/09/20')).toBe(false)\n expect(isValidDate('')).toBe(false)\n expect(isValidDate(null)).toBe(false)\n })\n})\n\ndescribe('calcDays\uFF08\u5317\u4EAC\u65F6\u95F4\u81EA\u7136\u65E5\uFF09', () => {\n it('\u5012\u8BA1\u65F6\uFF1A\u672A\u6765\u4E3A\u6B63\uFF0C\u5F53\u5929\u4E3A 0', () => {\n const today = new Date()\n const beijing = new Date(today.getTime() + 8 * 3600_000)\n const todayStr = `${beijing.getUTCFullYear()}-${String(beijing.getUTCMonth() + 1).padStart(2, '0')}-${String(\n beijing.getUTCDate()\n ).padStart(2, '0')}`\n expect(calcDays(todayStr)).toBe(0)\n })\n it('\u8FC7\u53BB\u65E5\u671F\u4E3A\u8D1F\uFF1Bcountup \u53D6\u7EDD\u5BF9\u503C', () => {\n expect(calcDays('2020-01-01')).toBeLessThan(0)\n expect(calcDays('2020-01-01', 'countup')).toBeGreaterThan(0)\n })\n it('\u975E\u6CD5\u65E5\u671F\u8FD4\u56DE 0', () => {\n expect(calcDays('bad-date')).toBe(0)\n })\n})\n\ndescribe('toDTO + sortEvents', () => {\n const base = {\n id: '1',\n user_id: 'u1',\n title: '\u4E8B\u4EF6',\n target_date: '2030-01-01',\n note: null,\n category: null,\n direction: 'countdown',\n is_pinned: 0,\n sort_order: 0,\n created_at: '2026-01-01T00:00:00.000Z',\n updated_at: '2026-01-01T00:00:00.000Z',\n } as EventRow\n\n it('toDTO \u8865\u9ED8\u8BA4\u503C\u5E76\u7B97 days', () => {\n const dto = toDTO(base)\n expect(dto.note).toBe('')\n expect(dto.category).toBe('other')\n expect(dto.is_pinned).toBe(false)\n expect(dto.days).toBeGreaterThan(0)\n })\n\n it('\u6392\u5E8F\uFF1A\u7F6E\u9876\u4F18\u5148\uFF0C\u5176\u4F59\u6309\u5269\u4F59\u5929\u6570\u5347\u5E8F\uFF08\u8FC7\u53BB\u6392\u6700\u540E\uFF09', () => {\n const far = { ...base, id: 'far', target_date: '2031-01-01' } as EventRow\n const near = { ...base, id: 'near', target_date: '2026-10-01' } as EventRow\n const past = { ...base, id: 'past', target_date: '2020-01-01' } as EventRow\n const pinnedFar = { ...far, id: 'pinnedFar', is_pinned: 1 } as EventRow\n const sorted = sortEvents([far, near, past, pinnedFar].map((r) => toDTO(r)))\n expect(sorted.map((e) => e.id)).toEqual(['pinnedFar', 'near', 'far', 'past'])\n })\n})\n\ndescribe('\u54CD\u5E94\u4FE1\u5C01', () => {\n it('ok \u8FD4\u56DE 200 + \u4E1A\u52A1\u4FE1\u5C01', () => {\n const env = ok({ a: 1 })\n expect(env.__adepHttp.status).toBe(200)\n expect(env.__adepHttp.body).toEqual({ code: 0, data: { a: 1 }, message: 'ok' })\n })\n it('fail \u6620\u5C04 HTTP \u72B6\u6001', () => {\n expect(fail(CODE.NO_AUTH, 'x').__adepHttp.status).toBe(401)\n expect(fail(CODE.BAD_PARAM, 'x').__adepHttp.status).toBe(400)\n expect(fail(CODE.NOT_FOUND, 'x').__adepHttp.status).toBe(404)\n })\n})\n" },
|
|
7551
|
+
{ path: "functions/_shared/date.ts", content: "/**\n * \u670D\u52A1\u7AEF\u65E5\u671F\u5DE5\u5177\uFF08\u79FB\u690D\u81EA cf-backend src/lib/date.ts\uFF09\u3002\n * \u4E0E\u5C0F\u7A0B\u5E8F utils/date.js \u7B97\u6CD5\u5BF9\u9F50\uFF1A\u5929\u6570\u6309\u300C\u5317\u4EAC\u65F6\u95F4\uFF08UTC+8\uFF09\u81EA\u7136\u65E5\u300D\u8BA1\u7B97\u3002\n */\nconst WEEK = ['\u5468\u65E5', '\u5468\u4E00', '\u5468\u4E8C', '\u5468\u4E09', '\u5468\u56DB', '\u5468\u4E94', '\u5468\u516D']\n\n/** \u5317\u4EAC\u65F6\u95F4\u4ECA\u5929 YYYY-MM-DD\uFF08\u81EA\u7136\u65E5\uFF0C\u4E0D\u4EE5 UTC \u65E5\u671F\u4E3A\u51C6\uFF09 */\nexport function todayStr(): string {\n const now = new Date(Date.now() + 8 * 3600_000)\n const y = now.getUTCFullYear()\n const m = now.getUTCMonth() + 1\n const d = now.getUTCDate()\n return fmt(y, m, d)\n}\n\nfunction fmt(y: number, m: number, d: number): string {\n return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`\n}\n\nexport function isValidDate(s: unknown): s is string {\n if (typeof s !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return false\n const [y, m, d] = s.split('-').map(Number)\n if (m < 1 || m > 12 || d < 1 || d > 31) return false\n const dt = new Date(y, m - 1, d)\n return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d\n}\n\n/** \u6309\u5317\u4EAC\u65F6\u95F4\u81EA\u7136\u65E5\u5DEE\uFF08\u6B63\u6570\u672A\u6765 / \u8D1F\u6570\u8FC7\u53BB\uFF1Bcountup \u53D6\u7EDD\u5BF9\u503C\uFF09 */\nexport function calcDays(\n targetDate: string,\n direction: 'countdown' | 'countup' = 'countdown'\n): number {\n if (!isValidDate(targetDate)) return 0\n const target = new Date(`${targetDate}T00:00:00+08:00`).getTime()\n const now = new Date()\n const nowBeijing = new Date(now.getTime() + 8 * 3600_000)\n const todayStart = new Date(\n Date.UTC(nowBeijing.getUTCFullYear(), nowBeijing.getUTCMonth(), nowBeijing.getUTCDate()) -\n 8 * 3600_000\n ).getTime()\n const diff = Math.round((target - todayStart) / 86400000)\n return direction === 'countup' ? Math.abs(diff) : diff\n}\n\nexport function weekdayOf(targetDate: string): string {\n if (!isValidDate(targetDate)) return ''\n const [y, m, d] = targetDate.split('-').map(Number)\n return WEEK[new Date(y, m - 1, d).getDay()]\n}\n\n/** \u670D\u52A1\u5668\u65F6\u95F4 ISO\uFF08UTC\uFF0C\u5B58\u50A8\u7528\uFF09 */\nexport function nowIso(): string {\n return new Date().toISOString()\n}\n\n/** \u4E8B\u4EF6\u884C \u2192 DTO\uFF08\u9644 days / weekday\uFF1Bis_pinned \u5E03\u5C14\u5316\uFF09 */\nexport interface EventRow {\n id: string\n user_id: string\n title: string\n target_date: string\n note: string | null\n category: string | null\n direction: 'countdown' | 'countup' | null\n is_pinned: number | boolean | null\n sort_order: number | null\n created_at: string | null\n updated_at: string | null\n}\n\nexport interface EventDTO {\n id: string\n title: string\n target_date: string\n note: string\n category: string\n direction: 'countdown' | 'countup'\n is_pinned: boolean\n sort_order: number\n created_at: string\n updated_at: string\n days: number\n}\n\nexport function toDTO(row: EventRow): EventDTO {\n const direction = row.direction === 'countup' ? 'countup' : 'countdown'\n const dto: EventDTO = {\n id: row.id,\n title: row.title,\n target_date: row.target_date,\n note: row.note ?? '',\n category: row.category ?? 'other',\n direction,\n is_pinned: !!row.is_pinned,\n sort_order: row.sort_order ?? 0,\n created_at: row.created_at ?? '',\n updated_at: row.updated_at ?? '',\n days: 0,\n }\n dto.days = calcDays(row.target_date, direction)\n return dto\n}\n\n/** \u5217\u8868\u6392\u5E8F\uFF1A\u7F6E\u9876\u5728\u524D\uFF0C\u5176\u4F59\u6309\u5269\u4F59\u5929\u6570\u5347\u5E8F\uFF08\u8FC7\u53BB\u7684\u5929\u6570\u6392\u5728\u6700\u540E\uFF09 */\nexport function sortEvents<T extends EventDTO>(events: T[]): T[] {\n return events.toSorted((a, b) => {\n if (a.is_pinned !== b.is_pinned) return a.is_pinned ? -1 : 1\n const da = a.days >= 0 ? a.days : Number.MAX_SAFE_INTEGER\n const db = b.days >= 0 ? b.days : Number.MAX_SAFE_INTEGER\n return da - db\n })\n}\n" },
|
|
7552
|
+
{ path: "functions/_shared/response.ts", content: "/**\n * \u7EDF\u4E00\u54CD\u5E94\u4FE1\u5C01\uFF08\u9010\u884C\u79FB\u690D\u81EA cf-backend src/lib/response.ts\uFF0C\u8F93\u51FA adep `__adepHttp` \u4FE1\u5C01\uFF09\u3002\n *\n * \u5C0F\u7A0B\u5E8F\u540E\u7AEF\u7528 Hono \u8FD4\u56DE `{code, data, message}` + HTTP \u72B6\u6001\uFF1Badep \u4E91\u51FD\u6570\u8FD4\u56DE\n * `__adepHttp` \u4FE1\u5C01\u5373\u53EF\u83B7\u5F97\u540C\u6837\u7684 status/body \u8BED\u4E49\uFF08ADR-0024 / CF-010\uFF0C\n * \u672C\u5730 dev \u4E0E\u5E73\u53F0\u7F51\u5173\u540C\u4E00\u5957\u8BC6\u522B\u903B\u8F91\uFF09\u3002\n */\nimport type { AdepHttpEnvelope } from '@adep/types'\n\nexport const CODE = {\n OK: 0,\n NO_AUTH: 40001,\n BAD_PARAM: 40002,\n NOT_FOUND: 40003,\n LIMIT_EXCEEDED: 40004,\n SERVER_ERROR: 50000,\n} as const\n\nexport type CodeValue = (typeof CODE)[keyof typeof CODE]\n\nconst HTTP_STATUS: Record<number, number> = {\n 0: 200,\n 40001: 401,\n 40002: 400,\n 40003: 404,\n 40004: 409,\n 50000: 500,\n}\n\nexport interface ApiBody<T> {\n code: number\n data: T | null\n message: string\n}\n\nconst JSON_HEADERS = { 'content-type': 'application/json; charset=utf-8' }\n\nfunction envelope<T>(body: ApiBody<T>, status: number): AdepHttpEnvelope {\n return {\n __adepHttp: {\n status,\n headers: JSON_HEADERS,\n body,\n },\n }\n}\n\nexport function ok<T>(data: T, message = 'ok'): AdepHttpEnvelope {\n return envelope({ code: CODE.OK, data, message }, 200)\n}\n\nexport function fail(code: CodeValue, message: string): AdepHttpEnvelope {\n return envelope({ code, data: null, message }, HTTP_STATUS[code] ?? 400)\n}\n" },
|
|
7553
|
+
{ path: "functions/_shared/route.ts", content: "/**\n * \u8DEF\u5F84\u5F52\u4E00\u5316\uFF1A\u5E73\u53F0\u7F51\u5173 / CLI dev \u4F20\u7ED9\u51FD\u6570\u7684 ctx.path \u662F\u5B8C\u6574 pathname\uFF08\u542B\n * functions_prefix /api\uFF0C\u89C1 AGENTS.md v1.57 \u8FB9\u754C\u8BF4\u660E\uFF09\u3002\u8FD9\u91CC\u5265\u6389\u524D\u7F00\uFF0C\u51FD\u6570\u5185\n * \u4E00\u5F8B\u6309\u300C\u524D\u7F00\u540E\u8DEF\u5F84\u300D\u5339\u914D\uFF08/events\u3001/auth/login \u7B49\uFF09\u3002\n */\nexport function routePath(ctxPath: string): string {\n if (ctxPath === '/api') return '/'\n if (ctxPath.startsWith('/api/')) return ctxPath.slice(4)\n return ctxPath\n}\n" },
|
|
7554
|
+
{ path: "functions/_shared/store.ts", content: "/**\n * \u6570\u636E\u8BBF\u95EE\u5C42\uFF08\u79FB\u690D\u81EA cf-backend src/db/queries.ts + src/db/client.ts\uFF09\u3002\n *\n * \u8FC1\u79FB\u70B9\uFF1ACloudflare D1 \u7684 prepare().bind().run()/all() \u2192 adep `cloud.db` \u94FE\u5F0F\n * builder\uFF08\u7B49\u4EF7 SQLite \u5B50\u96C6\uFF1Bsim \u5F15\u64CE\u4E0E\u5E73\u53F0\u9879\u76EE\u5E93\u540C\u4E00\u5957\u5B9E\u73B0\uFF0C\u89C1\n * `packages/runtime/src/database/builder`\uFF09\u3002\u8868\u7ED3\u6784\u7531 functions/schema.sql \u58F0\u660E\uFF0C\n * \u90E8\u7F72\u524D\u7528 `adep db migrate` \u5EFA\u8868\uFF1B\u672C\u5730 dev\uFF08adep dev / vite \u63D2\u4EF6\uFF09\u7684 sim \u5F15\u64CE\n * \u8BBF\u95EE\u65F6\u4F1A\u81EA\u52A8\u5EFA\u8868\u3002\n */\nimport type { CloudDb, SqlValue } from '@adep/types'\n\n/** users \u8868\u884C\uFF08\u8BFB\u51FA\u7684\u884C\u5B57\u6BB5\u53EF\u7A7A\uFF0C\u5199\u65F6\u8865\u5168\uFF09 */\nexport interface UserRow {\n id: string\n device_id: string\n nickname: string | null\n avatar_url: string | null\n created_at: string | null\n updated_at: string | null\n}\n\n/** events \u8868\u884C\uFF08\u8BFB\u51FA\u7684\u884C\u5B57\u6BB5\u53EF\u7A7A\uFF0C\u5199\u65F6\u8865\u5168\uFF09 */\nexport interface EventRow {\n id: string\n user_id: string\n title: string\n target_date: string\n note: string | null\n category: string | null\n direction: 'countdown' | 'countup' | null\n is_pinned: number | boolean | null\n sort_order: number | null\n created_at: string | null\n updated_at: string | null\n}\n\n/** \u884C\u5BF9\u8C61 \u2192 builder \u53EF\u63A5\u53D7\u7684 SqlValue \u8BB0\u5F55\uFF08\u7ED3\u6784\u517C\u5BB9\u5373\u53EF\uFF0C\u5185\u90E8\u90FD\u662F\u57FA\u672C\u7C7B\u578B\uFF09\u3002 */\nfunction sql(row: Record<string, unknown>): Record<string, SqlValue> {\n return row as unknown as Record<string, SqlValue>\n}\n\n/** \u65B0\u5EFA\u7528\u6237\u884C\u7684\u5B8C\u6574\u5F62\u72B6\uFF08\u5199\u5165\u4E13\u7528\uFF09\u3002 */\nexport interface NewUserRow {\n id: string\n device_id: string\n nickname: string\n avatar_url: string\n created_at: string\n updated_at: string\n}\n\n/** \u65B0\u5EFA\u4E8B\u4EF6\u884C\u7684\u5B8C\u6574\u5F62\u72B6\uFF08\u5199\u5165\u4E13\u7528\uFF09\u3002 */\nexport interface NewEventRow {\n id: string\n user_id: string\n title: string\n target_date: string\n note: string\n category: string\n direction: 'countdown' | 'countup'\n is_pinned: 0 | 1\n sort_order: number\n created_at: string\n updated_at: string\n}\n\n// ---------- users ----------\n\nexport async function findUserByDeviceId(db: CloudDb, deviceId: string): Promise<UserRow | null> {\n const row = await db.table('users').where('device_id', deviceId).first()\n return (row as unknown as UserRow | undefined) ?? null\n}\n\nexport async function findUserById(db: CloudDb, id: string): Promise<UserRow | null> {\n const row = await db.table('users').where('id', id).first()\n return (row as unknown as UserRow | undefined) ?? null\n}\n\nexport async function createUser(db: CloudDb, row: NewUserRow): Promise<void> {\n await db.table('users').insert(sql(row as unknown as Record<string, unknown>))\n}\n\n// ---------- events ----------\n\nexport async function listEvents(db: CloudDb, userId: string): Promise<EventRow[]> {\n const rows = await db.table('events').where('user_id', userId).get()\n return (rows as unknown as EventRow[]) ?? []\n}\n\nexport async function findEventById(\n db: CloudDb,\n id: string,\n userId: string\n): Promise<EventRow | null> {\n const row = await db.table('events').where('id', id).where('user_id', userId).first()\n return (row as unknown as EventRow | undefined) ?? null\n}\n\nexport async function countEvents(db: CloudDb, userId: string): Promise<number> {\n // cloud.db \u7684 count() \u8FD4\u56DE number\uFF08builder \u5185\u90E8\u53D6 SELECT count(*) AS n \u7684 n\uFF09\n const n = await db.table('events').where('user_id', userId).count()\n return typeof n === 'number' ? n : Number(n ?? 0)\n}\n\nexport async function insertEvent(db: CloudDb, row: NewEventRow): Promise<void> {\n await db.table('events').insert(sql(row as unknown as Record<string, unknown>))\n}\n\nexport async function updateEvent(\n db: CloudDb,\n id: string,\n userId: string,\n patch: Partial<NewEventRow>\n): Promise<void> {\n await db\n .table('events')\n .where('id', id)\n .where('user_id', userId)\n .update(sql(patch as unknown as Record<string, unknown>))\n}\n\nexport async function deleteEvent(db: CloudDb, id: string, userId: string): Promise<void> {\n await db.table('events').where('id', id).where('user_id', userId).delete()\n}\n" },
|
|
7555
|
+
{ path: "functions/auth.ts", content: "/**\n * \u4E91\u51FD\u6570\uFF1Aauth \u2014\u2014 \u767B\u5F55 / \u5F53\u524D\u7528\u6237\uFF08\u79FB\u690D\u81EA cf-backend src/routes/auth.ts\uFF09\u3002\n *\n * \u8DEF\u7531\uFF08\u5F52\u4E00\u5316\u540E\uFF0C\u524D\u7F00 /api \u7531 _shared/route.ts \u5265\u9664\uFF09\uFF1A\n * POST /auth/login { deviceId } \u2192 \u53D6/\u5EFA\u7528\u6237\uFF0C\u8FD4\u56DE { token, expires_in, user }\n * GET /auth/me \u2192 \u5F53\u524D\u7528\u6237 + \u4E8B\u4EF6\u8BA1\u6570\uFF08Bearer device_id\uFF09\n */\nimport type { FunctionContext } from '@adep/types'\nimport { ok, fail, CODE } from './_shared/response'\nimport {\n findUserByDeviceId,\n findUserById,\n createUser,\n countEvents,\n type NewUserRow,\n} from './_shared/store'\nimport { requireUser, isEnvelope } from './_shared/auth'\nimport { routePath } from './_shared/route'\nimport { nowIso } from './_shared/date'\n\ninterface UserDTO {\n id: string\n nickname: string\n avatar_url: string\n created_at: string\n}\n\nfunction toUserDTO(row: {\n id: string\n nickname: string | null\n avatar_url: string | null\n created_at: string | null\n}): UserDTO {\n return {\n id: row.id,\n nickname: row.nickname ?? '\u672C\u5730\u7528\u6237',\n avatar_url: row.avatar_url ?? '',\n created_at: row.created_at ?? '',\n }\n}\n\nexport default async function handle(ctx: FunctionContext) {\n const db = ctx.cloud.db\n if (!db) return fail(CODE.SERVER_ERROR, '\u6570\u636E\u5E93\u672A\u5C31\u7EEA\uFF0C\u8BF7\u5148\u6267\u884C adep db start \u5E76\u5EFA\u8868')\n\n const path = routePath(ctx.path)\n const method = String(ctx.method ?? 'GET').toUpperCase()\n\n // POST /auth/login \u2014\u2014 \u8BBE\u5907\u8EAB\u4EFD\u6362\u767B\u5F55\u6001\uFF08\u7B49\u4EF7 wx.login \u2192 code2Session\uFF09\n if (path === '/auth/login' && method === 'POST') {\n const body = (ctx.body ?? {}) as Record<string, unknown>\n const deviceId = typeof body.deviceId === 'string' ? body.deviceId.trim() : ''\n if (!deviceId || deviceId.length < 8 || deviceId.length > 128) {\n return fail(CODE.BAD_PARAM, 'deviceId \u53C2\u6570\u4E0D\u5408\u6CD5')\n }\n\n try {\n let user = await findUserByDeviceId(db, deviceId)\n if (!user) {\n const fresh: NewUserRow = {\n id: crypto.randomUUID(),\n device_id: deviceId,\n nickname: '\u672C\u5730\u7528\u6237',\n avatar_url: '',\n created_at: nowIso(),\n updated_at: nowIso(),\n }\n await createUser(db, fresh)\n user = fresh\n }\n return ok({\n token: deviceId, // \u8BBE\u5907\u8EAB\u4EFD\u5373 Bearer \u51ED\u636E\uFF08\u7B49\u4EF7 API Key\uFF1B\u524D\u7AEF\u6301\u4E45\u5316\u4E8E localStorage\uFF09\n expires_in: 7 * 24 * 3600,\n user: toUserDTO(user),\n })\n } catch (err) {\n console.error('[auth/login]', err)\n return fail(CODE.SERVER_ERROR, '\u767B\u5F55\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5')\n }\n }\n\n // GET /auth/me \u2014\u2014 \u5F53\u524D\u7528\u6237 + \u4E8B\u4EF6\u8BA1\u6570\n if (path === '/auth/me' && method === 'GET') {\n const authed = await requireUser(db, ctx)\n if (isEnvelope(authed)) return authed\n const { user } = authed\n const eventCount = await countEvents(db, user.id)\n const fresh = (await findUserById(db, user.id)) ?? user\n return ok({\n user: toUserDTO(fresh),\n event_count: eventCount,\n max_events: 200,\n })\n }\n\n return fail(CODE.NOT_FOUND, '\u63A5\u53E3\u4E0D\u5B58\u5728')\n}\n" },
|
|
7556
|
+
{ path: "functions/events.ts", content: "/**\n * \u4E91\u51FD\u6570\uFF1Aevents \u2014\u2014 \u4E8B\u4EF6 CRUD\uFF08\u79FB\u690D\u81EA cf-backend src/routes/events.ts\uFF09\u3002\n *\n * \u8DEF\u7531\uFF08\u5F52\u4E00\u5316\u540E\uFF09\uFF1A\n * GET /events?category=xxx \u2192 { events }\uFF08\u7F6E\u9876\u4F18\u5148\uFF0C\u5176\u4F59\u6309\u5269\u4F59\u5929\u6570\u5347\u5E8F\uFF09\n * POST /events \u2192 \u521B\u5EFA\uFF08\u4E0A\u9650 200 \u4E2A\uFF09\n * GET /events/:id \u2192 { event }\n * PUT /events/:id \u2192 \u5C40\u90E8\u66F4\u65B0\n * DELETE /events/:id \u2192 \u5220\u9664\n * \u9274\u6743\uFF1ABearer device_id\uFF08\u89C1 _shared/auth.ts\uFF09\u3002\n */\nimport type { FunctionContext } from '@adep/types'\nimport { ok, fail, CODE } from './_shared/response'\nimport { requireUser, isEnvelope } from './_shared/auth'\nimport { routePath } from './_shared/route'\nimport { isValidDate, toDTO, sortEvents, nowIso, type EventRow } from './_shared/date'\nimport {\n findEventById,\n insertEvent,\n updateEvent,\n deleteEvent,\n listEvents,\n countEvents,\n type NewEventRow,\n} from './_shared/store'\n\nconst CATEGORIES = new Set(['other', 'birthday', 'anniversary', 'exam', 'holiday'])\nconst MAX_EVENTS = 200\n\nfunction normalizeBody<T>(ctx: FunctionContext): T {\n if (ctx.body && typeof ctx.body === 'object') return ctx.body as T\n return {} as T\n}\n\nexport default async function handle(ctx: FunctionContext) {\n const db = ctx.cloud.db\n if (!db) return fail(CODE.SERVER_ERROR, '\u6570\u636E\u5E93\u672A\u5C31\u7EEA\uFF0C\u8BF7\u5148\u6267\u884C adep db start \u5E76\u5EFA\u8868')\n\n const authed = await requireUser(db, ctx)\n if (isEnvelope(authed)) return authed\n const { user } = authed\n\n const path = routePath(ctx.path)\n const method = String(ctx.method ?? 'GET').toUpperCase()\n\n // GET /events\n if (path === '/events' && method === 'GET') {\n const category = ctx.query?.category\n let events = (await listEvents(db, user.id)).map((row) => toDTO(row as unknown as EventRow))\n if (typeof category === 'string' && category && CATEGORIES.has(category)) {\n events = events.filter((e) => e.category === category)\n }\n return ok({ events: sortEvents(events) })\n }\n\n // POST /events\n if (path === '/events' && method === 'POST') {\n const body = normalizeBody<{\n title?: unknown\n target_date?: unknown\n note?: unknown\n category?: unknown\n direction?: unknown\n is_pinned?: unknown\n }>(ctx)\n\n const title = typeof body.title === 'string' ? body.title.trim() : ''\n const target_date = typeof body.target_date === 'string' ? body.target_date : ''\n const note = typeof body.note === 'string' ? body.note.trim() : ''\n const category = typeof body.category === 'string' ? body.category : 'other'\n const direction = body.direction === 'countup' ? 'countup' : 'countdown'\n const is_pinned = !!body.is_pinned\n\n if (!title) return fail(CODE.BAD_PARAM, '\u8BF7\u586B\u5199\u4E8B\u4EF6\u540D\u79F0')\n if (title.length > 50) return fail(CODE.BAD_PARAM, '\u540D\u79F0\u4E0D\u80FD\u8D85\u8FC7 50 \u4E2A\u5B57')\n if (!isValidDate(target_date)) return fail(CODE.BAD_PARAM, '\u8BF7\u9009\u62E9\u76EE\u6807\u65E5\u671F')\n if (note.length > 200) return fail(CODE.BAD_PARAM, '\u5907\u6CE8\u4E0D\u80FD\u8D85\u8FC7 200 \u4E2A\u5B57')\n if (!CATEGORIES.has(category)) return fail(CODE.BAD_PARAM, '\u5206\u7C7B\u4E0D\u5408\u6CD5')\n\n const count = await countEvents(db, user.id)\n if (count >= MAX_EVENTS) return fail(CODE.LIMIT_EXCEEDED, `\u6700\u591A\u521B\u5EFA ${MAX_EVENTS} \u4E2A\u4E8B\u4EF6`)\n\n const now = nowIso()\n const event: NewEventRow = {\n id: crypto.randomUUID(),\n user_id: user.id,\n title,\n target_date,\n note,\n category,\n direction,\n is_pinned: is_pinned ? 1 : 0,\n sort_order: 0,\n created_at: now,\n updated_at: now,\n }\n await insertEvent(db, event)\n return ok({ event: toDTO(event) }, '\u521B\u5EFA\u6210\u529F')\n }\n\n // GET /events/:id\n const matchId = /^\\/events\\/([^/]+)$/.exec(path)\n if (matchId && method === 'GET') {\n const event = await findEventById(db, matchId[1], user.id)\n if (!event) return fail(CODE.NOT_FOUND, '\u4E8B\u4EF6\u4E0D\u5B58\u5728')\n return ok({ event: toDTO(event as unknown as EventRow) })\n }\n\n // PUT /events/:id\n if (matchId && method === 'PUT') {\n const event = await findEventById(db, matchId[1], user.id)\n if (!event) return fail(CODE.NOT_FOUND, '\u4E8B\u4EF6\u4E0D\u5B58\u5728')\n\n const body = normalizeBody<{\n title?: unknown\n target_date?: unknown\n note?: unknown\n category?: unknown\n direction?: unknown\n is_pinned?: unknown\n }>(ctx)\n\n const patch: Partial<NewEventRow> = {}\n const { title, target_date, note, category, direction, is_pinned } = body\n if (title !== undefined) {\n if (typeof title !== 'string' || !title.trim()) return fail(CODE.BAD_PARAM, '\u8BF7\u586B\u5199\u4E8B\u4EF6\u540D\u79F0')\n if (title.trim().length > 50) return fail(CODE.BAD_PARAM, '\u540D\u79F0\u4E0D\u80FD\u8D85\u8FC7 50 \u4E2A\u5B57')\n patch.title = title.trim()\n }\n if (target_date !== undefined) {\n if (typeof target_date !== 'string' || !isValidDate(target_date)) {\n return fail(CODE.BAD_PARAM, '\u8BF7\u9009\u62E9\u76EE\u6807\u65E5\u671F')\n }\n patch.target_date = target_date\n }\n if (note !== undefined) {\n if (typeof note !== 'string') return fail(CODE.BAD_PARAM, '\u5907\u6CE8\u683C\u5F0F\u4E0D\u5408\u6CD5')\n if (note.trim().length > 200) return fail(CODE.BAD_PARAM, '\u5907\u6CE8\u4E0D\u80FD\u8D85\u8FC7 200 \u4E2A\u5B57')\n patch.note = note.trim()\n }\n if (category !== undefined) {\n if (typeof category !== 'string' || !CATEGORIES.has(category)) {\n return fail(CODE.BAD_PARAM, '\u5206\u7C7B\u4E0D\u5408\u6CD5')\n }\n patch.category = category\n }\n if (direction !== undefined) {\n if (direction !== 'countdown' && direction !== 'countup') {\n return fail(CODE.BAD_PARAM, '\u8BA1\u65F6\u65B9\u5F0F\u4E0D\u5408\u6CD5')\n }\n patch.direction = direction\n }\n if (is_pinned !== undefined) {\n patch.is_pinned = is_pinned ? 1 : 0\n }\n\n patch.updated_at = nowIso()\n await updateEvent(db, event.id, user.id, patch)\n const fresh = await findEventById(db, event.id, user.id)\n return ok({ event: toDTO((fresh ?? event) as unknown as EventRow) }, '\u5DF2\u4FDD\u5B58')\n }\n\n // DELETE /events/:id\n if (matchId && method === 'DELETE') {\n const event = await findEventById(db, matchId[1], user.id)\n if (!event) return fail(CODE.NOT_FOUND, '\u4E8B\u4EF6\u4E0D\u5B58\u5728')\n await deleteEvent(db, event.id, user.id)\n return ok({ id: event.id }, '\u5DF2\u5220\u9664')\n }\n\n return fail(CODE.NOT_FOUND, '\u63A5\u53E3\u4E0D\u5B58\u5728')\n}\n" },
|
|
7557
|
+
{ path: "functions/schema.sql", content: "-- \u5012\u6570\u65E5\u5E94\u7528\u6570\u636E\u5E93 Schema\uFF08\u79FB\u690D\u81EA cf-backend migrations/0001_initial.sql\uFF09\u3002\n-- \u7528\u6CD5\uFF1Aadep db migrate functions/schema.sql\uFF08\u5E73\u53F0\u4FA7\u5EFA\u8868\uFF09\uFF1B\n-- \u672C\u5730 dev\uFF08adep dev / vite \u63D2\u4EF6\uFF09\u7684 sim \u5F15\u64CE\u8BBF\u95EE\u65F6\u81EA\u52A8\u5EFA\u8868\uFF0C\u65E0\u9700\u624B\u52A8\u6267\u884C\u3002\n-- \u6BCF\u884C\u4E00\u6761\u8BED\u53E5\uFF0C\u4EE5\u5206\u53F7\u7ED3\u5C3E\uFF08migrate \u6309\u884C\u62C6\u5206\u9010\u6761\u6267\u884C\uFF09\u3002\n\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n device_id TEXT NOT NULL UNIQUE,\n nickname TEXT NOT NULL DEFAULT '\u672C\u5730\u7528\u6237',\n avatar_url TEXT NOT NULL DEFAULT '',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS events (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n target_date TEXT NOT NULL,\n note TEXT NOT NULL DEFAULT '',\n category TEXT NOT NULL DEFAULT 'other',\n direction TEXT NOT NULL DEFAULT 'countdown',\n is_pinned INTEGER NOT NULL DEFAULT 0,\n sort_order INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_events_user ON events(user_id);\nCREATE INDEX IF NOT EXISTS idx_users_device ON users(device_id);\n" },
|
|
7558
|
+
{ path: "web/index.html", content: `<!doctype html>
|
|
7559
|
+
<html lang="zh-CN">
|
|
7560
|
+
<head>
|
|
7561
|
+
<meta charset="UTF-8" />
|
|
7562
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
7563
|
+
<meta name="theme-color" content="#3B6EF6" />
|
|
7564
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='22' fill='%233B6EF6'/%3E%3Ctext x='50' y='68' font-size='52' font-family='PingFang SC,sans-serif' font-weight='700' fill='%23fff' text-anchor='middle'%3E\u65E5%3C/text%3E%3C/svg%3E" />
|
|
7565
|
+
<title>\u5012\u6570\u65E5</title>
|
|
7566
|
+
</head>
|
|
7567
|
+
<body>
|
|
7568
|
+
<div id="app"></div>
|
|
7569
|
+
<script type="module" src="/src/main.ts"></script>
|
|
7570
|
+
</body>
|
|
7571
|
+
</html>
|
|
7572
|
+
` },
|
|
7573
|
+
{ path: "web/package.json", content: '{\n "name": "countdown-web",\n "version": "0.1.0",\n "private": true,\n "type": "module",\n "description": "\u5012\u6570\u65E5\u524D\u7AEF\uFF08\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F\u79FB\u690D\uFF0CVue 3 + Vite\uFF09",\n "scripts": {\n "dev": "vite",\n "build": "vite build",\n "preview": "vite preview"\n },\n "dependencies": {\n "vue": "^3.5.42",\n "vue-router": "^5.0.0"\n },\n "devDependencies": {\n "@adep/cli": "^0.1.4",\n "@vitejs/plugin-vue": "^6.0.0",\n "esbuild-wasm": "^0.25.12",\n "typescript": "5.9.3",\n "vite": "^7.0.0"\n }\n}\n' },
|
|
7574
|
+
{ path: "web/src/App.vue", content: `<script setup lang="ts">
|
|
7575
|
+
// \u5E94\u7528\u6839\uFF1A\u8DEF\u7531\u89C6\u56FE + \u5E95\u90E8 TabBar\uFF08\u9996\u9875/\u6211\u7684\u663E\u793A\uFF0C\u7F16\u8F91\u9875\u9690\u85CF\uFF09+ \u6D6E\u5C42\u5BBF\u4E3B\u3002
|
|
7576
|
+
import { computed } from 'vue'
|
|
7577
|
+
import { useRoute } from 'vue-router'
|
|
7578
|
+
import TabBar from './ui/TabBar.vue'
|
|
7579
|
+
import UiHost from './ui/UiHost.vue'
|
|
7580
|
+
|
|
7581
|
+
const route = useRoute()
|
|
7582
|
+
|
|
7583
|
+
const showTabBar = computed(() => {
|
|
7584
|
+
return !route.path.startsWith('/edit')
|
|
7585
|
+
})
|
|
7586
|
+
</script>
|
|
7587
|
+
|
|
7588
|
+
<template>
|
|
7589
|
+
<div class="app">
|
|
7590
|
+
<router-view />
|
|
7591
|
+
<TabBar v-if="showTabBar" />
|
|
7592
|
+
<UiHost />
|
|
7593
|
+
</div>
|
|
7594
|
+
</template>
|
|
7595
|
+
|
|
7596
|
+
<style>
|
|
7597
|
+
@import './styles/app.css';
|
|
7598
|
+
</style>
|
|
7599
|
+
` },
|
|
7600
|
+
{ path: "web/src/env.d.ts", content: `/// <reference types="vite/client" />
|
|
7601
|
+
|
|
7602
|
+
declare module '*.vue' {
|
|
7603
|
+
import type { DefineComponent } from 'vue'
|
|
7604
|
+
const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>
|
|
7605
|
+
export default component
|
|
7606
|
+
}
|
|
7607
|
+
` },
|
|
7608
|
+
{ path: "web/src/lib/auth.ts", content: "/**\n * \u767B\u5F55\u72B6\u6001\u7BA1\u7406\uFF08\u79FB\u690D\u81EA\u5C0F\u7A0B\u5E8F utils/auth.js\uFF09\u3002\n *\n * \u6D41\u7A0B\uFF1AensureLogin() \u2192 \u6709 token \u76F4\u63A5\u8FD4\u56DE\u7F13\u5B58 user\uFF1B\u5426\u5219\u62FF\u8BBE\u5907\u8EAB\u4EFD device_id\n * POST /auth/login \u6362 token + user\uFF08\u7B49\u4EF7\u5C0F\u7A0B\u5E8F\u7684 wx.login \u2192 code2Session\uFF09\u3002\n * \u5E76\u53D1\u53EA\u53D1\u4E00\u6B21\u767B\u5F55\u8BF7\u6C42\uFF08loginPromise \u590D\u7528\uFF09\u3002\n */\nimport { request } from './request'\nimport { getDeviceId, resetDeviceId } from './device'\nimport { getToken, setToken, clearToken, TOKEN_KEY } from './request'\n\nconst USER_KEY = 'countdown_user'\n\nexport interface AppUser {\n id: string\n nickname: string\n avatar_url: string\n created_at: string\n}\n\nexport interface LoginResult {\n token: string\n expires_in: number\n user: AppUser\n}\n\nlet loginPromise: Promise<AppUser> | null = null\n\nexport function getUser(): AppUser | null {\n try {\n const raw = localStorage.getItem(USER_KEY)\n return raw ? (JSON.parse(raw) as AppUser) : null\n } catch {\n return null\n }\n}\n\nfunction saveUser(user: AppUser): void {\n localStorage.setItem(USER_KEY, JSON.stringify(user))\n}\n\nexport function clearSession(): void {\n clearToken()\n localStorage.removeItem(USER_KEY)\n}\n\n/** \u767B\u5F55\uFF1A\u8BBE\u5907\u8EAB\u4EFD \u2192 \u4E91\u51FD\u6570\u5EFA/\u53D6\u7528\u6237 \u2192 \u843D token + user\u3002 */\nexport async function login(): Promise<AppUser> {\n const deviceId = getDeviceId()\n const data = await request<LoginResult>('/api/auth/login', {\n method: 'POST',\n data: { deviceId },\n retry: false,\n })\n setToken(data.token)\n saveUser(data.user)\n return data.user\n}\n\n/** \u6709 token \u76F4\u63A5\u8FD4\u56DE\uFF0C\u5426\u5219\u767B\u5F55\uFF1B\u5E76\u53D1\u53EA\u53D1\u4E00\u6B21\u8BF7\u6C42\u3002 */\nexport function ensureLogin(): Promise<AppUser | null> {\n if (getToken()) return Promise.resolve(getUser())\n if (loginPromise) return loginPromise\n loginPromise = login().then(\n (user) => {\n loginPromise = null\n return user\n },\n (err: unknown) => {\n loginPromise = null\n throw err\n }\n )\n return loginPromise\n}\n\n/** \u9000\u51FA\u767B\u5F55\uFF1A\u6E05\u9664\u672C\u5730\u767B\u5F55\u6001\u4E0E\u4E8B\u4EF6\u7F13\u5B58\uFF1B\u4E0B\u6B21\u6253\u5F00\u81EA\u52A8\u7528\u65B0\u8EAB\u4EFD\u91CD\u65B0\u767B\u5F55\uFF08\u4E91\u7AEF\u6570\u636E\u4FDD\u7559\uFF09\u3002 */\nexport function logout(): void {\n clearSession()\n resetDeviceId()\n}\n\nexport { getToken, TOKEN_KEY }\n" },
|
|
7609
|
+
{ path: "web/src/lib/config.ts", content: "/**\n * \u5168\u5C40\u914D\u7F6E\uFF08\u79FB\u690D\u81EA\u5C0F\u7A0B\u5E8F utils/config.js\uFF09\u3002\n *\n * BASE_URL \u4E3A ''\uFF1A\u4E0E\u9875\u9762\u540C\u6E90\u3002\u672C\u5730\u5F00\u53D1\u7531 @adep/cli/vite \u4EE3\u7406 /api/* \u5230\u6A21\u62DF\u8FD0\u884C\u65F6\uFF1B\n * \u90E8\u7F72\u540E\u7531\u5E73\u53F0\u7F51\u5173\u628A /api/* \u8DEF\u7531\u5230\u4E91\u51FD\u6570\u3002\u4E91\u51FD\u6570\u8DEF\u5F84\u5373 /api/{fn}\uFF08adep.config.ts\n * functions_prefix=/api\uFF09\uFF0C\u6545\u8BF7\u6C42\u7EDF\u4E00\u5199 '/api/...'\u3002\n */\nexport const BASE_URL = ''\n\nexport const MAX_EVENTS = 200\n\nexport const CATEGORIES = [\n { value: 'other', label: '\u65E5\u5E38' },\n { value: 'birthday', label: '\u751F\u65E5' },\n { value: 'anniversary', label: '\u7EAA\u5FF5\u65E5' },\n { value: 'exam', label: '\u8003\u8BD5' },\n { value: 'holiday', label: '\u8282\u65E5' },\n] as const\n\nexport type CategoryValue = (typeof CATEGORIES)[number]['value']\n\nexport const CATEGORY_LABEL: Record<string, string> = {\n other: '\u65E5\u5E38',\n birthday: '\u751F\u65E5',\n anniversary: '\u7EAA\u5FF5\u65E5',\n exam: '\u8003\u8BD5',\n holiday: '\u8282\u65E5',\n}\n\n/** \u4E91\u51FD\u6570\u7EDF\u4E00\u54CD\u5E94\u4FE1\u5C01\uFF08\u79FB\u690D\u81EA cf-backend lib/response.ts\uFF09\u3002 */\nexport interface ApiBody<T> {\n code: number\n data: T | null\n message: string\n}\n\n/** \u4E1A\u52A1\u9519\u8BEF\u7801\uFF08\u4E0E cf-backend \u4E00\u81F4\uFF09\u3002 */\nexport const CODE = {\n OK: 0,\n NO_AUTH: 40001,\n BAD_PARAM: 40002,\n NOT_FOUND: 40003,\n LIMIT_EXCEEDED: 40004,\n SERVER_ERROR: 50000,\n} as const\n" },
|
|
7610
|
+
{ path: "web/src/lib/date.ts", content: "/**\n * \u65E5\u671F\u5DE5\u5177\uFF08\u9010\u884C\u79FB\u690D\u81EA\u5C0F\u7A0B\u5E8F utils/date.js\uFF0C\u7B97\u6CD5\u4E0E\u4E91\u51FD\u6570\u4FA7 _shared/date.ts \u4FDD\u6301\u4E00\u81F4\uFF09\u3002\n * \u5929\u6570\u6309\u300C\u672C\u5730\u65F6\u533A\u81EA\u7136\u65E5\u300D\u8BA1\u7B97\uFF1B\u4E91\u51FD\u6570\u4FA7\u6309\u5317\u4EAC\u65F6\u95F4\uFF08UTC+8\uFF09\u8BA1\u7B97\u2014\u2014\u7528\u6237\u5728\u4E2D\u56FD\u5927\u9646\u65F6\u4E24\u8005\u4E00\u81F4\u3002\n */\nimport { CATEGORY_LABEL } from './config'\n\nconst WEEK = ['\u5468\u65E5', '\u5468\u4E00', '\u5468\u4E8C', '\u5468\u4E09', '\u5468\u56DB', '\u5468\u4E94', '\u5468\u516D']\n\n/** \u4ECA\u5929\uFF08\u672C\u5730\u65F6\u533A\uFF09YYYY-MM-DD */\nexport function todayStr(): string {\n const d = new Date()\n return fmt(d.getFullYear(), d.getMonth() + 1, d.getDate())\n}\n\nexport function fmt(y: number, m: number, d: number): string {\n return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`\n}\n\nexport function isValidDate(s: unknown): s is string {\n if (typeof s !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return false\n const [y, m, d] = s.split('-').map(Number)\n if (m < 1 || m > 12 || d < 1 || d > 31) return false\n const dt = new Date(y, m - 1, d)\n return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d\n}\n\n/** \u8DDD\u76EE\u6807\u65E5\u671F\u7684\u81EA\u7136\u65E5\u5DEE\uFF1B\u6B63\u6570\u65E5\u8FD4\u56DE\u7EDD\u5BF9\u503C */\nexport function calcDays(\n targetDate: string,\n direction: 'countdown' | 'countup' = 'countdown'\n): number {\n if (!isValidDate(targetDate)) return 0\n const [y, m, d] = targetDate.split('-').map(Number)\n const target = new Date(y, m - 1, d).getTime()\n const now = new Date()\n const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()\n const diff = Math.round((target - today) / 86400000)\n return direction === 'countup' ? Math.abs(diff) : diff\n}\n\nexport function weekdayOf(targetDate: string): string {\n if (!isValidDate(targetDate)) return ''\n const [y, m, d] = targetDate.split('-').map(Number)\n return WEEK[new Date(y, m - 1, d).getDay()]\n}\n\n/** 2026-09-13 -> 9\u670813\u65E5 */\nexport function shortLabel(targetDate: string): string {\n if (!isValidDate(targetDate)) return ''\n const [, m, d] = targetDate.split('-').map(Number)\n return `${m}\u6708${d}\u65E5`\n}\n\n/**\n * \u6839\u636E\u5929\u6570\u8FD4\u56DE\u5C55\u793A\u7B49\u7EA7\uFF08\u4E0E\u5C0F\u7A0B\u5E8F\u4E00\u81F4\uFF09\n * normal >30 \u84DD | soon 7-30 \u6A59 | urgent <7 \u7EA2 | today \u5F53\u5929 \u7EA2 | past \u5DF2\u8FC7 \u7070\n */\nexport function levelOf(days: number, direction: 'countdown' | 'countup' = 'countdown'): string {\n if (direction === 'countup') return 'normal'\n if (days < 0) return 'past'\n if (days === 0) return 'today'\n if (days < 7) return 'urgent'\n if (days <= 30) return 'soon'\n return 'normal'\n}\n\n/** \u5929\u6570\u6587\u6848\u4E0E\u524D\u540E\u7F00 */\nexport function daysText(\n days: number,\n direction: 'countdown' | 'countup' = 'countdown'\n): { num: string; prefix: string; suffix: string; tip: string } {\n if (direction === 'countup') {\n return { num: String(days), prefix: '\u5DF2\u7ECF', suffix: '\u5929', tip: '\u5929\u5566' }\n }\n if (days === 0) return { num: '\u4ECA\u5929', prefix: '', suffix: '', tip: '\u5C31\u662F\u4ECA\u5929' }\n if (days > 0) return { num: String(days), prefix: '\u8FD8\u6709', suffix: '\u5929', tip: '' }\n return { num: String(-days), prefix: '\u5DF2\u8FC7', suffix: '\u5929', tip: '' }\n}\n\n/** \u4E8B\u4EF6 DTO\uFF08\u4E91\u51FD\u6570 /events \u8FD4\u56DE\u5E26 days \u7684\u5F62\u72B6\uFF09\u3002 */\nexport interface CountdownEvent {\n id: string\n title: string\n target_date: string\n note: string\n category: string\n direction: 'countdown' | 'countup'\n is_pinned: boolean\n sort_order: number\n created_at: string\n updated_at: string\n days: number\n}\n\n/** \u5217\u8868\u5C55\u793A\u88C5\u9970\uFF1A\u8865\u9F50\u7B49\u7EA7 / \u6587\u6848 / \u526F\u6807\u9898 / \u5206\u7C7B\u540D\uFF08\u79FB\u690D\u81EA index.js decorate\uFF09\u3002 */\nexport function decorateEvent(item: CountdownEvent): DecodedEvent {\n const days =\n typeof item.days === 'number' ? item.days : calcDays(item.target_date, item.direction)\n const t = daysText(days, item.direction)\n return {\n ...item,\n offset: 0,\n days,\n level: levelOf(days, item.direction),\n num: t.num,\n prefix: t.prefix,\n suffix: t.suffix,\n tip: t.tip,\n sub: `${item.target_date} ${weekdayOf(item.target_date)}`,\n categoryLabel: CATEGORY_LABEL[item.category] ?? '\u65E5\u5E38',\n }\n}\n\nexport interface DecodedEvent extends CountdownEvent {\n /** \u5DE6\u6ED1\u504F\u79FB\uFF08px\uFF09\uFF0C\u7528\u4E8E\u5361\u7247\u6ED1\u52A8\u52A8\u753B */\n offset: number\n level: string\n num: string\n prefix: string\n suffix: string\n tip: string\n sub: string\n categoryLabel: string\n}\n" },
|
|
7611
|
+
{ path: "web/src/lib/device.ts", content: "/**\n * \u8BBE\u5907\u8EAB\u4EFD\uFF08web \u7248\u7684\u300C\u5FAE\u4FE1 openid\u300D\u7B49\u4EF7\u7269\uFF09\u3002\n *\n * \u5C0F\u7A0B\u5E8F\u9760 wx.login \u2192 openid \u62FF\u5230\u8BBE\u5907\u7EA7\u552F\u4E00\u8EAB\u4EFD\uFF1Bweb \u79FB\u690D\u7528\u6D4F\u89C8\u5668 localStorage \u6301\u4E45\u5316\u7684\n * \u968F\u673A device_id \u627F\u62C5\u540C\u4E00\u89D2\u8272\uFF1A\u9996\u6B21\u8BBF\u95EE\u751F\u6210\u5E76\u843D\u76D8\uFF0C\u4E4B\u540E\u7A33\u5B9A\u590D\u7528\uFF08\u6E05\u7AD9\u70B9\u6570\u636E\u624D\u53D8\uFF09\u3002\n * \u4E91\u51FD\u6570\u628A\u5B83\u5F53 Bearer \u51ED\u636E\uFF08\u89C1 functions/_shared/auth.ts\uFF09\uFF0C\u8BED\u4E49\u7B49\u540C API Key\u3002\n */\nconst DEVICE_KEY = 'countdown_device_id'\n\nexport function getDeviceId(): string {\n let id = localStorage.getItem(DEVICE_KEY)\n if (id) return id\n id = generateDeviceId()\n localStorage.setItem(DEVICE_KEY, id)\n return id\n}\n\n/** \u767B\u51FA\u540E\u91CD\u65B0\u751F\u6210\u8EAB\u4EFD\uFF08\u539F\u8D26\u53F7\u6570\u636E\u4FDD\u7559\u5728\u4E91\u7AEF\uFF0C\u53EA\u662F\u4E0D\u518D\u5173\u8054\u65B0\u8EAB\u4EFD\uFF09\u3002 */\nexport function resetDeviceId(): void {\n localStorage.removeItem(DEVICE_KEY)\n}\n\nfunction generateDeviceId(): string {\n // crypto.randomUUID \u4E0D\u53EF\u7528\u65F6\u515C\u5E95\uFF08\u8001\u6D4F\u89C8\u5668 / \u975E\u5B89\u5168\u4E0A\u4E0B\u6587\uFF09\u3002\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return `dev_${crypto.randomUUID()}`\n }\n return `dev_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`\n}\n" },
|
|
7612
|
+
{ path: "web/src/lib/request.ts", content: "/**\n * \u7EDF\u4E00\u8BF7\u6C42\u5C01\u88C5\uFF08\u79FB\u690D\u81EA\u5C0F\u7A0B\u5E8F utils/request.js\uFF09\u3002\n *\n * \u5DEE\u5F02\u70B9\uFF1Awx.request \u2192 fetch\uFF1BBASE_URL=''\uFF08\u540C\u6E90 /api/*\uFF09\uFF1Btoken \u4ECE localStorage \u8BFB\u53D6\uFF1B\n * \u9047\u4E1A\u52A1\u7801 40001\uFF08\u672A\u767B\u5F55/\u5931\u6548\uFF09\u2192 \u6E05\u9664 token \u2192 \u9759\u9ED8\u91CD\u65B0\u767B\u5F55\u540E\u91CD\u8BD5\u4E00\u6B21 \u2192 \u4ECD\u5931\u8D25\u629B NEED_LOGIN\u3002\n * \u53EA\u8FD4\u56DE\u4E1A\u52A1 data\uFF08\u54CD\u5E94\u4FE1\u5C01 {code, data, message} \u7684 data\uFF09\uFF0C\u5931\u8D25\u629B Error\u3002\n */\nimport { BASE_URL } from './config'\n\nconst TOKEN_KEY = 'countdown_token'\n\nexport function getToken(): string {\n return localStorage.getItem(TOKEN_KEY) ?? ''\n}\n\nexport function setToken(token: string): void {\n localStorage.setItem(TOKEN_KEY, token)\n}\n\nexport function clearToken(): void {\n localStorage.removeItem(TOKEN_KEY)\n}\n\nexport interface RequestOptions {\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'\n data?: unknown\n retry?: boolean\n silent?: boolean\n header?: Record<string, string>\n}\n\ninterface ApiBody<T> {\n code: number\n data: T | null\n message: string\n}\n\nexport class RequestError extends Error {\n code: number\n status: number\n constructor(message: string, code: number, status: number) {\n super(message)\n this.name = 'RequestError'\n this.code = code\n this.status = status\n }\n}\n\n/** \u7279\u6B8A\u9519\u8BEF\uFF1Atoken \u5931\u6548\u4E14\u81EA\u52A8\u91CD\u767B\u5931\u8D25\uFF0C\u8C03\u7528\u65B9\u5E94\u5F15\u5BFC\u91CD\u65B0\u767B\u5F55\u3002 */\nexport const NEED_LOGIN = 'NEED_LOGIN'\n\nasync function doRequest<T>(path: string, options: RequestOptions): Promise<T> {\n const token = getToken()\n const url = BASE_URL + path\n const response = await fetch(url, {\n method: options.method ?? 'GET',\n headers: {\n 'Content-Type': 'application/json',\n ...(token ? { Authorization: `Bearer ${token}` } : {}),\n ...options.header,\n },\n body: options.data === undefined ? undefined : JSON.stringify(options.data),\n })\n\n let body: ApiBody<T>\n try {\n body = (await response.json()) as ApiBody<T>\n } catch {\n throw new RequestError(`\u8BF7\u6C42\u5931\u8D25(${response.status})`, -1, response.status)\n }\n\n if (response.status === 200 && body.code === 0) {\n return body.data as T\n }\n\n if (body.code === 40001) {\n clearToken()\n if (options.retry !== false) {\n // token \u5931\u6548\uFF1A\u9759\u9ED8\u91CD\u65B0\u767B\u5F55\u540E\u91CD\u8BD5\u4E00\u6B21\n const { login } = await import('./auth')\n try {\n await login()\n return doRequest(path, { ...options, retry: false })\n } catch {\n throw new RequestError(NEED_LOGIN, 40001, 401)\n }\n }\n throw new RequestError(NEED_LOGIN, 40001, 401)\n }\n\n const msg = body.message || `\u8BF7\u6C42\u5931\u8D25(${response.status})`\n if (!options.silent) console.warn('[request]', path, msg)\n throw new RequestError(msg, body.code, response.status)\n}\n\nexport function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n return doRequest<T>(path, options).catch((error: unknown) => {\n // \u7F51\u7EDC\u5F02\u5E38 \u2192 \u4E0E\u5C0F\u7A0B\u5E8F\u4E00\u81F4\u7684\u63D0\u793A\n if (error instanceof TypeError) {\n const e = new RequestError('\u7F51\u7EDC\u5F02\u5E38\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5', -1, 0)\n throw e\n }\n throw error\n })\n}\n\nexport { TOKEN_KEY }\n" },
|
|
7613
|
+
{ path: "web/src/main.ts", content: "// web/src/main.ts \u2014\u2014 \u5E94\u7528\u5165\u53E3\uFF08\u79FB\u690D\u81EA\u5C0F\u7A0B\u5E8F app.js\uFF1A\u542F\u52A8\u5373\u9759\u9ED8\u767B\u5F55\uFF09\u3002\nimport { createApp } from 'vue'\nimport App from './App.vue'\nimport router from './router'\nimport { ensureLogin } from './lib/auth'\n\n// \u542F\u52A8\u9759\u9ED8\u767B\u5F55\uFF08\u7B49\u4EF7\u5C0F\u7A0B\u5E8F app.js onLaunch \u7684 ensureLogin()\uFF09\uFF0C\u5931\u8D25\u4E0D\u963B\u585E\u6E32\u67D3\uFF0C\n// \u9875\u9762\u8BF7\u6C42\u65F6\u4F1A\u518D\u6B21 ensureLogin / \u81EA\u52A8\u91CD\u767B\u3002\nvoid ensureLogin().catch((err: unknown) => {\n console.warn('[app] \u81EA\u52A8\u767B\u5F55\u5931\u8D25\uFF0C\u5C06\u5728\u9875\u9762\u91CD\u8BD5', err)\n})\n\ncreateApp(App).use(router).mount('#app')\n" },
|
|
7614
|
+
{ path: "web/src/router.ts", content: "import { createRouter, createWebHistory } from 'vue-router'\n\nconst router = createRouter({\n history: createWebHistory(),\n routes: [\n { path: '/', name: 'home', component: () => import('./views/HomeView.vue') },\n { path: '/edit', name: 'edit', component: () => import('./views/EditView.vue') },\n { path: '/edit/:id', name: 'edit-id', component: () => import('./views/EditView.vue') },\n { path: '/profile', name: 'profile', component: () => import('./views/ProfileView.vue') },\n { path: '/:pathMatch(.*)*', redirect: '/' },\n ],\n})\n\nexport default router\n" },
|
|
7615
|
+
{ path: "web/src/styles/app.css", content: "/**\n * \u5168\u5C40\u6837\u5F0F\uFF08\u79FB\u690D\u81EA\u5C0F\u7A0B\u5E8F app.wxss + \u5404\u9875 wxss \u7684\u516C\u5171\u57FA\u8C03\uFF09\u3002\n *\n * rpx \u2192 rem \u6362\u7B97\uFF1A\u5C0F\u7A0B\u5E8F\u8BBE\u8BA1\u7A3F\u5BBD 750rpx\u3002web \u7AEF\u628A\u5E94\u7528\u58F3\uFF08.app\uFF09\u9650\u5236\u4E3A 480px \u5BBD\u7684\n * \u624B\u673A\u5F0F\u753B\u5E03\uFF0C\u4EE4 1rem = 100rpx = 64px\uFF08480 / 7.5\uFF09\uFF0C\u5168\u90E8\u539F wxss \u7684 `N rpx` \u6362\u7B97\u4E3A\n * `N/100 rem`\u3002rem \u662F\u76F8\u5BF9\u6839\u5143\u7D20 <html> \u7684\uFF0C\u6545\u57FA\u51C6\u5B57\u53F7\u8BBE\u5728 html \u4E0A\uFF1B\u89C6\u53E3\u7A84\u4E8E\n * 480px \u65F6\u6839\u5B57\u53F7\u968F\u89C6\u53E3\u7F29\u653E\uFF08100rpx = 13.333vw\uFF09\uFF0C\u5C0F\u7A97\u53E3\u4E0B\u6BD4\u4F8B\u4FDD\u6301\u4E00\u81F4\u3002\n */\n:root {\n --primary: #3b6ef6;\n --primary-2: #6a8dff;\n --bg: #f5f7fb;\n --card: #ffffff;\n --text-1: #1f2430;\n --text-2: #6b7385;\n --text-3: #9aa3b2;\n --line: #eef0f5;\n\n --normal: #3b6ef6; /* >30 \u5929 */\n --soon: #ff9f43; /* 7-30 \u5929 */\n --urgent: #ff5a5f; /* <7 \u5929 */\n --past: #a0a8b8; /* \u5DF2\u8FC7\u53BB */\n}\n\nhtml {\n /* 1rem = 100rpx\uFF08480px \u753B\u5E03\u4E0B 1rpx = 0.64px\uFF09\uFF1B\u968F\u89C6\u53E3\u7A84\u4E8E 480px \u7F29\u653E */\n font-size: 64px;\n}\n\n@media (max-width: 479px) {\n html {\n font-size: calc(100vw / 7.5); /* 1rpx = 1vw / 7.5 */\n }\n}\n\n* {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\n\nhtml,\nbody {\n height: 100%;\n}\n\nbody {\n background: #e8ebf1;\n font-family:\n -apple-system,\n BlinkMacSystemFont,\n 'PingFang SC',\n 'Helvetica Neue',\n Helvetica,\n sans-serif;\n -webkit-font-smoothing: antialiased;\n}\n\nbutton,\ninput,\ntextarea {\n font-family: inherit;\n outline: none;\n border: none;\n background: none;\n}\n\n/* \u5E94\u7528\u58F3\uFF1A\u5C45\u4E2D\u624B\u673A\u5F0F\u753B\u5E03\uFF08rem \u57FA\u51C6\u5728 html\uFF0C\u89C1\u6587\u4EF6\u5934\u6CE8\u91CA\uFF09 */\n.app {\n position: relative;\n max-width: 480px;\n height: 100dvh;\n margin: 0 auto;\n overflow: hidden;\n background: var(--bg);\n color: var(--text-1);\n}\n\n/* \u9875\u9762\u6EDA\u52A8\u5BB9\u5668\uFF08\u5404\u89C6\u56FE\u5185\u90E8\u4F7F\u7528\uFF09 */\n.page-scroll {\n position: absolute;\n inset: 0;\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n overscroll-behavior: contain;\n}\n\n.page-scroll::-webkit-scrollbar {\n width: 0;\n}\n\n.no-select {\n user-select: none;\n -webkit-user-select: none;\n -webkit-touch-callout: none;\n}\n" },
|
|
7616
|
+
{ path: "web/src/ui/TabBar.vue", content: `<script setup lang="ts">
|
|
7617
|
+
// \u81EA\u5B9A\u4E49 TabBar\uFF08\u79FB\u690D\u81EA custom-tab-bar/index.*\uFF09\uFF1A\u9996\u9875 / \uFF0B\uFF08\u65B0\u589E\uFF09/ \u6211\u7684\u3002
|
|
7618
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
7619
|
+
|
|
7620
|
+
const route = useRoute()
|
|
7621
|
+
const router = useRouter()
|
|
7622
|
+
|
|
7623
|
+
const tabs = [
|
|
7624
|
+
{ path: '/', text: '\u9996\u9875' },
|
|
7625
|
+
{ path: '/profile', text: '\u6211\u7684' },
|
|
7626
|
+
]
|
|
7627
|
+
|
|
7628
|
+
function selectedIndex(): number {
|
|
7629
|
+
const path = route.path
|
|
7630
|
+
if (path.startsWith('/profile')) return 1
|
|
7631
|
+
return 0
|
|
7632
|
+
}
|
|
7633
|
+
|
|
7634
|
+
function switchTab(path: string): void {
|
|
7635
|
+
if (route.path === path) return
|
|
7636
|
+
router.push(path)
|
|
7637
|
+
}
|
|
7638
|
+
|
|
7639
|
+
function onAdd(): void {
|
|
7640
|
+
router.push('/edit')
|
|
7641
|
+
}
|
|
7642
|
+
</script>
|
|
7643
|
+
|
|
7644
|
+
<template>
|
|
7645
|
+
<div class="tabbar">
|
|
7646
|
+
<div class="tabbar-inner">
|
|
7647
|
+
<div class="tab" :class="{ on: selectedIndex() === 0 }" @click="switchTab('/')">
|
|
7648
|
+
<svg
|
|
7649
|
+
class="icon"
|
|
7650
|
+
viewBox="0 0 24 24"
|
|
7651
|
+
fill="none"
|
|
7652
|
+
:stroke="selectedIndex() === 0 ? '#3B6EF6' : '#9AA3B2'"
|
|
7653
|
+
stroke-width="2"
|
|
7654
|
+
stroke-linecap="round"
|
|
7655
|
+
stroke-linejoin="round"
|
|
7656
|
+
>
|
|
7657
|
+
<path d="M3 10.4 12 3.5l9 6.9" />
|
|
7658
|
+
<path d="M5.5 9.6V20a1 1 0 0 0 1 1H10v-5h4v5h3.5a1 1 0 0 0 1-1V9.6" />
|
|
7659
|
+
</svg>
|
|
7660
|
+
<span class="tab-text">\u9996\u9875</span>
|
|
7661
|
+
</div>
|
|
7662
|
+
|
|
7663
|
+
<div class="plus-wrap" @click="onAdd">
|
|
7664
|
+
<div class="plus">
|
|
7665
|
+
<div class="bar bar-h"></div>
|
|
7666
|
+
<div class="bar bar-v"></div>
|
|
7667
|
+
</div>
|
|
7668
|
+
</div>
|
|
7669
|
+
|
|
7670
|
+
<div class="tab" :class="{ on: selectedIndex() === 1 }" @click="switchTab('/profile')">
|
|
7671
|
+
<svg
|
|
7672
|
+
class="icon"
|
|
7673
|
+
viewBox="0 0 24 24"
|
|
7674
|
+
fill="none"
|
|
7675
|
+
:stroke="selectedIndex() === 1 ? '#3B6EF6' : '#9AA3B2'"
|
|
7676
|
+
stroke-width="2"
|
|
7677
|
+
stroke-linecap="round"
|
|
7678
|
+
stroke-linejoin="round"
|
|
7679
|
+
>
|
|
7680
|
+
<circle cx="12" cy="8" r="3.6" />
|
|
7681
|
+
<path d="M4.8 20a7.2 7.2 0 0 1 14.4 0" />
|
|
7682
|
+
</svg>
|
|
7683
|
+
<span class="tab-text">\u6211\u7684</span>
|
|
7684
|
+
</div>
|
|
7685
|
+
</div>
|
|
7686
|
+
</div>
|
|
7687
|
+
</template>
|
|
7688
|
+
|
|
7689
|
+
<style scoped>
|
|
7690
|
+
.tabbar {
|
|
7691
|
+
position: absolute;
|
|
7692
|
+
left: 0;
|
|
7693
|
+
right: 0;
|
|
7694
|
+
bottom: 0;
|
|
7695
|
+
z-index: 999;
|
|
7696
|
+
background: #ffffff;
|
|
7697
|
+
box-shadow: 0 -0.04rem 0.2rem rgba(31, 36, 48, 0.06);
|
|
7698
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
7699
|
+
}
|
|
7700
|
+
|
|
7701
|
+
.tabbar-inner {
|
|
7702
|
+
height: 1rem;
|
|
7703
|
+
display: flex;
|
|
7704
|
+
align-items: center;
|
|
7705
|
+
position: relative;
|
|
7706
|
+
}
|
|
7707
|
+
|
|
7708
|
+
.tab {
|
|
7709
|
+
flex: 1;
|
|
7710
|
+
height: 100%;
|
|
7711
|
+
display: flex;
|
|
7712
|
+
flex-direction: column;
|
|
7713
|
+
align-items: center;
|
|
7714
|
+
justify-content: center;
|
|
7715
|
+
cursor: pointer;
|
|
7716
|
+
}
|
|
7717
|
+
|
|
7718
|
+
.tab-text {
|
|
7719
|
+
margin-top: 0.04rem;
|
|
7720
|
+
font-size: 0.22rem;
|
|
7721
|
+
color: #9aa3b2;
|
|
7722
|
+
line-height: 1;
|
|
7723
|
+
}
|
|
7724
|
+
|
|
7725
|
+
.tab.on .tab-text {
|
|
7726
|
+
color: #3b6ef6;
|
|
7727
|
+
font-weight: 500;
|
|
7728
|
+
}
|
|
7729
|
+
|
|
7730
|
+
.icon {
|
|
7731
|
+
width: 0.44rem;
|
|
7732
|
+
height: 0.44rem;
|
|
7733
|
+
}
|
|
7734
|
+
|
|
7735
|
+
.plus-wrap {
|
|
7736
|
+
width: 1.2rem;
|
|
7737
|
+
height: 100%;
|
|
7738
|
+
display: flex;
|
|
7739
|
+
align-items: center;
|
|
7740
|
+
justify-content: center;
|
|
7741
|
+
}
|
|
7742
|
+
|
|
7743
|
+
.plus {
|
|
7744
|
+
width: 0.92rem;
|
|
7745
|
+
height: 0.92rem;
|
|
7746
|
+
margin-top: -0.34rem;
|
|
7747
|
+
border-radius: 50%;
|
|
7748
|
+
background: linear-gradient(135deg, #3b6ef6, #6a8dff);
|
|
7749
|
+
box-shadow: 0 0.08rem 0.2rem rgba(59, 110, 246, 0.35);
|
|
7750
|
+
display: flex;
|
|
7751
|
+
align-items: center;
|
|
7752
|
+
justify-content: center;
|
|
7753
|
+
position: relative;
|
|
7754
|
+
cursor: pointer;
|
|
7755
|
+
transition: transform 0.12s ease;
|
|
7756
|
+
}
|
|
7757
|
+
|
|
7758
|
+
.plus:active {
|
|
7759
|
+
transform: scale(0.94);
|
|
7760
|
+
}
|
|
7761
|
+
|
|
7762
|
+
.bar {
|
|
7763
|
+
position: absolute;
|
|
7764
|
+
background: #ffffff;
|
|
7765
|
+
border-radius: 0.04rem;
|
|
7766
|
+
}
|
|
7767
|
+
|
|
7768
|
+
.bar-h {
|
|
7769
|
+
width: 0.36rem;
|
|
7770
|
+
height: 0.06rem;
|
|
7771
|
+
}
|
|
7772
|
+
|
|
7773
|
+
.bar-v {
|
|
7774
|
+
width: 0.06rem;
|
|
7775
|
+
height: 0.36rem;
|
|
7776
|
+
}
|
|
7777
|
+
</style>
|
|
7778
|
+
` },
|
|
7779
|
+
{ path: "web/src/ui/UiHost.vue", content: `<script setup lang="ts">
|
|
7780
|
+
// \u6D6E\u5C42\u5BBF\u4E3B\uFF1A\u6E32\u67D3 toast / modal / action sheet\uFF08App.vue \u6302\u8F7D\u4E00\u6B21\uFF09\u3002
|
|
7781
|
+
import { useUiState, settleModal, settleSheet, dismissAll } from './ui'
|
|
7782
|
+
|
|
7783
|
+
const state = useUiState()
|
|
7784
|
+
</script>
|
|
7785
|
+
|
|
7786
|
+
<template>
|
|
7787
|
+
<!-- Toast -->
|
|
7788
|
+
<Transition name="toast">
|
|
7789
|
+
<div v-if="state.toast" class="ui-toast">
|
|
7790
|
+
<svg
|
|
7791
|
+
v-if="state.toast.icon === 'success'"
|
|
7792
|
+
class="ui-toast-icon"
|
|
7793
|
+
viewBox="0 0 24 24"
|
|
7794
|
+
fill="none"
|
|
7795
|
+
stroke="currentColor"
|
|
7796
|
+
stroke-width="3"
|
|
7797
|
+
stroke-linecap="round"
|
|
7798
|
+
stroke-linejoin="round"
|
|
7799
|
+
>
|
|
7800
|
+
<path d="M20 6 9 17l-5-5" />
|
|
7801
|
+
</svg>
|
|
7802
|
+
<span>{{ state.toast.title }}</span>
|
|
7803
|
+
</div>
|
|
7804
|
+
</Transition>
|
|
7805
|
+
|
|
7806
|
+
<!-- Modal -->
|
|
7807
|
+
<Transition name="fade">
|
|
7808
|
+
<div v-if="state.modal" class="ui-mask" @click.self="settleModal(false)">
|
|
7809
|
+
<div class="ui-modal">
|
|
7810
|
+
<div class="ui-modal-title">{{ state.modal.title }}</div>
|
|
7811
|
+
<div class="ui-modal-content">{{ state.modal.content }}</div>
|
|
7812
|
+
<div class="ui-modal-actions">
|
|
7813
|
+
<button
|
|
7814
|
+
v-if="state.modal.showCancel"
|
|
7815
|
+
class="ui-modal-btn ui-modal-cancel"
|
|
7816
|
+
@click="settleModal(false)"
|
|
7817
|
+
>
|
|
7818
|
+
{{ state.modal.cancelText }}
|
|
7819
|
+
</button>
|
|
7820
|
+
<button
|
|
7821
|
+
class="ui-modal-btn ui-modal-confirm"
|
|
7822
|
+
:style="{ color: state.modal.confirmColor }"
|
|
7823
|
+
@click="settleModal(true)"
|
|
7824
|
+
>
|
|
7825
|
+
{{ state.modal.confirmText }}
|
|
7826
|
+
</button>
|
|
7827
|
+
</div>
|
|
7828
|
+
</div>
|
|
7829
|
+
</div>
|
|
7830
|
+
</Transition>
|
|
7831
|
+
|
|
7832
|
+
<!-- ActionSheet -->
|
|
7833
|
+
<Transition name="fade">
|
|
7834
|
+
<div v-if="state.sheet" class="ui-mask" @click.self="settleSheet(-1)">
|
|
7835
|
+
<div class="ui-sheet">
|
|
7836
|
+
<div class="ui-sheet-title">\u8BF7\u9009\u62E9</div>
|
|
7837
|
+
<button
|
|
7838
|
+
v-for="(item, index) in state.sheet.itemList"
|
|
7839
|
+
:key="item"
|
|
7840
|
+
class="ui-sheet-item"
|
|
7841
|
+
:style="{ color: state.sheet.itemColor }"
|
|
7842
|
+
@click="settleSheet(index)"
|
|
7843
|
+
>
|
|
7844
|
+
{{ item }}
|
|
7845
|
+
</button>
|
|
7846
|
+
<button class="ui-sheet-cancel" @click="settleSheet(-1)">\u53D6\u6D88</button>
|
|
7847
|
+
</div>
|
|
7848
|
+
</div>
|
|
7849
|
+
</Transition>
|
|
7850
|
+
</template>
|
|
7851
|
+
<style scoped>
|
|
7852
|
+
/* \u4E0E\u5C0F\u7A0B\u5E8F wx.showToast / wx.showModal / wx.showActionSheet \u89C6\u89C9\u5BF9\u9F50 */
|
|
7853
|
+
.ui-toast {
|
|
7854
|
+
position: fixed;
|
|
7855
|
+
left: 50%;
|
|
7856
|
+
top: 45%;
|
|
7857
|
+
transform: translate(-50%, -50%);
|
|
7858
|
+
z-index: 3000;
|
|
7859
|
+
max-width: 70%;
|
|
7860
|
+
padding: 0.24rem 0.36rem;
|
|
7861
|
+
border-radius: 0.16rem;
|
|
7862
|
+
background: rgba(0, 0, 0, 0.78);
|
|
7863
|
+
color: #fff;
|
|
7864
|
+
font-size: 0.28rem;
|
|
7865
|
+
line-height: 1.5;
|
|
7866
|
+
text-align: center;
|
|
7867
|
+
display: flex;
|
|
7868
|
+
flex-direction: column;
|
|
7869
|
+
align-items: center;
|
|
7870
|
+
gap: 0.1rem;
|
|
7871
|
+
pointer-events: none;
|
|
7872
|
+
}
|
|
7873
|
+
.ui-toast-icon {
|
|
7874
|
+
width: 0.4rem;
|
|
7875
|
+
height: 0.4rem;
|
|
7876
|
+
}
|
|
7877
|
+
.ui-mask {
|
|
7878
|
+
position: fixed;
|
|
7879
|
+
inset: 0;
|
|
7880
|
+
z-index: 2000;
|
|
7881
|
+
background: rgba(0, 0, 0, 0.45);
|
|
7882
|
+
display: flex;
|
|
7883
|
+
align-items: center;
|
|
7884
|
+
justify-content: center;
|
|
7885
|
+
}
|
|
7886
|
+
.ui-modal {
|
|
7887
|
+
width: 5.4rem;
|
|
7888
|
+
max-width: 82%;
|
|
7889
|
+
background: #fff;
|
|
7890
|
+
border-radius: 0.24rem;
|
|
7891
|
+
overflow: hidden;
|
|
7892
|
+
padding: 0.4rem 0.32rem 0;
|
|
7893
|
+
}
|
|
7894
|
+
.ui-modal-title {
|
|
7895
|
+
font-size: 0.34rem;
|
|
7896
|
+
font-weight: 600;
|
|
7897
|
+
color: #1f2430;
|
|
7898
|
+
text-align: center;
|
|
7899
|
+
}
|
|
7900
|
+
.ui-modal-content {
|
|
7901
|
+
margin-top: 0.16rem;
|
|
7902
|
+
font-size: 0.28rem;
|
|
7903
|
+
color: #6b7385;
|
|
7904
|
+
line-height: 1.6;
|
|
7905
|
+
text-align: center;
|
|
7906
|
+
word-break: break-word;
|
|
7907
|
+
}
|
|
7908
|
+
.ui-modal-actions {
|
|
7909
|
+
display: flex;
|
|
7910
|
+
margin: 0.32rem -0.32rem 0;
|
|
7911
|
+
border-top: 1px solid #f0f2f7;
|
|
7912
|
+
}
|
|
7913
|
+
.ui-modal-btn {
|
|
7914
|
+
flex: 1;
|
|
7915
|
+
height: 0.92rem;
|
|
7916
|
+
border: none;
|
|
7917
|
+
background: #fff;
|
|
7918
|
+
font-size: 0.3rem;
|
|
7919
|
+
cursor: pointer;
|
|
7920
|
+
}
|
|
7921
|
+
.ui-modal-btn + .ui-modal-btn {
|
|
7922
|
+
border-left: 1px solid #f0f2f7;
|
|
7923
|
+
}
|
|
7924
|
+
.ui-modal-cancel {
|
|
7925
|
+
color: #1f2430;
|
|
7926
|
+
}
|
|
7927
|
+
.ui-modal-confirm {
|
|
7928
|
+
font-weight: 600;
|
|
7929
|
+
}
|
|
7930
|
+
.ui-sheet {
|
|
7931
|
+
position: fixed;
|
|
7932
|
+
left: 0;
|
|
7933
|
+
right: 0;
|
|
7934
|
+
bottom: 0;
|
|
7935
|
+
background: #f5f7fb;
|
|
7936
|
+
padding: 0.08rem 0 calc(0.16rem + env(safe-area-inset-bottom));
|
|
7937
|
+
z-index: 2100;
|
|
7938
|
+
}
|
|
7939
|
+
.ui-sheet-title {
|
|
7940
|
+
text-align: center;
|
|
7941
|
+
font-size: 0.26rem;
|
|
7942
|
+
color: #9aa3b2;
|
|
7943
|
+
padding: 0.16rem 0 0.08rem;
|
|
7944
|
+
}
|
|
7945
|
+
.ui-sheet-item,
|
|
7946
|
+
.ui-sheet-cancel {
|
|
7947
|
+
display: block;
|
|
7948
|
+
width: auto;
|
|
7949
|
+
margin: 0.16rem 0.16rem 0;
|
|
7950
|
+
height: 0.96rem;
|
|
7951
|
+
line-height: 0.96rem;
|
|
7952
|
+
border: none;
|
|
7953
|
+
border-radius: 0.16rem;
|
|
7954
|
+
background: #fff;
|
|
7955
|
+
font-size: 0.3rem;
|
|
7956
|
+
cursor: pointer;
|
|
7957
|
+
text-align: center;
|
|
7958
|
+
}
|
|
7959
|
+
.ui-sheet-cancel {
|
|
7960
|
+
color: #1f2430;
|
|
7961
|
+
}
|
|
7962
|
+
.toast-enter-active,
|
|
7963
|
+
.toast-leave-active,
|
|
7964
|
+
.fade-enter-active,
|
|
7965
|
+
.fade-leave-active {
|
|
7966
|
+
transition: opacity 0.2s ease;
|
|
7967
|
+
}
|
|
7968
|
+
.toast-enter-from,
|
|
7969
|
+
.toast-leave-to,
|
|
7970
|
+
.fade-enter-from,
|
|
7971
|
+
.fade-leave-to {
|
|
7972
|
+
opacity: 0;
|
|
7973
|
+
}
|
|
7974
|
+
</style>
|
|
7975
|
+
` },
|
|
7976
|
+
{ path: "web/src/ui/ui.ts", content: "/**\n * \u8F7B\u91CF UI \u539F\u8BED\uFF08web \u7248 wx.showToast / wx.showModal / wx.showActionSheet\uFF09\u3002\n * \u54CD\u5E94\u5F0F store + App.vue \u91CC\u6302\u4E00\u4E2A UiHost \u7EC4\u4EF6\u6E32\u67D3\u6D6E\u5C42\u3002\n */\nimport { reactive } from 'vue'\n\nexport interface ToastOptions {\n title: string\n /** 'none' = \u65E0\u56FE\u6807\uFF1B'success' = \u5BF9\u52FE\u3002 */\n icon?: 'none' | 'success'\n duration?: number\n}\n\nexport interface ModalOptions {\n title: string\n content: string\n confirmColor?: string\n confirmText?: string\n cancelText?: string\n showCancel?: boolean\n}\n\nlet seq = 0\n\ninterface ToastState {\n id: number\n title: string\n icon: 'none' | 'success'\n}\ninterface ModalState {\n id: number\n title: string\n content: string\n confirmColor: string\n confirmText: string\n cancelText: string\n showCancel: boolean\n resolve: (ok: boolean) => void\n}\ninterface SheetState {\n id: number\n itemList: string[]\n itemColor: string\n resolve: (index: number) => void\n}\n\nconst state = reactive<{\n toast: ToastState | null\n modal: ModalState | null\n sheet: SheetState | null\n}>({\n toast: null,\n modal: null,\n sheet: null,\n})\n\nexport function useUiState() {\n return state\n}\n\n/** \u7C7B wx.showToast\uFF1Bduration \u540E\u81EA\u52A8\u6D88\u5931\u3002 */\nexport function showToast(options: ToastOptions | string): void {\n const opts = typeof options === 'string' ? { title: options } : options\n const id = ++seq\n state.toast = { id, title: opts.title, icon: opts.icon ?? 'none' }\n const duration = opts.duration ?? 2000\n if (duration > 0) {\n setTimeout(() => {\n if (state.toast?.id === id) state.toast = null\n }, duration)\n }\n}\n\n/** \u7C7B wx.showModal\uFF1B\u8FD4\u56DE Promise<boolean>\uFF08confirm \u2192 true\uFF09\u3002 */\nexport function showModal(options: ModalOptions): Promise<boolean> {\n return new Promise((resolve) => {\n state.modal = {\n id: ++seq,\n title: options.title,\n content: options.content,\n confirmColor: options.confirmColor ?? '#3B6EF6',\n confirmText: options.confirmText ?? '\u786E\u5B9A',\n cancelText: options.cancelText ?? '\u53D6\u6D88',\n showCancel: options.showCancel ?? true,\n resolve,\n }\n })\n}\n\n/** \u5173\u95ED\u5F53\u524D modal \u5E76\u56DE\u8C03\u7ED3\u679C\u3002 */\nexport function settleModal(ok: boolean): void {\n const modal = state.modal\n state.modal = null\n modal?.resolve(ok)\n}\n\n/** \u7C7B wx.showActionSheet\uFF1B\u8FD4\u56DE Promise<number>\uFF08\u70B9\u4E2D\u9879\u7684\u4E0B\u6807\uFF09\u3002 */\nexport function showActionSheet(options: {\n itemList: string[]\n itemColor?: string\n}): Promise<number> {\n return new Promise((resolve) => {\n state.sheet = {\n id: ++seq,\n itemList: options.itemList,\n itemColor: options.itemColor ?? '#1F2430',\n resolve,\n }\n })\n}\n\n/** \u5173\u95ED\u5F53\u524D action sheet \u5E76\u56DE\u8C03\u4E0B\u6807\uFF08-1 = \u672A\u9009\u4E2D\uFF09\u3002 */\nexport function settleSheet(index: number): void {\n const sheet = state.sheet\n state.sheet = null\n sheet?.resolve(index)\n}\n\nexport function dismissAll(): void {\n settleModal(false)\n settleSheet(-1)\n state.toast = null\n}\n" },
|
|
7977
|
+
{ path: "web/src/views/EditView.vue", content: `<script setup lang="ts">
|
|
7978
|
+
// \u6DFB\u52A0 / \u7F16\u8F91\u4E8B\u4EF6\uFF08\u79FB\u690D\u81EA pages/edit/*\uFF09\u3002
|
|
7979
|
+
// \u5B9E\u65F6\u5929\u6570\u9884\u89C8 + \u8868\u5355\uFF08\u540D\u79F0/\u65E5\u671F/\u5206\u7C7B/\u8BA1\u65F6\u65B9\u5F0F/\u5907\u6CE8/\u7F6E\u9876\uFF09+ \u4FDD\u5B58/\u5220\u9664\u3002
|
|
7980
|
+
import { computed, reactive, ref } from 'vue'
|
|
7981
|
+
import { useRoute, useRouter } from 'vue-router'
|
|
7982
|
+
import { request } from '../lib/request'
|
|
7983
|
+
import { ensureLogin } from '../lib/auth'
|
|
7984
|
+
import { calcDays, daysText, levelOf, isValidDate, todayStr, weekdayOf } from '../lib/date'
|
|
7985
|
+
import { CATEGORIES, CATEGORY_LABEL } from '../lib/config'
|
|
7986
|
+
import { showToast, showModal } from '../ui/ui'
|
|
7987
|
+
|
|
7988
|
+
const route = useRoute()
|
|
7989
|
+
const router = useRouter()
|
|
7990
|
+
|
|
7991
|
+
const id = typeof route.params.id === 'string' ? route.params.id : ''
|
|
7992
|
+
const isEdit = !!id
|
|
7993
|
+
|
|
7994
|
+
const form = reactive({
|
|
7995
|
+
title: '',
|
|
7996
|
+
target_date: todayStr(),
|
|
7997
|
+
note: '',
|
|
7998
|
+
category: 'other',
|
|
7999
|
+
direction: 'countdown' as 'countdown' | 'countup',
|
|
8000
|
+
is_pinned: false,
|
|
8001
|
+
})
|
|
8002
|
+
|
|
8003
|
+
const categoryIndex = ref(0)
|
|
8004
|
+
const dateStart = '1900-01-01'
|
|
8005
|
+
const dateEnd = '2100-12-31'
|
|
8006
|
+
const submitting = ref(false)
|
|
8007
|
+
const deleting = ref(false)
|
|
8008
|
+
|
|
8009
|
+
const preview = computed(() => {
|
|
8010
|
+
if (!isValidDate(form.target_date)) return null
|
|
8011
|
+
const days = calcDays(form.target_date, form.direction)
|
|
8012
|
+
const t = daysText(days, form.direction)
|
|
8013
|
+
return {
|
|
8014
|
+
level: levelOf(days, form.direction),
|
|
8015
|
+
text: \`\${t.prefix}\${t.num}\${t.suffix}\`,
|
|
8016
|
+
sub: \`\${form.target_date} \${weekdayOf(form.target_date)} \xB7 \${CATEGORY_LABEL[form.category] ?? '\u65E5\u5E38'}\`,
|
|
8017
|
+
}
|
|
8018
|
+
})
|
|
8019
|
+
|
|
8020
|
+
async function loadEvent(): Promise<void> {
|
|
8021
|
+
if (!id) return
|
|
8022
|
+
try {
|
|
8023
|
+
await ensureLogin()
|
|
8024
|
+
const res = await request<{
|
|
8025
|
+
event: {
|
|
8026
|
+
title: string
|
|
8027
|
+
target_date: string
|
|
8028
|
+
note: string
|
|
8029
|
+
category: string
|
|
8030
|
+
direction: 'countdown' | 'countup'
|
|
8031
|
+
is_pinned: boolean
|
|
8032
|
+
}
|
|
8033
|
+
}>(\`/api/events/\${id}\`)
|
|
8034
|
+
const e = res.event
|
|
8035
|
+
const idx = Math.max(
|
|
8036
|
+
0,
|
|
8037
|
+
CATEGORIES.findIndex((c) => c.value === e.category)
|
|
8038
|
+
)
|
|
8039
|
+
form.title = e.title
|
|
8040
|
+
form.target_date = e.target_date
|
|
8041
|
+
form.note = e.note || ''
|
|
8042
|
+
form.category = e.category || 'other'
|
|
8043
|
+
form.direction = e.direction || 'countdown'
|
|
8044
|
+
form.is_pinned = !!e.is_pinned
|
|
8045
|
+
categoryIndex.value = idx
|
|
8046
|
+
} catch (err) {
|
|
8047
|
+
showToast({ title: err instanceof Error ? err.message : '\u52A0\u8F7D\u5931\u8D25', icon: 'none' })
|
|
8048
|
+
setTimeout(() => router.back(), 800)
|
|
8049
|
+
}
|
|
8050
|
+
}
|
|
8051
|
+
|
|
8052
|
+
if (id) void loadEvent()
|
|
8053
|
+
|
|
8054
|
+
async function onSave(): Promise<void> {
|
|
8055
|
+
if (submitting.value) return
|
|
8056
|
+
const title = form.title.trim()
|
|
8057
|
+
|
|
8058
|
+
if (!title) {
|
|
8059
|
+
showToast({ title: '\u8BF7\u586B\u5199\u4E8B\u4EF6\u540D\u79F0', icon: 'none' })
|
|
8060
|
+
return
|
|
8061
|
+
}
|
|
8062
|
+
if (title.length > 50) {
|
|
8063
|
+
showToast({ title: '\u540D\u79F0\u4E0D\u80FD\u8D85\u8FC7 50 \u4E2A\u5B57', icon: 'none' })
|
|
8064
|
+
return
|
|
8065
|
+
}
|
|
8066
|
+
if (!isValidDate(form.target_date)) {
|
|
8067
|
+
showToast({ title: '\u8BF7\u9009\u62E9\u76EE\u6807\u65E5\u671F', icon: 'none' })
|
|
8068
|
+
return
|
|
8069
|
+
}
|
|
8070
|
+
if (form.note.length > 200) {
|
|
8071
|
+
showToast({ title: '\u5907\u6CE8\u4E0D\u80FD\u8D85\u8FC7 200 \u4E2A\u5B57', icon: 'none' })
|
|
8072
|
+
return
|
|
8073
|
+
}
|
|
8074
|
+
|
|
8075
|
+
const payload = {
|
|
8076
|
+
title,
|
|
8077
|
+
target_date: form.target_date,
|
|
8078
|
+
note: form.note.trim(),
|
|
8079
|
+
category: form.category,
|
|
8080
|
+
direction: form.direction,
|
|
8081
|
+
is_pinned: !!form.is_pinned,
|
|
8082
|
+
}
|
|
8083
|
+
|
|
8084
|
+
submitting.value = true
|
|
8085
|
+
try {
|
|
8086
|
+
await ensureLogin()
|
|
8087
|
+
if (isEdit) {
|
|
8088
|
+
await request(\`/api/events/\${id}\`, { method: 'PUT', data: payload })
|
|
8089
|
+
} else {
|
|
8090
|
+
await request('/api/events', { method: 'POST', data: payload })
|
|
8091
|
+
}
|
|
8092
|
+
showToast({ title: isEdit ? '\u5DF2\u4FDD\u5B58' : '\u5DF2\u6DFB\u52A0', icon: 'success' })
|
|
8093
|
+
setTimeout(() => router.back(), 600)
|
|
8094
|
+
} catch (err) {
|
|
8095
|
+
submitting.value = false
|
|
8096
|
+
showToast({ title: err instanceof Error ? err.message : '\u4FDD\u5B58\u5931\u8D25', icon: 'none' })
|
|
8097
|
+
}
|
|
8098
|
+
}
|
|
8099
|
+
|
|
8100
|
+
async function onDelete(): Promise<void> {
|
|
8101
|
+
if (!id || deleting.value) return
|
|
8102
|
+
const ok = await showModal({
|
|
8103
|
+
title: '\u5220\u9664\u4E8B\u4EF6',
|
|
8104
|
+
content: \`\u786E\u5B9A\u5220\u9664\u300C\${form.title}\u300D\u5417\uFF1F\u5220\u9664\u540E\u4E0D\u53EF\u6062\u590D\u3002\`,
|
|
8105
|
+
confirmColor: '#FF5A5F',
|
|
8106
|
+
confirmText: '\u5220\u9664',
|
|
8107
|
+
})
|
|
8108
|
+
if (!ok) return
|
|
8109
|
+
deleting.value = true
|
|
8110
|
+
try {
|
|
8111
|
+
await request(\`/api/events/\${id}\`, { method: 'DELETE' })
|
|
8112
|
+
showToast({ title: '\u5DF2\u5220\u9664', icon: 'success' })
|
|
8113
|
+
setTimeout(() => router.back(), 600)
|
|
8114
|
+
} catch (err) {
|
|
8115
|
+
deleting.value = false
|
|
8116
|
+
showToast({ title: err instanceof Error ? err.message : '\u5220\u9664\u5931\u8D25', icon: 'none' })
|
|
8117
|
+
}
|
|
8118
|
+
}
|
|
8119
|
+
|
|
8120
|
+
function goBack(): void {
|
|
8121
|
+
router.back()
|
|
8122
|
+
}
|
|
8123
|
+
</script>
|
|
8124
|
+
|
|
8125
|
+
<template>
|
|
8126
|
+
<div class="page">
|
|
8127
|
+
<!-- \u9876\u90E8\u5BFC\u822A\uFF08web \u7248\u539F\u751F\u5BFC\u822A\u680F\uFF09 -->
|
|
8128
|
+
<header class="nav">
|
|
8129
|
+
<button class="nav-back" @click="goBack">\u2039</button>
|
|
8130
|
+
<span class="nav-title">{{ isEdit ? '\u7F16\u8F91\u4E8B\u4EF6' : '\u6DFB\u52A0\u4E8B\u4EF6' }}</span>
|
|
8131
|
+
<span class="nav-side"></span>
|
|
8132
|
+
</header>
|
|
8133
|
+
|
|
8134
|
+
<div class="page-scroll">
|
|
8135
|
+
<!-- \u5B9E\u65F6\u9884\u89C8 -->
|
|
8136
|
+
<div v-if="preview" class="preview" :class="'level-' + preview.level">
|
|
8137
|
+
<div class="pv-text">{{ preview.text }}</div>
|
|
8138
|
+
<div class="pv-sub">{{ preview.sub }}</div>
|
|
8139
|
+
</div>
|
|
8140
|
+
|
|
8141
|
+
<div class="form">
|
|
8142
|
+
<div class="group">
|
|
8143
|
+
<div class="label">\u4E8B\u4EF6\u540D\u79F0</div>
|
|
8144
|
+
<input class="input" v-model="form.title" placeholder="\u4F8B\u5982\uFF1A\u5C0F\u660E\u7684\u751F\u65E5" maxlength="50" />
|
|
8145
|
+
</div>
|
|
8146
|
+
|
|
8147
|
+
<div class="group">
|
|
8148
|
+
<div class="label">\u76EE\u6807\u65E5\u671F</div>
|
|
8149
|
+
<input
|
|
8150
|
+
class="input picker-input"
|
|
8151
|
+
type="date"
|
|
8152
|
+
v-model="form.target_date"
|
|
8153
|
+
:min="dateStart"
|
|
8154
|
+
:max="dateEnd"
|
|
8155
|
+
/>
|
|
8156
|
+
</div>
|
|
8157
|
+
|
|
8158
|
+
<div class="group">
|
|
8159
|
+
<div class="label">\u5206\u7C7B</div>
|
|
8160
|
+
<div class="picker">
|
|
8161
|
+
<select v-model="form.category" class="picker-select">
|
|
8162
|
+
<option v-for="c in CATEGORIES" :key="c.value" :value="c.value">{{ c.label }}</option>
|
|
8163
|
+
</select>
|
|
8164
|
+
<span class="arrow">\u203A</span>
|
|
8165
|
+
</div>
|
|
8166
|
+
</div>
|
|
8167
|
+
|
|
8168
|
+
<div class="group">
|
|
8169
|
+
<div class="label">\u8BA1\u65F6\u65B9\u5F0F</div>
|
|
8170
|
+
<div class="radio-group">
|
|
8171
|
+
<label
|
|
8172
|
+
class="radio"
|
|
8173
|
+
:class="{ on: form.direction === 'countdown' }"
|
|
8174
|
+
@click="form.direction = 'countdown'"
|
|
8175
|
+
>
|
|
8176
|
+
<span class="radio-dot" :class="{ checked: form.direction === 'countdown' }"></span>
|
|
8177
|
+
<span>\u5012\u6570\u65E5</span>
|
|
8178
|
+
</label>
|
|
8179
|
+
<label
|
|
8180
|
+
class="radio"
|
|
8181
|
+
:class="{ on: form.direction === 'countup' }"
|
|
8182
|
+
@click="form.direction = 'countup'"
|
|
8183
|
+
>
|
|
8184
|
+
<span class="radio-dot" :class="{ checked: form.direction === 'countup' }"></span>
|
|
8185
|
+
<span>\u6B63\u6570\u65E5</span>
|
|
8186
|
+
</label>
|
|
8187
|
+
</div>
|
|
8188
|
+
<div class="hint">\u5012\u6570\u65E5 = \u8DDD\u79BB\u76EE\u6807\u8FD8\u6709\u591A\u5C11\u5929\uFF1B\u6B63\u6570\u65E5 = \u4ECE\u90A3\u5929\u8D77\u5DF2\u7ECF\u8FC7\u4E86\u591A\u5C11\u5929</div>
|
|
8189
|
+
</div>
|
|
8190
|
+
|
|
8191
|
+
<div class="group">
|
|
8192
|
+
<div class="label">\u5907\u6CE8 <span class="opt">\u9009\u586B</span></div>
|
|
8193
|
+
<textarea
|
|
8194
|
+
class="textarea"
|
|
8195
|
+
v-model="form.note"
|
|
8196
|
+
placeholder="\u5199\u70B9\u4EC0\u4E48\u2026"
|
|
8197
|
+
maxlength="200"
|
|
8198
|
+
rows="3"
|
|
8199
|
+
/>
|
|
8200
|
+
<div class="counter">{{ form.note.length }}/200</div>
|
|
8201
|
+
</div>
|
|
8202
|
+
|
|
8203
|
+
<div class="group group-row">
|
|
8204
|
+
<div>
|
|
8205
|
+
<div class="label">\u7F6E\u9876\u663E\u793A</div>
|
|
8206
|
+
<div class="hint">\u7F6E\u9876\u7684\u4E8B\u4EF6\u6392\u5728\u5217\u8868\u6700\u524D\u9762</div>
|
|
8207
|
+
</div>
|
|
8208
|
+
<button
|
|
8209
|
+
class="switch"
|
|
8210
|
+
:class="{ on: form.is_pinned }"
|
|
8211
|
+
@click="form.is_pinned = !form.is_pinned"
|
|
8212
|
+
>
|
|
8213
|
+
<span class="switch-knob"></span>
|
|
8214
|
+
</button>
|
|
8215
|
+
</div>
|
|
8216
|
+
</div>
|
|
8217
|
+
|
|
8218
|
+
<div class="footer">
|
|
8219
|
+
<button class="btn btn-save" :disabled="submitting" @click="onSave">
|
|
8220
|
+
{{ submitting ? '\u4FDD\u5B58\u4E2D\u2026' : isEdit ? '\u4FDD\u5B58\u4FEE\u6539' : '\u521B\u5EFA\u4E8B\u4EF6' }}
|
|
8221
|
+
</button>
|
|
8222
|
+
<button v-if="isEdit" class="btn btn-del" :disabled="deleting" @click="onDelete">
|
|
8223
|
+
{{ deleting ? '\u5220\u9664\u4E2D\u2026' : '\u5220\u9664\u4E8B\u4EF6' }}
|
|
8224
|
+
</button>
|
|
8225
|
+
</div>
|
|
8226
|
+
</div>
|
|
8227
|
+
</div>
|
|
8228
|
+
</template>
|
|
8229
|
+
|
|
8230
|
+
<style scoped>
|
|
8231
|
+
.page {
|
|
8232
|
+
height: 100%;
|
|
8233
|
+
padding-bottom: calc(0.6rem + env(safe-area-inset-bottom));
|
|
8234
|
+
}
|
|
8235
|
+
|
|
8236
|
+
.nav {
|
|
8237
|
+
position: absolute;
|
|
8238
|
+
top: 0;
|
|
8239
|
+
left: 0;
|
|
8240
|
+
right: 0;
|
|
8241
|
+
z-index: 10;
|
|
8242
|
+
height: 0.88rem;
|
|
8243
|
+
display: flex;
|
|
8244
|
+
align-items: center;
|
|
8245
|
+
justify-content: space-between;
|
|
8246
|
+
background: linear-gradient(135deg, #3b6ef6, #6a8dff);
|
|
8247
|
+
padding: 0 0.2rem;
|
|
8248
|
+
}
|
|
8249
|
+
|
|
8250
|
+
.nav-back {
|
|
8251
|
+
width: 0.72rem;
|
|
8252
|
+
height: 100%;
|
|
8253
|
+
color: #ffffff;
|
|
8254
|
+
font-size: 0.56rem;
|
|
8255
|
+
line-height: 1;
|
|
8256
|
+
cursor: pointer;
|
|
8257
|
+
display: flex;
|
|
8258
|
+
align-items: center;
|
|
8259
|
+
}
|
|
8260
|
+
|
|
8261
|
+
.nav-title {
|
|
8262
|
+
color: #ffffff;
|
|
8263
|
+
font-size: 0.34rem;
|
|
8264
|
+
font-weight: 600;
|
|
8265
|
+
}
|
|
8266
|
+
|
|
8267
|
+
.nav-side {
|
|
8268
|
+
width: 0.72rem;
|
|
8269
|
+
}
|
|
8270
|
+
|
|
8271
|
+
.page-scroll {
|
|
8272
|
+
padding-top: 0.88rem;
|
|
8273
|
+
}
|
|
8274
|
+
|
|
8275
|
+
/* ---------- \u9884\u89C8 ---------- */
|
|
8276
|
+
.preview {
|
|
8277
|
+
margin: 0.24rem 0.32rem 0.08rem;
|
|
8278
|
+
padding: 0.36rem 0.4rem;
|
|
8279
|
+
border-radius: 0.24rem;
|
|
8280
|
+
background: linear-gradient(135deg, #3b6ef6, #6a8dff);
|
|
8281
|
+
box-shadow: 0 0.12rem 0.28rem rgba(59, 110, 246, 0.24);
|
|
8282
|
+
text-align: center;
|
|
8283
|
+
}
|
|
8284
|
+
|
|
8285
|
+
.preview.level-past {
|
|
8286
|
+
background: linear-gradient(135deg, #a0a8b8, #b9c0cc);
|
|
8287
|
+
box-shadow: 0 0.12rem 0.28rem rgba(160, 168, 184, 0.24);
|
|
8288
|
+
}
|
|
8289
|
+
|
|
8290
|
+
.pv-text {
|
|
8291
|
+
display: block;
|
|
8292
|
+
color: #ffffff;
|
|
8293
|
+
font-size: 0.56rem;
|
|
8294
|
+
font-weight: 700;
|
|
8295
|
+
line-height: 1.2;
|
|
8296
|
+
}
|
|
8297
|
+
|
|
8298
|
+
.pv-sub {
|
|
8299
|
+
display: block;
|
|
8300
|
+
margin-top: 0.12rem;
|
|
8301
|
+
color: rgba(255, 255, 255, 0.85);
|
|
8302
|
+
font-size: 0.24rem;
|
|
8303
|
+
}
|
|
8304
|
+
|
|
8305
|
+
/* ---------- \u8868\u5355 ---------- */
|
|
8306
|
+
.form {
|
|
8307
|
+
margin: 0.24rem 0.32rem 0;
|
|
8308
|
+
background: #ffffff;
|
|
8309
|
+
border-radius: 0.24rem;
|
|
8310
|
+
padding: 0.08rem 0.32rem;
|
|
8311
|
+
box-shadow: 0 0.08rem 0.24rem rgba(31, 36, 48, 0.05);
|
|
8312
|
+
}
|
|
8313
|
+
|
|
8314
|
+
.group {
|
|
8315
|
+
padding: 0.28rem 0;
|
|
8316
|
+
border-bottom: 1px solid #f0f2f7;
|
|
8317
|
+
}
|
|
8318
|
+
|
|
8319
|
+
.group:last-child {
|
|
8320
|
+
border-bottom: none;
|
|
8321
|
+
}
|
|
8322
|
+
|
|
8323
|
+
.group-row {
|
|
8324
|
+
display: flex;
|
|
8325
|
+
align-items: center;
|
|
8326
|
+
justify-content: space-between;
|
|
8327
|
+
}
|
|
8328
|
+
|
|
8329
|
+
.label {
|
|
8330
|
+
font-size: 0.28rem;
|
|
8331
|
+
color: #1f2430;
|
|
8332
|
+
font-weight: 500;
|
|
8333
|
+
margin-bottom: 0.16rem;
|
|
8334
|
+
}
|
|
8335
|
+
|
|
8336
|
+
.group-row .label {
|
|
8337
|
+
margin-bottom: 0.08rem;
|
|
8338
|
+
}
|
|
8339
|
+
|
|
8340
|
+
.opt {
|
|
8341
|
+
font-size: 0.22rem;
|
|
8342
|
+
color: #b4bac6;
|
|
8343
|
+
font-weight: 400;
|
|
8344
|
+
}
|
|
8345
|
+
|
|
8346
|
+
.input,
|
|
8347
|
+
.textarea {
|
|
8348
|
+
width: 100%;
|
|
8349
|
+
font-size: 0.3rem;
|
|
8350
|
+
color: #1f2430;
|
|
8351
|
+
}
|
|
8352
|
+
|
|
8353
|
+
.input::placeholder,
|
|
8354
|
+
.textarea::placeholder {
|
|
8355
|
+
color: #b4bac6;
|
|
8356
|
+
}
|
|
8357
|
+
|
|
8358
|
+
.textarea {
|
|
8359
|
+
min-height: 0.8rem;
|
|
8360
|
+
line-height: 1.6;
|
|
8361
|
+
resize: none;
|
|
8362
|
+
}
|
|
8363
|
+
|
|
8364
|
+
.picker-input {
|
|
8365
|
+
cursor: pointer;
|
|
8366
|
+
}
|
|
8367
|
+
|
|
8368
|
+
.picker {
|
|
8369
|
+
display: flex;
|
|
8370
|
+
align-items: center;
|
|
8371
|
+
justify-content: space-between;
|
|
8372
|
+
font-size: 0.3rem;
|
|
8373
|
+
color: #1f2430;
|
|
8374
|
+
position: relative;
|
|
8375
|
+
}
|
|
8376
|
+
|
|
8377
|
+
.picker-select {
|
|
8378
|
+
width: 100%;
|
|
8379
|
+
font-size: 0.3rem;
|
|
8380
|
+
color: #1f2430;
|
|
8381
|
+
appearance: none;
|
|
8382
|
+
-webkit-appearance: none;
|
|
8383
|
+
background: transparent;
|
|
8384
|
+
cursor: pointer;
|
|
8385
|
+
padding: 0.06rem 0;
|
|
8386
|
+
}
|
|
8387
|
+
|
|
8388
|
+
.arrow {
|
|
8389
|
+
color: #c4c9d4;
|
|
8390
|
+
font-size: 0.36rem;
|
|
8391
|
+
pointer-events: none;
|
|
8392
|
+
position: absolute;
|
|
8393
|
+
right: 0;
|
|
8394
|
+
}
|
|
8395
|
+
|
|
8396
|
+
.radio-group {
|
|
8397
|
+
display: flex;
|
|
8398
|
+
}
|
|
8399
|
+
|
|
8400
|
+
.radio {
|
|
8401
|
+
display: flex;
|
|
8402
|
+
align-items: center;
|
|
8403
|
+
margin-right: 0.48rem;
|
|
8404
|
+
padding: 0.12rem 0.24rem;
|
|
8405
|
+
border-radius: 999rem;
|
|
8406
|
+
background: #f5f7fb;
|
|
8407
|
+
font-size: 0.28rem;
|
|
8408
|
+
color: #6b7385;
|
|
8409
|
+
cursor: pointer;
|
|
8410
|
+
user-select: none;
|
|
8411
|
+
}
|
|
8412
|
+
|
|
8413
|
+
.radio.on {
|
|
8414
|
+
background: rgba(59, 110, 246, 0.1);
|
|
8415
|
+
color: #3b6ef6;
|
|
8416
|
+
}
|
|
8417
|
+
|
|
8418
|
+
.radio-dot {
|
|
8419
|
+
width: 0.32rem;
|
|
8420
|
+
height: 0.32rem;
|
|
8421
|
+
border-radius: 50%;
|
|
8422
|
+
border: 2px solid #c4c9d4;
|
|
8423
|
+
margin-right: 0.08rem;
|
|
8424
|
+
display: inline-flex;
|
|
8425
|
+
align-items: center;
|
|
8426
|
+
justify-content: center;
|
|
8427
|
+
}
|
|
8428
|
+
|
|
8429
|
+
.radio-dot.checked {
|
|
8430
|
+
border-color: #3b6ef6;
|
|
8431
|
+
}
|
|
8432
|
+
|
|
8433
|
+
.radio-dot.checked::after {
|
|
8434
|
+
content: '';
|
|
8435
|
+
width: 0.14rem;
|
|
8436
|
+
height: 0.14rem;
|
|
8437
|
+
border-radius: 50%;
|
|
8438
|
+
background: #3b6ef6;
|
|
8439
|
+
}
|
|
8440
|
+
|
|
8441
|
+
.hint {
|
|
8442
|
+
margin-top: 0.14rem;
|
|
8443
|
+
font-size: 0.22rem;
|
|
8444
|
+
color: #b4bac6;
|
|
8445
|
+
line-height: 1.5;
|
|
8446
|
+
}
|
|
8447
|
+
|
|
8448
|
+
.counter {
|
|
8449
|
+
margin-top: 0.08rem;
|
|
8450
|
+
text-align: right;
|
|
8451
|
+
font-size: 0.22rem;
|
|
8452
|
+
color: #c4c9d4;
|
|
8453
|
+
}
|
|
8454
|
+
|
|
8455
|
+
/* \u5F00\u5173\uFF08\u79FB\u690D wx switch\uFF09 */
|
|
8456
|
+
.switch {
|
|
8457
|
+
width: 0.96rem;
|
|
8458
|
+
height: 0.56rem;
|
|
8459
|
+
border-radius: 999rem;
|
|
8460
|
+
background: #e2e4ea;
|
|
8461
|
+
position: relative;
|
|
8462
|
+
cursor: pointer;
|
|
8463
|
+
transition: background 0.2s ease;
|
|
8464
|
+
flex-shrink: 0;
|
|
8465
|
+
}
|
|
8466
|
+
|
|
8467
|
+
.switch.on {
|
|
8468
|
+
background: #3b6ef6;
|
|
8469
|
+
}
|
|
8470
|
+
|
|
8471
|
+
.switch-knob {
|
|
8472
|
+
position: absolute;
|
|
8473
|
+
top: 0.04rem;
|
|
8474
|
+
left: 0.04rem;
|
|
8475
|
+
width: 0.48rem;
|
|
8476
|
+
height: 0.48rem;
|
|
8477
|
+
border-radius: 50%;
|
|
8478
|
+
background: #ffffff;
|
|
8479
|
+
box-shadow: 0 0.02rem 0.06rem rgba(0, 0, 0, 0.18);
|
|
8480
|
+
transition: transform 0.2s ease;
|
|
8481
|
+
}
|
|
8482
|
+
|
|
8483
|
+
.switch.on .switch-knob {
|
|
8484
|
+
transform: translateX(0.4rem);
|
|
8485
|
+
}
|
|
8486
|
+
|
|
8487
|
+
/* ---------- \u5E95\u90E8\u6309\u94AE ---------- */
|
|
8488
|
+
.footer {
|
|
8489
|
+
padding: 0.48rem 0.32rem 0;
|
|
8490
|
+
}
|
|
8491
|
+
|
|
8492
|
+
.btn {
|
|
8493
|
+
display: flex;
|
|
8494
|
+
align-items: center;
|
|
8495
|
+
justify-content: center;
|
|
8496
|
+
width: 100%;
|
|
8497
|
+
height: 0.92rem;
|
|
8498
|
+
border-radius: 999rem;
|
|
8499
|
+
font-size: 0.3rem;
|
|
8500
|
+
cursor: pointer;
|
|
8501
|
+
}
|
|
8502
|
+
|
|
8503
|
+
.btn-save {
|
|
8504
|
+
background: linear-gradient(135deg, #3b6ef6, #6a8dff);
|
|
8505
|
+
color: #ffffff;
|
|
8506
|
+
box-shadow: 0 0.1rem 0.24rem rgba(59, 110, 246, 0.28);
|
|
8507
|
+
}
|
|
8508
|
+
|
|
8509
|
+
.btn-save:disabled {
|
|
8510
|
+
opacity: 0.7;
|
|
8511
|
+
}
|
|
8512
|
+
|
|
8513
|
+
.btn-del {
|
|
8514
|
+
margin-top: 0.24rem;
|
|
8515
|
+
background: #ffffff;
|
|
8516
|
+
color: #ff5a5f;
|
|
8517
|
+
border: 1px solid #ffd9da;
|
|
8518
|
+
}
|
|
8519
|
+
|
|
8520
|
+
.btn-del:disabled {
|
|
8521
|
+
opacity: 0.7;
|
|
8522
|
+
}
|
|
8523
|
+
</style>
|
|
8524
|
+
` },
|
|
8525
|
+
{ path: "web/src/views/HomeView.vue", content: `<script setup lang="ts">
|
|
8526
|
+
// \u9996\u9875\uFF1A\u4E8B\u4EF6\u5217\u8868\uFF08\u79FB\u690D\u81EA pages/index/*\uFF09\u3002
|
|
8527
|
+
// \u6E10\u53D8\u5934\u90E8 + \u603B\u6570/\u4E0B\u4E00\u4E2A + \u5206\u7C7B\u7B5B\u9009 + \u5DE6\u6ED1\u7F6E\u9876/\u5220\u9664 + \u957F\u6309\u83DC\u5355 + \u4E0B\u62C9\u5237\u65B0 + \u672C\u5730\u7F13\u5B58\u79D2\u5F00\u3002
|
|
8528
|
+
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|
8529
|
+
import { useRouter } from 'vue-router'
|
|
8530
|
+
import { request } from '../lib/request'
|
|
8531
|
+
import { ensureLogin } from '../lib/auth'
|
|
8532
|
+
import { decorateEvent, type CountdownEvent, type DecodedEvent } from '../lib/date'
|
|
8533
|
+
import { CATEGORIES } from '../lib/config'
|
|
8534
|
+
import { showToast, showModal, showActionSheet } from '../ui/ui'
|
|
8535
|
+
|
|
8536
|
+
const CACHE_KEY = 'events_cache'
|
|
8537
|
+
const ACTION_RPX = 160 // \u5DE6\u6ED1\u9732\u51FA\u7684\u64CD\u4F5C\u533A\u5BBD\u5EA6\uFF08rpx\uFF09\uFF0C\u8FD0\u884C\u65F6\u6362\u7B97\u6210 px
|
|
8538
|
+
|
|
8539
|
+
const router = useRouter()
|
|
8540
|
+
|
|
8541
|
+
const scrollEl = ref<HTMLElement | null>(null)
|
|
8542
|
+
|
|
8543
|
+
const data = reactive({
|
|
8544
|
+
loaded: false,
|
|
8545
|
+
list: [] as DecodedEvent[],
|
|
8546
|
+
allList: [] as DecodedEvent[],
|
|
8547
|
+
filters: [{ value: '', label: '\u5168\u90E8' }].concat(CATEGORIES),
|
|
8548
|
+
activeFilter: '',
|
|
8549
|
+
total: 0,
|
|
8550
|
+
nextUp: null as null | { title: string; num: string; prefix: string; suffix: string },
|
|
8551
|
+
openIndex: -1,
|
|
8552
|
+
})
|
|
8553
|
+
|
|
8554
|
+
const actionWidth = ref(1.6 * 64) // 1.6rem * \u9ED8\u8BA4 64px\uFF1B\u8FD0\u884C\u65F6\u6309\u5B9E\u9645 font-size \u6821\u6B63
|
|
8555
|
+
|
|
8556
|
+
function applyList(raw: CountdownEvent[]): void {
|
|
8557
|
+
const all = raw.map(decorateEvent)
|
|
8558
|
+
const next = all.find((e) => e.direction !== 'countup' && e.days >= 0)
|
|
8559
|
+
const list = data.activeFilter ? all.filter((e) => e.category === data.activeFilter) : all
|
|
8560
|
+
data.allList = all
|
|
8561
|
+
data.list = list
|
|
8562
|
+
data.total = all.length
|
|
8563
|
+
data.openIndex = -1
|
|
8564
|
+
data.nextUp = next
|
|
8565
|
+
? { title: next.title, num: next.num, prefix: next.prefix || '', suffix: next.suffix || '' }
|
|
8566
|
+
: null
|
|
8567
|
+
}
|
|
8568
|
+
|
|
8569
|
+
async function loadEvents(force = false): Promise<void> {
|
|
8570
|
+
if (!force && data.loaded) {
|
|
8571
|
+
try {
|
|
8572
|
+
const cached = localStorage.getItem(CACHE_KEY)
|
|
8573
|
+
if (cached) {
|
|
8574
|
+
const parsed = JSON.parse(cached) as CountdownEvent[]
|
|
8575
|
+
if (parsed.length) applyList(parsed)
|
|
8576
|
+
}
|
|
8577
|
+
} catch {
|
|
8578
|
+
/* \u7F13\u5B58\u635F\u574F\u5FFD\u7565 */
|
|
8579
|
+
}
|
|
8580
|
+
}
|
|
8581
|
+
|
|
8582
|
+
try {
|
|
8583
|
+
await ensureLogin()
|
|
8584
|
+
const res = await request<{ events: CountdownEvent[] }>('/api/events')
|
|
8585
|
+
localStorage.setItem(CACHE_KEY, JSON.stringify(res.events ?? []))
|
|
8586
|
+
applyList(res.events ?? [])
|
|
8587
|
+
data.loaded = true
|
|
8588
|
+
} catch (err) {
|
|
8589
|
+
console.warn('[index] \u52A0\u8F7D\u5931\u8D25', err)
|
|
8590
|
+
data.loaded = true
|
|
8591
|
+
if (!data.allList.length) {
|
|
8592
|
+
showToast({ title: err instanceof Error ? err.message : '\u52A0\u8F7D\u5931\u8D25', icon: 'none' })
|
|
8593
|
+
}
|
|
8594
|
+
}
|
|
8595
|
+
}
|
|
8596
|
+
|
|
8597
|
+
function onFilter(value: string): void {
|
|
8598
|
+
data.activeFilter = value
|
|
8599
|
+
applyList(data.allList)
|
|
8600
|
+
}
|
|
8601
|
+
|
|
8602
|
+
// ---------- \u5361\u7247\u4EA4\u4E92 ----------
|
|
8603
|
+
|
|
8604
|
+
/** \u6ED1\u52A8\u72B6\u6001\uFF08\u6309\u5361\u7247\u4E0B\u6807\u8FFD\u8E2A\uFF09\u3002 */
|
|
8605
|
+
const swipe = reactive<{
|
|
8606
|
+
startX: number
|
|
8607
|
+
startY: number
|
|
8608
|
+
lastX: number
|
|
8609
|
+
touchIndex: number | undefined
|
|
8610
|
+
moved: boolean
|
|
8611
|
+
justSwiped: boolean
|
|
8612
|
+
}>({ startX: 0, startY: 0, lastX: 0, touchIndex: undefined, moved: false, justSwiped: false })
|
|
8613
|
+
|
|
8614
|
+
let longPressTimer: ReturnType<typeof setTimeout> | null = null
|
|
8615
|
+
|
|
8616
|
+
function onCardPointerDown(event: PointerEvent, index: number): void {
|
|
8617
|
+
swipe.startX = event.clientX
|
|
8618
|
+
swipe.startY = event.clientY
|
|
8619
|
+
swipe.touchIndex = index
|
|
8620
|
+
swipe.moved = false
|
|
8621
|
+
swipe.lastX = data.list[index]?.offset ?? 0
|
|
8622
|
+
// \u957F\u6309\u83DC\u5355
|
|
8623
|
+
clearLongPress()
|
|
8624
|
+
longPressTimer = setTimeout(() => {
|
|
8625
|
+
const item = data.list[index]
|
|
8626
|
+
if (!item) return
|
|
8627
|
+
void onLongPress(item)
|
|
8628
|
+
}, 600)
|
|
8629
|
+
}
|
|
8630
|
+
|
|
8631
|
+
function onCardPointerMove(event: PointerEvent): void {
|
|
8632
|
+
if (swipe.touchIndex === undefined) return
|
|
8633
|
+
const dx = event.clientX - swipe.startX
|
|
8634
|
+
const dy = event.clientY - swipe.startY
|
|
8635
|
+
if (!swipe.moved && Math.abs(dy) > Math.abs(dx)) {
|
|
8636
|
+
// \u7EB5\u5411\u6EDA\u52A8\u610F\u56FE\uFF1A\u53D6\u6D88\u957F\u6309\uFF0C\u4E0D\u62E6\u622A
|
|
8637
|
+
clearLongPress()
|
|
8638
|
+
return
|
|
8639
|
+
}
|
|
8640
|
+
swipe.moved = true
|
|
8641
|
+
clearLongPress()
|
|
8642
|
+
|
|
8643
|
+
const min = -actionWidth.value
|
|
8644
|
+
let x = (swipe.lastX ?? 0) + dx
|
|
8645
|
+
if (x > 0) x = 0
|
|
8646
|
+
if (x < min) x = min
|
|
8647
|
+
if (Math.abs(x - (swipe.lastX ?? 0)) < 2) return
|
|
8648
|
+
|
|
8649
|
+
const idx = swipe.touchIndex
|
|
8650
|
+
const cur = data.list[idx]?.offset ?? 0
|
|
8651
|
+
if (Math.abs(x - cur) < 3) return
|
|
8652
|
+
data.list[idx] = { ...data.list[idx]!, offset: x }
|
|
8653
|
+
}
|
|
8654
|
+
|
|
8655
|
+
function onCardPointerUp(): void {
|
|
8656
|
+
if (swipe.touchIndex === undefined) return
|
|
8657
|
+
clearLongPress()
|
|
8658
|
+
const idx = swipe.touchIndex
|
|
8659
|
+
const x = data.list[idx]?.offset ?? 0
|
|
8660
|
+
const open = x < -actionWidth.value / 2
|
|
8661
|
+
const target = open ? -actionWidth.value : 0
|
|
8662
|
+
|
|
8663
|
+
// \u540C\u65F6\u53EA\u5141\u8BB8\u4E00\u4E2A\u5361\u7247\u6ED1\u5F00
|
|
8664
|
+
if (open && data.openIndex !== -1 && data.openIndex !== idx) {
|
|
8665
|
+
data.list[data.openIndex] = { ...data.list[data.openIndex]!, offset: 0 }
|
|
8666
|
+
}
|
|
8667
|
+
data.list[idx] = { ...data.list[idx]!, offset: target }
|
|
8668
|
+
data.openIndex = open ? idx : -1
|
|
8669
|
+
|
|
8670
|
+
swipe.justSwiped = swipe.moved
|
|
8671
|
+
swipe.touchIndex = undefined
|
|
8672
|
+
swipe.moved = false
|
|
8673
|
+
}
|
|
8674
|
+
|
|
8675
|
+
function onCardClick(index: number, id: string): void {
|
|
8676
|
+
if (swipe.justSwiped) {
|
|
8677
|
+
// \u521A\u6ED1\u52A8\u5B8C\uFF0C\u8FD9\u4E00\u4E0B click \u4E0D\u5F53\u4F5C\u70B9\u51FB
|
|
8678
|
+
swipe.justSwiped = false
|
|
8679
|
+
return
|
|
8680
|
+
}
|
|
8681
|
+
if (data.openIndex !== -1) {
|
|
8682
|
+
closeAll()
|
|
8683
|
+
return
|
|
8684
|
+
}
|
|
8685
|
+
void router.push(\`/edit/\${id}\`)
|
|
8686
|
+
}
|
|
8687
|
+
|
|
8688
|
+
function closeAll(): void {
|
|
8689
|
+
data.list.forEach((item, i) => {
|
|
8690
|
+
if (item.offset !== 0) data.list[i] = { ...item, offset: 0 }
|
|
8691
|
+
})
|
|
8692
|
+
data.openIndex = -1
|
|
8693
|
+
}
|
|
8694
|
+
|
|
8695
|
+
function clearLongPress(): void {
|
|
8696
|
+
if (longPressTimer !== null) {
|
|
8697
|
+
clearTimeout(longPressTimer)
|
|
8698
|
+
longPressTimer = null
|
|
8699
|
+
}
|
|
8700
|
+
}
|
|
8701
|
+
|
|
8702
|
+
async function onTogglePin(id: string, pinned: boolean): Promise<void> {
|
|
8703
|
+
const next = !pinned
|
|
8704
|
+
try {
|
|
8705
|
+
await request(\`/api/events/\${id}\`, { method: 'PUT', data: { is_pinned: next } })
|
|
8706
|
+
showToast({ title: next ? '\u5DF2\u7F6E\u9876' : '\u5DF2\u53D6\u6D88\u7F6E\u9876', icon: 'none' })
|
|
8707
|
+
await loadEvents(true)
|
|
8708
|
+
} catch (err) {
|
|
8709
|
+
showToast({ title: err instanceof Error ? err.message : '\u64CD\u4F5C\u5931\u8D25', icon: 'none' })
|
|
8710
|
+
}
|
|
8711
|
+
}
|
|
8712
|
+
|
|
8713
|
+
async function onDelete(id: string, title: string): Promise<void> {
|
|
8714
|
+
const ok = await showModal({
|
|
8715
|
+
title: '\u5220\u9664\u4E8B\u4EF6',
|
|
8716
|
+
content: \`\u786E\u5B9A\u5220\u9664\u300C\${title}\u300D\u5417\uFF1F\u5220\u9664\u540E\u4E0D\u53EF\u6062\u590D\u3002\`,
|
|
8717
|
+
confirmColor: '#FF5A5F',
|
|
8718
|
+
confirmText: '\u5220\u9664',
|
|
8719
|
+
})
|
|
8720
|
+
if (!ok) return
|
|
8721
|
+
try {
|
|
8722
|
+
await request(\`/api/events/\${id}\`, { method: 'DELETE' })
|
|
8723
|
+
showToast({ title: '\u5DF2\u5220\u9664', icon: 'success' })
|
|
8724
|
+
await loadEvents(true)
|
|
8725
|
+
} catch (err) {
|
|
8726
|
+
showToast({ title: err instanceof Error ? err.message : '\u5220\u9664\u5931\u8D25', icon: 'none' })
|
|
8727
|
+
}
|
|
8728
|
+
}
|
|
8729
|
+
|
|
8730
|
+
async function onLongPress(item: DecodedEvent): Promise<void> {
|
|
8731
|
+
const index = await showActionSheet({
|
|
8732
|
+
itemList: [item.is_pinned ? '\u53D6\u6D88\u7F6E\u9876' : '\u7F6E\u9876', '\u5220\u9664'],
|
|
8733
|
+
itemColor: '#1F2430',
|
|
8734
|
+
})
|
|
8735
|
+
if (index === 0) {
|
|
8736
|
+
await onTogglePin(item.id, item.is_pinned)
|
|
8737
|
+
} else if (index === 1) {
|
|
8738
|
+
await onDelete(item.id, item.title)
|
|
8739
|
+
}
|
|
8740
|
+
}
|
|
8741
|
+
|
|
8742
|
+
// ---------- \u4E0B\u62C9\u5237\u65B0 ----------
|
|
8743
|
+
|
|
8744
|
+
const pull = reactive({
|
|
8745
|
+
distance: 0, // \u4E0B\u62C9\u504F\u79FB px
|
|
8746
|
+
state: 'idle' as 'idle' | 'pulling' | 'ready' | 'refreshing',
|
|
8747
|
+
})
|
|
8748
|
+
|
|
8749
|
+
let pullStartY = 0
|
|
8750
|
+
let pullTracking = false
|
|
8751
|
+
let pullMoved = false
|
|
8752
|
+
|
|
8753
|
+
function onPullPointerDown(event: PointerEvent): void {
|
|
8754
|
+
if (pull.state === 'refreshing') return
|
|
8755
|
+
if ((scrollEl.value?.scrollTop ?? 0) <= 0) {
|
|
8756
|
+
pullStartY = event.clientY
|
|
8757
|
+
pullTracking = true
|
|
8758
|
+
pullMoved = false
|
|
8759
|
+
}
|
|
8760
|
+
}
|
|
8761
|
+
|
|
8762
|
+
function onPullPointerMove(event: PointerEvent): void {
|
|
8763
|
+
if (!pullTracking || pull.state === 'refreshing') return
|
|
8764
|
+
const dy = event.clientY - pullStartY
|
|
8765
|
+
if (dy <= 0) return
|
|
8766
|
+
pullMoved = true
|
|
8767
|
+
event.preventDefault()
|
|
8768
|
+
pull.distance = Math.min(dy * 0.5, 90)
|
|
8769
|
+
pull.state = pull.distance > 50 ? 'ready' : 'pulling'
|
|
8770
|
+
}
|
|
8771
|
+
|
|
8772
|
+
function onPullPointerUp(): void {
|
|
8773
|
+
if (!pullTracking) return
|
|
8774
|
+
pullTracking = false
|
|
8775
|
+
if (pull.state === 'ready') {
|
|
8776
|
+
pull.state = 'refreshing'
|
|
8777
|
+
pull.distance = 44
|
|
8778
|
+
void loadEvents(true).finally(() => {
|
|
8779
|
+
pull.state = 'idle'
|
|
8780
|
+
pull.distance = 0
|
|
8781
|
+
})
|
|
8782
|
+
} else if (pullMoved) {
|
|
8783
|
+
pull.state = 'idle'
|
|
8784
|
+
pull.distance = 0
|
|
8785
|
+
}
|
|
8786
|
+
pullMoved = false
|
|
8787
|
+
}
|
|
8788
|
+
|
|
8789
|
+
// ---------- \u751F\u547D\u5468\u671F ----------
|
|
8790
|
+
|
|
8791
|
+
onMounted(() => {
|
|
8792
|
+
// \u5DE6\u6ED1\u64CD\u4F5C\u533A\u5BBD\u5EA6\u6309\u5B9E\u9645 font-size \u6362\u7B97\uFF081.6rem\uFF09
|
|
8793
|
+
const root = scrollEl.value?.closest('.app') as HTMLElement | null
|
|
8794
|
+
if (root) {
|
|
8795
|
+
const fs = parseFloat(getComputedStyle(root).fontSize)
|
|
8796
|
+
if (Number.isFinite(fs) && fs > 0) actionWidth.value = (ACTION_RPX / 100) * fs
|
|
8797
|
+
}
|
|
8798
|
+
|
|
8799
|
+
const el = scrollEl.value
|
|
8800
|
+
if (el) {
|
|
8801
|
+
// \u4E0B\u62C9\u5237\u65B0\u76D1\u542C\uFF08pointerdown \u5728 scrollTop=0 \u65F6\u542F\u7528\uFF0Cmove \u9636\u6BB5 preventDefault \u963B\u6B62\u539F\u751F\u6EDA\u52A8\uFF09
|
|
8802
|
+
el.addEventListener('pointerdown', onPullPointerDown, { passive: true })
|
|
8803
|
+
el.addEventListener('pointermove', onPullPointerMove, { passive: false })
|
|
8804
|
+
el.addEventListener('pointerup', onPullPointerUp)
|
|
8805
|
+
el.addEventListener('pointercancel', onPullPointerUp)
|
|
8806
|
+
}
|
|
8807
|
+
|
|
8808
|
+
void loadEvents(false)
|
|
8809
|
+
})
|
|
8810
|
+
|
|
8811
|
+
onUnmounted(() => {
|
|
8812
|
+
clearLongPress()
|
|
8813
|
+
const el = scrollEl.value
|
|
8814
|
+
if (el) {
|
|
8815
|
+
el.removeEventListener('pointerdown', onPullPointerDown)
|
|
8816
|
+
el.removeEventListener('pointermove', onPullPointerMove)
|
|
8817
|
+
el.removeEventListener('pointerup', onPullPointerUp)
|
|
8818
|
+
el.removeEventListener('pointercancel', onPullPointerUp)
|
|
8819
|
+
}
|
|
8820
|
+
})
|
|
8821
|
+
|
|
8822
|
+
const pullLabel = computed(() => {
|
|
8823
|
+
if (pull.state === 'refreshing') return '\u6B63\u5728\u5237\u65B0\u2026'
|
|
8824
|
+
if (pull.state === 'ready') return '\u677E\u5F00\u5237\u65B0'
|
|
8825
|
+
return '\u4E0B\u62C9\u5237\u65B0'
|
|
8826
|
+
})
|
|
8827
|
+
</script>
|
|
8828
|
+
|
|
8829
|
+
<template>
|
|
8830
|
+
<div class="page">
|
|
8831
|
+
<!-- \u9876\u90E8\u84DD\u8272\u6E10\u53D8\u5BFC\u822A -->
|
|
8832
|
+
<header class="header">
|
|
8833
|
+
<div class="nav">
|
|
8834
|
+
<span class="nav-title">\u5012\u6570\u65E5</span>
|
|
8835
|
+
</div>
|
|
8836
|
+
<div class="summary">
|
|
8837
|
+
<div class="summary-left">
|
|
8838
|
+
<div class="summary-num">{{ data.total }}</div>
|
|
8839
|
+
<div class="summary-label">\u4E2A\u91CD\u8981\u65E5\u5B50</div>
|
|
8840
|
+
</div>
|
|
8841
|
+
<div class="summary-right" v-if="data.nextUp">
|
|
8842
|
+
<div class="summary-label">\u4E0B\u4E00\u4E2A</div>
|
|
8843
|
+
<div class="summary-next">
|
|
8844
|
+
<span class="next-title">{{ data.nextUp.title }}</span>
|
|
8845
|
+
<span class="next-days"
|
|
8846
|
+
>{{ data.nextUp.prefix }}{{ data.nextUp.num }}{{ data.nextUp.suffix }}</span
|
|
8847
|
+
>
|
|
8848
|
+
</div>
|
|
8849
|
+
</div>
|
|
8850
|
+
</div>
|
|
8851
|
+
</header>
|
|
8852
|
+
|
|
8853
|
+
<!-- \u5185\u5BB9\u533A -->
|
|
8854
|
+
<div ref="scrollEl" class="page-scroll body">
|
|
8855
|
+
<!-- \u4E0B\u62C9\u5237\u65B0\u6307\u793A -->
|
|
8856
|
+
<div class="pull-indicator" :style="{ height: pull.distance + 'px' }">
|
|
8857
|
+
<span class="pull-spinner" :class="{ spin: pull.state === 'refreshing' }"></span>
|
|
8858
|
+
<span class="pull-text">{{ pullLabel }}</span>
|
|
8859
|
+
</div>
|
|
8860
|
+
|
|
8861
|
+
<!-- \u5206\u7C7B\u7B5B\u9009 -->
|
|
8862
|
+
<div v-if="data.total > 0" class="filters">
|
|
8863
|
+
<span
|
|
8864
|
+
v-for="f in data.filters"
|
|
8865
|
+
:key="f.value"
|
|
8866
|
+
class="chip"
|
|
8867
|
+
:class="{ 'chip-on': data.activeFilter === f.value }"
|
|
8868
|
+
@click="onFilter(f.value)"
|
|
8869
|
+
>
|
|
8870
|
+
{{ f.label }}
|
|
8871
|
+
</span>
|
|
8872
|
+
</div>
|
|
8873
|
+
|
|
8874
|
+
<!-- \u5217\u8868 -->
|
|
8875
|
+
<div v-if="data.list.length > 0" class="list no-select">
|
|
8876
|
+
<div v-for="(item, idx) in data.list" :key="item.id" class="card-row">
|
|
8877
|
+
<div class="card-actions">
|
|
8878
|
+
<span class="act act-pin" @click.stop="onTogglePin(item.id, item.is_pinned)">{{
|
|
8879
|
+
item.is_pinned ? '\u53D6\u6D88\u7F6E\u9876' : '\u7F6E\u9876'
|
|
8880
|
+
}}</span>
|
|
8881
|
+
<span class="act act-del" @click.stop="onDelete(item.id, item.title)">\u5220\u9664</span>
|
|
8882
|
+
</div>
|
|
8883
|
+
|
|
8884
|
+
<div
|
|
8885
|
+
class="card"
|
|
8886
|
+
:style="{ transform: \`translateX(\${item.offset}px)\`, touchAction: 'pan-y' }"
|
|
8887
|
+
@pointerdown="onCardPointerDown($event, idx)"
|
|
8888
|
+
@pointermove="onCardPointerMove($event)"
|
|
8889
|
+
@pointerup="onCardPointerUp"
|
|
8890
|
+
@pointercancel="onCardPointerUp"
|
|
8891
|
+
@click="onCardClick(idx, item.id)"
|
|
8892
|
+
>
|
|
8893
|
+
<div class="card-bar" :class="'level-' + item.level"></div>
|
|
8894
|
+
<div class="card-main">
|
|
8895
|
+
<div class="card-left">
|
|
8896
|
+
<div class="card-title">
|
|
8897
|
+
<span class="title-text">{{ item.title }}</span>
|
|
8898
|
+
<span v-if="item.is_pinned" class="pin-tag">\u7F6E\u9876</span>
|
|
8899
|
+
</div>
|
|
8900
|
+
<div class="card-sub">{{ item.sub }} \xB7 {{ item.categoryLabel }}</div>
|
|
8901
|
+
<div v-if="item.note" class="card-note">{{ item.note }}</div>
|
|
8902
|
+
</div>
|
|
8903
|
+
<div class="card-right">
|
|
8904
|
+
<div class="days">
|
|
8905
|
+
<span v-if="item.prefix" class="days-prefix">{{ item.prefix }}</span>
|
|
8906
|
+
<span class="days-num" :class="'level-' + item.level">{{ item.num }}</span>
|
|
8907
|
+
<span v-if="item.suffix" class="days-unit">{{ item.suffix }}</span>
|
|
8908
|
+
</div>
|
|
8909
|
+
</div>
|
|
8910
|
+
</div>
|
|
8911
|
+
</div>
|
|
8912
|
+
</div>
|
|
8913
|
+
</div>
|
|
8914
|
+
|
|
8915
|
+
<!-- \u7A7A\u6001 -->
|
|
8916
|
+
<div v-if="data.loaded && data.list.length === 0" class="empty">
|
|
8917
|
+
<div class="empty-icon">
|
|
8918
|
+
<div class="empty-bar"></div>
|
|
8919
|
+
<div class="empty-dot"></div>
|
|
8920
|
+
</div>
|
|
8921
|
+
<div class="empty-title">{{ data.total > 0 ? '\u8BE5\u5206\u7C7B\u4E0B\u8FD8\u6CA1\u6709\u4E8B\u4EF6' : '\u8FD8\u6CA1\u6709\u5012\u6570\u65E5' }}</div>
|
|
8922
|
+
<div class="empty-tip">
|
|
8923
|
+
{{ data.total > 0 ? '\u6362\u4E2A\u5206\u7C7B\u770B\u770B\uFF0C\u6216\u6DFB\u52A0\u4E00\u4E2A\u65B0\u7684' : '\u70B9\u51FB\u4E0B\u65B9 + \u8BB0\u5F55\u7B2C\u4E00\u4E2A\u91CD\u8981\u65E5\u5B50' }}
|
|
8924
|
+
</div>
|
|
8925
|
+
<div class="empty-btn" @click="router.push('/edit')">\u7ACB\u5373\u6DFB\u52A0</div>
|
|
8926
|
+
</div>
|
|
8927
|
+
|
|
8928
|
+
<div v-if="data.list.length > 0" class="foot-tip">\u5DE6\u6ED1\u5361\u7247\u53EF\u7F6E\u9876 / \u5220\u9664\uFF0C\u957F\u6309\u4E5F\u6709\u83DC\u5355</div>
|
|
8929
|
+
</div>
|
|
8930
|
+
</div>
|
|
8931
|
+
</template>
|
|
8932
|
+
|
|
8933
|
+
<style scoped>
|
|
8934
|
+
.page {
|
|
8935
|
+
height: 100%;
|
|
8936
|
+
padding-bottom: calc(1.6rem + env(safe-area-inset-bottom));
|
|
8937
|
+
}
|
|
8938
|
+
|
|
8939
|
+
/* ---------- \u9876\u90E8\u6E10\u53D8\u5BFC\u822A ---------- */
|
|
8940
|
+
.header {
|
|
8941
|
+
position: absolute;
|
|
8942
|
+
top: 0;
|
|
8943
|
+
left: 0;
|
|
8944
|
+
right: 0;
|
|
8945
|
+
z-index: 10;
|
|
8946
|
+
background: linear-gradient(135deg, #3b6ef6, #6a8dff);
|
|
8947
|
+
border-bottom-left-radius: 0.4rem;
|
|
8948
|
+
border-bottom-right-radius: 0.4rem;
|
|
8949
|
+
}
|
|
8950
|
+
|
|
8951
|
+
.nav {
|
|
8952
|
+
display: flex;
|
|
8953
|
+
align-items: center;
|
|
8954
|
+
height: 0.88rem;
|
|
8955
|
+
padding-left: 0.4rem;
|
|
8956
|
+
}
|
|
8957
|
+
|
|
8958
|
+
.nav-title {
|
|
8959
|
+
color: #ffffff;
|
|
8960
|
+
font-size: 0.36rem;
|
|
8961
|
+
font-weight: 600;
|
|
8962
|
+
}
|
|
8963
|
+
|
|
8964
|
+
.summary {
|
|
8965
|
+
height: 1.76rem;
|
|
8966
|
+
padding: 0 0.4rem 0.28rem;
|
|
8967
|
+
display: flex;
|
|
8968
|
+
align-items: flex-end;
|
|
8969
|
+
justify-content: space-between;
|
|
8970
|
+
}
|
|
8971
|
+
|
|
8972
|
+
.summary-num {
|
|
8973
|
+
color: #ffffff;
|
|
8974
|
+
font-size: 0.66rem;
|
|
8975
|
+
font-weight: 700;
|
|
8976
|
+
line-height: 1;
|
|
8977
|
+
}
|
|
8978
|
+
|
|
8979
|
+
.summary-label {
|
|
8980
|
+
color: rgba(255, 255, 255, 0.82);
|
|
8981
|
+
font-size: 0.24rem;
|
|
8982
|
+
margin-top: 0.12rem;
|
|
8983
|
+
}
|
|
8984
|
+
|
|
8985
|
+
.summary-right {
|
|
8986
|
+
text-align: right;
|
|
8987
|
+
max-width: 46%;
|
|
8988
|
+
}
|
|
8989
|
+
|
|
8990
|
+
.summary-next {
|
|
8991
|
+
margin-top: 0.08rem;
|
|
8992
|
+
color: #ffffff;
|
|
8993
|
+
font-size: 0.28rem;
|
|
8994
|
+
}
|
|
8995
|
+
|
|
8996
|
+
.next-title {
|
|
8997
|
+
opacity: 0.9;
|
|
8998
|
+
margin-right: 0.12rem;
|
|
8999
|
+
}
|
|
9000
|
+
|
|
9001
|
+
.next-days {
|
|
9002
|
+
font-weight: 600;
|
|
9003
|
+
}
|
|
9004
|
+
|
|
9005
|
+
/* ---------- \u5185\u5BB9\u533A ---------- */
|
|
9006
|
+
.body {
|
|
9007
|
+
padding-top: 2.64rem;
|
|
9008
|
+
padding-bottom: 0.4rem;
|
|
9009
|
+
}
|
|
9010
|
+
|
|
9011
|
+
/* \u4E0B\u62C9\u5237\u65B0 */
|
|
9012
|
+
.pull-indicator {
|
|
9013
|
+
display: flex;
|
|
9014
|
+
align-items: center;
|
|
9015
|
+
justify-content: center;
|
|
9016
|
+
gap: 0.12rem;
|
|
9017
|
+
overflow: hidden;
|
|
9018
|
+
color: #9aa3b2;
|
|
9019
|
+
font-size: 0.24rem;
|
|
9020
|
+
}
|
|
9021
|
+
|
|
9022
|
+
.pull-spinner {
|
|
9023
|
+
width: 0.28rem;
|
|
9024
|
+
height: 0.28rem;
|
|
9025
|
+
border-radius: 50%;
|
|
9026
|
+
border: 0.04rem solid #d7dbe4;
|
|
9027
|
+
border-top-color: #3b6ef6;
|
|
9028
|
+
}
|
|
9029
|
+
|
|
9030
|
+
.pull-spinner.spin {
|
|
9031
|
+
animation: pull-spin 0.8s linear infinite;
|
|
9032
|
+
}
|
|
9033
|
+
|
|
9034
|
+
@keyframes pull-spin {
|
|
9035
|
+
to {
|
|
9036
|
+
transform: rotate(360deg);
|
|
9037
|
+
}
|
|
9038
|
+
}
|
|
9039
|
+
|
|
9040
|
+
/* ---------- \u5206\u7C7B\u7B5B\u9009 ---------- */
|
|
9041
|
+
.filters {
|
|
9042
|
+
white-space: nowrap;
|
|
9043
|
+
padding: 0.24rem 0 0.04rem;
|
|
9044
|
+
overflow-x: auto;
|
|
9045
|
+
scrollbar-width: none;
|
|
9046
|
+
}
|
|
9047
|
+
|
|
9048
|
+
.filters::-webkit-scrollbar {
|
|
9049
|
+
display: none;
|
|
9050
|
+
}
|
|
9051
|
+
|
|
9052
|
+
.chip {
|
|
9053
|
+
display: inline-block;
|
|
9054
|
+
padding: 0.12rem 0.28rem;
|
|
9055
|
+
margin-right: 0.16rem;
|
|
9056
|
+
background: #ffffff;
|
|
9057
|
+
border-radius: 999rem;
|
|
9058
|
+
font-size: 0.26rem;
|
|
9059
|
+
color: #6b7385;
|
|
9060
|
+
cursor: pointer;
|
|
9061
|
+
user-select: none;
|
|
9062
|
+
}
|
|
9063
|
+
|
|
9064
|
+
.chip:first-child {
|
|
9065
|
+
margin-left: 0.32rem;
|
|
9066
|
+
}
|
|
9067
|
+
|
|
9068
|
+
.chip:last-child {
|
|
9069
|
+
margin-right: 0.32rem;
|
|
9070
|
+
}
|
|
9071
|
+
|
|
9072
|
+
.chip-on {
|
|
9073
|
+
background: linear-gradient(135deg, #3b6ef6, #6a8dff);
|
|
9074
|
+
color: #ffffff;
|
|
9075
|
+
}
|
|
9076
|
+
|
|
9077
|
+
/* ---------- \u5361\u7247 ---------- */
|
|
9078
|
+
.list {
|
|
9079
|
+
padding: 0.16rem 0.32rem 0;
|
|
9080
|
+
}
|
|
9081
|
+
|
|
9082
|
+
.card-row {
|
|
9083
|
+
position: relative;
|
|
9084
|
+
margin-bottom: 0.24rem;
|
|
9085
|
+
border-radius: 0.2rem;
|
|
9086
|
+
overflow: hidden;
|
|
9087
|
+
}
|
|
9088
|
+
|
|
9089
|
+
.card-actions {
|
|
9090
|
+
position: absolute;
|
|
9091
|
+
right: 0;
|
|
9092
|
+
top: 0;
|
|
9093
|
+
bottom: 0;
|
|
9094
|
+
width: 1.6rem;
|
|
9095
|
+
display: flex;
|
|
9096
|
+
}
|
|
9097
|
+
|
|
9098
|
+
.act {
|
|
9099
|
+
flex: 1;
|
|
9100
|
+
display: flex;
|
|
9101
|
+
align-items: center;
|
|
9102
|
+
justify-content: center;
|
|
9103
|
+
color: #ffffff;
|
|
9104
|
+
font-size: 0.26rem;
|
|
9105
|
+
cursor: pointer;
|
|
9106
|
+
}
|
|
9107
|
+
|
|
9108
|
+
.act-pin {
|
|
9109
|
+
background: #8a94a6;
|
|
9110
|
+
}
|
|
9111
|
+
|
|
9112
|
+
.act-del {
|
|
9113
|
+
background: #ff5a5f;
|
|
9114
|
+
}
|
|
9115
|
+
|
|
9116
|
+
.card {
|
|
9117
|
+
position: relative;
|
|
9118
|
+
z-index: 2;
|
|
9119
|
+
background: #ffffff;
|
|
9120
|
+
border-radius: 0.2rem;
|
|
9121
|
+
box-shadow: 0 0.08rem 0.24rem rgba(31, 36, 48, 0.06);
|
|
9122
|
+
transition: transform 0.22s ease;
|
|
9123
|
+
cursor: pointer;
|
|
9124
|
+
}
|
|
9125
|
+
|
|
9126
|
+
.card-bar {
|
|
9127
|
+
position: absolute;
|
|
9128
|
+
left: 0;
|
|
9129
|
+
top: 0;
|
|
9130
|
+
bottom: 0;
|
|
9131
|
+
width: 0.08rem;
|
|
9132
|
+
border-radius: 0.2rem 0 0 0.2rem;
|
|
9133
|
+
}
|
|
9134
|
+
|
|
9135
|
+
.card-main {
|
|
9136
|
+
display: flex;
|
|
9137
|
+
align-items: center;
|
|
9138
|
+
padding: 0.32rem 0.28rem 0.32rem 0.36rem;
|
|
9139
|
+
min-height: 1.4rem;
|
|
9140
|
+
}
|
|
9141
|
+
|
|
9142
|
+
.card-left {
|
|
9143
|
+
flex: 1;
|
|
9144
|
+
min-width: 0;
|
|
9145
|
+
}
|
|
9146
|
+
|
|
9147
|
+
.card-title {
|
|
9148
|
+
display: flex;
|
|
9149
|
+
align-items: center;
|
|
9150
|
+
}
|
|
9151
|
+
|
|
9152
|
+
.title-text {
|
|
9153
|
+
font-size: 0.32rem;
|
|
9154
|
+
font-weight: 600;
|
|
9155
|
+
color: #1f2430;
|
|
9156
|
+
overflow: hidden;
|
|
9157
|
+
text-overflow: ellipsis;
|
|
9158
|
+
white-space: nowrap;
|
|
9159
|
+
}
|
|
9160
|
+
|
|
9161
|
+
.pin-tag {
|
|
9162
|
+
margin-left: 0.12rem;
|
|
9163
|
+
font-size: 0.2rem;
|
|
9164
|
+
color: #3b6ef6;
|
|
9165
|
+
background: rgba(59, 110, 246, 0.1);
|
|
9166
|
+
padding: 0.04rem 0.1rem;
|
|
9167
|
+
border-radius: 0.06rem;
|
|
9168
|
+
flex-shrink: 0;
|
|
9169
|
+
}
|
|
9170
|
+
|
|
9171
|
+
.card-sub {
|
|
9172
|
+
margin-top: 0.12rem;
|
|
9173
|
+
font-size: 0.24rem;
|
|
9174
|
+
color: #9aa3b2;
|
|
9175
|
+
}
|
|
9176
|
+
|
|
9177
|
+
.card-note {
|
|
9178
|
+
margin-top: 0.1rem;
|
|
9179
|
+
font-size: 0.24rem;
|
|
9180
|
+
color: #6b7385;
|
|
9181
|
+
overflow: hidden;
|
|
9182
|
+
text-overflow: ellipsis;
|
|
9183
|
+
white-space: nowrap;
|
|
9184
|
+
}
|
|
9185
|
+
|
|
9186
|
+
.card-right {
|
|
9187
|
+
margin-left: 0.2rem;
|
|
9188
|
+
display: flex;
|
|
9189
|
+
align-items: baseline;
|
|
9190
|
+
}
|
|
9191
|
+
|
|
9192
|
+
.days {
|
|
9193
|
+
display: flex;
|
|
9194
|
+
align-items: baseline;
|
|
9195
|
+
}
|
|
9196
|
+
|
|
9197
|
+
.days-num {
|
|
9198
|
+
font-size: 0.58rem;
|
|
9199
|
+
font-weight: 700;
|
|
9200
|
+
line-height: 1;
|
|
9201
|
+
}
|
|
9202
|
+
|
|
9203
|
+
.days-prefix {
|
|
9204
|
+
font-size: 0.22rem;
|
|
9205
|
+
margin-right: 0.06rem;
|
|
9206
|
+
}
|
|
9207
|
+
|
|
9208
|
+
.days-unit {
|
|
9209
|
+
font-size: 0.24rem;
|
|
9210
|
+
margin-left: 0.04rem;
|
|
9211
|
+
}
|
|
9212
|
+
|
|
9213
|
+
.level-normal {
|
|
9214
|
+
color: #3b6ef6;
|
|
9215
|
+
}
|
|
9216
|
+
.level-soon {
|
|
9217
|
+
color: #ff9f43;
|
|
9218
|
+
}
|
|
9219
|
+
.level-urgent {
|
|
9220
|
+
color: #ff5a5f;
|
|
9221
|
+
}
|
|
9222
|
+
.level-today {
|
|
9223
|
+
color: #ff5a5f;
|
|
9224
|
+
}
|
|
9225
|
+
.level-past {
|
|
9226
|
+
color: #a0a8b8;
|
|
9227
|
+
}
|
|
9228
|
+
|
|
9229
|
+
.card-bar.level-normal {
|
|
9230
|
+
background: #3b6ef6;
|
|
9231
|
+
}
|
|
9232
|
+
.card-bar.level-soon {
|
|
9233
|
+
background: #ff9f43;
|
|
9234
|
+
}
|
|
9235
|
+
.card-bar.level-urgent {
|
|
9236
|
+
background: #ff5a5f;
|
|
9237
|
+
}
|
|
9238
|
+
.card-bar.level-today {
|
|
9239
|
+
background: #ff5a5f;
|
|
9240
|
+
}
|
|
9241
|
+
.card-bar.level-past {
|
|
9242
|
+
background: #d7dbe4;
|
|
9243
|
+
}
|
|
9244
|
+
|
|
9245
|
+
/* ---------- \u7A7A\u6001 ---------- */
|
|
9246
|
+
.empty {
|
|
9247
|
+
padding: 1.2rem 0.6rem 0;
|
|
9248
|
+
text-align: center;
|
|
9249
|
+
}
|
|
9250
|
+
|
|
9251
|
+
.empty-icon {
|
|
9252
|
+
width: 1.4rem;
|
|
9253
|
+
height: 1.4rem;
|
|
9254
|
+
border-radius: 50%;
|
|
9255
|
+
background: linear-gradient(135deg, #e8eeff, #f3f6ff);
|
|
9256
|
+
margin: 0 auto 0.32rem;
|
|
9257
|
+
position: relative;
|
|
9258
|
+
}
|
|
9259
|
+
|
|
9260
|
+
.empty-bar {
|
|
9261
|
+
position: absolute;
|
|
9262
|
+
left: 0.4rem;
|
|
9263
|
+
top: 0.66rem;
|
|
9264
|
+
width: 0.6rem;
|
|
9265
|
+
height: 0.08rem;
|
|
9266
|
+
background: #c7d4ff;
|
|
9267
|
+
border-radius: 0.04rem;
|
|
9268
|
+
}
|
|
9269
|
+
|
|
9270
|
+
.empty-dot {
|
|
9271
|
+
position: absolute;
|
|
9272
|
+
right: 0.36rem;
|
|
9273
|
+
top: 0.52rem;
|
|
9274
|
+
width: 0.18rem;
|
|
9275
|
+
height: 0.18rem;
|
|
9276
|
+
border-radius: 50%;
|
|
9277
|
+
background: #ff9f43;
|
|
9278
|
+
}
|
|
9279
|
+
|
|
9280
|
+
.empty-title {
|
|
9281
|
+
font-size: 0.32rem;
|
|
9282
|
+
color: #1f2430;
|
|
9283
|
+
font-weight: 600;
|
|
9284
|
+
}
|
|
9285
|
+
|
|
9286
|
+
.empty-tip {
|
|
9287
|
+
margin-top: 0.12rem;
|
|
9288
|
+
font-size: 0.26rem;
|
|
9289
|
+
color: #9aa3b2;
|
|
9290
|
+
}
|
|
9291
|
+
|
|
9292
|
+
.empty-btn {
|
|
9293
|
+
margin: 0.4rem auto 0;
|
|
9294
|
+
width: 2.8rem;
|
|
9295
|
+
height: 0.8rem;
|
|
9296
|
+
line-height: 0.8rem;
|
|
9297
|
+
border-radius: 999rem;
|
|
9298
|
+
background: linear-gradient(135deg, #3b6ef6, #6a8dff);
|
|
9299
|
+
color: #ffffff;
|
|
9300
|
+
font-size: 0.28rem;
|
|
9301
|
+
box-shadow: 0 0.1rem 0.24rem rgba(59, 110, 246, 0.28);
|
|
9302
|
+
cursor: pointer;
|
|
9303
|
+
}
|
|
9304
|
+
|
|
9305
|
+
.foot-tip {
|
|
9306
|
+
text-align: center;
|
|
9307
|
+
color: #b4bac6;
|
|
9308
|
+
font-size: 0.22rem;
|
|
9309
|
+
padding: 0.2rem 0 0.08rem;
|
|
9310
|
+
}
|
|
9311
|
+
</style>
|
|
9312
|
+
` },
|
|
9313
|
+
{ path: "web/src/views/ProfileView.vue", content: `<script setup lang="ts">
|
|
9314
|
+
// \u6211\u7684\uFF08\u79FB\u690D\u81EA pages/profile/*\uFF09\u3002
|
|
9315
|
+
// \u7528\u6237\u5361\u7247 + \u5206\u7C7B\u7EDF\u8BA1 + \u6E05\u7F13\u5B58/\u670D\u52A1\u5730\u5740/\u7248\u672C + \u6570\u636E\u8BF4\u660E + \u9000\u51FA\u767B\u5F55\u3002
|
|
9316
|
+
import { computed, onMounted, reactive } from 'vue'
|
|
9317
|
+
import { request } from '../lib/request'
|
|
9318
|
+
import { ensureLogin, getUser, logout } from '../lib/auth'
|
|
9319
|
+
import { CATEGORY_LABEL } from '../lib/config'
|
|
9320
|
+
import { showModal, showToast } from '../ui/ui'
|
|
9321
|
+
|
|
9322
|
+
const CACHE_KEY = 'events_cache'
|
|
9323
|
+
const VERSION = '1.0.0'
|
|
9324
|
+
|
|
9325
|
+
const data = reactive({
|
|
9326
|
+
user: null as null | {
|
|
9327
|
+
id: string
|
|
9328
|
+
nickname: string
|
|
9329
|
+
avatar_url: string
|
|
9330
|
+
created_at: string
|
|
9331
|
+
},
|
|
9332
|
+
eventCount: 0,
|
|
9333
|
+
maxEvents: 200,
|
|
9334
|
+
stats: [] as Array<{ label: string; value: number }>,
|
|
9335
|
+
})
|
|
9336
|
+
|
|
9337
|
+
const apiHost = computed(() => {
|
|
9338
|
+
const url = new URL(window.location.href)
|
|
9339
|
+
return url.host || ''
|
|
9340
|
+
})
|
|
9341
|
+
|
|
9342
|
+
function formatCreatedAt(raw: string | undefined): string {
|
|
9343
|
+
if (!raw) return ''
|
|
9344
|
+
const d = new Date(raw)
|
|
9345
|
+
if (Number.isNaN(d.getTime())) return raw
|
|
9346
|
+
return \`\${d.getFullYear()}\u5E74\${d.getMonth() + 1}\u6708\${d.getDate()}\u65E5\`
|
|
9347
|
+
}
|
|
9348
|
+
|
|
9349
|
+
async function loadStats(): Promise<void> {
|
|
9350
|
+
data.user = getUser()
|
|
9351
|
+
try {
|
|
9352
|
+
await ensureLogin()
|
|
9353
|
+
const [me, listData] = await Promise.all([
|
|
9354
|
+
request<{
|
|
9355
|
+
user: { id: string; nickname: string; avatar_url: string; created_at: string }
|
|
9356
|
+
event_count: number
|
|
9357
|
+
max_events: number
|
|
9358
|
+
}>('/api/auth/me'),
|
|
9359
|
+
request<{ events: Array<{ category: string }> }>('/api/events').catch(() => ({ events: [] })),
|
|
9360
|
+
])
|
|
9361
|
+
const events = listData.events ?? []
|
|
9362
|
+
const counter: Record<string, number> = {}
|
|
9363
|
+
events.forEach((e) => {
|
|
9364
|
+
const key = e.category || 'other'
|
|
9365
|
+
counter[key] = (counter[key] ?? 0) + 1
|
|
9366
|
+
})
|
|
9367
|
+
const stats = Object.keys(counter).map((k) => ({
|
|
9368
|
+
label: CATEGORY_LABEL[k] ?? '\u65E5\u5E38',
|
|
9369
|
+
value: counter[k] as number,
|
|
9370
|
+
}))
|
|
9371
|
+
data.user = me.user
|
|
9372
|
+
data.eventCount = me.event_count
|
|
9373
|
+
data.maxEvents = me.max_events
|
|
9374
|
+
data.stats = stats
|
|
9375
|
+
} catch (err) {
|
|
9376
|
+
console.warn('[profile] \u52A0\u8F7D\u5931\u8D25', err)
|
|
9377
|
+
}
|
|
9378
|
+
}
|
|
9379
|
+
|
|
9380
|
+
async function onClearCache(): Promise<void> {
|
|
9381
|
+
const ok = await showModal({
|
|
9382
|
+
title: '\u6E05\u7A7A\u672C\u5730\u7F13\u5B58',
|
|
9383
|
+
content: '\u53EA\u4F1A\u6E05\u9664\u672C\u673A\u7684\u4E34\u65F6\u7F13\u5B58\uFF0C\u4E91\u7AEF\u6570\u636E\u4E0D\u53D7\u5F71\u54CD\u3002',
|
|
9384
|
+
confirmText: '\u6E05\u7A7A',
|
|
9385
|
+
})
|
|
9386
|
+
if (!ok) return
|
|
9387
|
+
localStorage.removeItem(CACHE_KEY)
|
|
9388
|
+
showToast({ title: '\u5DF2\u6E05\u7A7A', icon: 'success' })
|
|
9389
|
+
}
|
|
9390
|
+
|
|
9391
|
+
async function onLogout(): Promise<void> {
|
|
9392
|
+
const ok = await showModal({
|
|
9393
|
+
title: '\u9000\u51FA\u767B\u5F55',
|
|
9394
|
+
content: '\u9000\u51FA\u540E\u672C\u5730\u767B\u5F55\u72B6\u6001\u5C06\u88AB\u6E05\u9664\uFF0C\u4E0B\u6B21\u6253\u5F00\u4F1A\u81EA\u52A8\u91CD\u65B0\u767B\u5F55\u3002',
|
|
9395
|
+
confirmText: '\u9000\u51FA',
|
|
9396
|
+
})
|
|
9397
|
+
if (!ok) return
|
|
9398
|
+
logout()
|
|
9399
|
+
localStorage.removeItem(CACHE_KEY)
|
|
9400
|
+
showToast({ title: '\u5DF2\u9000\u51FA', icon: 'success' })
|
|
9401
|
+
setTimeout(() => {
|
|
9402
|
+
window.location.href = '/'
|
|
9403
|
+
}, 400)
|
|
9404
|
+
}
|
|
9405
|
+
|
|
9406
|
+
async function onCopyHost(): Promise<void> {
|
|
9407
|
+
try {
|
|
9408
|
+
await navigator.clipboard.writeText(apiHost.value)
|
|
9409
|
+
showToast({ title: '\u5DF2\u590D\u5236', icon: 'success' })
|
|
9410
|
+
} catch {
|
|
9411
|
+
showToast({ title: '\u590D\u5236\u5931\u8D25', icon: 'none' })
|
|
9412
|
+
}
|
|
9413
|
+
}
|
|
9414
|
+
|
|
9415
|
+
onMounted(() => {
|
|
9416
|
+
void loadStats()
|
|
9417
|
+
})
|
|
9418
|
+
</script>
|
|
9419
|
+
|
|
9420
|
+
<template>
|
|
9421
|
+
<div class="page">
|
|
9422
|
+
<header class="header">
|
|
9423
|
+
<div class="nav">
|
|
9424
|
+
<span class="nav-title">\u6211\u7684</span>
|
|
9425
|
+
</div>
|
|
9426
|
+
|
|
9427
|
+
<div class="user-card">
|
|
9428
|
+
<div class="avatar">
|
|
9429
|
+
<span class="avatar-text">\u65E5</span>
|
|
9430
|
+
</div>
|
|
9431
|
+
<div class="user-info">
|
|
9432
|
+
<div class="user-name">{{ data.user?.nickname || '\u672C\u5730\u7528\u6237' }}</div>
|
|
9433
|
+
<div v-if="data.user?.created_at" class="user-meta">
|
|
9434
|
+
\u6CE8\u518C\u4E8E {{ formatCreatedAt(data.user.created_at) }}
|
|
9435
|
+
</div>
|
|
9436
|
+
</div>
|
|
9437
|
+
<div class="user-count">
|
|
9438
|
+
<div class="count-num">{{ data.eventCount }}</div>
|
|
9439
|
+
<div class="count-label">/ {{ data.maxEvents }} \u4E2A</div>
|
|
9440
|
+
</div>
|
|
9441
|
+
</div>
|
|
9442
|
+
</header>
|
|
9443
|
+
|
|
9444
|
+
<div class="page-scroll body">
|
|
9445
|
+
<div v-if="data.stats.length > 0" class="panel">
|
|
9446
|
+
<div class="panel-title">\u5206\u7C7B\u7EDF\u8BA1</div>
|
|
9447
|
+
<div class="stats">
|
|
9448
|
+
<div v-for="stat in data.stats" :key="stat.label" class="stat">
|
|
9449
|
+
<div class="stat-num">{{ stat.value }}</div>
|
|
9450
|
+
<div class="stat-label">{{ stat.label }}</div>
|
|
9451
|
+
</div>
|
|
9452
|
+
</div>
|
|
9453
|
+
</div>
|
|
9454
|
+
|
|
9455
|
+
<div class="panel">
|
|
9456
|
+
<div class="cell" @click="onClearCache">
|
|
9457
|
+
<span class="cell-title">\u6E05\u7A7A\u672C\u5730\u7F13\u5B58</span>
|
|
9458
|
+
<span class="arrow">\u203A</span>
|
|
9459
|
+
</div>
|
|
9460
|
+
<div class="cell" @click="onCopyHost">
|
|
9461
|
+
<span class="cell-title">\u670D\u52A1\u5730\u5740</span>
|
|
9462
|
+
<span class="cell-value">{{ apiHost }}</span>
|
|
9463
|
+
</div>
|
|
9464
|
+
<div class="cell">
|
|
9465
|
+
<span class="cell-title">\u7248\u672C</span>
|
|
9466
|
+
<span class="cell-value">v{{ VERSION }}</span>
|
|
9467
|
+
</div>
|
|
9468
|
+
</div>
|
|
9469
|
+
|
|
9470
|
+
<div class="tips">
|
|
9471
|
+
<div class="tips-title">\u6570\u636E\u5B58\u50A8\u8BF4\u660E</div>
|
|
9472
|
+
<div class="tips-text">
|
|
9473
|
+
\u4F60\u7684\u4E8B\u4EF6\u6570\u636E\u4FDD\u5B58\u5728 adep \u5E73\u53F0\u9879\u76EE\u6570\u636E\u5E93\uFF08\u672C\u5730\u5F00\u53D1\u4E3A\u6A21\u62DF\u8FD0\u884C\u65F6 / \u90E8\u7F72\u540E\u4E3A\u5E73\u53F0\u9879\u76EE\u5E93\uFF09\uFF0C
|
|
9474
|
+
\u6362\u6D4F\u89C8\u5668\u767B\u5F55\u540C\u4E00\u4E2A\u8BBE\u5907\u8EAB\u4EFD\u5373\u53EF\u7EE7\u7EED\u67E5\u770B\uFF0C\u6E05\u7F13\u5B58\u4E0D\u4F1A\u4E22\u5931\u4E91\u7AEF\u6570\u636E\u3002
|
|
9475
|
+
</div>
|
|
9476
|
+
</div>
|
|
9477
|
+
|
|
9478
|
+
<button class="logout" @click="onLogout">\u9000\u51FA\u767B\u5F55</button>
|
|
9479
|
+
</div>
|
|
9480
|
+
</div>
|
|
9481
|
+
</template>
|
|
9482
|
+
|
|
9483
|
+
<style scoped>
|
|
9484
|
+
.page {
|
|
9485
|
+
height: 100%;
|
|
9486
|
+
padding-bottom: calc(1.8rem + env(safe-area-inset-bottom));
|
|
9487
|
+
}
|
|
9488
|
+
|
|
9489
|
+
.header {
|
|
9490
|
+
position: absolute;
|
|
9491
|
+
top: 0;
|
|
9492
|
+
left: 0;
|
|
9493
|
+
right: 0;
|
|
9494
|
+
z-index: 10;
|
|
9495
|
+
background: linear-gradient(135deg, #3b6ef6, #6a8dff);
|
|
9496
|
+
border-bottom-left-radius: 0.4rem;
|
|
9497
|
+
border-bottom-right-radius: 0.4rem;
|
|
9498
|
+
padding-bottom: 0.28rem;
|
|
9499
|
+
}
|
|
9500
|
+
|
|
9501
|
+
.nav {
|
|
9502
|
+
display: flex;
|
|
9503
|
+
align-items: center;
|
|
9504
|
+
height: 0.88rem;
|
|
9505
|
+
padding-left: 0.4rem;
|
|
9506
|
+
}
|
|
9507
|
+
|
|
9508
|
+
.nav-title {
|
|
9509
|
+
color: #ffffff;
|
|
9510
|
+
font-size: 0.36rem;
|
|
9511
|
+
font-weight: 600;
|
|
9512
|
+
}
|
|
9513
|
+
|
|
9514
|
+
.user-card {
|
|
9515
|
+
margin: 0.08rem 0.32rem 0;
|
|
9516
|
+
padding: 0.32rem;
|
|
9517
|
+
background: rgba(255, 255, 255, 0.16);
|
|
9518
|
+
border-radius: 0.24rem;
|
|
9519
|
+
display: flex;
|
|
9520
|
+
align-items: center;
|
|
9521
|
+
}
|
|
9522
|
+
|
|
9523
|
+
.avatar {
|
|
9524
|
+
width: 1.04rem;
|
|
9525
|
+
height: 1.04rem;
|
|
9526
|
+
border-radius: 50%;
|
|
9527
|
+
background: #ffffff;
|
|
9528
|
+
display: flex;
|
|
9529
|
+
align-items: center;
|
|
9530
|
+
justify-content: center;
|
|
9531
|
+
flex-shrink: 0;
|
|
9532
|
+
}
|
|
9533
|
+
|
|
9534
|
+
.avatar-text {
|
|
9535
|
+
color: #3b6ef6;
|
|
9536
|
+
font-size: 0.4rem;
|
|
9537
|
+
font-weight: 600;
|
|
9538
|
+
}
|
|
9539
|
+
|
|
9540
|
+
.user-info {
|
|
9541
|
+
flex: 1;
|
|
9542
|
+
margin-left: 0.24rem;
|
|
9543
|
+
min-width: 0;
|
|
9544
|
+
}
|
|
9545
|
+
|
|
9546
|
+
.user-name {
|
|
9547
|
+
color: #ffffff;
|
|
9548
|
+
font-size: 0.32rem;
|
|
9549
|
+
font-weight: 600;
|
|
9550
|
+
}
|
|
9551
|
+
|
|
9552
|
+
.user-meta {
|
|
9553
|
+
margin-top: 0.1rem;
|
|
9554
|
+
color: rgba(255, 255, 255, 0.8);
|
|
9555
|
+
font-size: 0.24rem;
|
|
9556
|
+
}
|
|
9557
|
+
|
|
9558
|
+
.user-count {
|
|
9559
|
+
text-align: right;
|
|
9560
|
+
flex-shrink: 0;
|
|
9561
|
+
}
|
|
9562
|
+
|
|
9563
|
+
.count-num {
|
|
9564
|
+
color: #ffffff;
|
|
9565
|
+
font-size: 0.44rem;
|
|
9566
|
+
font-weight: 700;
|
|
9567
|
+
line-height: 1;
|
|
9568
|
+
}
|
|
9569
|
+
|
|
9570
|
+
.count-label {
|
|
9571
|
+
margin-top: 0.08rem;
|
|
9572
|
+
color: rgba(255, 255, 255, 0.8);
|
|
9573
|
+
font-size: 0.22rem;
|
|
9574
|
+
}
|
|
9575
|
+
|
|
9576
|
+
.body {
|
|
9577
|
+
padding: 3.2rem 0.32rem 0;
|
|
9578
|
+
}
|
|
9579
|
+
|
|
9580
|
+
.panel {
|
|
9581
|
+
background: #ffffff;
|
|
9582
|
+
border-radius: 0.24rem;
|
|
9583
|
+
padding: 0.08rem 0.32rem;
|
|
9584
|
+
margin-bottom: 0.24rem;
|
|
9585
|
+
box-shadow: 0 0.08rem 0.24rem rgba(31, 36, 48, 0.05);
|
|
9586
|
+
}
|
|
9587
|
+
|
|
9588
|
+
.panel-title {
|
|
9589
|
+
padding: 0.24rem 0 0.08rem;
|
|
9590
|
+
font-size: 0.26rem;
|
|
9591
|
+
color: #9aa3b2;
|
|
9592
|
+
}
|
|
9593
|
+
|
|
9594
|
+
.stats {
|
|
9595
|
+
display: flex;
|
|
9596
|
+
flex-wrap: wrap;
|
|
9597
|
+
padding-bottom: 0.2rem;
|
|
9598
|
+
}
|
|
9599
|
+
|
|
9600
|
+
.stat {
|
|
9601
|
+
width: 25%;
|
|
9602
|
+
text-align: center;
|
|
9603
|
+
padding: 0.16rem 0;
|
|
9604
|
+
}
|
|
9605
|
+
|
|
9606
|
+
.stat-num {
|
|
9607
|
+
font-size: 0.4rem;
|
|
9608
|
+
font-weight: 700;
|
|
9609
|
+
color: #3b6ef6;
|
|
9610
|
+
}
|
|
9611
|
+
|
|
9612
|
+
.stat-label {
|
|
9613
|
+
margin-top: 0.08rem;
|
|
9614
|
+
font-size: 0.22rem;
|
|
9615
|
+
color: #9aa3b2;
|
|
9616
|
+
}
|
|
9617
|
+
|
|
9618
|
+
.cell {
|
|
9619
|
+
display: flex;
|
|
9620
|
+
align-items: center;
|
|
9621
|
+
justify-content: space-between;
|
|
9622
|
+
padding: 0.3rem 0;
|
|
9623
|
+
border-bottom: 1px solid #f0f2f7;
|
|
9624
|
+
cursor: pointer;
|
|
9625
|
+
}
|
|
9626
|
+
|
|
9627
|
+
.cell:last-child {
|
|
9628
|
+
border-bottom: none;
|
|
9629
|
+
}
|
|
9630
|
+
|
|
9631
|
+
.cell-title {
|
|
9632
|
+
font-size: 0.28rem;
|
|
9633
|
+
color: #1f2430;
|
|
9634
|
+
}
|
|
9635
|
+
|
|
9636
|
+
.cell-value {
|
|
9637
|
+
font-size: 0.26rem;
|
|
9638
|
+
color: #9aa3b2;
|
|
9639
|
+
max-width: 52%;
|
|
9640
|
+
overflow: hidden;
|
|
9641
|
+
text-overflow: ellipsis;
|
|
9642
|
+
white-space: nowrap;
|
|
9643
|
+
}
|
|
9644
|
+
|
|
9645
|
+
.arrow {
|
|
9646
|
+
color: #c4c9d4;
|
|
9647
|
+
font-size: 0.36rem;
|
|
7304
9648
|
}
|
|
7305
9649
|
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
`;
|
|
7309
|
-
var ADEP_TSCONFIG_JSON = `{
|
|
7310
|
-
"compilerOptions": {
|
|
7311
|
-
"target": "ES2021",
|
|
7312
|
-
"module": "ESNext",
|
|
7313
|
-
"moduleResolution": "Bundler",
|
|
7314
|
-
"strict": true,
|
|
7315
|
-
"noEmit": true,
|
|
7316
|
-
"skipLibCheck": true,
|
|
7317
|
-
"types": [],
|
|
7318
|
-
"lib": ["ES2021", "DOM"]
|
|
7319
|
-
},
|
|
7320
|
-
"include": ["**/*.ts", "**/*.d.ts"]
|
|
9650
|
+
.tips {
|
|
9651
|
+
padding: 0.08rem 0.08rem 0.32rem;
|
|
7321
9652
|
}
|
|
7322
|
-
`;
|
|
7323
9653
|
|
|
7324
|
-
|
|
7325
|
-
|
|
9654
|
+
.tips-title {
|
|
9655
|
+
font-size: 0.26rem;
|
|
9656
|
+
color: #6b7385;
|
|
9657
|
+
font-weight: 500;
|
|
9658
|
+
}
|
|
7326
9659
|
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
// @adep/client:浏览器端云函数调用 SDK(IDE-031)。模板默认在 web/src/lib/adep.ts
|
|
7333
|
-
// 封装并由 App.vue 示例调用;运行时 fetch('/api/{name}') 在 dev 态被 @adep/vite-plugin
|
|
7334
|
-
// 拦截(CLI 代理到本地 adep dev / Web IDE 经 postMessage 转发到 IDE 主线程),
|
|
7335
|
-
// 生产态由平台网关路由到已发布云函数。
|
|
7336
|
-
"@adep/client": ADEP_CLIENT_VERSION
|
|
7337
|
-
};
|
|
7338
|
-
var WEB_DEV_DEPENDENCIES = {
|
|
7339
|
-
vite: "^4.4.0",
|
|
7340
|
-
"@vitejs/plugin-vue": "^4.3.0",
|
|
7341
|
-
// vite 内部动态 import('esbuild-wasm'),必须在 package.json 显式声明才能被 Nodebox 解析
|
|
7342
|
-
"esbuild-wasm": "0.18.20",
|
|
7343
|
-
// @adep/vite-plugin:云函数 Vite 插件(IDE-030)。dev 时自动注入 /api/* fetch 拦截器
|
|
7344
|
-
// (Web IDE 预览经 postMessage 转发到 IDE 主线程执行云函数草稿),若注入 createDevServer
|
|
7345
|
-
// (CLI 场景)则启动本地 adep dev server 并按 functions_prefix 代理 /{prefix}/* 到云函数。
|
|
7346
|
-
"@adep/vite-plugin": ADEP_VITE_PLUGIN_VERSION
|
|
7347
|
-
};
|
|
7348
|
-
function webProjectFiles() {
|
|
7349
|
-
return [
|
|
7350
|
-
{
|
|
7351
|
-
path: "package.json",
|
|
7352
|
-
content: JSON.stringify(
|
|
7353
|
-
{
|
|
7354
|
-
name: "adep-web",
|
|
7355
|
-
version: "0.0.0",
|
|
7356
|
-
type: "module",
|
|
7357
|
-
scripts: { dev: "vite" },
|
|
7358
|
-
dependencies: WEB_DEPENDENCIES,
|
|
7359
|
-
devDependencies: WEB_DEV_DEPENDENCIES
|
|
7360
|
-
},
|
|
7361
|
-
null,
|
|
7362
|
-
2
|
|
7363
|
-
)
|
|
7364
|
-
},
|
|
7365
|
-
{
|
|
7366
|
-
path: "index.html",
|
|
7367
|
-
content: [
|
|
7368
|
-
`<!doctype html>`,
|
|
7369
|
-
`<html lang="zh-CN">`,
|
|
7370
|
-
`<head>`,
|
|
7371
|
-
` <meta charset="utf-8" />`,
|
|
7372
|
-
` <meta name="viewport" content="width=device-width, initial-scale=1" />`,
|
|
7373
|
-
` <title>adep web</title>`,
|
|
7374
|
-
`</head>`,
|
|
7375
|
-
`<body>`,
|
|
7376
|
-
` <div id="app"></div>`,
|
|
7377
|
-
` <script type="module" src="/src/main.ts"></script>`,
|
|
7378
|
-
`</body>`,
|
|
7379
|
-
`</html>`,
|
|
7380
|
-
``
|
|
7381
|
-
].join("\n")
|
|
7382
|
-
},
|
|
7383
|
-
{
|
|
7384
|
-
path: "vite.config.ts",
|
|
7385
|
-
content: [
|
|
7386
|
-
`// web/vite.config.ts \u2014\u2014 \u6807\u51C6 Vite \u914D\u7F6E\uFF08Vue 3 + @adep/vite-plugin \u4E91\u51FD\u6570\u4EE3\u7406\uFF09\u3002`,
|
|
7387
|
-
`// @adep/vite-plugin \u5728 dev \u65F6\uFF1A`,
|
|
7388
|
-
`// 1. \u6CE8\u5165 /api/* fetch \u62E6\u622A\u5668\uFF08Web IDE \u9884\u89C8\u7ECF postMessage \u8F6C\u53D1\u5230 IDE \u4E3B\u7EBF\u7A0B\u6267\u884C\u4E91\u51FD\u6570\u8349\u7A3F\uFF09\uFF1B`,
|
|
7389
|
-
`// 2. \u82E5\u6CE8\u5165 createDevServer\uFF08CLI \u573A\u666F\uFF09\uFF0C\u542F\u52A8\u672C\u5730 adep dev \u5E76\u4EE3\u7406 /api/* \u5230\u4E91\u51FD\u6570\u3002`,
|
|
7390
|
-
`// \u6D4F\u89C8\u5668\u5185 vite-dev \u9884\u89C8\uFF08@adep/web-container buildDocument\uFF09\u6CE8\u5165\u540C\u4E00\u4EFD\u811A\u672C\uFF0C\u65E0\u9700\u5728\u6B64\u5185\u8054\u3002`,
|
|
7391
|
-
`import { defineConfig } from 'vite'`,
|
|
7392
|
-
`import vue from '@vitejs/plugin-vue'`,
|
|
7393
|
-
`import { adepPlugin } from '@adep/vite-plugin'`,
|
|
7394
|
-
``,
|
|
7395
|
-
`export default defineConfig({`,
|
|
7396
|
-
` plugins: [vue(), adepPlugin()],`,
|
|
7397
|
-
`})`,
|
|
7398
|
-
``
|
|
7399
|
-
].join("\n")
|
|
7400
|
-
},
|
|
7401
|
-
{
|
|
7402
|
-
path: "src/main.ts",
|
|
7403
|
-
content: [
|
|
7404
|
-
`// web/src/main.ts \u2014\u2014 \u5E94\u7528\u5165\u53E3\uFF08Vue 3 + Vite\uFF09\u3002`,
|
|
7405
|
-
`// \u5F00\u53D1\uFF1A\u5E95\u90E8\u300C\u6D4F\u89C8\u5668\u7EC8\u7AEF\u300D\u8FD0\u884C npm run dev\uFF08\u6216\u7B49 Nodebox \u81EA\u52A8\u62C9\u8D77\uFF09\uFF0C\u4FDD\u5B58\u540E\u9884\u89C8 HMR \u5237\u65B0\u3002`,
|
|
7406
|
-
`import { createApp } from 'vue'`,
|
|
7407
|
-
`import App from './App.vue'`,
|
|
7408
|
-
``,
|
|
7409
|
-
`createApp(App).mount('#app')`,
|
|
7410
|
-
``
|
|
7411
|
-
].join("\n")
|
|
7412
|
-
},
|
|
7413
|
-
{
|
|
7414
|
-
path: "src/lib/adep.ts",
|
|
7415
|
-
content: [
|
|
7416
|
-
`// web/src/lib/adep.ts \u2014\u2014 @adep/client \u6D4F\u89C8\u5668\u7AEF\u5C01\u88C5\uFF08IDE-032\uFF09\u3002`,
|
|
7417
|
-
`// \u5F00\u53D1\u6001\uFF08vite dev + @adep/vite-plugin\uFF09\uFF1AinvokeFunction \u8D70 fetch('/api/*')\uFF0C`,
|
|
7418
|
-
`// \u7531 vite \u63D2\u4EF6\u62E6\u622A\u5230\u672C\u5730\u4E91\u51FD\u6570\u6267\u884C\uFF08CLI\uFF09\u6216\u7ECF postMessage \u8F6C\u53D1\u5230 IDE \u4E3B\u7EBF\u7A0B\uFF08Web IDE\uFF09\u3002`,
|
|
7419
|
-
`// \u751F\u4EA7\u6001\uFF08\u5DF2\u90E8\u7F72\u7AD9\u70B9\uFF09\uFF1Afetch('/api/*') \u7531\u5E73\u53F0\u7F51\u5173\u8DEF\u7531\u5230\u5DF2\u53D1\u5E03\u4E91\u51FD\u6570\u3002`,
|
|
7420
|
-
`import { createAdepClient } from '@adep/client'`,
|
|
7421
|
-
``,
|
|
7422
|
-
`export const adep = createAdepClient()`,
|
|
7423
|
-
``,
|
|
7424
|
-
`export const { invokeFunction } = adep`,
|
|
7425
|
-
``
|
|
7426
|
-
].join("\n")
|
|
7427
|
-
},
|
|
7428
|
-
{
|
|
7429
|
-
path: "src/App.vue",
|
|
7430
|
-
content: [
|
|
7431
|
-
`<script setup lang="ts">`,
|
|
7432
|
-
`import { ref } from 'vue'`,
|
|
7433
|
-
`import { invokeFunction } from './lib/adep'`,
|
|
7434
|
-
``,
|
|
7435
|
-
`const title = ref('adep web')`,
|
|
7436
|
-
`const clicks = ref(0)`,
|
|
7437
|
-
`const fnResult = ref<string>('')`,
|
|
7438
|
-
`const fnLoading = ref(false)`,
|
|
7439
|
-
``,
|
|
7440
|
-
`async function callHello() {`,
|
|
7441
|
-
` fnLoading.value = true`,
|
|
7442
|
-
` fnResult.value = ''`,
|
|
7443
|
-
` try {`,
|
|
7444
|
-
` const result = await invokeFunction<{ message: string }>('hello')`,
|
|
7445
|
-
` fnResult.value = result.message`,
|
|
7446
|
-
` } catch (err) {`,
|
|
7447
|
-
` fnResult.value = \`\u8C03\u7528\u5931\u8D25\uFF1A\${err instanceof Error ? err.message : String(err)}\``,
|
|
7448
|
-
` } finally {`,
|
|
7449
|
-
` fnLoading.value = false`,
|
|
7450
|
-
` }`,
|
|
7451
|
-
`}`,
|
|
7452
|
-
`</script>`,
|
|
7453
|
-
``,
|
|
7454
|
-
`<template>`,
|
|
7455
|
-
` <main>`,
|
|
7456
|
-
` <h1>{{ title }}</h1>`,
|
|
7457
|
-
` <p>\u5728 web/src/App.vue \u91CC\u7F16\u8F91\uFF0C\u4FDD\u5B58\u540E\u9884\u89C8\u81EA\u52A8\u5237\u65B0\uFF08HMR\uFF09\u3002</p>`,
|
|
7458
|
-
` <button @click="clicks++">clicks: {{ clicks }}</button>`,
|
|
7459
|
-
` <div style="margin-top: 16px;">`,
|
|
7460
|
-
` <button @click="callHello" :disabled="fnLoading">`,
|
|
7461
|
-
` {{ fnLoading ? '\u8C03\u7528\u4E2D\u2026' : '\u8C03\u7528 hello \u51FD\u6570' }}`,
|
|
7462
|
-
` </button>`,
|
|
7463
|
-
` <p v-if="fnResult" style="margin-top: 8px; color: #059669;">\u7ED3\u679C\uFF1A{{ fnResult }}</p>`,
|
|
7464
|
-
` </div>`,
|
|
7465
|
-
` </main>`,
|
|
7466
|
-
`</template>`,
|
|
7467
|
-
``,
|
|
7468
|
-
`<style scoped>`,
|
|
7469
|
-
`main {`,
|
|
7470
|
-
` margin: 24px;`,
|
|
7471
|
-
` font-family: system-ui, -apple-system, sans-serif;`,
|
|
7472
|
-
`}`,
|
|
7473
|
-
`button {`,
|
|
7474
|
-
` padding: 6px 14px;`,
|
|
7475
|
-
` border: 1px solid #6366f1;`,
|
|
7476
|
-
` border-radius: 6px;`,
|
|
7477
|
-
` background: #eef2ff;`,
|
|
7478
|
-
` cursor: pointer;`,
|
|
7479
|
-
`}`,
|
|
7480
|
-
`button:disabled {`,
|
|
7481
|
-
` opacity: 0.6;`,
|
|
7482
|
-
` cursor: not-allowed;`,
|
|
7483
|
-
`}`,
|
|
7484
|
-
`</style>`,
|
|
7485
|
-
``
|
|
7486
|
-
].join("\n")
|
|
7487
|
-
}
|
|
7488
|
-
];
|
|
9660
|
+
.tips-text {
|
|
9661
|
+
margin-top: 0.12rem;
|
|
9662
|
+
font-size: 0.24rem;
|
|
9663
|
+
color: #a0a8b8;
|
|
9664
|
+
line-height: 1.7;
|
|
7489
9665
|
}
|
|
7490
|
-
|
|
7491
|
-
|
|
9666
|
+
|
|
9667
|
+
.logout {
|
|
9668
|
+
display: flex;
|
|
9669
|
+
align-items: center;
|
|
9670
|
+
justify-content: center;
|
|
9671
|
+
width: 100%;
|
|
9672
|
+
height: 0.92rem;
|
|
9673
|
+
background: #ffffff;
|
|
9674
|
+
color: #ff5a5f;
|
|
9675
|
+
font-size: 0.3rem;
|
|
9676
|
+
border-radius: 999rem;
|
|
9677
|
+
border: 1px solid #ffd9da;
|
|
9678
|
+
cursor: pointer;
|
|
7492
9679
|
}
|
|
9680
|
+
</style>
|
|
9681
|
+
` },
|
|
9682
|
+
{ path: "web/tsconfig.json", content: '{\n "compilerOptions": {\n "target": "ES2021",\n "module": "ESNext",\n "moduleResolution": "Bundler",\n "strict": true,\n "noEmit": true,\n "skipLibCheck": true,\n "jsx": "preserve",\n "resolveJsonModule": true,\n "esModuleInterop": true,\n "lib": ["ES2021", "DOM", "DOM.Iterable"],\n "types": ["vite/client"]\n },\n "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]\n}\n' },
|
|
9683
|
+
{ path: "web/vite.config.ts", content: "// web/vite.config.ts \u2014\u2014 \u5012\u6570\u65E5\u5E94\u7528\u524D\u7AEF\uFF08Vue 3 + Vite\uFF09\u3002\n//\n// \u672C\u5730\u5F00\u53D1\uFF08npm run dev\uFF0C\u5728 apps/countdown \u6839\u6267\u884C\uFF09\uFF1A\n// - root \u56FA\u5B9A\u4E3A\u672C\u76EE\u5F55\uFF08web/\uFF09\uFF0C\u524D\u7AEF\u6E90\u7801\u4ECE web/src \u52A0\u8F7D\uFF1B\n// - command === 'serve' \u65F6\u52A8\u6001\u52A0\u8F7D @adep/cli/vite\uFF1A\u8BA9 vite \u5728\u8FDB\u7A0B\u5185\u542F\u52A8 `adep dev`\n// \uFF08\u6A21\u62DF\u8FD0\u884C\u65F6\uFF0Ccwd = \u5E94\u7528\u6839 apps/countdown\uFF0C\u8BFB\u53D6 adep.config.ts \u4E0E functions/\uFF09\uFF0C\n// \u5E76\u628A /api/* \u4EE3\u7406\u5230\u5B83\u2014\u2014\u4E00\u6761\u547D\u4EE4\u540C\u65F6\u8C03\u8BD5\u524D\u7AEF + \u4E91\u51FD\u6570\uFF08\u51FD\u6570\u6539\u52A8\u70ED\u91CD\u8F7D\uFF09\u3002\n//\n// \u5E73\u53F0\u6784\u5EFA\uFF08adep publish --only frontend \u7684\u670D\u52A1\u7AEF vite build\uFF09\uFF1A\n// - \u5E73\u53F0\u628A web/ \u8349\u7A3F\u5199\u5165\u4E34\u65F6\u76EE\u5F55\u540E\u6267\u884C bun install + vite build\uFF0Ccommand === 'build'\uFF0C\n// \u672C\u914D\u7F6E**\u4E0D\u4F1A**\u52A0\u8F7D @adep/cli/vite\uFF08\u4E0D\u89E3\u6790\u3001\u4E0D\u5B89\u88C5\u5176\u8FD0\u884C\uFF0C\u5E73\u53F0\u65E0\u9700\u53D1\u5E03\u7248 cli \u7684\n// ./vite \u5B50\u8DEF\u5F84\uFF09\uFF0C\u4FDD\u8BC1\u53D1\u5E03\u4EA7\u7269\u53EF\u72EC\u7ACB\u6784\u5EFA\uFF1BdevDependencies \u91CC\u7684 @adep/cli \u4EC5\u672C\u5730\u7528\u3002\nimport { defineConfig, type Plugin } from 'vite'\nimport { fileURLToPath, URL } from 'node:url'\nimport vue from '@vitejs/plugin-vue'\n\nexport default defineConfig(async ({ command }) => {\n const plugins: Plugin[] = [vue()]\n\n if (command === 'serve') {\n // \u672C\u5730\u5F00\u53D1\uFF1A\u8FDB\u7A0B\u5185 adep dev + /api \u4EE3\u7406\uFF08\u52A8\u6001 import\uFF0C\u6784\u5EFA\u671F\u4E0D\u89E3\u6790\u8BE5\u6A21\u5757\uFF09\u3002\n const adep = (await import('@adep/cli/vite')).default\n plugins.push(adep({ cwd: fileURLToPath(new URL('..', import.meta.url)) }) as unknown as Plugin)\n }\n\n return {\n root: fileURLToPath(new URL('.', import.meta.url)),\n plugins,\n build: {\n outDir: 'dist',\n emptyOutDir: true,\n },\n server: {\n port: 5173,\n host: '127.0.0.1',\n },\n }\n})\n" }
|
|
9684
|
+
];
|
|
7493
9685
|
|
|
7494
9686
|
// packages/cli/src/init.ts
|
|
7495
|
-
var TEMPLATE_NAMES = ["empty", "function", "fullstack"];
|
|
9687
|
+
var TEMPLATE_NAMES = ["empty", "function", "fullstack", "countdown"];
|
|
7496
9688
|
var InitError = class extends Error {
|
|
7497
9689
|
constructor(code, message) {
|
|
7498
9690
|
super(message);
|
|
@@ -7504,10 +9696,10 @@ var requireJson = createRequire(import.meta.url);
|
|
|
7504
9696
|
var CLI_VERSION = requireJson("../package.json").version;
|
|
7505
9697
|
var ADEP_CONFIG_HEADER = `// adep \u9879\u76EE\u914D\u7F6E\uFF1ACLI\uFF08dev / serve / deploy\uFF09\u4E0E\u4E91\u51FD\u6570 vite \u63D2\u4EF6\u6309 default export \u8BFB\u53D6\u3002
|
|
7506
9698
|
// - name\uFF1A\u9879\u76EE\u6807\u8BC6\uFF08deploy / db \u7B49\u547D\u4EE4\u7684\u7F3A\u7701 project slug\uFF09
|
|
7507
|
-
// - template\uFF1A\u811A\u624B\u67B6\u6A21\u677F\uFF08empty / function / fullstack\uFF0Cinit \u65F6\u786E\u5B9A\uFF09
|
|
9699
|
+
// - template\uFF1A\u811A\u624B\u67B6\u6A21\u677F\uFF08empty / function / fullstack / countdown\uFF0Cinit \u65F6\u786E\u5B9A\uFF09
|
|
7508
9700
|
// - functionsDir\uFF1A\u4E91\u51FD\u6570\u76EE\u5F55\uFF08dev / serve / deploy \u8BFB\u53D6\uFF0C\u7F3A\u7701 functions/\uFF09
|
|
7509
9701
|
// - functions_prefix\uFF1A\u4E91\u51FD\u6570\u8DEF\u7531\u524D\u7F00\uFF0C**\u9ED8\u8BA4 /api**\uFF08\u4E0E\u7EBF\u4E0A\u51FD\u6570\u8DEF\u7531 /api/{fn} \u5BF9\u9F50\uFF09\u3002
|
|
7510
|
-
// dev \u8BBF\u95EE\u8DEF\u5F84\u4E3A /{prefix}/{fnName}\uFF08\u5982 /api/hello\uFF09\
|
|
9702
|
+
// dev / serve / \u5E73\u53F0\u8BBF\u95EE\u8DEF\u5F84\u4E00\u81F4\u4E3A /{prefix}/{fnName}\uFF08\u5982 /api/hello\uFF09\u3002
|
|
7511
9703
|
// vite \u63D2\u4EF6\uFF08vite.config.ts \u7684 adepPlugin()\uFF09\u6309\u6B64\u524D\u7F00\u628A\u524D\u7AEF /{prefix}/* \u8BF7\u6C42\u4EE3\u7406\u5230\u4E91\u51FD\u6570\u3002
|
|
7512
9704
|
// \u8FB9\u754C\uFF1A\u53EA\u5F71\u54CD\u672C\u5730 HTTP \u5165\u53E3\u8DEF\u7531\uFF1B\u51FD\u6570\u4E92\u8C03\uFF08ctx.cloud.invoke\uFF09\u4E0E\u7EBF\u4E0A\u90E8\u7F72\u8DEF\u5F84\u6309\u51FD\u6570\u540D\uFF0C\u4E0D\u53D7\u5F71\u54CD\u3002`;
|
|
7513
9705
|
var PACKAGE_JSON = (name, template) => {
|
|
@@ -7538,6 +9730,106 @@ var PACKAGE_JSON = (name, template) => {
|
|
|
7538
9730
|
return `${JSON.stringify(pkg, null, 2)}
|
|
7539
9731
|
`;
|
|
7540
9732
|
};
|
|
9733
|
+
var COUNTDOWN_PACKAGE_JSON = (name) => {
|
|
9734
|
+
const pkg = {
|
|
9735
|
+
name,
|
|
9736
|
+
version: "0.1.0",
|
|
9737
|
+
private: true,
|
|
9738
|
+
type: "module",
|
|
9739
|
+
description: "\u5012\u6570\u65E5 / \u7EAA\u5FF5\u65E5\uFF08countdown \u6A21\u677F\uFF09\u2014\u2014\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F\u79FB\u690D\u7684 adep \u5E94\u7528\uFF1Afunctions/ \u4E91\u51FD\u6570 + web/ \u524D\u7AEF\uFF08Vue 3 + Vite\uFF09",
|
|
9740
|
+
scripts: {
|
|
9741
|
+
dev: "vite -c web/vite.config.ts",
|
|
9742
|
+
"dev:api": "adep dev",
|
|
9743
|
+
build: "vite build -c web/vite.config.ts",
|
|
9744
|
+
preview: "vite preview -c web/vite.config.ts",
|
|
9745
|
+
start: "adep serve --host 0.0.0.0 --schema functions/schema.sql --static ./web/dist --spa",
|
|
9746
|
+
test: "vitest run",
|
|
9747
|
+
"test:watch": "vitest",
|
|
9748
|
+
"db:init": "adep db start && adep db migrate functions/schema.sql",
|
|
9749
|
+
deploy: "adep publish",
|
|
9750
|
+
"deploy:functions": "adep publish --only functions",
|
|
9751
|
+
"deploy:frontend": "adep publish --only frontend",
|
|
9752
|
+
"frontend:sync": "adep frontend sync",
|
|
9753
|
+
doctor: "adep doctor"
|
|
9754
|
+
},
|
|
9755
|
+
// 依赖分两处声明:根 = @adep/cli + @adep/types + 前端构建链;web/package.json = 前端运行依赖。
|
|
9756
|
+
// vitest 用 ^3.2.4:vitest 4.x 的 packument 触发 npm 10.9 arborist edgesOut 崩溃(官方源同样),
|
|
9757
|
+
// 与 apps/countdown 保持一致;双目录安装形态(不用 npm workspaces),README 已写明两条 install。
|
|
9758
|
+
dependencies: {
|
|
9759
|
+
"@adep/cli": CLI_VERSION
|
|
9760
|
+
},
|
|
9761
|
+
devDependencies: {
|
|
9762
|
+
"@adep/types": "^0.2.0",
|
|
9763
|
+
"@vitejs/plugin-vue": "^6.0.0",
|
|
9764
|
+
typescript: "5.9.3",
|
|
9765
|
+
vite: "^7.0.0",
|
|
9766
|
+
vitest: "^3.2.4",
|
|
9767
|
+
vue: "^3.5.42"
|
|
9768
|
+
},
|
|
9769
|
+
engines: {
|
|
9770
|
+
node: ">=18",
|
|
9771
|
+
bun: ">=1.0"
|
|
9772
|
+
}
|
|
9773
|
+
};
|
|
9774
|
+
return `${JSON.stringify(pkg, null, 2)}
|
|
9775
|
+
`;
|
|
9776
|
+
};
|
|
9777
|
+
var COUNTDOWN_README = (name) => `# ${name}
|
|
9778
|
+
|
|
9779
|
+
\u5012\u6570\u65E5 / \u7EAA\u5FF5\u65E5\uFF08countdown \u6A21\u677F\uFF09\u2014\u2014\u7531\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F\u300C\u5012\u6570\u65E5\u300D\u79FB\u690D\u7684 adep \u5168\u6808\u5E94\u7528\uFF1A
|
|
9780
|
+
\`functions/\` \u4E91\u51FD\u6570\uFF08auth / events + \`_shared/\` \u5171\u4EAB\u6A21\u5757\uFF09+ \`web/\` \u524D\u7AEF\uFF08Vue 3 + Vite\uFF0C
|
|
9781
|
+
\u4E09\u9875 Home / Edit / Profile + \u81EA\u5B9A\u4E49 TabBar + \u5DE6\u6ED1\u5220\u9664 / \u957F\u6309\u7F6E\u9876 / \u4E0B\u62C9\u5237\u65B0\uFF0C\u754C\u9762\u4E0E\u5C0F\u7A0B\u5E8F\u5BF9\u9F50\uFF09\u3002
|
|
9782
|
+
|
|
9783
|
+
## \u5FEB\u901F\u5F00\u59CB
|
|
9784
|
+
|
|
9785
|
+
\`\`\`bash
|
|
9786
|
+
npm install # \u6839\uFF1A@adep/cli + \u524D\u7AEF\u6784\u5EFA\u94FE\uFF08vue/vite/plugin-vue/vitest\uFF09
|
|
9787
|
+
npm install --prefix web # web/\uFF1A\u524D\u7AEF\u8FD0\u884C\u4F9D\u8D56\uFF08vue\uFF09
|
|
9788
|
+
npm run dev # \u5168\u6808\u5F00\u53D1\uFF1Avite\uFF08web/\uFF09\u8FDB\u7A0B\u5185\u542F\u52A8 adep dev + /api \u4EE3\u7406\uFF0C\u524D\u7AEF HMR + \u51FD\u6570\u70ED\u91CD\u8F7D
|
|
9789
|
+
npm test # \u51FD\u6570\u5171\u4EAB\u903B\u8F91\u5355\u5143\u6D4B\u8BD5\uFF08vitest\uFF09
|
|
9790
|
+
npm run build # \u6784\u5EFA\u524D\u7AEF \u2192 web/dist
|
|
9791
|
+
npm run start # \u72EC\u7ACB\u8FD0\u884C\uFF08\u672C\u5730\u6A21\u62DF\u8FD0\u884C\u65F6 + \u9759\u6001\u6258\u7BA1 + --schema \u81EA\u52A8\u5EFA\u8868\uFF09\uFF0C\u6D4F\u89C8\u5668\u5F00 http://127.0.0.1:8787
|
|
9792
|
+
\`\`\`
|
|
9793
|
+
|
|
9794
|
+
## \u90E8\u7F72\u5230 adep \u5E73\u53F0
|
|
9795
|
+
|
|
9796
|
+
\`\`\`bash
|
|
9797
|
+
adep login # \u767B\u5F55\uFF08\u5199 ~/.adep/credentials\uFF09
|
|
9798
|
+
npm run deploy # \u5168\u6808\u53D1\u5E03\uFF1A\u51FD\u6570 deploy\uFF08\u542B _shared/\uFF09+ \u524D\u7AEF sync + publish
|
|
9799
|
+
npm run deploy:functions # \u53EA\u53D1\u5E03\u4E91\u51FD\u6570
|
|
9800
|
+
npm run deploy:frontend # \u53EA\u53D1\u5E03\u524D\u7AEF
|
|
9801
|
+
\`\`\`
|
|
9802
|
+
|
|
9803
|
+
\u53D1\u5E03\u540E\u8BBF\u95EE \`https://{project-slug}.adep.localhost:3001\`\uFF08\u672C\u5730 dev \u5E73\u53F0\uFF09\u6216\u7EBF\u4E0A\u7AD9\u70B9\u5730\u5740\u3002
|
|
9804
|
+
|
|
9805
|
+
## \u72EC\u7ACB\u90E8\u7F72\uFF08\u4E0D\u4F9D\u8D56 adep \u5E73\u53F0\uFF09
|
|
9806
|
+
|
|
9807
|
+
\`npm run build && npm run start\` \u5373\u53EF\u672C\u5730\u72EC\u7ACB\u8DD1\uFF08adep serve \u4E0E\u5E73\u53F0\u540C\u6E90\u6267\u884C\u5668\uFF09\u3002
|
|
9808
|
+
\u5BB9\u5668\u5316\u90E8\u7F72\u53C2\u8003 \`apps/countdown\` \u7684 \`Dockerfile\` + \`compose.yaml\`\uFF08\u5355\u5BB9\u5668 =
|
|
9809
|
+
serve \u627F\u8F7D /api/* + \u9759\u6001\u6258\u7BA1 web/dist + .adep/sim/ \u6570\u636E\u5377\uFF09\u3002
|
|
9810
|
+
|
|
9811
|
+
## \u76EE\u5F55\u7ED3\u6784
|
|
9812
|
+
|
|
9813
|
+
- \`adep.config.ts\`\uFF1A\u9879\u76EE\u914D\u7F6E\uFF08name / template / functionsDir / functions_prefix \u9ED8\u8BA4 /api\uFF09
|
|
9814
|
+
- \`functions/\`\uFF1A\u4E91\u51FD\u6570\u76EE\u5F55\u2014\u2014\`auth.ts\`\uFF08\u767B\u5F55/\u4F1A\u8BDD\uFF09\u3001\`events.ts\`\uFF08\u4E8B\u4EF6 CRUD + \u7F6E\u9876\u6392\u5E8F\uFF09\u3001
|
|
9815
|
+
\`schema.sql\`\uFF08users / events \u8868\uFF09\u3001\`_shared/\`\uFF08auth / store / date / response / route \u5171\u4EAB\u6A21\u5757\uFF09
|
|
9816
|
+
- \`web/\`\uFF1A\u524D\u7AEF\u5DE5\u7A0B\u6839\uFF08web/vite.config.ts + web/src/*\uFF09\u2014\u2014\`web/src/views/\` \u4E09\u9875\u3001
|
|
9817
|
+
\`web/src/ui/\`\uFF08TabBar / UiHost \u590D\u523B wx.showToast \u7B49\uFF09\u3001\`web/src/lib/\`\uFF08request / date / device / auth\uFF09
|
|
9818
|
+
- \`web/src/lib/request.ts\`\uFF1Afetch \u5C01\u88C5\uFF08\u4E1A\u52A1\u4FE1\u5C01 + 40001 \u9759\u9ED8\u91CD\u767B\uFF09
|
|
9819
|
+
- \`tsconfig.json\` / \`adep.d.ts\`\uFF1ATypeScript \u914D\u7F6E\u4E0E\u4E91\u51FD\u6570\u4E0A\u4E0B\u6587\u7C7B\u578B\u58F0\u660E
|
|
9820
|
+
|
|
9821
|
+
## \u7528\u6237\u4F53\u7CFB\u8BF4\u660E\uFF08\u767B\u5F55\u94FE\u8DEF\uFF09
|
|
9822
|
+
|
|
9823
|
+
\u5C0F\u7A0B\u5E8F\u7AEF \`wx.login\` \u2192 openid \u7684\u94FE\u8DEF\uFF0Cweb \u7AEF\u4EE5\u300C\u8BBE\u5907\u8EAB\u4EFD device_id\u300D\u66FF\u4EE3\uFF1A\u9996\u6B21\u8BBF\u95EE\u751F\u6210\u5E76
|
|
9824
|
+
\u6301\u4E45\u5316\u4E8E localStorage\uFF08web/src/lib/device.ts\uFF09\uFF0C\u4F5C\u4E3A Bearer \u51ED\u636E\u8BBF\u95EE\u51FD\u6570\uFF08\u7B49\u4EF7 API Key\uFF09\u3002
|
|
9825
|
+
\u51FD\u6570\u4FA7 \`functions/_shared/auth.ts\` \u67E5 users \u8868\u6821\u9A8C\uFF0C\u672A\u767B\u5F55/\u5931\u6548\u8FD4\u56DE\u4E1A\u52A1\u7801 40001\uFF0C
|
|
9826
|
+
\u524D\u7AEF request.ts \u636E\u6B64\u9759\u9ED8\u91CD\u65B0\u767B\u5F55\u3002
|
|
9827
|
+
|
|
9828
|
+
## \u66F4\u591A
|
|
9829
|
+
|
|
9830
|
+
- \u79FB\u690D\u7ECF\u9A8C\u4E0E\u9010\u9879\u5BF9\u9F50\u8BF4\u660E\uFF1A\`packages/docs/tutorial-countdown.md\`\uFF08adep \u4ED3\u5E93\u6587\u6863\uFF09
|
|
9831
|
+
- \u72EC\u7ACB\u90E8\u7F72\u5BB9\u5668\u914D\u7F6E\u89C1 adep \u4ED3\u5E93 \`apps/countdown\`\uFF08Dockerfile / compose.yaml\uFF09
|
|
9832
|
+
`;
|
|
7541
9833
|
var README = (name, template) => `# ${name}
|
|
7542
9834
|
|
|
7543
9835
|
AgentDeploy \u5168\u6808\u9879\u76EE\uFF08\u6A21\u677F\uFF1A${template}\uFF09\u3002
|
|
@@ -7697,8 +9989,8 @@ async function initProject(cwd, name, template) {
|
|
|
7697
9989
|
throw new InitError("DIR_EXISTS", `\u76EE\u5F55 ${projectPath} \u5DF2\u5B58\u5728\uFF1A\u8BF7\u6362\u4E00\u4E2A\u540D\u5B57\u6216\u5148\u5220\u9664`);
|
|
7698
9990
|
}
|
|
7699
9991
|
const files = [
|
|
7700
|
-
// 根 package.json
|
|
7701
|
-
{ path: "package.json", content: PACKAGE_JSON(name, template) },
|
|
9992
|
+
// 根 package.json:应用定义(countdown 模板 = 独立 vite 工程根在 web/,npm workspaces 装齐根 + web/)。
|
|
9993
|
+
template === "countdown" ? { path: "package.json", content: COUNTDOWN_PACKAGE_JSON(name) } : { path: "package.json", content: PACKAGE_JSON(name, template) },
|
|
7702
9994
|
// adep.config.ts:经 shared/sdk/adep-config 的 renderAdepConfig 渲染(schema 唯一真相源)。
|
|
7703
9995
|
// template 是 init 元数据(不在 AdepConfig 中),经 extraFields 传入。
|
|
7704
9996
|
{
|
|
@@ -7711,7 +10003,10 @@ async function initProject(cwd, name, template) {
|
|
|
7711
10003
|
// 云函数 TypeScript 类型提示:tsconfig 把 adep.d.ts 纳入 include,写 ctx: AdepContext 即有补全。
|
|
7712
10004
|
{ path: "tsconfig.json", content: ADEP_TSCONFIG_JSON },
|
|
7713
10005
|
{ path: "adep.d.ts", content: ADEP_ENV_DTS },
|
|
7714
|
-
{
|
|
10006
|
+
{
|
|
10007
|
+
path: "README.md",
|
|
10008
|
+
content: template === "countdown" ? COUNTDOWN_README(name) : README(name, template)
|
|
10009
|
+
},
|
|
7715
10010
|
{ path: ".gitignore", content: "node_modules/\ndata/\n.adep/\nsite/\n" }
|
|
7716
10011
|
];
|
|
7717
10012
|
if (template === "function") {
|
|
@@ -7726,6 +10021,10 @@ async function initProject(cwd, name, template) {
|
|
|
7726
10021
|
for (const file of webSrcFiles()) {
|
|
7727
10022
|
files.push({ path: `web/${file.path}`, content: file.content });
|
|
7728
10023
|
}
|
|
10024
|
+
} else if (template === "countdown") {
|
|
10025
|
+
for (const file of COUNTDOWN_TEMPLATE_FILES) {
|
|
10026
|
+
files.push({ path: file.path, content: file.content });
|
|
10027
|
+
}
|
|
7729
10028
|
} else {
|
|
7730
10029
|
files.push({ path: "README-EMPTY.md", content: EMPTY_INDEX });
|
|
7731
10030
|
}
|
|
@@ -8722,20 +11021,30 @@ function registerDevServers(program2, ctx) {
|
|
|
8722
11021
|
).option(
|
|
8723
11022
|
"--watch",
|
|
8724
11023
|
"\u70ED\u91CD\u8F7D\uFF1A\u76D1\u542C functions/ \u4E0E .env* \u53D8\u66F4\u81EA\u52A8\u751F\u6548\uFF0C\u51FD\u6570 console \u8F93\u51FA\u76F4\u8FBE\u7EC8\u7AEF\uFF08\u5F00\u53D1\u6001\u8C03\u8BD5\u7528\uFF09"
|
|
8725
|
-
).
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
11024
|
+
).option(
|
|
11025
|
+
"--schema <file>",
|
|
11026
|
+
"\u6570\u636E\u5E93\u7ED3\u6784\u6587\u4EF6\uFF1A\u542F\u52A8\u65F6\u5BF9\u672C\u5730\u6A21\u62DF\u5E93\u9010\u6761\u5E94\u7528\uFF08\u5EFA\u8868\u8BED\u53E5\u5EFA\u8BAE IF NOT EXISTS\uFF0C\u5E42\u7B49\u53EF\u91CD\u590D\u542F\u52A8\uFF09"
|
|
11027
|
+
).option(
|
|
11028
|
+
"--spa",
|
|
11029
|
+
"SPA \u56DE\u9000\uFF1A\u9759\u6001\u8D44\u6E90\u672A\u547D\u4E2D\u65F6\u56DE\u9000 index.html\uFF08\u524D\u7AEF\u8DEF\u7531\u76F4\u8BBF /profile \u7B49\u9875\u9762\u9700\u8981\uFF09"
|
|
11030
|
+
).action(
|
|
11031
|
+
async (flags) => {
|
|
11032
|
+
const command = ctx.io("serve");
|
|
11033
|
+
await command.run(async () => {
|
|
11034
|
+
const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
11035
|
+
await startServeServer2({
|
|
11036
|
+
cwd: ctx.cwd,
|
|
11037
|
+
...flags.port === void 0 ? {} : { port: Number(flags.port) },
|
|
11038
|
+
...flags.host === void 0 ? {} : { host: flags.host },
|
|
11039
|
+
...flags.static === void 0 ? {} : { staticDir: flags.static },
|
|
11040
|
+
...flags.watch === void 0 ? {} : { watch: true },
|
|
11041
|
+
...flags.schema === void 0 ? {} : { schemaFile: flags.schema },
|
|
11042
|
+
...flags.spa === void 0 ? {} : { spaFallback: true }
|
|
11043
|
+
});
|
|
11044
|
+
await new Promise(() => void 0);
|
|
8735
11045
|
});
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
});
|
|
11046
|
+
}
|
|
11047
|
+
);
|
|
8739
11048
|
}
|
|
8740
11049
|
|
|
8741
11050
|
// packages/cli/src/commands/publish.ts
|
|
@@ -8743,9 +11052,9 @@ init_auth();
|
|
|
8743
11052
|
init_client();
|
|
8744
11053
|
init_deploy();
|
|
8745
11054
|
import { stat as stat8 } from "node:fs/promises";
|
|
8746
|
-
import { join as join14, resolve as
|
|
11055
|
+
import { join as join14, resolve as resolve8 } from "node:path";
|
|
8747
11056
|
async function resolveProjectId2(client, cwd, slug) {
|
|
8748
|
-
const resolvedSlug = await resolveSlug(
|
|
11057
|
+
const resolvedSlug = await resolveSlug(resolve8(cwd), slug);
|
|
8749
11058
|
const listed = await client.request("/api/v1/projects", {}, "PROJECT_LIST_FAILED");
|
|
8750
11059
|
const match = listed.projects.find((project2) => project2.slug === resolvedSlug);
|
|
8751
11060
|
if (match === void 0) {
|